Compare commits
31 Commits
eval-sourc
...
v2.0.8
Author | SHA1 | Date | |
---|---|---|---|
7bccb73729 | |||
78df5fc2a5 | |||
b53cc810f5 | |||
5386ceb94c | |||
5a4dd11955 | |||
affb7a5311 | |||
1e1133046a | |||
06b321588e | |||
909901b0f3 | |||
865f95c0b6 | |||
cb1e04b0d6 | |||
b0c2e66613 | |||
d162b5dbd8 | |||
64565b1bbb | |||
f721980bf0 | |||
b47712a0f4 | |||
57f3d4eba0 | |||
064a865c9b | |||
61bf60783c | |||
5dc718b78d | |||
41f8cb404d | |||
c6c58af12c | |||
15a0a87251 | |||
59a8614f1c | |||
7cf11e177c | |||
1a44652470 | |||
51d16f812a | |||
25de5653e8 | |||
cb6014d69f | |||
36736eb8a0 | |||
a13a6002c5 |
37
.github/workflows/e2e-couchdb.yml
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
name: "e2e-couchdb"
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- labeled
|
||||||
|
- opened
|
||||||
|
env:
|
||||||
|
OPENMCT_DATABASE_NAME: openmct
|
||||||
|
COUCH_ADMIN_USER: admin
|
||||||
|
COUCH_ADMIN_PASSWORD: password
|
||||||
|
COUCH_BASE_LOCAL: http://localhost:5984
|
||||||
|
COUCH_NODE_NAME: nonode@nohost
|
||||||
|
jobs:
|
||||||
|
e2e-couchdb:
|
||||||
|
if: ${{ github.event.label.name == 'pr:e2e:couchdb' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- run : docker-compose up -d -f src/plugins/persistence/couch/couchdb-compose.yaml
|
||||||
|
- run : sh src/plugins/persistence/couch/setup-couchdb.sh
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
- run: npx playwright@1.23.0 install
|
||||||
|
- run: npm install
|
||||||
|
- run: sh src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
|
||||||
|
- run: npm run test:e2e:couchdb
|
||||||
|
- run: ls -latr
|
||||||
|
- name: Archive test results
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
path: test-results
|
||||||
|
- name: Archive html test results
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
path: html-test-results
|
@ -30,18 +30,37 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This common function creates a `domainObject` with default options. It is the preferred way of creating objects
|
* Defines parameters to be used in the creation of a domain object.
|
||||||
* in the e2e suite when uninterested in properties of the objects themselves.
|
* @typedef {Object} CreateObjectOptions
|
||||||
* @param {import('@playwright/test').Page} page
|
* @property {string} type the type of domain object to create (e.g.: "Sine Wave Generator").
|
||||||
* @param {string} type
|
* @property {string} [name] the desired name of the created domain object.
|
||||||
* @param {string | undefined} name
|
* @property {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the Identifier or uuid of the parent object.
|
||||||
*/
|
*/
|
||||||
async function createDomainObjectWithDefaults(page, type, name) {
|
|
||||||
// Navigate to focus the 'My Items' folder, and hide the object tree
|
/**
|
||||||
// This is necessary so that subsequent objects can be created without a parent
|
* Contains information about the newly created domain object.
|
||||||
// TODO: Ideally this would navigate to a common `e2e` folder
|
* @typedef {Object} CreatedObjectInfo
|
||||||
await page.goto('./#/browse/mine?hideTree=true');
|
* @property {string} name the name of the created object
|
||||||
|
* @property {string} uuid the uuid of the created object
|
||||||
|
* @property {string} url the relative url to the object (for use with `page.goto()`)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This common function creates a domain object with the default options. It is the preferred way of creating objects
|
||||||
|
* in the e2e suite when uninterested in properties of the objects themselves.
|
||||||
|
*
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {CreateObjectOptions} options
|
||||||
|
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
|
||||||
|
*/
|
||||||
|
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) {
|
||||||
|
const parentUrl = await getHashUrlToDomainObject(page, parent);
|
||||||
|
|
||||||
|
// Navigate to the parent object. This is necessary to create the object
|
||||||
|
// in the correct location, such as a folder, layout, or plot.
|
||||||
|
await page.goto(`${parentUrl}?hideTree=true`);
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
//Click the Create button
|
//Click the Create button
|
||||||
await page.click('button:has-text("Create")');
|
await page.click('button:has-text("Create")');
|
||||||
|
|
||||||
@ -50,7 +69,7 @@ async function createDomainObjectWithDefaults(page, type, name) {
|
|||||||
|
|
||||||
// Modify the name input field of the domain object to accept 'name'
|
// Modify the name input field of the domain object to accept 'name'
|
||||||
if (name) {
|
if (name) {
|
||||||
const nameInput = page.locator('input[type="text"]').nth(2);
|
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
|
||||||
await nameInput.fill("");
|
await nameInput.fill("");
|
||||||
await nameInput.fill(name);
|
await nameInput.fill(name);
|
||||||
}
|
}
|
||||||
@ -63,30 +82,187 @@ async function createDomainObjectWithDefaults(page, type, name) {
|
|||||||
page.waitForSelector('.c-message-banner__message')
|
page.waitForSelector('.c-message-banner__message')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return name || `Unnamed ${type}`;
|
// Wait until the URL is updated
|
||||||
|
await page.waitForURL(`**/${parent}/*`);
|
||||||
|
const uuid = await getFocusedObjectUuid(page);
|
||||||
|
const objectUrl = await getHashUrlToDomainObject(page, uuid);
|
||||||
|
|
||||||
|
if (await _isInEditMode(page, uuid)) {
|
||||||
|
// Save (exit edit mode)
|
||||||
|
await page.locator('button[title="Save"]').click();
|
||||||
|
await page.locator('li[title="Save and Finish Editing"]').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: name || `Unnamed ${type}`,
|
||||||
|
uuid: uuid,
|
||||||
|
url: objectUrl
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the given `domainObject`'s context menu from the object tree.
|
* Open the given `domainObject`'s context menu from the object tree.
|
||||||
* Expands the 'My Items' folder if it is not already expanded.
|
* Expands the path to the object and scrolls to it if necessary.
|
||||||
|
*
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
* @param {string} myItemsFolderName the name of the "My Items" folder
|
* @param {string} url the url to the object
|
||||||
* @param {string} domainObjectName the display name of the `domainObject`
|
|
||||||
*/
|
*/
|
||||||
async function openObjectTreeContextMenu(page, myItemsFolderName, domainObjectName) {
|
async function openObjectTreeContextMenu(page, url) {
|
||||||
const myItemsFolder = page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3);
|
await page.goto(url);
|
||||||
const className = await myItemsFolder.getAttribute('class');
|
await page.click('button[title="Show selected item in tree"]');
|
||||||
if (!className.includes('c-disclosure-triangle--expanded')) {
|
await page.locator('.is-navigated-object').click({
|
||||||
await myItemsFolder.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.locator(`a:has-text("${domainObjectName}")`).click({
|
|
||||||
button: 'right'
|
button: 'right'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the UUID of the currently focused object by parsing the current URL
|
||||||
|
* and returning the last UUID in the path.
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @returns {Promise<string>} the uuid of the focused object
|
||||||
|
*/
|
||||||
|
async function getFocusedObjectUuid(page) {
|
||||||
|
const UUIDv4Regexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi;
|
||||||
|
const focusedObjectUuid = await page.evaluate((regexp) => {
|
||||||
|
return window.location.href.split('?')[0].match(regexp).at(-1);
|
||||||
|
}, UUIDv4Regexp);
|
||||||
|
|
||||||
|
return focusedObjectUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the hashUrl to the domainObject given its uuid.
|
||||||
|
* Useful for directly navigating to the given domainObject.
|
||||||
|
*
|
||||||
|
* URLs returned will be of the form `'./browse/#/mine/<uuid0>/<uuid1>/...'`
|
||||||
|
*
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {string} uuid the uuid of the object to get the url for
|
||||||
|
* @returns {Promise<string>} the url of the object
|
||||||
|
*/
|
||||||
|
async function getHashUrlToDomainObject(page, uuid) {
|
||||||
|
const hashUrl = await page.evaluate(async (objectUuid) => {
|
||||||
|
const path = await window.openmct.objects.getOriginalPath(objectUuid);
|
||||||
|
let url = './#/browse/' + [...path].reverse()
|
||||||
|
.map((object) => window.openmct.objects.makeKeyString(object.identifier))
|
||||||
|
.join('/');
|
||||||
|
|
||||||
|
// Drop the vestigial '/ROOT' if it exists
|
||||||
|
if (url.includes('/ROOT')) {
|
||||||
|
url = url.split('/ROOT').join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}, uuid);
|
||||||
|
|
||||||
|
return hashUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilizes the OpenMCT API to detect if the given object has an active transaction (is in Edit mode).
|
||||||
|
* @private
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} identifier
|
||||||
|
* @return {Promise<boolean>} true if the object has an active transaction, false otherwise
|
||||||
|
*/
|
||||||
|
async function _isInEditMode(page, identifier) {
|
||||||
|
// eslint-disable-next-line no-return-await
|
||||||
|
return await page.evaluate((objectIdentifier) => window.openmct.objects.isTransactionActive(objectIdentifier), identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} OffsetValues
|
||||||
|
* @property {string | undefined} hours
|
||||||
|
* @property {string | undefined} mins
|
||||||
|
* @property {string | undefined} secs
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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('.pr-time__buttons .icon-check').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createDomainObjectWithDefaults,
|
createDomainObjectWithDefaults,
|
||||||
openObjectTreeContextMenu
|
openObjectTreeContextMenu,
|
||||||
|
getHashUrlToDomainObject,
|
||||||
|
getFocusedObjectUuid,
|
||||||
|
setFixedTimeMode,
|
||||||
|
setRealTimeMode,
|
||||||
|
setStartOffset,
|
||||||
|
setEndOffset
|
||||||
};
|
};
|
||||||
|
28
e2e/helper/addInitExampleFaultProvider.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 should be used to install the Example Fault Provider, this will also install the FaultManagementPlugin (neither of which are installed by default).
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const openmct = window.openmct;
|
||||||
|
openmct.install(openmct.plugins.example.ExampleFaultSource());
|
||||||
|
});
|
30
e2e/helper/addInitExampleFaultProviderStatic.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 should be used to install the Example Fault Provider, this will also install the FaultManagementPlugin (neither of which are installed by default).
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const openmct = window.openmct;
|
||||||
|
const staticFaults = true;
|
||||||
|
|
||||||
|
openmct.install(openmct.plugins.example.ExampleFaultSource(staticFaults));
|
||||||
|
});
|
28
e2e/helper/addInitFaultManagementPlugin.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 should be used to install the Example Fault Provider, this will also install the FaultManagementPlugin (neither of which are installed by default).
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const openmct = window.openmct;
|
||||||
|
openmct.install(openmct.plugins.FaultManagement());
|
||||||
|
});
|
277
e2e/helper/faultUtils.js
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function navigateToFaultManagementWithExample(page) {
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
await page.addInitScript({ path: path.join(__dirname, './', 'addInitExampleFaultProvider.js') });
|
||||||
|
|
||||||
|
await navigateToFaultItemInTree(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function navigateToFaultManagementWithStaticExample(page) {
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
await page.addInitScript({ path: path.join(__dirname, './', 'addInitExampleFaultProviderStatic.js') });
|
||||||
|
|
||||||
|
await navigateToFaultItemInTree(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function navigateToFaultManagementWithoutExample(page) {
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
await page.addInitScript({ path: path.join(__dirname, './', 'addInitFaultManagementPlugin.js') });
|
||||||
|
|
||||||
|
await navigateToFaultItemInTree(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function navigateToFaultItemInTree(page) {
|
||||||
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
|
// Click text=Fault Management
|
||||||
|
await page.click('text=Fault Management'); // this verifies the plugin has been added
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function acknowledgeFault(page, rowNumber) {
|
||||||
|
await openFaultRowMenu(page, rowNumber);
|
||||||
|
await page.locator('.c-menu >> text="Acknowledge"').click();
|
||||||
|
// Click [aria-label="Save"]
|
||||||
|
await page.locator('[aria-label="Save"]').click();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function shelveMultipleFaults(page, ...nums) {
|
||||||
|
const selectRows = nums.map((num) => {
|
||||||
|
return selectFaultItem(page, num);
|
||||||
|
});
|
||||||
|
await Promise.all(selectRows);
|
||||||
|
|
||||||
|
await page.locator('button:has-text("Shelve")').click();
|
||||||
|
await page.locator('[aria-label="Save"]').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function acknowledgeMultipleFaults(page, ...nums) {
|
||||||
|
const selectRows = nums.map((num) => {
|
||||||
|
return selectFaultItem(page, num);
|
||||||
|
});
|
||||||
|
await Promise.all(selectRows);
|
||||||
|
|
||||||
|
await page.locator('button:has-text("Acknowledge")').click();
|
||||||
|
await page.locator('[aria-label="Save"]').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function shelveFault(page, rowNumber) {
|
||||||
|
await openFaultRowMenu(page, rowNumber);
|
||||||
|
await page.locator('.c-menu >> text="Shelve"').click();
|
||||||
|
// Click [aria-label="Save"]
|
||||||
|
await page.locator('[aria-label="Save"]').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function changeViewTo(page, view) {
|
||||||
|
await page.locator('.c-fault-mgmt__search-row select').first().selectOption(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function sortFaultsBy(page, sort) {
|
||||||
|
await page.locator('.c-fault-mgmt__list-header-sortButton select').selectOption(sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function enterSearchTerm(page, term) {
|
||||||
|
await page.locator('.c-fault-mgmt-search [aria-label="Search Input"]').fill(term);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function clearSearch(page) {
|
||||||
|
await enterSearchTerm(page, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function selectFaultItem(page, rowNumber) {
|
||||||
|
// eslint-disable-next-line playwright/no-force-option
|
||||||
|
await page.check(`.c-fault-mgmt-item > input >> nth=${rowNumber - 1}`, { force: true }); // this will not work without force true, saw this may be a pw bug
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function getHighestSeverity(page) {
|
||||||
|
const criticalCount = await page.locator('[title=CRITICAL]').count();
|
||||||
|
const warningCount = await page.locator('[title=WARNING]').count();
|
||||||
|
|
||||||
|
if (criticalCount > 0) {
|
||||||
|
return 'CRITICAL';
|
||||||
|
} else if (warningCount > 0) {
|
||||||
|
return 'WARNING';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'WATCH';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function getLowestSeverity(page) {
|
||||||
|
const warningCount = await page.locator('[title=WARNING]').count();
|
||||||
|
const watchCount = await page.locator('[title=WATCH]').count();
|
||||||
|
|
||||||
|
if (watchCount > 0) {
|
||||||
|
return 'WATCH';
|
||||||
|
} else if (warningCount > 0) {
|
||||||
|
return 'WARNING';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'CRITICAL';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function getFaultResultCount(page) {
|
||||||
|
const count = await page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').count();
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
function getFault(page, rowNumber) {
|
||||||
|
const fault = page.locator(`.c-faults-list-view-item-body > .c-fault-mgmt__list >> nth=${rowNumber - 1}`);
|
||||||
|
|
||||||
|
return fault;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
function getFaultByName(page, name) {
|
||||||
|
const fault = page.locator(`.c-fault-mgmt__list-faultname:has-text("${name}")`);
|
||||||
|
|
||||||
|
return fault;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function getFaultName(page, rowNumber) {
|
||||||
|
const faultName = await page.locator(`.c-fault-mgmt__list-faultname >> nth=${rowNumber - 1}`).textContent();
|
||||||
|
|
||||||
|
return faultName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function getFaultSeverity(page, rowNumber) {
|
||||||
|
const faultSeverity = await page.locator(`.c-faults-list-view-item-body .c-fault-mgmt__list-severity >> nth=${rowNumber - 1}`).getAttribute('title');
|
||||||
|
|
||||||
|
return faultSeverity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function getFaultNamespace(page, rowNumber) {
|
||||||
|
const faultNamespace = await page.locator(`.c-fault-mgmt__list-path >> nth=${rowNumber - 1}`).textContent();
|
||||||
|
|
||||||
|
return faultNamespace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function getFaultTriggerTime(page, rowNumber) {
|
||||||
|
const faultTriggerTime = await page.locator(`.c-fault-mgmt__list-trigTime >> nth=${rowNumber - 1} >> .c-fault-mgmt-item__value`).textContent();
|
||||||
|
|
||||||
|
return faultTriggerTime.toString().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function openFaultRowMenu(page, rowNumber) {
|
||||||
|
// select
|
||||||
|
await page.locator(`.c-fault-mgmt-item > .c-fault-mgmt__list-action-button >> nth=${rowNumber - 1}`).click();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
module.exports = {
|
||||||
|
navigateToFaultManagementWithExample,
|
||||||
|
navigateToFaultManagementWithStaticExample,
|
||||||
|
navigateToFaultManagementWithoutExample,
|
||||||
|
navigateToFaultItemInTree,
|
||||||
|
acknowledgeFault,
|
||||||
|
shelveMultipleFaults,
|
||||||
|
acknowledgeMultipleFaults,
|
||||||
|
shelveFault,
|
||||||
|
changeViewTo,
|
||||||
|
sortFaultsBy,
|
||||||
|
enterSearchTerm,
|
||||||
|
clearSearch,
|
||||||
|
selectFaultItem,
|
||||||
|
getHighestSeverity,
|
||||||
|
getLowestSeverity,
|
||||||
|
getFaultResultCount,
|
||||||
|
getFault,
|
||||||
|
getFaultByName,
|
||||||
|
getFaultName,
|
||||||
|
getFaultSeverity,
|
||||||
|
getFaultNamespace,
|
||||||
|
getFaultTriggerTime,
|
||||||
|
openFaultRowMenu
|
||||||
|
};
|
@ -23,19 +23,66 @@
|
|||||||
const { test, expect } = require('../../baseFixtures.js');
|
const { test, expect } = require('../../baseFixtures.js');
|
||||||
const { createDomainObjectWithDefaults } = require('../../appActions.js');
|
const { createDomainObjectWithDefaults } = require('../../appActions.js');
|
||||||
|
|
||||||
test.describe('appActions tests', () => {
|
test.describe('AppActions', () => {
|
||||||
test('createDomainObjectsWithDefaults can create multiple objects in a row', async ({ page }) => {
|
test('createDomainObjectsWithDefaults', async ({ page }) => {
|
||||||
await page.goto('./', { waitUntil: 'networkidle' });
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
await createDomainObjectWithDefaults(page, 'Timer', 'Timer Foo');
|
|
||||||
await createDomainObjectWithDefaults(page, 'Timer', 'Timer Bar');
|
|
||||||
await createDomainObjectWithDefaults(page, 'Timer', 'Timer Baz');
|
|
||||||
|
|
||||||
// Expand the tree
|
const e2eFolder = await createDomainObjectWithDefaults(page, {
|
||||||
await page.click('.c-disclosure-triangle');
|
type: 'Folder',
|
||||||
|
name: 'e2e folder'
|
||||||
|
});
|
||||||
|
|
||||||
// Verify the objects were created
|
await test.step('Create multiple flat objects in a row', async () => {
|
||||||
await expect(page.locator('a :text("Timer Foo")')).toBeVisible();
|
const timer1 = await createDomainObjectWithDefaults(page, {
|
||||||
await expect(page.locator('a :text("Timer Bar")')).toBeVisible();
|
type: 'Timer',
|
||||||
await expect(page.locator('a :text("Timer Baz")')).toBeVisible();
|
name: 'Timer Foo',
|
||||||
|
parent: e2eFolder.uuid
|
||||||
|
});
|
||||||
|
const timer2 = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Timer',
|
||||||
|
name: 'Timer Bar',
|
||||||
|
parent: e2eFolder.uuid
|
||||||
|
});
|
||||||
|
const timer3 = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Timer',
|
||||||
|
name: 'Timer Baz',
|
||||||
|
parent: e2eFolder.uuid
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(timer1.url, { waitUntil: 'networkidle' });
|
||||||
|
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Foo');
|
||||||
|
await page.goto(timer2.url, { waitUntil: 'networkidle' });
|
||||||
|
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Bar');
|
||||||
|
await page.goto(timer3.url, { waitUntil: 'networkidle' });
|
||||||
|
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Baz');
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Create multiple nested objects in a row', async () => {
|
||||||
|
const folder1 = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Folder',
|
||||||
|
name: 'Folder Foo',
|
||||||
|
parent: e2eFolder.uuid
|
||||||
|
});
|
||||||
|
const folder2 = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Folder',
|
||||||
|
name: 'Folder Bar',
|
||||||
|
parent: folder1.uuid
|
||||||
|
});
|
||||||
|
const folder3 = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Folder',
|
||||||
|
name: 'Folder Baz',
|
||||||
|
parent: folder2.uuid
|
||||||
|
});
|
||||||
|
await page.goto(folder1.url, { waitUntil: 'networkidle' });
|
||||||
|
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Foo');
|
||||||
|
await page.goto(folder2.url, { waitUntil: 'networkidle' });
|
||||||
|
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Bar');
|
||||||
|
await page.goto(folder3.url, { waitUntil: 'networkidle' });
|
||||||
|
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Baz');
|
||||||
|
|
||||||
|
expect(folder1.url).toBe(`${e2eFolder.url}/${folder1.uuid}`);
|
||||||
|
expect(folder2.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}`);
|
||||||
|
expect(folder3.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}/${folder3.uuid}`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -58,7 +58,7 @@ test.describe('Renaming Timer Object', () => {
|
|||||||
//Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
//Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||||
await page.goto('./', { waitUntil: 'networkidle' });
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
//We provide some helper functions in appActions like createDomainObjectWithDefaults. This example will create a Timer object
|
//We provide some helper functions in appActions like createDomainObjectWithDefaults. This example will create a Timer object
|
||||||
await createDomainObjectWithDefaults(page, 'Timer');
|
await createDomainObjectWithDefaults(page, { type: 'Timer' });
|
||||||
//Assert the object to be created and check it's name in the title
|
//Assert the object to be created and check it's name in the title
|
||||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer');
|
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer');
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ test.describe('Renaming Timer Object', () => {
|
|||||||
//Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
//Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||||
await page.goto('./', { waitUntil: 'networkidle' });
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
//We provide some helper functions in appActions like createDomainObjectWithDefaults. This example will create a Timer object
|
//We provide some helper functions in appActions like createDomainObjectWithDefaults. This example will create a Timer object
|
||||||
await createDomainObjectWithDefaults(page, 'Timer');
|
await createDomainObjectWithDefaults(page, { type: 'Timer' });
|
||||||
//Expect the object to be created and check it's name in the title
|
//Expect the object to be created and check it's name in the title
|
||||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer');
|
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer');
|
||||||
|
|
||||||
|
@ -31,29 +31,13 @@ TODO: Provide additional validation of object properties as it grows.
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const { createDomainObjectWithDefaults } = require('../../appActions.js');
|
||||||
const { test, expect } = require('../../pluginFixtures.js');
|
const { test, expect } = require('../../pluginFixtures.js');
|
||||||
|
|
||||||
test('Generate Visual Test Data @localStorage', async ({ page, context, openmctConfig }) => {
|
test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
|
||||||
const { myItemsFolderName } = openmctConfig;
|
|
||||||
|
|
||||||
//Go to baseURL
|
//Go to baseURL
|
||||||
await page.goto('./', { waitUntil: 'networkidle' });
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
|
const overlayPlot = await createDomainObjectWithDefaults(page, { type: 'Overlay Plot' });
|
||||||
await page.locator('button:has-text("Create")').click();
|
|
||||||
|
|
||||||
// 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')
|
|
||||||
]);
|
|
||||||
|
|
||||||
// save (exit edit mode)
|
|
||||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
|
||||||
await page.locator('text=Save and Finish Editing').click();
|
|
||||||
|
|
||||||
// click create button
|
// click create button
|
||||||
await page.locator('button:has-text("Create")').click();
|
await page.locator('button:has-text("Create")').click();
|
||||||
@ -67,16 +51,12 @@ test('Generate Visual Test Data @localStorage', async ({ page, context, openmctC
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
page.waitForNavigation(),
|
page.waitForNavigation(),
|
||||||
page.locator('text=OK').click(),
|
page.locator('text=OK').click(),
|
||||||
//Wait for Save Banner to appear1
|
//Wait for Save Banner to appear
|
||||||
page.waitForSelector('.c-message-banner__message')
|
page.waitForSelector('.c-message-banner__message')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// focus the overlay plot
|
// focus the overlay plot
|
||||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
await page.goto(overlayPlot.url);
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation(),
|
|
||||||
page.locator('text=Unnamed Overlay Plot').first().click()
|
|
||||||
]);
|
|
||||||
|
|
||||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
|
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
|
||||||
//Save localStorage for future test execution
|
//Save localStorage for future test execution
|
||||||
|
108
e2e/tests/functional/couchdb.e2e.spec.js
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 meant to be executed against a couchdb container. More doc to come
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { test, expect } = require('../../baseFixtures');
|
||||||
|
|
||||||
|
test.describe("CouchDB Status Indicator @couchdb", () => {
|
||||||
|
test.use({ failOnConsoleError: false });
|
||||||
|
//TODO BeforeAll Verify CouchDB Connectivity with APIContext
|
||||||
|
test('Shows green if connected', async ({ page }) => {
|
||||||
|
await page.route('**/openmct/mine', route => {
|
||||||
|
route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
//Go to baseURL
|
||||||
|
await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', { waitUntil: 'networkidle' });
|
||||||
|
await expect(page.locator('div:has-text("CouchDB is connected")').nth(3)).toBeVisible();
|
||||||
|
});
|
||||||
|
test('Shows red if not connected', async ({ page }) => {
|
||||||
|
await page.route('**/openmct/**', route => {
|
||||||
|
route.fulfill({
|
||||||
|
status: 503,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
//Go to baseURL
|
||||||
|
await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', { waitUntil: 'networkidle' });
|
||||||
|
await expect(page.locator('div:has-text("CouchDB is offline")').nth(3)).toBeVisible();
|
||||||
|
});
|
||||||
|
test('Shows unknown if it receives an unexpected response code', async ({ page }) => {
|
||||||
|
await page.route('**/openmct/mine', route => {
|
||||||
|
route.fulfill({
|
||||||
|
status: 418,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
//Go to baseURL
|
||||||
|
await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', { waitUntil: 'networkidle' });
|
||||||
|
await expect(page.locator('div:has-text("CouchDB connectivity unknown")').nth(3)).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("CouchDB initialization @couchdb", () => {
|
||||||
|
test.use({ failOnConsoleError: false });
|
||||||
|
test("'My Items' folder is created if it doesn't exist", async ({ page }) => {
|
||||||
|
// Store any relevant PUT requests that happen on the page
|
||||||
|
const createMineFolderRequests = [];
|
||||||
|
page.on('request', req => {
|
||||||
|
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||||
|
if (req.method() === 'PUT' && req.url().endsWith('openmct/mine')) {
|
||||||
|
createMineFolderRequests.push(req);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Override the first request to GET openmct/mine to return a 404
|
||||||
|
await page.route('**/openmct/mine', route => {
|
||||||
|
route.fulfill({
|
||||||
|
status: 404,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({})
|
||||||
|
});
|
||||||
|
}, { times: 1 });
|
||||||
|
|
||||||
|
// Go to baseURL
|
||||||
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
|
// Verify that error banner is displayed
|
||||||
|
const bannerMessage = await page.locator('.c-message-banner__message').innerText();
|
||||||
|
expect(bannerMessage).toEqual('Failed to retrieve object mine');
|
||||||
|
|
||||||
|
// Verify that a PUT request to create "My Items" folder was made
|
||||||
|
expect.poll(() => createMineFolderRequests.length, {
|
||||||
|
message: 'Verify that PUT request to create "mine" folder was made',
|
||||||
|
timeout: 1000
|
||||||
|
}).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
});
|
@ -35,7 +35,10 @@ test.describe('Example Event Generator CRUD Operations', () => {
|
|||||||
//Create a name for the object
|
//Create a name for the object
|
||||||
const newObjectName = 'Test Event Generator';
|
const newObjectName = 'Test Event Generator';
|
||||||
|
|
||||||
await createDomainObjectWithDefaults(page, 'Event Message Generator', newObjectName);
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Event Message Generator',
|
||||||
|
name: newObjectName
|
||||||
|
});
|
||||||
|
|
||||||
//Assertions against newly created object which define standard behavior
|
//Assertions against newly created object which define standard behavior
|
||||||
await expect(page.waitForURL(/.*&view=table/)).toBeTruthy();
|
await expect(page.waitForURL(/.*&view=table/)).toBeTruthy();
|
||||||
|
212
e2e/tests/functional/moveAndLinkObjects.e2e.spec.js
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
/*
|
||||||
|
This test suite is dedicated to tests which verify the basic operations surrounding moving & linking objects.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { test, expect } = require('../../pluginFixtures');
|
||||||
|
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||||
|
|
||||||
|
test.describe('Move & link item tests', () => {
|
||||||
|
test('Create a basic object and verify that it can be moved to another folder', async ({ page, openmctConfig }) => {
|
||||||
|
const { myItemsFolderName } = openmctConfig;
|
||||||
|
|
||||||
|
// Go to Open MCT
|
||||||
|
await page.goto('./');
|
||||||
|
|
||||||
|
const parentFolder = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Folder',
|
||||||
|
name: 'Parent Folder'
|
||||||
|
});
|
||||||
|
const childFolder = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Folder',
|
||||||
|
name: 'Child Folder',
|
||||||
|
parent: parentFolder.uuid
|
||||||
|
});
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Folder',
|
||||||
|
name: 'Grandchild Folder',
|
||||||
|
parent: childFolder.uuid
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attempt to move parent to its own grandparent
|
||||||
|
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||||
|
await page.locator('.c-disclosure-triangle >> nth=0').click();
|
||||||
|
|
||||||
|
await page.locator(`a:has-text("Parent Folder") >> nth=0`).click({
|
||||||
|
button: 'right'
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.locator('li.icon-move').click();
|
||||||
|
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=0').click();
|
||||||
|
await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
|
||||||
|
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||||
|
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=1').click();
|
||||||
|
await page.locator('form[name="mctForm"] >> text=Child Folder').click();
|
||||||
|
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||||
|
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=2').click();
|
||||||
|
await page.locator('form[name="mctForm"] >> text=Grandchild Folder').click();
|
||||||
|
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||||
|
await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
|
||||||
|
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||||
|
await page.locator('[aria-label="Cancel"]').click();
|
||||||
|
|
||||||
|
// Move Child Folder from Parent Folder to My Items
|
||||||
|
await page.locator('.c-disclosure-triangle >> nth=0').click();
|
||||||
|
await page.locator('.c-disclosure-triangle >> nth=1').click();
|
||||||
|
|
||||||
|
await page.locator(`a:has-text("Child Folder") >> nth=0`).click({
|
||||||
|
button: 'right'
|
||||||
|
});
|
||||||
|
await page.locator('li.icon-move').click();
|
||||||
|
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
|
||||||
|
|
||||||
|
await page.locator('text=OK').click();
|
||||||
|
|
||||||
|
// Expect that Child Folder is in My Items, the root folder
|
||||||
|
expect(page.locator(`text=${myItemsFolderName} >> nth=0:has(text=Child Folder)`)).toBeTruthy();
|
||||||
|
});
|
||||||
|
test('Create a basic object and verify that it cannot be moved to telemetry object without Composition Provider', async ({ page, openmctConfig }) => {
|
||||||
|
const { myItemsFolderName } = openmctConfig;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
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=${myItemsFolderName}`).click();
|
||||||
|
await page.locator('text=OK').click();
|
||||||
|
|
||||||
|
// Open My Items
|
||||||
|
await page.locator(`text=Open MCT ${myItemsFolderName} >> 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 ${myItemsFolderName} >> 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('Create a basic object and verify that it can be linked to another folder', async ({ page, openmctConfig }) => {
|
||||||
|
const { myItemsFolderName } = openmctConfig;
|
||||||
|
|
||||||
|
// Go to Open MCT
|
||||||
|
await page.goto('./');
|
||||||
|
|
||||||
|
const parentFolder = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Folder',
|
||||||
|
name: 'Parent Folder'
|
||||||
|
});
|
||||||
|
const childFolder = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Folder',
|
||||||
|
name: 'Child Folder',
|
||||||
|
parent: parentFolder.uuid
|
||||||
|
});
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Folder',
|
||||||
|
name: 'Grandchild Folder',
|
||||||
|
parent: childFolder.uuid
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attempt to link parent to its own grandparent
|
||||||
|
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||||
|
await page.locator('.c-disclosure-triangle >> nth=0').click();
|
||||||
|
|
||||||
|
await page.locator(`a:has-text("Parent Folder") >> nth=0`).click({
|
||||||
|
button: 'right'
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.locator('li.icon-link').click();
|
||||||
|
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=0').click();
|
||||||
|
await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
|
||||||
|
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||||
|
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=1').click();
|
||||||
|
await page.locator('form[name="mctForm"] >> text=Child Folder').click();
|
||||||
|
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||||
|
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=2').click();
|
||||||
|
await page.locator('form[name="mctForm"] >> text=Grandchild Folder').click();
|
||||||
|
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||||
|
await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
|
||||||
|
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||||
|
await page.locator('[aria-label="Cancel"]').click();
|
||||||
|
|
||||||
|
// Link Child Folder from Parent Folder to My Items
|
||||||
|
await page.locator('.c-disclosure-triangle >> nth=0').click();
|
||||||
|
await page.locator('.c-disclosure-triangle >> nth=1').click();
|
||||||
|
|
||||||
|
await page.locator(`a:has-text("Child Folder") >> nth=0`).click({
|
||||||
|
button: 'right'
|
||||||
|
});
|
||||||
|
await page.locator('li.icon-link').click();
|
||||||
|
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
|
||||||
|
|
||||||
|
await page.locator('text=OK').click();
|
||||||
|
|
||||||
|
// Expect that Child Folder is in My Items, the root folder
|
||||||
|
expect(page.locator(`text=${myItemsFolderName} >> nth=0:has(text=Child Folder)`)).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.fixme('Cannot move a previously created domain object to non-peristable object in Move Modal', async ({ page }) => {
|
||||||
|
//Create a domain object
|
||||||
|
//Save Domain object
|
||||||
|
//Move Object and verify that cannot select non-persistable object
|
||||||
|
//Move Object to My Items
|
||||||
|
//Verify successful move
|
||||||
|
});
|
@ -1,148 +0,0 @@
|
|||||||
/*****************************************************************************
|
|
||||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
|
||||||
* as represented by the Administrator of the National Aeronautics and Space
|
|
||||||
* Administration. All rights reserved.
|
|
||||||
*
|
|
||||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*
|
|
||||||
* Open MCT includes source code licensed under additional open source
|
|
||||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
||||||
* this source code distribution or the Licensing information page available
|
|
||||||
* at runtime from the About dialog for additional information.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
/*
|
|
||||||
This test suite is dedicated to tests which verify the basic operations surrounding moving objects.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const { test, expect } = require('../../pluginFixtures');
|
|
||||||
|
|
||||||
test.describe('Move item tests', () => {
|
|
||||||
test('Create a basic object and verify that it can be moved to another folder', async ({ page, openmctConfig }) => {
|
|
||||||
const { myItemsFolderName } = openmctConfig;
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
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 ${myItemsFolderName} >> 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=${myItemsFolderName}`).click();
|
|
||||||
|
|
||||||
await page.locator('text=OK').click();
|
|
||||||
|
|
||||||
// Expect that Folder 2 is in My Items, the root folder
|
|
||||||
expect(page.locator(`text=${myItemsFolderName} >> nth=0:has(text=${folder2})`)).toBeTruthy();
|
|
||||||
});
|
|
||||||
test('Create a basic object and verify that it cannot be moved to telemetry object without Composition Provider', async ({ page, openmctConfig }) => {
|
|
||||||
const { myItemsFolderName } = openmctConfig;
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
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=${myItemsFolderName}`).click();
|
|
||||||
await page.locator('text=OK').click();
|
|
||||||
|
|
||||||
// Open My Items
|
|
||||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> 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 ${myItemsFolderName} >> 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('Cannot move a previously created domain object to non-peristable object in Move Modal', async ({ page }) => {
|
|
||||||
//Create a domain object
|
|
||||||
//Save Domain object
|
|
||||||
//Move Object and verify that cannot select non-persistable object
|
|
||||||
//Move Object to My Items
|
|
||||||
//Verify successful move
|
|
||||||
});
|
|
@ -27,6 +27,7 @@ demonstrate some playwright for test developers. This pattern should not be re-u
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const { test, expect } = require('../../../../pluginFixtures.js');
|
const { test, expect } = require('../../../../pluginFixtures.js');
|
||||||
|
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||||
|
|
||||||
let conditionSetUrl;
|
let conditionSetUrl;
|
||||||
let getConditionSetIdentifierFromUrl;
|
let getConditionSetIdentifierFromUrl;
|
||||||
@ -178,3 +179,24 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('Basic Condition Set Use', () => {
|
||||||
|
test('Can add a condition', async ({ page }) => {
|
||||||
|
//Navigate to baseURL
|
||||||
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
|
// Create a new condition set
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Condition Set',
|
||||||
|
name: "Test Condition Set"
|
||||||
|
});
|
||||||
|
// Change the object to edit mode
|
||||||
|
await page.locator('[title="Edit"]').click();
|
||||||
|
|
||||||
|
// Click Add Condition button
|
||||||
|
await page.locator('#addCondition').click();
|
||||||
|
// Check that the new Unnamed Condition section appears
|
||||||
|
const numOfUnnamedConditions = await page.locator('text=Unnamed Condition').count();
|
||||||
|
expect(numOfUnnamedConditions).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -0,0 +1,186 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
const { test, expect } = require('../../../../pluginFixtures');
|
||||||
|
const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions');
|
||||||
|
|
||||||
|
test.describe('Testing Display Layout @unstable', () => {
|
||||||
|
let sineWaveObject;
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
|
await setRealTimeMode(page);
|
||||||
|
|
||||||
|
// Create Sine Wave Generator
|
||||||
|
sineWaveObject = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Sine Wave Generator',
|
||||||
|
name: "Test Sine Wave Generator"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in real time', async ({ page }) => {
|
||||||
|
// Create a Display Layout
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Display Layout',
|
||||||
|
name: "Test Display Layout"
|
||||||
|
});
|
||||||
|
// Edit Display Layout
|
||||||
|
await page.locator('[title="Edit"]').click();
|
||||||
|
|
||||||
|
// Expand the 'My Items' folder in the left tree
|
||||||
|
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||||
|
// Add the Sine Wave Generator to the Display Layout and save changes
|
||||||
|
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
|
||||||
|
await page.locator('button[title="Save"]').click();
|
||||||
|
await page.locator('text=Save and Finish Editing').click();
|
||||||
|
|
||||||
|
// Subscribe to the Sine Wave Generator data
|
||||||
|
// On getting data, check if the value found in the Display Layout is the most recent value
|
||||||
|
// from the Sine Wave Generator
|
||||||
|
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
|
||||||
|
const formattedTelemetryValue = await getTelemValuePromise;
|
||||||
|
const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`);
|
||||||
|
const displayLayoutValue = await displayLayoutValuePromise.textContent();
|
||||||
|
const trimmedDisplayValue = displayLayoutValue.trim();
|
||||||
|
|
||||||
|
await expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
|
||||||
|
});
|
||||||
|
test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in fixed time', async ({ page }) => {
|
||||||
|
// Create a Display Layout
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Display Layout',
|
||||||
|
name: "Test Display Layout"
|
||||||
|
});
|
||||||
|
// Edit Display Layout
|
||||||
|
await page.locator('[title="Edit"]').click();
|
||||||
|
|
||||||
|
// Expand the 'My Items' folder in the left tree
|
||||||
|
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||||
|
// Add the Sine Wave Generator to the Display Layout and save changes
|
||||||
|
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
|
||||||
|
await page.locator('button[title="Save"]').click();
|
||||||
|
await page.locator('text=Save and Finish Editing').click();
|
||||||
|
|
||||||
|
// Subscribe to the Sine Wave Generator data
|
||||||
|
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
|
||||||
|
// Set an offset of 1 minute and then change the time mode to fixed to set a 1 minute historical window
|
||||||
|
await setStartOffset(page, { mins: '1' });
|
||||||
|
await setFixedTimeMode(page);
|
||||||
|
|
||||||
|
// On getting data, check if the value found in the Display Layout is the most recent value
|
||||||
|
// from the Sine Wave Generator
|
||||||
|
const formattedTelemetryValue = await getTelemValuePromise;
|
||||||
|
const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`);
|
||||||
|
const displayLayoutValue = await displayLayoutValuePromise.textContent();
|
||||||
|
const trimmedDisplayValue = displayLayoutValue.trim();
|
||||||
|
|
||||||
|
await expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
|
||||||
|
});
|
||||||
|
test('items in a display layout can be removed with object tree context menu when viewing the display layout', async ({ page }) => {
|
||||||
|
// Create a Display Layout
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Display Layout',
|
||||||
|
name: "Test Display Layout"
|
||||||
|
});
|
||||||
|
// Edit Display Layout
|
||||||
|
await page.locator('[title="Edit"]').click();
|
||||||
|
|
||||||
|
// Expand the 'My Items' folder in the left tree
|
||||||
|
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||||
|
// Add the Sine Wave Generator to the Display Layout and save changes
|
||||||
|
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
|
||||||
|
await page.locator('button[title="Save"]').click();
|
||||||
|
await page.locator('text=Save and Finish Editing').click();
|
||||||
|
|
||||||
|
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);
|
||||||
|
|
||||||
|
// Expand the Display Layout so we can remove the sine wave generator
|
||||||
|
await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
|
||||||
|
|
||||||
|
// Bring up context menu and remove
|
||||||
|
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').first().click({ button: 'right' });
|
||||||
|
await page.locator('text=Remove').click();
|
||||||
|
await page.locator('text=OK').click();
|
||||||
|
|
||||||
|
// delete
|
||||||
|
|
||||||
|
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
|
||||||
|
});
|
||||||
|
test('items in a display layout can be removed with object tree context menu when viewing another item', async ({ page }) => {
|
||||||
|
// Create a Display Layout
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Display Layout',
|
||||||
|
name: "Test Display Layout"
|
||||||
|
});
|
||||||
|
// Edit Display Layout
|
||||||
|
await page.locator('[title="Edit"]').click();
|
||||||
|
|
||||||
|
// Expand the 'My Items' folder in the left tree
|
||||||
|
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||||
|
// Add the Sine Wave Generator to the Display Layout and save changes
|
||||||
|
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
|
||||||
|
await page.locator('button[title="Save"]').click();
|
||||||
|
await page.locator('text=Save and Finish Editing').click();
|
||||||
|
|
||||||
|
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);
|
||||||
|
|
||||||
|
// Expand the Display Layout so we can remove the sine wave generator
|
||||||
|
await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
|
||||||
|
|
||||||
|
// Click the original Sine Wave Generator to navigate away from the Display Layout
|
||||||
|
await page.locator('.c-tree__item .c-tree__item__name:text("Test Sine Wave Generator")').click();
|
||||||
|
|
||||||
|
// Bring up context menu and remove
|
||||||
|
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').click({ button: 'right' });
|
||||||
|
await page.locator('text=Remove').click();
|
||||||
|
await page.locator('text=OK').click();
|
||||||
|
|
||||||
|
// navigate back to the display layout to confirm it has been removed
|
||||||
|
await page.locator('.c-tree__item .c-tree__item__name:text("Test Display Layout")').click();
|
||||||
|
|
||||||
|
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Util for subscribing to a telemetry object by object identifier
|
||||||
|
* Limitations: Currently only works to return telemetry once to the node scope
|
||||||
|
* To Do: See if there's a way to await this multiple times to allow for multiple
|
||||||
|
* values to be returned over time
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {string} objectIdentifier identifier for object
|
||||||
|
* @returns {Promise<string>} the formatted sin telemetry value
|
||||||
|
*/
|
||||||
|
async function subscribeToTelemetry(page, objectIdentifier) {
|
||||||
|
const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getTelemValue', resolve));
|
||||||
|
|
||||||
|
await page.evaluate(async (telemetryIdentifier) => {
|
||||||
|
const telemetryObject = await window.openmct.objects.get(telemetryIdentifier);
|
||||||
|
const metadata = window.openmct.telemetry.getMetadata(telemetryObject);
|
||||||
|
const formats = await window.openmct.telemetry.getFormatMap(metadata);
|
||||||
|
window.openmct.telemetry.subscribe(telemetryObject, (obj) => {
|
||||||
|
const sinVal = obj.sin;
|
||||||
|
const formattedSinVal = formats.sin.format(sinVal);
|
||||||
|
window.getTelemValue(formattedSinVal);
|
||||||
|
});
|
||||||
|
}, objectIdentifier);
|
||||||
|
|
||||||
|
return getTelemValuePromise;
|
||||||
|
}
|
@ -0,0 +1,237 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
const { test, expect } = require('../../../../pluginFixtures');
|
||||||
|
const utils = require('../../../../helper/faultUtils');
|
||||||
|
|
||||||
|
test.describe('The Fault Management Plugin using example faults', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await utils.navigateToFaultManagementWithExample(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Shows a criticality icon for every fault', async ({ page }) => {
|
||||||
|
const faultCount = await page.locator('c-fault-mgmt__list').count();
|
||||||
|
const criticalityIconCount = await page.locator('c-fault-mgmt__list-severity').count();
|
||||||
|
|
||||||
|
expect.soft(faultCount).toEqual(criticalityIconCount);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('When selecting a fault, it has an "is-selected" class and it\'s information shows in the inspector', async ({ page }) => {
|
||||||
|
await utils.selectFaultItem(page, 1);
|
||||||
|
|
||||||
|
const selectedFaultName = await page.locator('.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname').textContent();
|
||||||
|
const inspectorFaultNameCount = await page.locator(`.c-inspector__properties >> :text("${selectedFaultName}")`).count();
|
||||||
|
|
||||||
|
await expect.soft(page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').first()).toHaveClass(/is-selected/);
|
||||||
|
expect.soft(inspectorFaultNameCount).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('When selecting multiple faults, no specific fault information is shown in the inspector', async ({ page }) => {
|
||||||
|
await utils.selectFaultItem(page, 1);
|
||||||
|
await utils.selectFaultItem(page, 2);
|
||||||
|
|
||||||
|
const selectedRows = page.locator('.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname');
|
||||||
|
expect.soft(await selectedRows.count()).toEqual(2);
|
||||||
|
|
||||||
|
const firstSelectedFaultName = await selectedRows.nth(0).textContent();
|
||||||
|
const secondSelectedFaultName = await selectedRows.nth(1).textContent();
|
||||||
|
const firstNameInInspectorCount = await page.locator(`.c-inspector__properties >> :text("${firstSelectedFaultName}")`).count();
|
||||||
|
const secondNameInInspectorCount = await page.locator(`.c-inspector__properties >> :text("${secondSelectedFaultName}")`).count();
|
||||||
|
|
||||||
|
expect.soft(firstNameInInspectorCount).toEqual(0);
|
||||||
|
expect.soft(secondNameInInspectorCount).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Allows you to shelve a fault', async ({ page }) => {
|
||||||
|
const shelvedFaultName = await utils.getFaultName(page, 2);
|
||||||
|
const beforeShelvedFault = utils.getFaultByName(page, shelvedFaultName);
|
||||||
|
|
||||||
|
expect.soft(await beforeShelvedFault.count()).toBe(1);
|
||||||
|
|
||||||
|
await utils.shelveFault(page, 2);
|
||||||
|
|
||||||
|
// check it is removed from standard view
|
||||||
|
const afterShelvedFault = utils.getFaultByName(page, shelvedFaultName);
|
||||||
|
expect.soft(await afterShelvedFault.count()).toBe(0);
|
||||||
|
|
||||||
|
await utils.changeViewTo(page, 'shelved');
|
||||||
|
|
||||||
|
const shelvedViewFault = utils.getFaultByName(page, shelvedFaultName);
|
||||||
|
|
||||||
|
expect.soft(await shelvedViewFault.count()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Allows you to acknowledge a fault', async ({ page }) => {
|
||||||
|
const acknowledgedFaultName = await utils.getFaultName(page, 3);
|
||||||
|
|
||||||
|
await utils.acknowledgeFault(page, 3);
|
||||||
|
|
||||||
|
const fault = utils.getFault(page, 3);
|
||||||
|
await expect.soft(fault).toHaveClass(/is-acknowledged/);
|
||||||
|
|
||||||
|
await utils.changeViewTo(page, 'acknowledged');
|
||||||
|
|
||||||
|
const acknowledgedViewFaultName = await utils.getFaultName(page, 1);
|
||||||
|
expect.soft(acknowledgedFaultName).toEqual(acknowledgedViewFaultName);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Allows you to shelve multiple faults', async ({ page }) => {
|
||||||
|
const shelvedFaultNameOne = await utils.getFaultName(page, 1);
|
||||||
|
const shelvedFaultNameFour = await utils.getFaultName(page, 4);
|
||||||
|
|
||||||
|
const beforeShelvedFaultOne = utils.getFaultByName(page, shelvedFaultNameOne);
|
||||||
|
const beforeShelvedFaultFour = utils.getFaultByName(page, shelvedFaultNameFour);
|
||||||
|
|
||||||
|
expect.soft(await beforeShelvedFaultOne.count()).toBe(1);
|
||||||
|
expect.soft(await beforeShelvedFaultFour.count()).toBe(1);
|
||||||
|
|
||||||
|
await utils.shelveMultipleFaults(page, 1, 4);
|
||||||
|
|
||||||
|
// check it is removed from standard view
|
||||||
|
const afterShelvedFaultOne = utils.getFaultByName(page, shelvedFaultNameOne);
|
||||||
|
const afterShelvedFaultFour = utils.getFaultByName(page, shelvedFaultNameFour);
|
||||||
|
expect.soft(await afterShelvedFaultOne.count()).toBe(0);
|
||||||
|
expect.soft(await afterShelvedFaultFour.count()).toBe(0);
|
||||||
|
|
||||||
|
await utils.changeViewTo(page, 'shelved');
|
||||||
|
|
||||||
|
const shelvedViewFaultOne = utils.getFaultByName(page, shelvedFaultNameOne);
|
||||||
|
const shelvedViewFaultFour = utils.getFaultByName(page, shelvedFaultNameFour);
|
||||||
|
|
||||||
|
expect.soft(await shelvedViewFaultOne.count()).toBe(1);
|
||||||
|
expect.soft(await shelvedViewFaultFour.count()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Allows you to acknowledge multiple faults', async ({ page }) => {
|
||||||
|
const acknowledgedFaultNameTwo = await utils.getFaultName(page, 2);
|
||||||
|
const acknowledgedFaultNameFive = await utils.getFaultName(page, 5);
|
||||||
|
|
||||||
|
await utils.acknowledgeMultipleFaults(page, 2, 5);
|
||||||
|
|
||||||
|
const faultTwo = utils.getFault(page, 2);
|
||||||
|
const faultFive = utils.getFault(page, 5);
|
||||||
|
|
||||||
|
// check they have been acknowledged
|
||||||
|
await expect.soft(faultTwo).toHaveClass(/is-acknowledged/);
|
||||||
|
await expect.soft(faultFive).toHaveClass(/is-acknowledged/);
|
||||||
|
|
||||||
|
await utils.changeViewTo(page, 'acknowledged');
|
||||||
|
|
||||||
|
const acknowledgedViewFaultTwo = utils.getFaultByName(page, acknowledgedFaultNameTwo);
|
||||||
|
const acknowledgedViewFaultFive = utils.getFaultByName(page, acknowledgedFaultNameFive);
|
||||||
|
|
||||||
|
expect.soft(await acknowledgedViewFaultTwo.count()).toBe(1);
|
||||||
|
expect.soft(await acknowledgedViewFaultFive.count()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Allows you to search faults', async ({ page }) => {
|
||||||
|
const faultThreeNamespace = await utils.getFaultNamespace(page, 3);
|
||||||
|
const faultTwoName = await utils.getFaultName(page, 2);
|
||||||
|
const faultFiveTriggerTime = await utils.getFaultTriggerTime(page, 5);
|
||||||
|
|
||||||
|
// should be all faults (5)
|
||||||
|
let faultResultCount = await utils.getFaultResultCount(page);
|
||||||
|
expect.soft(faultResultCount).toEqual(5);
|
||||||
|
|
||||||
|
// search namespace
|
||||||
|
await utils.enterSearchTerm(page, faultThreeNamespace);
|
||||||
|
|
||||||
|
faultResultCount = await utils.getFaultResultCount(page);
|
||||||
|
expect.soft(faultResultCount).toEqual(1);
|
||||||
|
expect.soft(await utils.getFaultNamespace(page, 1)).toEqual(faultThreeNamespace);
|
||||||
|
|
||||||
|
// all faults
|
||||||
|
await utils.clearSearch(page);
|
||||||
|
faultResultCount = await utils.getFaultResultCount(page);
|
||||||
|
expect.soft(faultResultCount).toEqual(5);
|
||||||
|
|
||||||
|
// search name
|
||||||
|
await utils.enterSearchTerm(page, faultTwoName);
|
||||||
|
|
||||||
|
faultResultCount = await utils.getFaultResultCount(page);
|
||||||
|
expect.soft(faultResultCount).toEqual(1);
|
||||||
|
expect.soft(await utils.getFaultName(page, 1)).toEqual(faultTwoName);
|
||||||
|
|
||||||
|
// all faults
|
||||||
|
await utils.clearSearch(page);
|
||||||
|
faultResultCount = await utils.getFaultResultCount(page);
|
||||||
|
expect.soft(faultResultCount).toEqual(5);
|
||||||
|
|
||||||
|
// search triggerTime
|
||||||
|
await utils.enterSearchTerm(page, faultFiveTriggerTime);
|
||||||
|
|
||||||
|
faultResultCount = await utils.getFaultResultCount(page);
|
||||||
|
expect.soft(faultResultCount).toEqual(1);
|
||||||
|
expect.soft(await utils.getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Allows you to sort faults', async ({ page }) => {
|
||||||
|
const highestSeverity = await utils.getHighestSeverity(page);
|
||||||
|
const lowestSeverity = await utils.getLowestSeverity(page);
|
||||||
|
const faultOneName = 'Example Fault 1';
|
||||||
|
const faultFiveName = 'Example Fault 5';
|
||||||
|
let firstFaultName = await utils.getFaultName(page, 1);
|
||||||
|
|
||||||
|
expect.soft(firstFaultName).toEqual(faultOneName);
|
||||||
|
|
||||||
|
await utils.sortFaultsBy(page, 'oldest-first');
|
||||||
|
|
||||||
|
firstFaultName = await utils.getFaultName(page, 1);
|
||||||
|
expect.soft(firstFaultName).toEqual(faultFiveName);
|
||||||
|
|
||||||
|
await utils.sortFaultsBy(page, 'severity');
|
||||||
|
|
||||||
|
const sortedHighestSeverity = await utils.getFaultSeverity(page, 1);
|
||||||
|
const sortedLowestSeverity = await utils.getFaultSeverity(page, 5);
|
||||||
|
expect.soft(sortedHighestSeverity).toEqual(highestSeverity);
|
||||||
|
expect.soft(sortedLowestSeverity).toEqual(lowestSeverity);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('The Fault Management Plugin without using example faults', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await utils.navigateToFaultManagementWithoutExample(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Shows no faults when no faults are provided', async ({ page }) => {
|
||||||
|
const faultCount = await page.locator('c-fault-mgmt__list').count();
|
||||||
|
|
||||||
|
expect.soft(faultCount).toEqual(0);
|
||||||
|
|
||||||
|
await utils.changeViewTo(page, 'acknowledged');
|
||||||
|
const acknowledgedCount = await page.locator('c-fault-mgmt__list').count();
|
||||||
|
expect.soft(acknowledgedCount).toEqual(0);
|
||||||
|
|
||||||
|
await utils.changeViewTo(page, 'shelved');
|
||||||
|
const shelvedCount = await page.locator('c-fault-mgmt__list').count();
|
||||||
|
expect.soft(shelvedCount).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Will return no faults when searching', async ({ page }) => {
|
||||||
|
await utils.enterSearchTerm(page, 'fault');
|
||||||
|
|
||||||
|
const faultCount = await page.locator('c-fault-mgmt__list').count();
|
||||||
|
|
||||||
|
expect.soft(faultCount).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
@ -25,7 +25,7 @@ This test suite is dedicated to tests which verify the basic operations surround
|
|||||||
but only assume that example imagery is present.
|
but only assume that example imagery is present.
|
||||||
*/
|
*/
|
||||||
/* globals process */
|
/* globals process */
|
||||||
|
const { v4: uuid } = require('uuid');
|
||||||
const { waitForAnimations } = require('../../../../baseFixtures');
|
const { waitForAnimations } = require('../../../../baseFixtures');
|
||||||
const { test, expect } = require('../../../../pluginFixtures');
|
const { test, expect } = require('../../../../pluginFixtures');
|
||||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||||
@ -41,7 +41,7 @@ test.describe('Example Imagery Object', () => {
|
|||||||
await page.goto('./', { waitUntil: 'networkidle' });
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
// Create a default 'Example Imagery' object
|
// Create a default 'Example Imagery' object
|
||||||
createDomainObjectWithDefaults(page, 'Example Imagery');
|
createDomainObjectWithDefaults(page, { type: 'Example Imagery' });
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
page.waitForNavigation(),
|
page.waitForNavigation(),
|
||||||
@ -573,6 +573,40 @@ test.describe('Example Imagery in Tabs view', () => {
|
|||||||
test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
|
test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('Example Imagery in Time Strip', () => {
|
||||||
|
test('ensure that clicking a thumbnail loads the image in large view', async ({ page, browserName }) => {
|
||||||
|
test.info().annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/nasa/openmct/issues/5632'
|
||||||
|
});
|
||||||
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
|
const timeStripObject = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Time Strip',
|
||||||
|
name: 'Time Strip'.concat(' ', uuid())
|
||||||
|
});
|
||||||
|
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Example Imagery',
|
||||||
|
name: 'Example Imagery'.concat(' ', uuid()),
|
||||||
|
parent: timeStripObject.uuid
|
||||||
|
});
|
||||||
|
// Navigate to timestrip
|
||||||
|
await page.goto(timeStripObject.url);
|
||||||
|
|
||||||
|
await page.locator('.c-imagery-tsv-container').hover();
|
||||||
|
// get url of the hovered image
|
||||||
|
const hoveredImg = page.locator('.c-imagery-tsv div.c-imagery-tsv__image-wrapper:hover img');
|
||||||
|
const hoveredImgSrc = await hoveredImg.getAttribute('src');
|
||||||
|
expect(hoveredImgSrc).toBeTruthy();
|
||||||
|
await page.locator('.c-imagery-tsv-container').click();
|
||||||
|
// get image of view large container
|
||||||
|
const viewLargeImg = page.locator('img.c-imagery__main-image__image');
|
||||||
|
const viewLargeImgSrc = await viewLargeImg.getAttribute('src');
|
||||||
|
expect(viewLargeImgSrc).toBeTruthy();
|
||||||
|
expect(viewLargeImgSrc).toEqual(hoveredImgSrc);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
*/
|
*/
|
||||||
|
120
e2e/tests/functional/plugins/lad/lad.e2e.spec.js
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
const { test, expect } = require('../../../../pluginFixtures');
|
||||||
|
const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions');
|
||||||
|
|
||||||
|
test.describe('Testing LAD table @unstable', () => {
|
||||||
|
let sineWaveObject;
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
|
await setRealTimeMode(page);
|
||||||
|
|
||||||
|
// Create Sine Wave Generator
|
||||||
|
sineWaveObject = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Sine Wave Generator',
|
||||||
|
name: "Test Sine Wave Generator"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('telemetry value exactly matches latest telemetry value received in real time', async ({ page }) => {
|
||||||
|
// Create LAD table
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'LAD Table',
|
||||||
|
name: "Test LAD Table"
|
||||||
|
});
|
||||||
|
// Edit LAD table
|
||||||
|
await page.locator('[title="Edit"]').click();
|
||||||
|
|
||||||
|
// Expand the 'My Items' folder in the left tree
|
||||||
|
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||||
|
// Add the Sine Wave Generator to the LAD table and save changes
|
||||||
|
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-lad-table-wrapper');
|
||||||
|
await page.locator('button[title="Save"]').click();
|
||||||
|
await page.locator('text=Save and Finish Editing').click();
|
||||||
|
|
||||||
|
// Subscribe to the Sine Wave Generator data
|
||||||
|
// On getting data, check if the value found in the LAD table is the most recent value
|
||||||
|
// from the Sine Wave Generator
|
||||||
|
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
|
||||||
|
const subscribeTelemValue = await getTelemValuePromise;
|
||||||
|
const ladTableValuePromise = await page.waitForSelector(`text="${subscribeTelemValue}"`);
|
||||||
|
const ladTableValue = await ladTableValuePromise.textContent();
|
||||||
|
|
||||||
|
expect(ladTableValue).toBe(subscribeTelemValue);
|
||||||
|
});
|
||||||
|
test('telemetry value exactly matches latest telemetry value received in fixed time', async ({ page }) => {
|
||||||
|
// Create LAD table
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'LAD Table',
|
||||||
|
name: "Test LAD Table"
|
||||||
|
});
|
||||||
|
// Edit LAD table
|
||||||
|
await page.locator('[title="Edit"]').click();
|
||||||
|
|
||||||
|
// Expand the 'My Items' folder in the left tree
|
||||||
|
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||||
|
// Add the Sine Wave Generator to the LAD table and save changes
|
||||||
|
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-lad-table-wrapper');
|
||||||
|
await page.locator('button[title="Save"]').click();
|
||||||
|
await page.locator('text=Save and Finish Editing').click();
|
||||||
|
|
||||||
|
// Subscribe to the Sine Wave Generator data
|
||||||
|
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
|
||||||
|
// Set an offset of 1 minute and then change the time mode to fixed to set a 1 minute historical window
|
||||||
|
await setStartOffset(page, { mins: '1' });
|
||||||
|
await setFixedTimeMode(page);
|
||||||
|
|
||||||
|
// On getting data, check if the value found in the LAD table is the most recent value
|
||||||
|
// from the Sine Wave Generator
|
||||||
|
const subscribeTelemValue = await getTelemValuePromise;
|
||||||
|
const ladTableValuePromise = await page.waitForSelector(`text="${subscribeTelemValue}"`);
|
||||||
|
const ladTableValue = await ladTableValuePromise.textContent();
|
||||||
|
|
||||||
|
expect(ladTableValue).toBe(subscribeTelemValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Util for subscribing to a telemetry object by object identifier
|
||||||
|
* Limitations: Currently only works to return telemetry once to the node scope
|
||||||
|
* To Do: See if there's a way to await this multiple times to allow for multiple
|
||||||
|
* values to be returned over time
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {string} objectIdentifier identifier for object
|
||||||
|
* @returns {Promise<string>} the formatted sin telemetry value
|
||||||
|
*/
|
||||||
|
async function subscribeToTelemetry(page, objectIdentifier) {
|
||||||
|
const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getTelemValue', resolve));
|
||||||
|
|
||||||
|
await page.evaluate(async (telemetryIdentifier) => {
|
||||||
|
const telemetryObject = await window.openmct.objects.get(telemetryIdentifier);
|
||||||
|
const metadata = window.openmct.telemetry.getMetadata(telemetryObject);
|
||||||
|
const formats = await window.openmct.telemetry.getFormatMap(metadata);
|
||||||
|
window.openmct.telemetry.subscribe(telemetryObject, (obj) => {
|
||||||
|
const sinVal = obj.sin;
|
||||||
|
const formattedSinVal = formats.sin.format(sinVal);
|
||||||
|
window.getTelemValue(formattedSinVal);
|
||||||
|
});
|
||||||
|
}, objectIdentifier);
|
||||||
|
|
||||||
|
return getTelemValuePromise;
|
||||||
|
}
|
@ -21,7 +21,7 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
const { test, expect } = require('../../../../pluginFixtures');
|
const { test, expect } = require('../../../../pluginFixtures');
|
||||||
const { openObjectTreeContextMenu } = require('../../../../appActions');
|
const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const TEST_TEXT = 'Testing text for entries.';
|
const TEST_TEXT = 'Testing text for entries.';
|
||||||
@ -30,8 +30,9 @@ const CUSTOM_NAME = 'CUSTOM_NAME';
|
|||||||
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
|
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
|
||||||
|
|
||||||
test.describe('Restricted Notebook', () => {
|
test.describe('Restricted Notebook', () => {
|
||||||
|
let notebook;
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await startAndAddRestrictedNotebookObject(page);
|
notebook = await startAndAddRestrictedNotebookObject(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Can be renamed @addInit', async ({ page }) => {
|
test('Can be renamed @addInit', async ({ page }) => {
|
||||||
@ -39,9 +40,7 @@ test.describe('Restricted Notebook', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Can be deleted if there are no locked pages @addInit', async ({ page, openmctConfig }) => {
|
test('Can be deleted if there are no locked pages @addInit', async ({ page, openmctConfig }) => {
|
||||||
const { myItemsFolderName } = openmctConfig;
|
await openObjectTreeContextMenu(page, notebook.url);
|
||||||
|
|
||||||
await openObjectTreeContextMenu(page, myItemsFolderName, `Unnamed ${CUSTOM_NAME}`);
|
|
||||||
|
|
||||||
const menuOptions = page.locator('.c-menu ul');
|
const menuOptions = page.locator('.c-menu ul');
|
||||||
await expect.soft(menuOptions).toContainText('Remove');
|
await expect.soft(menuOptions).toContainText('Remove');
|
||||||
@ -76,9 +75,9 @@ test.describe('Restricted Notebook', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Restricted Notebook with at least one entry and with the page locked @addInit', () => {
|
test.describe('Restricted Notebook with at least one entry and with the page locked @addInit', () => {
|
||||||
|
let notebook;
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await startAndAddRestrictedNotebookObject(page);
|
notebook = await startAndAddRestrictedNotebookObject(page);
|
||||||
await enterTextEntry(page);
|
await enterTextEntry(page);
|
||||||
await lockPage(page);
|
await lockPage(page);
|
||||||
|
|
||||||
@ -86,9 +85,8 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
|
|||||||
await page.locator('button.c-notebook__toggle-nav-button').click();
|
await page.locator('button.c-notebook__toggle-nav-button').click();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Locked page should now be in a locked state @addInit @unstable', async ({ page, openmctConfig }, testInfo) => {
|
test('Locked page should now be in a locked state @addInit @unstable', async ({ page }, testInfo) => {
|
||||||
test.fixme(testInfo.project === 'chrome-beta', "Test is unreliable on chrome-beta");
|
test.fixme(testInfo.project === 'chrome-beta', "Test is unreliable on chrome-beta");
|
||||||
const { myItemsFolderName } = openmctConfig;
|
|
||||||
// main lock message on page
|
// main lock message on page
|
||||||
const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed');
|
const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed');
|
||||||
expect.soft(await lockMessage.count()).toEqual(1);
|
expect.soft(await lockMessage.count()).toEqual(1);
|
||||||
@ -98,7 +96,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
|
|||||||
expect.soft(await pageLockIcon.count()).toEqual(1);
|
expect.soft(await pageLockIcon.count()).toEqual(1);
|
||||||
|
|
||||||
// no way to remove a restricted notebook with a locked page
|
// no way to remove a restricted notebook with a locked page
|
||||||
await openObjectTreeContextMenu(page, myItemsFolderName, `Unnamed ${CUSTOM_NAME}`);
|
await openObjectTreeContextMenu(page, notebook.url);
|
||||||
const menuOptions = page.locator('.c-menu ul');
|
const menuOptions = page.locator('.c-menu ul');
|
||||||
|
|
||||||
await expect(menuOptions).not.toContainText('Remove');
|
await expect(menuOptions).not.toContainText('Remove');
|
||||||
@ -178,13 +176,8 @@ async function startAndAddRestrictedNotebookObject(page) {
|
|||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitRestrictedNotebook.js') });
|
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitRestrictedNotebook.js') });
|
||||||
await page.goto('./', { waitUntil: 'networkidle' });
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
await page.click('button:has-text("Create")');
|
|
||||||
await page.click(`text=${CUSTOM_NAME}`); // secondarily tests renamability also
|
return createDomainObjectWithDefaults(page, { type: CUSTOM_NAME });
|
||||||
// Click text=OK
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
|
||||||
page.click('text=OK')
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,7 +36,7 @@ async function createNotebookAndEntry(page, iterations = 1) {
|
|||||||
//Go to baseURL
|
//Go to baseURL
|
||||||
await page.goto('./', { waitUntil: 'networkidle' });
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
createDomainObjectWithDefaults(page, 'Notebook');
|
createDomainObjectWithDefaults(page, { type: 'Notebook' });
|
||||||
|
|
||||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||||
// Click text=To start a new entry, click here or drag and drop any object
|
// Click text=To start a new entry, click here or drag and drop any object
|
||||||
@ -56,19 +56,23 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
|
|||||||
await createNotebookAndEntry(page, iterations);
|
await createNotebookAndEntry(page, iterations);
|
||||||
|
|
||||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||||
// Click text=To start a new entry, click here or drag and drop any object
|
// Hover and click "Add Tag" button
|
||||||
|
// Hover is needed here to "slow down" the actions while running in headless mode
|
||||||
|
await page.hover(`button:has-text("Add Tag") >> nth = ${iteration}`);
|
||||||
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
|
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
|
||||||
|
|
||||||
// Click [placeholder="Type to select tag"]
|
// Click inside the tag search input
|
||||||
await page.locator('[placeholder="Type to select tag"]').click();
|
await page.locator('[placeholder="Type to select tag"]').click();
|
||||||
// Click text=Driving
|
// Select the "Driving" tag
|
||||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
|
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
|
||||||
|
|
||||||
// Click button:has-text("Add Tag")
|
// Hover and click "Add Tag" button
|
||||||
|
// Hover is needed here to "slow down" the actions while running in headless mode
|
||||||
|
await page.hover(`button:has-text("Add Tag") >> nth = ${iteration}`);
|
||||||
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
|
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
|
||||||
// Click [placeholder="Type to select tag"]
|
// Click inside the tag search input
|
||||||
await page.locator('[placeholder="Type to select tag"]').click();
|
await page.locator('[placeholder="Type to select tag"]').click();
|
||||||
// Click text=Science
|
// Select the "Science" tag
|
||||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
|
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -130,7 +134,8 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
|||||||
await createNotebookEntryAndTags(page);
|
await createNotebookEntryAndTags(page);
|
||||||
await page.locator('[aria-label="Notebook Entries"]').click();
|
await page.locator('[aria-label="Notebook Entries"]').click();
|
||||||
// Delete Driving
|
// Delete Driving
|
||||||
await page.locator('text=Science Driving Add Tag >> button').nth(1).click();
|
await page.hover('.c-tag__label:has-text("Driving")');
|
||||||
|
await page.locator('.c-tag__label:has-text("Driving") ~ .c-completed-tag-deletion').click();
|
||||||
|
|
||||||
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
|
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
|
||||||
await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving");
|
await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving");
|
||||||
@ -139,11 +144,28 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
|||||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
|
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
|
||||||
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
|
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Can delete objects with tags and neither return in search', async ({ page }) => {
|
||||||
|
await createNotebookEntryAndTags(page);
|
||||||
|
// Delete Notebook
|
||||||
|
await page.locator('button[title="More options"]').click();
|
||||||
|
await page.locator('li[title="Remove this object from its containing object."]').click();
|
||||||
|
await page.locator('button:has-text("OK")').click();
|
||||||
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
|
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||||
|
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed');
|
||||||
|
await expect(page.locator('text=No matching results.')).toBeVisible();
|
||||||
|
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sci');
|
||||||
|
await expect(page.locator('text=No matching results.')).toBeVisible();
|
||||||
|
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('dri');
|
||||||
|
await expect(page.locator('text=No matching results.')).toBeVisible();
|
||||||
|
});
|
||||||
test('Tags persist across reload', async ({ page }) => {
|
test('Tags persist across reload', async ({ page }) => {
|
||||||
//Go to baseURL
|
//Go to baseURL
|
||||||
await page.goto('./', { waitUntil: 'networkidle' });
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
await createDomainObjectWithDefaults(page, 'Clock');
|
await createDomainObjectWithDefaults(page, { type: 'Clock' });
|
||||||
|
|
||||||
const ITERATIONS = 4;
|
const ITERATIONS = 4;
|
||||||
await createNotebookEntryAndTags(page, ITERATIONS);
|
await createNotebookEntryAndTags(page, ITERATIONS);
|
||||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 18 KiB |
@ -20,55 +20,26 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
|
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||||
const { test, expect } = require('../../../../pluginFixtures');
|
const { test, expect } = require('../../../../pluginFixtures');
|
||||||
|
|
||||||
test.describe('Telemetry Table', () => {
|
test.describe('Telemetry Table', () => {
|
||||||
test('unpauses and filters data when paused by button and user changes bounds', async ({ page, openmctConfig }) => {
|
test('unpauses and filters data when paused by button and user changes bounds', async ({ page }) => {
|
||||||
test.info().annotations.push({
|
test.info().annotations.push({
|
||||||
type: 'issue',
|
type: 'issue',
|
||||||
description: 'https://github.com/nasa/openmct/issues/5113'
|
description: 'https://github.com/nasa/openmct/issues/5113'
|
||||||
});
|
});
|
||||||
|
|
||||||
const { myItemsFolderName } = openmctConfig;
|
|
||||||
const bannerMessage = '.c-message-banner__message';
|
|
||||||
const createButton = 'button:has-text("Create")';
|
|
||||||
|
|
||||||
await page.goto('./', { waitUntil: 'networkidle' });
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
// Click create button
|
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
|
||||||
await page.locator(createButton).click();
|
await createDomainObjectWithDefaults(page, {
|
||||||
await page.locator('li:has-text("Telemetry Table")').click();
|
type: 'Sine Wave Generator',
|
||||||
|
parent: table.uuid
|
||||||
await Promise.all([
|
});
|
||||||
page.waitForNavigation(),
|
|
||||||
page.locator('text=OK').click(),
|
|
||||||
// Wait for Save Banner to appear
|
|
||||||
page.waitForSelector(bannerMessage)
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Save (exit edit mode)
|
|
||||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(3).click();
|
|
||||||
await page.locator('text=Save and Finish Editing').click();
|
|
||||||
|
|
||||||
// Click create button
|
|
||||||
await page.locator(createButton).click();
|
|
||||||
|
|
||||||
// add Sine Wave Generator with defaults
|
|
||||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation(),
|
|
||||||
page.locator('text=OK').click(),
|
|
||||||
// Wait for Save Banner to appear
|
|
||||||
page.waitForSelector(bannerMessage)
|
|
||||||
]);
|
|
||||||
|
|
||||||
// focus the Telemetry Table
|
// focus the Telemetry Table
|
||||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
page.goto(table.url);
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation(),
|
|
||||||
page.locator('text=Unnamed Telemetry Table').first().click()
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Click pause button
|
// Click pause button
|
||||||
const pauseButton = page.locator('button.c-button.icon-pause');
|
const pauseButton = page.locator('button.c-button.icon-pause');
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
const { test, expect } = require('../../../../baseFixtures');
|
const { test, expect } = require('../../../../baseFixtures');
|
||||||
|
const { setFixedTimeMode, setRealTimeMode, setStartOffset, setEndOffset } = require('../../../../appActions');
|
||||||
|
|
||||||
test.describe('Time conductor operations', () => {
|
test.describe('Time conductor operations', () => {
|
||||||
test('validate start time does not exceeds end time', async ({ page }) => {
|
test('validate start time does not exceeds end time', async ({ page }) => {
|
||||||
@ -146,89 +147,24 @@ test.describe('Time conductor input fields real-time mode', () => {
|
|||||||
expect(page.url()).toContain(`startDelta=${startDelta}`);
|
expect(page.url()).toContain(`startDelta=${startDelta}`);
|
||||||
expect(page.url()).toContain(`endDelta=${endDelta}`);
|
expect(page.url()).toContain(`endDelta=${endDelta}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.fixme('time conductor history in fixed time mode will track changing start and end times', async ({ page }) => {
|
||||||
|
// change start time, verify it's tracked in history
|
||||||
|
// change end time, verify it's tracked in history
|
||||||
|
});
|
||||||
|
|
||||||
|
test.fixme('time conductor history in realtime mode will track changing start and end times', async ({ page }) => {
|
||||||
|
// change start offset, verify it's tracked in history
|
||||||
|
// change end offset, verify it's tracked in history
|
||||||
|
});
|
||||||
|
|
||||||
|
test.fixme('time conductor history allows you to set a historical timeframe', async ({ page }) => {
|
||||||
|
// make sure there are historical history options
|
||||||
|
// select an option and make sure the time conductor start and end bounds are updated correctly
|
||||||
|
});
|
||||||
|
|
||||||
|
test.fixme('time conductor history allows you to set a realtime offsets', async ({ page }) => {
|
||||||
|
// make sure there are realtime history options
|
||||||
|
// select an option and verify the offsets are updated correctly
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* @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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -24,9 +24,10 @@ const { test, expect } = require('../../../../pluginFixtures');
|
|||||||
const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions');
|
const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||||
|
|
||||||
test.describe('Timer', () => {
|
test.describe('Timer', () => {
|
||||||
|
let timer;
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('./', { waitUntil: 'networkidle' });
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
await createDomainObjectWithDefaults(page, 'timer');
|
timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Can perform actions on the Timer', async ({ page, openmctConfig }) => {
|
test('Can perform actions on the Timer', async ({ page, openmctConfig }) => {
|
||||||
@ -35,13 +36,13 @@ test.describe('Timer', () => {
|
|||||||
description: 'https://github.com/nasa/openmct/issues/4313'
|
description: 'https://github.com/nasa/openmct/issues/4313'
|
||||||
});
|
});
|
||||||
|
|
||||||
const { myItemsFolderName } = await openmctConfig;
|
const timerUrl = timer.url;
|
||||||
|
|
||||||
await test.step("From the tree context menu", async () => {
|
await test.step("From the tree context menu", async () => {
|
||||||
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Start');
|
await triggerTimerContextMenuAction(page, timerUrl, 'Start');
|
||||||
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Pause');
|
await triggerTimerContextMenuAction(page, timerUrl, 'Pause');
|
||||||
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Restart at 0');
|
await triggerTimerContextMenuAction(page, timerUrl, 'Restart at 0');
|
||||||
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Stop');
|
await triggerTimerContextMenuAction(page, timerUrl, 'Stop');
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step("From the 3dot menu", async () => {
|
await test.step("From the 3dot menu", async () => {
|
||||||
@ -74,9 +75,9 @@ test.describe('Timer', () => {
|
|||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
* @param {TimerAction} action
|
* @param {TimerAction} action
|
||||||
*/
|
*/
|
||||||
async function triggerTimerContextMenuAction(page, myItemsFolderName, action) {
|
async function triggerTimerContextMenuAction(page, timerUrl, action) {
|
||||||
const menuAction = `.c-menu ul li >> text="${action}"`;
|
const menuAction = `.c-menu ul li >> text="${action}"`;
|
||||||
await openObjectTreeContextMenu(page, myItemsFolderName, "Unnamed Timer");
|
await openObjectTreeContextMenu(page, timerUrl);
|
||||||
await page.locator(menuAction).click();
|
await page.locator(menuAction).click();
|
||||||
assertTimerStateAfterAction(page, action);
|
assertTimerStateAfterAction(page, action);
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const { test, expect } = require('../../pluginFixtures');
|
const { test, expect } = require('../../pluginFixtures');
|
||||||
|
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||||
|
const { v4: uuid } = require('uuid');
|
||||||
|
|
||||||
test.describe('Grand Search', () => {
|
test.describe('Grand Search', () => {
|
||||||
test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page, openmctConfig }) => {
|
test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page, openmctConfig }) => {
|
||||||
@ -107,15 +109,21 @@ test.describe("Search Tests @unstable", () => {
|
|||||||
|
|
||||||
// Verify that no results are found
|
// Verify that no results are found
|
||||||
expect(await searchResults.count()).toBe(0);
|
expect(await searchResults.count()).toBe(0);
|
||||||
|
|
||||||
|
// Verify proper message appears
|
||||||
|
await expect(page.locator('text=No matching results.')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Validate single object in search result', async ({ page }) => {
|
test('Validate single object in search result @couchdb', async ({ page }) => {
|
||||||
//Go to baseURL
|
//Go to baseURL
|
||||||
await page.goto("./", { waitUntil: "networkidle" });
|
await page.goto("./", { waitUntil: "networkidle" });
|
||||||
|
|
||||||
// Create a folder object
|
// Create a folder object
|
||||||
const folderName = 'testFolder';
|
const folderName = uuid();
|
||||||
await createFolderObject(page, folderName);
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'folder',
|
||||||
|
name: folderName
|
||||||
|
});
|
||||||
|
|
||||||
// Full search for object
|
// Full search for object
|
||||||
await page.type("input[type=search]", folderName);
|
await page.type("input[type=search]", folderName);
|
||||||
@ -124,7 +132,7 @@ test.describe("Search Tests @unstable", () => {
|
|||||||
await waitForSearchCompletion(page);
|
await waitForSearchCompletion(page);
|
||||||
|
|
||||||
// Get the search results
|
// Get the search results
|
||||||
const searchResults = await page.locator(searchResultSelector);
|
const searchResults = page.locator(searchResultSelector);
|
||||||
|
|
||||||
// Verify that one result is found
|
// Verify that one result is found
|
||||||
expect(await searchResults.count()).toBe(1);
|
expect(await searchResults.count()).toBe(1);
|
||||||
|
138
e2e/tests/functional/tree.e2e.spec.js
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
const { test, expect } = require('../../pluginFixtures.js');
|
||||||
|
const {
|
||||||
|
createDomainObjectWithDefaults,
|
||||||
|
openObjectTreeContextMenu
|
||||||
|
} = require('../../appActions.js');
|
||||||
|
|
||||||
|
test.describe('Tree operations', () => {
|
||||||
|
test('Renaming an object reorders the tree @unstable', async ({ page, openmctConfig }) => {
|
||||||
|
const { myItemsFolderName } = openmctConfig;
|
||||||
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Folder',
|
||||||
|
name: 'Foo'
|
||||||
|
});
|
||||||
|
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Folder',
|
||||||
|
name: 'Bar'
|
||||||
|
});
|
||||||
|
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Folder',
|
||||||
|
name: 'Baz'
|
||||||
|
});
|
||||||
|
|
||||||
|
const clock1 = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Clock',
|
||||||
|
name: 'aaa'
|
||||||
|
});
|
||||||
|
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Clock',
|
||||||
|
name: 'www'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expand the root folder
|
||||||
|
await expandTreePaneItemByName(page, myItemsFolderName);
|
||||||
|
|
||||||
|
await test.step("Reorders objects with the same tree depth", async () => {
|
||||||
|
await getAndAssertTreeItems(page, ['aaa', 'Bar', 'Baz', 'Foo', 'www']);
|
||||||
|
await renameObjectFromContextMenu(page, clock1.url, 'zzz');
|
||||||
|
await getAndAssertTreeItems(page, ['Bar', 'Baz', 'Foo', 'www', 'zzz']);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("Reorders links to objects as well as original objects", async () => {
|
||||||
|
await page.click('role=treeitem[name=/Bar/]');
|
||||||
|
await page.dragAndDrop('role=treeitem[name=/www/]', '.c-object-view');
|
||||||
|
await page.dragAndDrop('role=treeitem[name=/zzz/]', '.c-object-view');
|
||||||
|
await page.click('role=treeitem[name=/Baz/]');
|
||||||
|
await page.dragAndDrop('role=treeitem[name=/www/]', '.c-object-view');
|
||||||
|
await page.dragAndDrop('role=treeitem[name=/zzz/]', '.c-object-view');
|
||||||
|
await page.click('role=treeitem[name=/Foo/]');
|
||||||
|
await page.dragAndDrop('role=treeitem[name=/www/]', '.c-object-view');
|
||||||
|
await page.dragAndDrop('role=treeitem[name=/zzz/]', '.c-object-view');
|
||||||
|
// Expand the unopened folders
|
||||||
|
await expandTreePaneItemByName(page, 'Bar');
|
||||||
|
await expandTreePaneItemByName(page, 'Baz');
|
||||||
|
await expandTreePaneItemByName(page, 'Foo');
|
||||||
|
|
||||||
|
await renameObjectFromContextMenu(page, clock1.url, '___');
|
||||||
|
await getAndAssertTreeItems(page,
|
||||||
|
[
|
||||||
|
"___",
|
||||||
|
"Bar",
|
||||||
|
"___",
|
||||||
|
"www",
|
||||||
|
"Baz",
|
||||||
|
"___",
|
||||||
|
"www",
|
||||||
|
"Foo",
|
||||||
|
"___",
|
||||||
|
"www",
|
||||||
|
"www"
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {Array<string>} expected
|
||||||
|
*/
|
||||||
|
async function getAndAssertTreeItems(page, expected) {
|
||||||
|
const treeItems = page.locator('[role="treeitem"]');
|
||||||
|
const allTexts = await treeItems.allInnerTexts();
|
||||||
|
// Get rid of root folder ('My Items') as its position will not change
|
||||||
|
allTexts.shift();
|
||||||
|
expect(allTexts).toEqual(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {string} name
|
||||||
|
*/
|
||||||
|
async function expandTreePaneItemByName(page, name) {
|
||||||
|
const treePane = page.locator('#tree-pane');
|
||||||
|
const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
|
||||||
|
const expandTriangle = treeItem.locator('.c-disclosure-triangle');
|
||||||
|
await expandTriangle.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {string} myItemsFolderName
|
||||||
|
* @param {string} url
|
||||||
|
* @param {string} newName
|
||||||
|
*/
|
||||||
|
async function renameObjectFromContextMenu(page, url, newName) {
|
||||||
|
await openObjectTreeContextMenu(page, url);
|
||||||
|
await page.click('li:text("Edit Properties")');
|
||||||
|
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
|
||||||
|
await nameInput.fill("");
|
||||||
|
await nameInput.fill(newName);
|
||||||
|
await page.click('[aria-label="Save"]');
|
||||||
|
}
|
@ -53,7 +53,7 @@ test.describe('Visual - addInit', () => {
|
|||||||
//Go to baseURL
|
//Go to baseURL
|
||||||
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
|
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
await createDomainObjectWithDefaults(page, CUSTOM_NAME);
|
await createDomainObjectWithDefaults(page, { type: CUSTOM_NAME });
|
||||||
|
|
||||||
// Take a snapshot of the newly created CUSTOM_NAME notebook
|
// Take a snapshot of the newly created CUSTOM_NAME notebook
|
||||||
await percySnapshot(page, `Restricted Notebook with CUSTOM_NAME (theme: '${theme}')`);
|
await percySnapshot(page, `Restricted Notebook with CUSTOM_NAME (theme: '${theme}')`);
|
||||||
|
101
e2e/tests/visual/components/tree.visual.spec.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
const { test } = require('../../../pluginFixtures.js');
|
||||||
|
const { createDomainObjectWithDefaults } = require('../../../appActions.js');
|
||||||
|
|
||||||
|
const percySnapshot = require('@percy/playwright');
|
||||||
|
|
||||||
|
test.describe('Visual - Tree Pane', () => {
|
||||||
|
test('Tree pane in various states @unstable', async ({ page, theme, openmctConfig }) => {
|
||||||
|
const { myItemsFolderName } = openmctConfig;
|
||||||
|
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
|
const foo = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Folder',
|
||||||
|
name: "Foo Folder"
|
||||||
|
});
|
||||||
|
|
||||||
|
const bar = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Folder',
|
||||||
|
name: "Bar Folder",
|
||||||
|
parent: foo.uuid
|
||||||
|
});
|
||||||
|
|
||||||
|
const baz = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Folder',
|
||||||
|
name: "Baz Folder",
|
||||||
|
parent: bar.uuid
|
||||||
|
});
|
||||||
|
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Clock',
|
||||||
|
name: 'A Clock'
|
||||||
|
});
|
||||||
|
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Clock',
|
||||||
|
name: 'Z Clock'
|
||||||
|
});
|
||||||
|
|
||||||
|
const treePane = "#tree-pane";
|
||||||
|
|
||||||
|
await percySnapshot(page, `Tree Pane w/ collapsed tree (theme: ${theme})`, {
|
||||||
|
scope: treePane
|
||||||
|
});
|
||||||
|
|
||||||
|
await expandTreePaneItemByName(page, myItemsFolderName);
|
||||||
|
|
||||||
|
await page.goto(foo.url);
|
||||||
|
await page.dragAndDrop('role=treeitem[name=/A Clock/]', '.c-object-view');
|
||||||
|
await page.dragAndDrop('role=treeitem[name=/Z Clock/]', '.c-object-view');
|
||||||
|
await page.goto(bar.url);
|
||||||
|
await page.dragAndDrop('role=treeitem[name=/A Clock/]', '.c-object-view');
|
||||||
|
await page.dragAndDrop('role=treeitem[name=/Z Clock/]', '.c-object-view');
|
||||||
|
await page.goto(baz.url);
|
||||||
|
await page.dragAndDrop('role=treeitem[name=/A Clock/]', '.c-object-view');
|
||||||
|
await page.dragAndDrop('role=treeitem[name=/Z Clock/]', '.c-object-view');
|
||||||
|
|
||||||
|
await percySnapshot(page, `Tree Pane w/ single level expanded (theme: ${theme})`, {
|
||||||
|
scope: treePane
|
||||||
|
});
|
||||||
|
|
||||||
|
await expandTreePaneItemByName(page, foo.name);
|
||||||
|
await expandTreePaneItemByName(page, bar.name);
|
||||||
|
await expandTreePaneItemByName(page, baz.name);
|
||||||
|
|
||||||
|
await percySnapshot(page, `Tree Pane w/ multiple levels expanded (theme: ${theme})`, {
|
||||||
|
scope: treePane
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {string} name
|
||||||
|
*/
|
||||||
|
async function expandTreePaneItemByName(page, name) {
|
||||||
|
const treePane = page.locator('#tree-pane');
|
||||||
|
const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
|
||||||
|
const expandTriangle = treeItem.locator('.c-disclosure-triangle');
|
||||||
|
await expandTriangle.click();
|
||||||
|
}
|
@ -67,9 +67,9 @@ test.describe('Visual - Default', () => {
|
|||||||
await percySnapshot(page, `About (theme: '${theme}')`);
|
await percySnapshot(page, `About (theme: '${theme}')`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Visual - Default Condition Set', async ({ page, theme }) => {
|
test.fixme('Visual - Default Condition Set', async ({ page, theme }) => {
|
||||||
|
|
||||||
await createDomainObjectWithDefaults(page, 'Condition Set');
|
await createDomainObjectWithDefaults(page, { type: 'Condition Set' });
|
||||||
|
|
||||||
// Take a snapshot of the newly created Condition Set object
|
// Take a snapshot of the newly created Condition Set object
|
||||||
await percySnapshot(page, `Default Condition Set (theme: '${theme}')`);
|
await percySnapshot(page, `Default Condition Set (theme: '${theme}')`);
|
||||||
@ -81,7 +81,7 @@ test.describe('Visual - Default', () => {
|
|||||||
description: 'https://github.com/nasa/openmct/issues/5349'
|
description: 'https://github.com/nasa/openmct/issues/5349'
|
||||||
});
|
});
|
||||||
|
|
||||||
await createDomainObjectWithDefaults(page, 'Condition Widget');
|
await createDomainObjectWithDefaults(page, { type: 'Condition Widget' });
|
||||||
|
|
||||||
// Take a snapshot of the newly created Condition Widget object
|
// Take a snapshot of the newly created Condition Widget object
|
||||||
await percySnapshot(page, `Default Condition Widget (theme: '${theme}')`);
|
await percySnapshot(page, `Default Condition Widget (theme: '${theme}')`);
|
||||||
@ -137,8 +137,8 @@ test.describe('Visual - Default', () => {
|
|||||||
await percySnapshot(page, `removed amplitude property value (theme: '${theme}')`);
|
await percySnapshot(page, `removed amplitude property value (theme: '${theme}')`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Visual - Save Successful Banner', async ({ page, theme }) => {
|
test.fixme('Visual - Save Successful Banner', async ({ page, theme }) => {
|
||||||
await createDomainObjectWithDefaults(page, 'Timer');
|
await createDomainObjectWithDefaults(page, { type: 'Timer' });
|
||||||
|
|
||||||
await page.locator('.c-message-banner__message').hover({ trial: true });
|
await page.locator('.c-message-banner__message').hover({ trial: true });
|
||||||
await percySnapshot(page, `Banner message shown (theme: '${theme}')`);
|
await percySnapshot(page, `Banner message shown (theme: '${theme}')`);
|
||||||
@ -159,8 +159,8 @@ test.describe('Visual - Default', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Visual - Default Gauge is correct', async ({ page, theme }) => {
|
test.fixme('Visual - Default Gauge is correct', async ({ page, theme }) => {
|
||||||
await createDomainObjectWithDefaults(page, 'Gauge');
|
await createDomainObjectWithDefaults(page, { type: 'Gauge' });
|
||||||
|
|
||||||
// Take a snapshot of the newly created Gauge object
|
// Take a snapshot of the newly created Gauge object
|
||||||
await percySnapshot(page, `Default Gauge (theme: '${theme}')`);
|
await percySnapshot(page, `Default Gauge (theme: '${theme}')`);
|
||||||
|
78
e2e/tests/visual/faultManagement.visual.spec.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const { test } = require('../../pluginFixtures');
|
||||||
|
const percySnapshot = require('@percy/playwright');
|
||||||
|
|
||||||
|
const utils = require('../../helper/faultUtils');
|
||||||
|
|
||||||
|
test.describe('The Fault Management Plugin Visual Test', () => {
|
||||||
|
|
||||||
|
test('icon test', async ({ page, theme }) => {
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
await page.addInitScript({ path: path.join(__dirname, '../../helper/', 'addInitFaultManagementPlugin.js') });
|
||||||
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
|
await percySnapshot(page, `Fault Management icon appears in tree (theme: '${theme}')`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fault list and acknowledged faults', async ({ page, theme }) => {
|
||||||
|
await utils.navigateToFaultManagementWithStaticExample(page);
|
||||||
|
|
||||||
|
await percySnapshot(page, `Shows a list of faults in the standard view (theme: '${theme}')`);
|
||||||
|
|
||||||
|
await utils.acknowledgeFault(page, 1);
|
||||||
|
await utils.changeViewTo(page, 'acknowledged');
|
||||||
|
|
||||||
|
await percySnapshot(page, `Acknowledged faults, have a checkmark on the fault icon and appear in the acknowldeged view (theme: '${theme}')`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shelved faults', async ({ page, theme }) => {
|
||||||
|
await utils.navigateToFaultManagementWithStaticExample(page);
|
||||||
|
|
||||||
|
await utils.shelveFault(page, 1);
|
||||||
|
await utils.changeViewTo(page, 'shelved');
|
||||||
|
|
||||||
|
await percySnapshot(page, `Shelved faults appear in the shelved view (theme: '${theme}')`);
|
||||||
|
|
||||||
|
await utils.openFaultRowMenu(page, 1);
|
||||||
|
|
||||||
|
await percySnapshot(page, `Shelved faults have a 3-dot menu with Unshelve option enabled (theme: '${theme}')`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('3-dot menu for fault', async ({ page, theme }) => {
|
||||||
|
await utils.navigateToFaultManagementWithStaticExample(page);
|
||||||
|
|
||||||
|
await utils.openFaultRowMenu(page, 1);
|
||||||
|
|
||||||
|
await percySnapshot(page, `Faults have a 3-dot menu with Acknowledge, Shelve and Unshelve (Unshelve is disabled) options (theme: '${theme}')`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ability to acknowledge or shelve', async ({ page, theme }) => {
|
||||||
|
await utils.navigateToFaultManagementWithStaticExample(page);
|
||||||
|
|
||||||
|
await utils.selectFaultItem(page, 1);
|
||||||
|
|
||||||
|
await percySnapshot(page, `Selected faults highlight the ability to Acknowledge or Shelve above the fault list (theme: '${theme}')`);
|
||||||
|
});
|
||||||
|
});
|
@ -46,7 +46,10 @@ test.describe('Grand Search', () => {
|
|||||||
// await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
// await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||||
// await page.locator('text=Save and Finish Editing').click();
|
// await page.locator('text=Save and Finish Editing').click();
|
||||||
const folder1 = 'Folder1';
|
const folder1 = 'Folder1';
|
||||||
await createDomainObjectWithDefaults(page, 'Folder', folder1);
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Folder',
|
||||||
|
name: folder1
|
||||||
|
});
|
||||||
|
|
||||||
// Click [aria-label="OpenMCT Search"] input[type="search"]
|
// Click [aria-label="OpenMCT Search"] input[type="search"]
|
||||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||||
|
@ -20,59 +20,36 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
export default function () {
|
import utils from './utils';
|
||||||
|
|
||||||
|
export default function (staticFaults = false) {
|
||||||
return function install(openmct) {
|
return function install(openmct) {
|
||||||
openmct.install(openmct.plugins.FaultManagement());
|
openmct.install(openmct.plugins.FaultManagement());
|
||||||
|
|
||||||
|
const faultsData = utils.randomFaults(staticFaults);
|
||||||
|
|
||||||
openmct.faults.addProvider({
|
openmct.faults.addProvider({
|
||||||
request(domainObject, options) {
|
request(domainObject, options) {
|
||||||
const faults = JSON.parse(localStorage.getItem('faults'));
|
return Promise.resolve(faultsData);
|
||||||
|
|
||||||
return Promise.resolve(faults.alarms);
|
|
||||||
},
|
},
|
||||||
subscribe(domainObject, callback) {
|
subscribe(domainObject, callback) {
|
||||||
const faultsData = JSON.parse(localStorage.getItem('faults')).alarms;
|
return () => {};
|
||||||
|
|
||||||
function getRandomIndex(start, end) {
|
|
||||||
return Math.floor(start + (Math.random() * (end - start + 1)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let id = setInterval(() => {
|
|
||||||
const index = getRandomIndex(0, faultsData.length - 1);
|
|
||||||
const randomFaultData = faultsData[index];
|
|
||||||
const randomFault = randomFaultData.fault;
|
|
||||||
randomFault.currentValueInfo.value = Math.random();
|
|
||||||
callback({
|
|
||||||
fault: randomFault,
|
|
||||||
type: 'alarms'
|
|
||||||
});
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(id);
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
supportsRequest(domainObject) {
|
supportsRequest(domainObject) {
|
||||||
const faults = localStorage.getItem('faults');
|
return domainObject.type === 'faultManagement';
|
||||||
|
|
||||||
return faults && domainObject.type === 'faultManagement';
|
|
||||||
},
|
},
|
||||||
supportsSubscribe(domainObject) {
|
supportsSubscribe(domainObject) {
|
||||||
const faults = localStorage.getItem('faults');
|
return domainObject.type === 'faultManagement';
|
||||||
|
|
||||||
return faults && domainObject.type === 'faultManagement';
|
|
||||||
},
|
},
|
||||||
acknowledgeFault(fault, { comment = '' }) {
|
acknowledgeFault(fault, { comment = '' }) {
|
||||||
console.log('acknowledgeFault', fault);
|
utils.acknowledgeFault(fault);
|
||||||
console.log('comment', comment);
|
|
||||||
|
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
success: true
|
success: true
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
shelveFault(fault, shelveData) {
|
shelveFault(fault, duration) {
|
||||||
console.log('shelveFault', fault);
|
utils.shelveFault(fault, duration);
|
||||||
console.log('shelveData', shelveData);
|
|
||||||
|
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
success: true
|
success: true
|
76
example/faultManagement/utils.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
const SEVERITIES = ['WATCH', 'WARNING', 'CRITICAL'];
|
||||||
|
const NAMESPACE = '/Example/fault-';
|
||||||
|
const getRandom = {
|
||||||
|
severity: () => SEVERITIES[Math.floor(Math.random() * 3)],
|
||||||
|
value: () => Math.random() + Math.floor(Math.random() * 21) - 10,
|
||||||
|
fault: (num, staticFaults) => {
|
||||||
|
let val = getRandom.value();
|
||||||
|
let severity = getRandom.severity();
|
||||||
|
let time = Date.now() - num;
|
||||||
|
|
||||||
|
if (staticFaults) {
|
||||||
|
let severityIndex = num > 3 ? num % 3 : num;
|
||||||
|
|
||||||
|
val = num;
|
||||||
|
severity = SEVERITIES[severityIndex - 1];
|
||||||
|
time = num;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: num,
|
||||||
|
fault: {
|
||||||
|
acknowledged: false,
|
||||||
|
currentValueInfo: {
|
||||||
|
value: val,
|
||||||
|
rangeCondition: severity,
|
||||||
|
monitoringResult: severity
|
||||||
|
},
|
||||||
|
id: `id-${num}`,
|
||||||
|
name: `Example Fault ${num}`,
|
||||||
|
namespace: NAMESPACE + num,
|
||||||
|
seqNum: 0,
|
||||||
|
severity: severity,
|
||||||
|
shelved: false,
|
||||||
|
shortDescription: '',
|
||||||
|
triggerTime: time,
|
||||||
|
triggerValueInfo: {
|
||||||
|
value: val,
|
||||||
|
rangeCondition: severity,
|
||||||
|
monitoringResult: severity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function shelveFault(fault, opts = {
|
||||||
|
shelved: true,
|
||||||
|
comment: '',
|
||||||
|
shelveDuration: 90000
|
||||||
|
}) {
|
||||||
|
fault.shelved = true;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
fault.shelved = false;
|
||||||
|
}, opts.shelveDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
function acknowledgeFault(fault) {
|
||||||
|
fault.acknowledged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomFaults(staticFaults, count = 5) {
|
||||||
|
let faults = [];
|
||||||
|
|
||||||
|
for (let x = 1, y = count + 1; x < y; x++) {
|
||||||
|
faults.push(getRandom.fault(x, staticFaults));
|
||||||
|
}
|
||||||
|
|
||||||
|
return faults;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
randomFaults,
|
||||||
|
shelveFault,
|
||||||
|
acknowledgeFault
|
||||||
|
};
|
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "openmct",
|
"name": "openmct",
|
||||||
"version": "2.1.0-SNAPSHOT",
|
"version": "2.0.8",
|
||||||
"description": "The Open MCT core platform",
|
"description": "The Open MCT core platform",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/eslint-parser": "7.18.9",
|
"@babel/eslint-parser": "7.18.9",
|
||||||
"@braintree/sanitize-url": "6.0.0",
|
"@braintree/sanitize-url": "6.0.0",
|
||||||
"@percy/cli": "1.7.2",
|
"@percy/cli": "1.8.1",
|
||||||
"@percy/playwright": "1.0.4",
|
"@percy/playwright": "1.0.4",
|
||||||
"@playwright/test": "1.23.0",
|
"@playwright/test": "1.23.0",
|
||||||
"@types/eventemitter3": "^1.0.0",
|
"@types/eventemitter3": "^1.0.0",
|
||||||
@ -90,7 +90,8 @@
|
|||||||
"test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
|
"test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
|
||||||
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
|
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
|
||||||
"test:e2e": "npx playwright test",
|
"test:e2e": "npx playwright test",
|
||||||
"test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert @unstable",
|
"test:e2e:couchdb": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @couchdb",
|
||||||
|
"test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert \"@unstable|@couchdb\"",
|
||||||
"test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable",
|
"test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable",
|
||||||
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
|
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
|
||||||
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots",
|
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots",
|
||||||
|
@ -40,6 +40,8 @@ const ANNOTATION_TYPES = Object.freeze({
|
|||||||
PLOT_SPATIAL: 'PLOT_SPATIAL'
|
PLOT_SPATIAL: 'PLOT_SPATIAL'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ANNOTATION_TYPE = 'annotation';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} Tag
|
* @typedef {Object} Tag
|
||||||
* @property {String} key a unique identifier for the tag
|
* @property {String} key a unique identifier for the tag
|
||||||
@ -54,7 +56,7 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
|
|
||||||
this.ANNOTATION_TYPES = ANNOTATION_TYPES;
|
this.ANNOTATION_TYPES = ANNOTATION_TYPES;
|
||||||
|
|
||||||
this.openmct.types.addType('annotation', {
|
this.openmct.types.addType(ANNOTATION_TYPE, {
|
||||||
name: 'Annotation',
|
name: 'Annotation',
|
||||||
description: 'A user created note or comment about time ranges, pixel space, and geospatial features.',
|
description: 'A user created note or comment about time ranges, pixel space, and geospatial features.',
|
||||||
creatable: false,
|
creatable: false,
|
||||||
@ -136,6 +138,10 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
this.availableTags[tagKey] = tagsDefinition;
|
this.availableTags[tagKey] = tagsDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isAnnotation(domainObject) {
|
||||||
|
return domainObject && (domainObject.type === ANNOTATION_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
getAvailableTags() {
|
getAvailableTags() {
|
||||||
if (this.availableTags) {
|
if (this.availableTags) {
|
||||||
const rearrangedToArray = Object.keys(this.availableTags).map(tagKey => {
|
const rearrangedToArray = Object.keys(this.availableTags).map(tagKey => {
|
||||||
@ -271,7 +277,10 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
const searchResults = (await Promise.all(this.openmct.objects.search(matchingTagKeys, abortController, this.openmct.objects.SEARCH_TYPES.TAGS))).flat();
|
const searchResults = (await Promise.all(this.openmct.objects.search(matchingTagKeys, abortController, this.openmct.objects.SEARCH_TYPES.TAGS))).flat();
|
||||||
const appliedTagSearchResults = this.#addTagMetaInformationToResults(searchResults, matchingTagKeys);
|
const appliedTagSearchResults = this.#addTagMetaInformationToResults(searchResults, matchingTagKeys);
|
||||||
const appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults);
|
const appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults);
|
||||||
|
const resultsWithValidPath = appliedTargetsModels.filter(result => {
|
||||||
|
return this.openmct.objects.isReachable(result.targetModels?.[0]?.originalPath);
|
||||||
|
});
|
||||||
|
|
||||||
return appliedTargetsModels;
|
return resultsWithValidPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,15 +27,26 @@ describe("The Annotation API", () => {
|
|||||||
let openmct;
|
let openmct;
|
||||||
let mockObjectProvider;
|
let mockObjectProvider;
|
||||||
let mockDomainObject;
|
let mockDomainObject;
|
||||||
|
let mockFolderObject;
|
||||||
let mockAnnotationObject;
|
let mockAnnotationObject;
|
||||||
|
|
||||||
beforeEach((done) => {
|
beforeEach((done) => {
|
||||||
openmct = createOpenMct();
|
openmct = createOpenMct();
|
||||||
openmct.install(new ExampleTagsPlugin());
|
openmct.install(new ExampleTagsPlugin());
|
||||||
const availableTags = openmct.annotation.getAvailableTags();
|
const availableTags = openmct.annotation.getAvailableTags();
|
||||||
|
mockFolderObject = {
|
||||||
|
type: 'root',
|
||||||
|
name: 'folderFoo',
|
||||||
|
location: '',
|
||||||
|
identifier: {
|
||||||
|
key: 'someParent',
|
||||||
|
namespace: 'fooNameSpace'
|
||||||
|
}
|
||||||
|
};
|
||||||
mockDomainObject = {
|
mockDomainObject = {
|
||||||
type: 'notebook',
|
type: 'notebook',
|
||||||
name: 'fooRabbitNotebook',
|
name: 'fooRabbitNotebook',
|
||||||
|
location: 'fooNameSpace:someParent',
|
||||||
identifier: {
|
identifier: {
|
||||||
key: 'some-object',
|
key: 'some-object',
|
||||||
namespace: 'fooNameSpace'
|
namespace: 'fooNameSpace'
|
||||||
@ -68,6 +79,8 @@ describe("The Annotation API", () => {
|
|||||||
return mockDomainObject;
|
return mockDomainObject;
|
||||||
} else if (identifier.key === mockAnnotationObject.identifier.key) {
|
} else if (identifier.key === mockAnnotationObject.identifier.key) {
|
||||||
return mockAnnotationObject;
|
return mockAnnotationObject;
|
||||||
|
} else if (identifier.key === mockFolderObject.identifier.key) {
|
||||||
|
return mockFolderObject;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -150,6 +163,7 @@ describe("The Annotation API", () => {
|
|||||||
// use local worker
|
// use local worker
|
||||||
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
|
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
|
||||||
openmct.objects.inMemorySearchProvider.worker = null;
|
openmct.objects.inMemorySearchProvider.worker = null;
|
||||||
|
await openmct.objects.inMemorySearchProvider.index(mockFolderObject);
|
||||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject);
|
await openmct.objects.inMemorySearchProvider.index(mockDomainObject);
|
||||||
await openmct.objects.inMemorySearchProvider.index(mockAnnotationObject);
|
await openmct.objects.inMemorySearchProvider.index(mockAnnotationObject);
|
||||||
});
|
});
|
||||||
|
@ -63,6 +63,8 @@ class InMemorySearchProvider {
|
|||||||
this.localSearchForTags = this.localSearchForTags.bind(this);
|
this.localSearchForTags = this.localSearchForTags.bind(this);
|
||||||
this.localSearchForNotebookAnnotations = this.localSearchForNotebookAnnotations.bind(this);
|
this.localSearchForNotebookAnnotations = this.localSearchForNotebookAnnotations.bind(this);
|
||||||
this.onAnnotationCreation = this.onAnnotationCreation.bind(this);
|
this.onAnnotationCreation = this.onAnnotationCreation.bind(this);
|
||||||
|
this.onCompositionAdded = this.onCompositionAdded.bind(this);
|
||||||
|
this.onCompositionRemoved = this.onCompositionRemoved.bind(this);
|
||||||
this.onerror = this.onWorkerError.bind(this);
|
this.onerror = this.onWorkerError.bind(this);
|
||||||
this.startIndexing = this.startIndexing.bind(this);
|
this.startIndexing = this.startIndexing.bind(this);
|
||||||
|
|
||||||
@ -75,6 +77,12 @@ class InMemorySearchProvider {
|
|||||||
this.worker.port.close();
|
this.worker.port.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Object.keys(this.indexedCompositions).forEach(keyString => {
|
||||||
|
const composition = this.indexedCompositions[keyString];
|
||||||
|
composition.off('add', this.onCompositionAdded);
|
||||||
|
composition.off('remove', this.onCompositionRemoved);
|
||||||
|
});
|
||||||
|
|
||||||
this.destroyObservers(this.indexedIds);
|
this.destroyObservers(this.indexedIds);
|
||||||
this.destroyObservers(this.indexedCompositions);
|
this.destroyObservers(this.indexedCompositions);
|
||||||
});
|
});
|
||||||
@ -259,7 +267,6 @@ class InMemorySearchProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onAnnotationCreation(annotationObject) {
|
onAnnotationCreation(annotationObject) {
|
||||||
|
|
||||||
const objectProvider = this.openmct.objects.getProvider(annotationObject.identifier);
|
const objectProvider = this.openmct.objects.getProvider(annotationObject.identifier);
|
||||||
if (objectProvider === undefined || objectProvider.search === undefined) {
|
if (objectProvider === undefined || objectProvider.search === undefined) {
|
||||||
const provider = this;
|
const provider = this;
|
||||||
@ -281,17 +288,34 @@ class InMemorySearchProvider {
|
|||||||
provider.index(domainObject);
|
provider.index(domainObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
onCompositionMutation(domainObject, composition) {
|
onCompositionAdded(newDomainObjectToIndex) {
|
||||||
const provider = this;
|
const provider = this;
|
||||||
const indexedComposition = domainObject.composition;
|
// The object comes in as a mutable domain object, which has functions,
|
||||||
const identifiersToIndex = composition
|
// which the index function cannot handle as it will eventually be serialized
|
||||||
.filter(identifier => !indexedComposition
|
// using structuredClone. Thus we're using JSON.parse/JSON.stringify to discard
|
||||||
.some(indexedIdentifier => this.openmct.objects
|
// those functions.
|
||||||
.areIdsEqual([identifier, indexedIdentifier])));
|
const nonMutableDomainObject = JSON.parse(JSON.stringify(newDomainObjectToIndex));
|
||||||
|
|
||||||
identifiersToIndex.forEach(identifier => {
|
const objectProvider = this.openmct.objects.getProvider(nonMutableDomainObject.identifier);
|
||||||
this.openmct.objects.get(identifier).then(objectToIndex => provider.index(objectToIndex));
|
if (objectProvider === undefined || objectProvider.search === undefined) {
|
||||||
});
|
provider.index(nonMutableDomainObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCompositionRemoved(domainObjectToRemoveIdentifier) {
|
||||||
|
const keyString = this.openmct.objects.makeKeyString(domainObjectToRemoveIdentifier);
|
||||||
|
if (this.indexedIds[keyString]) {
|
||||||
|
// we store the unobserve function in the indexedId map
|
||||||
|
this.indexedIds[keyString]();
|
||||||
|
delete this.indexedIds[keyString];
|
||||||
|
}
|
||||||
|
|
||||||
|
const composition = this.indexedCompositions[keyString];
|
||||||
|
if (composition) {
|
||||||
|
composition.off('add', this.onCompositionAdded);
|
||||||
|
composition.off('remove', this.onCompositionRemoved);
|
||||||
|
delete this.indexedCompositions[keyString];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -305,6 +329,7 @@ class InMemorySearchProvider {
|
|||||||
async index(domainObject) {
|
async index(domainObject) {
|
||||||
const provider = this;
|
const provider = this;
|
||||||
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||||
|
const composition = this.openmct.composition.get(domainObject);
|
||||||
|
|
||||||
if (!this.indexedIds[keyString]) {
|
if (!this.indexedIds[keyString]) {
|
||||||
this.indexedIds[keyString] = this.openmct.objects.observe(
|
this.indexedIds[keyString] = this.openmct.objects.observe(
|
||||||
@ -312,11 +337,12 @@ class InMemorySearchProvider {
|
|||||||
'name',
|
'name',
|
||||||
this.onNameMutation.bind(this, domainObject)
|
this.onNameMutation.bind(this, domainObject)
|
||||||
);
|
);
|
||||||
this.indexedCompositions[keyString] = this.openmct.objects.observe(
|
if (composition) {
|
||||||
domainObject,
|
composition.on('add', this.onCompositionAdded);
|
||||||
'composition',
|
composition.on('remove', this.onCompositionRemoved);
|
||||||
this.onCompositionMutation.bind(this, domainObject)
|
this.indexedCompositions[keyString] = composition;
|
||||||
);
|
}
|
||||||
|
|
||||||
if (domainObject.type === 'annotation') {
|
if (domainObject.type === 'annotation') {
|
||||||
this.indexedTags[keyString] = this.openmct.objects.observe(
|
this.indexedTags[keyString] = this.openmct.objects.observe(
|
||||||
domainObject,
|
domainObject,
|
||||||
@ -338,8 +364,6 @@ class InMemorySearchProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const composition = this.openmct.composition.get(domainObject);
|
|
||||||
|
|
||||||
if (composition !== undefined) {
|
if (composition !== undefined) {
|
||||||
const children = await composition.load();
|
const children = await composition.load();
|
||||||
|
|
||||||
|
@ -34,11 +34,11 @@ import InMemorySearchProvider from './InMemorySearchProvider';
|
|||||||
* Uniquely identifies a domain object.
|
* Uniquely identifies a domain object.
|
||||||
*
|
*
|
||||||
* @typedef Identifier
|
* @typedef Identifier
|
||||||
* @memberof module:openmct.ObjectAPI~
|
|
||||||
* @property {string} namespace the namespace to/from which this domain
|
* @property {string} namespace the namespace to/from which this domain
|
||||||
* object should be loaded/stored.
|
* object should be loaded/stored.
|
||||||
* @property {string} key a unique identifier for the domain object
|
* @property {string} key a unique identifier for the domain object
|
||||||
* within that namespace
|
* within that namespace
|
||||||
|
* @memberof module:openmct.ObjectAPI~
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -230,15 +230,10 @@ export default class ObjectAPI {
|
|||||||
return result;
|
return result;
|
||||||
}).catch((result) => {
|
}).catch((result) => {
|
||||||
console.warn(`Failed to retrieve ${keystring}:`, result);
|
console.warn(`Failed to retrieve ${keystring}:`, result);
|
||||||
this.openmct.notifications.error(`Failed to retrieve object ${keystring}`);
|
|
||||||
|
|
||||||
delete this.cache[keystring];
|
delete this.cache[keystring];
|
||||||
|
|
||||||
if (!result) {
|
result = this.applyGetInterceptors(identifier);
|
||||||
//no result means resource either doesn't exist or is missing
|
|
||||||
//otherwise it's an error, and we shouldn't apply interceptors
|
|
||||||
result = this.applyGetInterceptors(identifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
@ -615,27 +610,60 @@ export default class ObjectAPI {
|
|||||||
* @param {module:openmct.ObjectAPI~Identifier[]} identifiers
|
* @param {module:openmct.ObjectAPI~Identifier[]} identifiers
|
||||||
*/
|
*/
|
||||||
areIdsEqual(...identifiers) {
|
areIdsEqual(...identifiers) {
|
||||||
|
const firstIdentifier = utils.parseKeyString(identifiers[0]);
|
||||||
|
|
||||||
return identifiers.map(utils.parseKeyString)
|
return identifiers.map(utils.parseKeyString)
|
||||||
.every(identifier => {
|
.every(identifier => {
|
||||||
return identifier === identifiers[0]
|
return identifier === firstIdentifier
|
||||||
|| (identifier.namespace === identifiers[0].namespace
|
|| (identifier.namespace === firstIdentifier.namespace
|
||||||
&& identifier.key === identifiers[0].key);
|
&& identifier.key === firstIdentifier.key);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getOriginalPath(identifier, path = []) {
|
/**
|
||||||
return this.get(identifier).then((domainObject) => {
|
* Given an original path check if the path is reachable via root
|
||||||
path.push(domainObject);
|
* @param {Array<Object>} originalPath an array of path objects to check
|
||||||
let location = domainObject.location;
|
* @returns {boolean} whether the domain object is reachable
|
||||||
|
*/
|
||||||
|
isReachable(originalPath) {
|
||||||
|
if (originalPath && originalPath.length) {
|
||||||
|
return (originalPath[originalPath.length - 1].type === 'root');
|
||||||
|
}
|
||||||
|
|
||||||
if (location) {
|
return false;
|
||||||
return this.getOriginalPath(utils.parseKeyString(location), path);
|
}
|
||||||
} else {
|
|
||||||
return path;
|
#pathContainsDomainObject(keyStringToCheck, path) {
|
||||||
}
|
if (!keyStringToCheck) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.some(pathElement => {
|
||||||
|
const identifierToCheck = utils.parseKeyString(keyStringToCheck);
|
||||||
|
|
||||||
|
return this.areIdsEqual(identifierToCheck, pathElement.identifier);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an identifier, constructs the original path by walking up its parents
|
||||||
|
* @param {module:openmct.ObjectAPI~Identifier} identifier
|
||||||
|
* @param {Array<module:openmct.DomainObject>} path an array of path objects
|
||||||
|
* @returns {Promise<Array<module:openmct.DomainObject>>} a promise containing an array of domain objects
|
||||||
|
*/
|
||||||
|
async getOriginalPath(identifier, path = []) {
|
||||||
|
const domainObject = await this.get(identifier);
|
||||||
|
path.push(domainObject);
|
||||||
|
const { location } = domainObject;
|
||||||
|
if (location && (!this.#pathContainsDomainObject(location, path))) {
|
||||||
|
// if we have a location, and we don't already have this in our constructed path,
|
||||||
|
// then keep walking up the path
|
||||||
|
return this.getOriginalPath(utils.parseKeyString(location), path);
|
||||||
|
} else {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
isObjectPathToALink(domainObject, objectPath) {
|
isObjectPathToALink(domainObject, objectPath) {
|
||||||
return objectPath !== undefined
|
return objectPath !== undefined
|
||||||
&& objectPath.length > 1
|
&& objectPath.length > 1
|
||||||
|
@ -377,6 +377,73 @@ describe("The Object API", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getOriginalPath", () => {
|
||||||
|
let mockGrandParentObject;
|
||||||
|
let mockParentObject;
|
||||||
|
let mockChildObject;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const mockObjectProvider = jasmine.createSpyObj("mock object provider", [
|
||||||
|
"create",
|
||||||
|
"update",
|
||||||
|
"get"
|
||||||
|
]);
|
||||||
|
|
||||||
|
mockGrandParentObject = {
|
||||||
|
type: 'folder',
|
||||||
|
name: 'Grand Parent Folder',
|
||||||
|
location: 'fooNameSpace:child',
|
||||||
|
identifier: {
|
||||||
|
key: 'grandParent',
|
||||||
|
namespace: 'fooNameSpace'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mockParentObject = {
|
||||||
|
type: 'folder',
|
||||||
|
name: 'Parent Folder',
|
||||||
|
location: 'fooNameSpace:grandParent',
|
||||||
|
identifier: {
|
||||||
|
key: 'parent',
|
||||||
|
namespace: 'fooNameSpace'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mockChildObject = {
|
||||||
|
type: 'folder',
|
||||||
|
name: 'Child Folder',
|
||||||
|
location: 'fooNameSpace:parent',
|
||||||
|
identifier: {
|
||||||
|
key: 'child',
|
||||||
|
namespace: 'fooNameSpace'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line require-await
|
||||||
|
mockObjectProvider.get = async (identifier) => {
|
||||||
|
if (identifier.key === mockGrandParentObject.identifier.key) {
|
||||||
|
return mockGrandParentObject;
|
||||||
|
} else if (identifier.key === mockParentObject.identifier.key) {
|
||||||
|
return mockParentObject;
|
||||||
|
} else if (identifier.key === mockChildObject.identifier.key) {
|
||||||
|
return mockChildObject;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
openmct.objects.addProvider('fooNameSpace', mockObjectProvider);
|
||||||
|
|
||||||
|
mockObjectProvider.create.and.returnValue(Promise.resolve(true));
|
||||||
|
mockObjectProvider.update.and.returnValue(Promise.resolve(true));
|
||||||
|
|
||||||
|
openmct.objects.addProvider('fooNameSpace', mockObjectProvider);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can construct paths even with cycles', async () => {
|
||||||
|
const objectPath = await objectAPI.getOriginalPath(mockChildObject.identifier);
|
||||||
|
expect(objectPath.length).toEqual(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("transactions", () => {
|
describe("transactions", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(openmct.editor, 'isEditing').and.returnValue(true);
|
spyOn(openmct.editor, 'isEditing').and.returnValue(true);
|
||||||
|
@ -91,6 +91,10 @@ define([
|
|||||||
* @returns keyString
|
* @returns keyString
|
||||||
*/
|
*/
|
||||||
function makeKeyString(identifier) {
|
function makeKeyString(identifier) {
|
||||||
|
if (!identifier) {
|
||||||
|
throw new Error("Cannot make key string from null identifier");
|
||||||
|
}
|
||||||
|
|
||||||
if (isKeyString(identifier)) {
|
if (isKeyString(identifier)) {
|
||||||
return identifier;
|
return identifier;
|
||||||
}
|
}
|
||||||
|
@ -97,11 +97,11 @@ export default {
|
|||||||
|
|
||||||
},
|
},
|
||||||
followTimeContext() {
|
followTimeContext() {
|
||||||
this.timeContext.on('bounds', this.reloadTelemetry);
|
this.timeContext.on('bounds', this.reloadTelemetryOnBoundsChange);
|
||||||
},
|
},
|
||||||
stopFollowingTimeContext() {
|
stopFollowingTimeContext() {
|
||||||
if (this.timeContext) {
|
if (this.timeContext) {
|
||||||
this.timeContext.off('bounds', this.reloadTelemetry);
|
this.timeContext.off('bounds', this.reloadTelemetryOnBoundsChange);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addToComposition(telemetryObject) {
|
addToComposition(telemetryObject) {
|
||||||
@ -181,6 +181,11 @@ export default {
|
|||||||
this.composition.on('remove', this.removeTelemetryObject);
|
this.composition.on('remove', this.removeTelemetryObject);
|
||||||
this.composition.load();
|
this.composition.load();
|
||||||
},
|
},
|
||||||
|
reloadTelemetryOnBoundsChange(bounds, isTick) {
|
||||||
|
if (!isTick) {
|
||||||
|
this.reloadTelemetry();
|
||||||
|
}
|
||||||
|
},
|
||||||
reloadTelemetry() {
|
reloadTelemetry() {
|
||||||
this.valuesByTimestamp = {};
|
this.valuesByTimestamp = {};
|
||||||
|
|
||||||
|
@ -51,7 +51,11 @@ export default class TelemetryCriterion extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initialize() {
|
initialize() {
|
||||||
this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry);
|
this.telemetryObjectIdAsString = "";
|
||||||
|
if (![undefined, null, ""].includes(this.telemetryDomainObjectDefinition?.telemetry)) {
|
||||||
|
this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry);
|
||||||
|
}
|
||||||
|
|
||||||
this.updateTelemetryObjects(this.telemetryDomainObjectDefinition.telemetryObjects);
|
this.updateTelemetryObjects(this.telemetryDomainObjectDefinition.telemetryObjects);
|
||||||
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
|
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
|
||||||
this.subscribeForStaleData();
|
this.subscribeForStaleData();
|
||||||
|
@ -517,7 +517,19 @@ export default {
|
|||||||
initializeItems() {
|
initializeItems() {
|
||||||
this.telemetryViewMap = {};
|
this.telemetryViewMap = {};
|
||||||
this.objectViewMap = {};
|
this.objectViewMap = {};
|
||||||
this.layoutItems.forEach(this.trackItem);
|
|
||||||
|
let removedItems = [];
|
||||||
|
this.layoutItems.forEach((item) => {
|
||||||
|
if (item.identifier) {
|
||||||
|
if (this.containsObject(item.identifier)) {
|
||||||
|
this.trackItem(item);
|
||||||
|
} else {
|
||||||
|
removedItems.push(this.openmct.objects.makeKeyString(item.identifier));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
removedItems.forEach(this.removeFromConfiguration);
|
||||||
},
|
},
|
||||||
isItemAlreadyTracked(child) {
|
isItemAlreadyTracked(child) {
|
||||||
let found = false;
|
let found = false;
|
||||||
|
@ -232,10 +232,12 @@ export default {
|
|||||||
this.removeSelectable();
|
this.removeSelectable();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.telemetryCollection.off('add', this.setLatestValues);
|
if (this.telemetryCollection) {
|
||||||
this.telemetryCollection.off('clear', this.refreshData);
|
this.telemetryCollection.off('add', this.setLatestValues);
|
||||||
|
this.telemetryCollection.off('clear', this.refreshData);
|
||||||
|
|
||||||
this.telemetryCollection.destroy();
|
this.telemetryCollection.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
if (this.mutablePromise) {
|
if (this.mutablePromise) {
|
||||||
this.mutablePromise.then(() => {
|
this.mutablePromise.then(() => {
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
import { createOpenMct, resetApplicationState } from 'utils/testing';
|
import { createOpenMct, resetApplicationState } from 'utils/testing';
|
||||||
|
import Vue from 'vue';
|
||||||
import DisplayLayoutPlugin from './plugin';
|
import DisplayLayoutPlugin from './plugin';
|
||||||
|
|
||||||
describe('the plugin', function () {
|
describe('the plugin', function () {
|
||||||
@ -117,6 +118,59 @@ describe('the plugin', function () {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('on load', () => {
|
||||||
|
let displayLayoutItem;
|
||||||
|
let item;
|
||||||
|
|
||||||
|
beforeEach((done) => {
|
||||||
|
item = {
|
||||||
|
'width': 32,
|
||||||
|
'height': 18,
|
||||||
|
'x': 78,
|
||||||
|
'y': 8,
|
||||||
|
'identifier': {
|
||||||
|
'namespace': '',
|
||||||
|
'key': 'bdeb91ab-3a7e-4a71-9dd2-39d73644e136'
|
||||||
|
},
|
||||||
|
'hasFrame': true,
|
||||||
|
'type': 'line-view', // so no telemetry functionality is triggered, just want to test the sync
|
||||||
|
'id': 'c0ff485a-344c-4e70-8d83-a9d9998a69fc'
|
||||||
|
|
||||||
|
};
|
||||||
|
displayLayoutItem = {
|
||||||
|
'composition': [
|
||||||
|
// no item in compostion, but item in configuration items
|
||||||
|
],
|
||||||
|
'configuration': {
|
||||||
|
'items': [
|
||||||
|
item
|
||||||
|
],
|
||||||
|
'layoutGrid': [
|
||||||
|
10,
|
||||||
|
10
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'name': 'Display Layout',
|
||||||
|
'type': 'layout',
|
||||||
|
'identifier': {
|
||||||
|
'namespace': '',
|
||||||
|
'key': 'c5e636c1-6771-4c9c-b933-8665cab189b3'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const applicableViews = openmct.objectViews.get(displayLayoutItem, []);
|
||||||
|
const displayLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'layout.view');
|
||||||
|
const view = displayLayoutViewProvider.view(displayLayoutItem);
|
||||||
|
view.show(child, false);
|
||||||
|
|
||||||
|
Vue.nextTick(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('will sync compostion and layout items', () => {
|
||||||
|
expect(displayLayoutItem.configuration.items.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('the alpha numeric format view', () => {
|
describe('the alpha numeric format view', () => {
|
||||||
let displayLayoutItem;
|
let displayLayoutItem;
|
||||||
let telemetryItem;
|
let telemetryItem;
|
||||||
|
@ -71,6 +71,8 @@ import FaultManagementToolbar from './FaultManagementToolbar.vue';
|
|||||||
|
|
||||||
import { FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS, FILTER_ITEMS, SORT_ITEMS } from './constants';
|
import { FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS, FILTER_ITEMS, SORT_ITEMS } from './constants';
|
||||||
|
|
||||||
|
const SEARCH_KEYS = ['id', 'triggerValueInfo', 'currentValueInfo', 'triggerTime', 'severity', 'name', 'shortDescription', 'namespace'];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
FaultManagementListHeader,
|
FaultManagementListHeader,
|
||||||
@ -125,27 +127,19 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
filterUsingSearchTerm(fault) {
|
filterUsingSearchTerm(fault) {
|
||||||
if (fault?.id?.toString().toLowerCase().includes(this.searchTerm)) {
|
if (!fault) {
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fault?.triggerValueInfo?.toString().toLowerCase().includes(this.searchTerm)) {
|
let match = false;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fault?.currentValueInfo?.toString().toLowerCase().includes(this.searchTerm)) {
|
SEARCH_KEYS.forEach((key) => {
|
||||||
return true;
|
if (fault[key]?.toString().toLowerCase().includes(this.searchTerm)) {
|
||||||
}
|
match = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (fault?.triggerTime.toString().toLowerCase().includes(this.searchTerm)) {
|
return match;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fault?.severity.toString().toLowerCase().includes(this.searchTerm)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
},
|
||||||
isSelected(fault) {
|
isSelected(fault) {
|
||||||
return Boolean(this.selectedFaults[fault.id]);
|
return Boolean(this.selectedFaults[fault.id]);
|
||||||
|
@ -24,10 +24,22 @@ import {
|
|||||||
createOpenMct,
|
createOpenMct,
|
||||||
resetApplicationState
|
resetApplicationState
|
||||||
} from '../../utils/testing';
|
} from '../../utils/testing';
|
||||||
import { FAULT_MANAGEMENT_TYPE } from './constants';
|
import {
|
||||||
|
FAULT_MANAGEMENT_TYPE,
|
||||||
|
FAULT_MANAGEMENT_VIEW,
|
||||||
|
FAULT_MANAGEMENT_NAMESPACE
|
||||||
|
} from './constants';
|
||||||
|
|
||||||
describe("The Fault Management Plugin", () => {
|
describe("The Fault Management Plugin", () => {
|
||||||
let openmct;
|
let openmct;
|
||||||
|
const faultDomainObject = {
|
||||||
|
name: 'it is not your fault',
|
||||||
|
type: FAULT_MANAGEMENT_TYPE,
|
||||||
|
identifier: {
|
||||||
|
key: 'nobodies',
|
||||||
|
namespace: 'fault'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
openmct = createOpenMct();
|
openmct = createOpenMct();
|
||||||
@ -38,15 +50,54 @@ describe("The Fault Management Plugin", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('is not installed by default', () => {
|
it('is not installed by default', () => {
|
||||||
let typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;
|
const typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;
|
||||||
|
|
||||||
expect(typeDef.name).toBe('Unknown Type');
|
expect(typeDef.name).toBe('Unknown Type');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can be installed', () => {
|
it('can be installed', () => {
|
||||||
openmct.install(openmct.plugins.FaultManagement());
|
openmct.install(openmct.plugins.FaultManagement());
|
||||||
let typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;
|
const typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;
|
||||||
|
|
||||||
expect(typeDef.name).toBe('Fault Management');
|
expect(typeDef.name).toBe('Fault Management');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('once it is installed', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
openmct.install(openmct.plugins.FaultManagement());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides a view for fault management types', () => {
|
||||||
|
const applicableViews = openmct.objectViews.get(faultDomainObject, []);
|
||||||
|
const faultManagementView = applicableViews.find(
|
||||||
|
(viewProvider) => viewProvider.key === FAULT_MANAGEMENT_VIEW
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(applicableViews.length).toEqual(1);
|
||||||
|
expect(faultManagementView).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides an inspector view for fault management types', () => {
|
||||||
|
const faultDomainObjectSelection = [[
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
item: faultDomainObject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]];
|
||||||
|
const applicableInspectorViews = openmct.inspectorViews.get(faultDomainObjectSelection);
|
||||||
|
|
||||||
|
expect(applicableInspectorViews.length).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates a root object for fault management', async () => {
|
||||||
|
const root = await openmct.objects.getRoot();
|
||||||
|
const rootCompositionCollection = openmct.composition.get(root);
|
||||||
|
const rootComposition = await rootCompositionCollection.load();
|
||||||
|
const faultObject = rootComposition.find(obj => obj.identifier.namespace === FAULT_MANAGEMENT_NAMESPACE);
|
||||||
|
|
||||||
|
expect(faultObject).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
68
src/plugins/imagery/components/ImageThumbnail.vue
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<!--
|
||||||
|
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="c-imagery__thumb c-thumb"
|
||||||
|
:class="{
|
||||||
|
'active': active,
|
||||||
|
'selected': selected,
|
||||||
|
'real-time': realTime
|
||||||
|
}"
|
||||||
|
:title="image.formattedTime"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href=""
|
||||||
|
:download="image.imageDownloadName"
|
||||||
|
@click.prevent
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="c-thumb__image"
|
||||||
|
:src="image.url"
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
image: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
selected: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
realTime: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@ -166,26 +166,15 @@
|
|||||||
class="c-imagery__thumbs-scroll-area"
|
class="c-imagery__thumbs-scroll-area"
|
||||||
@scroll="handleScroll"
|
@scroll="handleScroll"
|
||||||
>
|
>
|
||||||
<div
|
<ImageThumbnail
|
||||||
v-for="(image, index) in imageHistory"
|
v-for="(image, index) in imageHistory"
|
||||||
:key="image.url + image.time"
|
:key="image.url + image.time"
|
||||||
class="c-imagery__thumb c-thumb"
|
:image="image"
|
||||||
:class="{ selected: focusedImageIndex === index && isPaused }"
|
:active="focusedImageIndex === index"
|
||||||
:title="image.formattedTime"
|
:selected="focusedImageIndex === index && isPaused"
|
||||||
@click="thumbnailClicked(index)"
|
:real-time="!isFixed"
|
||||||
>
|
@click.native="thumbnailClicked(index)"
|
||||||
<a
|
/>
|
||||||
href=""
|
|
||||||
:download="image.imageDownloadName"
|
|
||||||
@click.prevent
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="c-thumb__image"
|
|
||||||
:src="image.url"
|
|
||||||
>
|
|
||||||
</a>
|
|
||||||
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@ -205,6 +194,7 @@ import moment from 'moment';
|
|||||||
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
|
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
|
||||||
import Compass from './Compass/Compass.vue';
|
import Compass from './Compass/Compass.vue';
|
||||||
import ImageControls from './ImageControls.vue';
|
import ImageControls from './ImageControls.vue';
|
||||||
|
import ImageThumbnail from './ImageThumbnail.vue';
|
||||||
import imageryData from "../../imagery/mixins/imageryData";
|
import imageryData from "../../imagery/mixins/imageryData";
|
||||||
|
|
||||||
const REFRESH_CSS_MS = 500;
|
const REFRESH_CSS_MS = 500;
|
||||||
@ -229,9 +219,11 @@ const SHOW_THUMBS_THRESHOLD_HEIGHT = 200;
|
|||||||
const SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT = 600;
|
const SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT = 600;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
name: 'ImageryView',
|
||||||
components: {
|
components: {
|
||||||
Compass,
|
Compass,
|
||||||
ImageControls
|
ImageControls,
|
||||||
|
ImageThumbnail
|
||||||
},
|
},
|
||||||
mixins: [imageryData],
|
mixins: [imageryData],
|
||||||
inject: ['openmct', 'domainObject', 'objectPath', 'currentView', 'imageFreshnessOptions'],
|
inject: ['openmct', 'domainObject', 'objectPath', 'currentView', 'imageFreshnessOptions'],
|
||||||
@ -254,6 +246,7 @@ export default {
|
|||||||
visibleLayers: [],
|
visibleLayers: [],
|
||||||
durationFormatter: undefined,
|
durationFormatter: undefined,
|
||||||
imageHistory: [],
|
imageHistory: [],
|
||||||
|
bounds: {},
|
||||||
timeSystem: timeSystem,
|
timeSystem: timeSystem,
|
||||||
keyString: undefined,
|
keyString: undefined,
|
||||||
autoScroll: true,
|
autoScroll: true,
|
||||||
@ -526,20 +519,17 @@ export default {
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
imageHistory: {
|
imageHistory: {
|
||||||
handler(newHistory, oldHistory) {
|
handler(newHistory, _oldHistory) {
|
||||||
const newSize = newHistory.length;
|
const newSize = newHistory.length;
|
||||||
let imageIndex;
|
let imageIndex = newSize > 0 ? newSize - 1 : undefined;
|
||||||
if (this.focusedImageTimestamp !== undefined) {
|
if (this.focusedImageTimestamp !== undefined) {
|
||||||
const foundImageIndex = newHistory.findIndex(img => img.time === this.focusedImageTimestamp);
|
const foundImageIndex = newHistory.findIndex(img => img.time === this.focusedImageTimestamp);
|
||||||
imageIndex = foundImageIndex > -1
|
if (foundImageIndex > -1) {
|
||||||
? foundImageIndex
|
imageIndex = foundImageIndex;
|
||||||
: newSize - 1;
|
}
|
||||||
} else {
|
|
||||||
imageIndex = newSize > 0
|
|
||||||
? newSize - 1
|
|
||||||
: undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setFocusedImage(imageIndex);
|
||||||
this.nextImageIndex = imageIndex;
|
this.nextImageIndex = imageIndex;
|
||||||
|
|
||||||
if (this.previousFocusedImage && newHistory.length) {
|
if (this.previousFocusedImage && newHistory.length) {
|
||||||
@ -569,6 +559,16 @@ export default {
|
|||||||
this.resetAgeCSS();
|
this.resetAgeCSS();
|
||||||
this.updateRelatedTelemetryForFocusedImage();
|
this.updateRelatedTelemetryForFocusedImage();
|
||||||
this.getImageNaturalDimensions();
|
this.getImageNaturalDimensions();
|
||||||
|
},
|
||||||
|
bounds() {
|
||||||
|
this.scrollToFocused();
|
||||||
|
},
|
||||||
|
isFixed(newValue) {
|
||||||
|
const isRealTime = !newValue;
|
||||||
|
// if realtime unpause which will focus on latest image
|
||||||
|
if (isRealTime) {
|
||||||
|
this.paused(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
@ -610,6 +610,7 @@ export default {
|
|||||||
this.handleScroll = _.debounce(this.handleScroll, SCROLL_LATENCY);
|
this.handleScroll = _.debounce(this.handleScroll, SCROLL_LATENCY);
|
||||||
this.handleThumbWindowResizeEnded = _.debounce(this.handleThumbWindowResizeEnded, SCROLL_LATENCY);
|
this.handleThumbWindowResizeEnded = _.debounce(this.handleThumbWindowResizeEnded, SCROLL_LATENCY);
|
||||||
this.handleThumbWindowResizeStart = _.debounce(this.handleThumbWindowResizeStart, SCROLL_LATENCY);
|
this.handleThumbWindowResizeStart = _.debounce(this.handleThumbWindowResizeStart, SCROLL_LATENCY);
|
||||||
|
this.scrollToFocused = _.debounce(this.scrollToFocused, 400);
|
||||||
|
|
||||||
if (this.$refs.thumbsWrapper) {
|
if (this.$refs.thumbsWrapper) {
|
||||||
this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);
|
this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);
|
||||||
@ -845,7 +846,8 @@ export default {
|
|||||||
if (domThumb) {
|
if (domThumb) {
|
||||||
domThumb.scrollIntoView({
|
domThumb.scrollIntoView({
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
block: 'center'
|
block: 'center',
|
||||||
|
inline: 'center'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -258,13 +258,22 @@
|
|||||||
min-width: $w;
|
min-width: $w;
|
||||||
width: $w;
|
width: $w;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: $colorSelectedBg;
|
||||||
|
color: $colorSelectedFg;
|
||||||
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
background: $colorThumbHoverBg;
|
background: $colorThumbHoverBg;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
background: $colorPausedBg !important;
|
// fixed time - selected bg will match active bg color
|
||||||
color: $colorPausedFg !important;
|
background: $colorSelectedBg;
|
||||||
|
color: $colorSelectedFg;
|
||||||
|
&.real-time {
|
||||||
|
// real time - bg orange when selected
|
||||||
|
background: $colorPausedBg !important;
|
||||||
|
color: $colorPausedFg !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__image {
|
&__image {
|
||||||
|
@ -139,6 +139,7 @@ export default {
|
|||||||
// forcibly reset the imageContainer size to prevent an aspect ratio distortion
|
// forcibly reset the imageContainer size to prevent an aspect ratio distortion
|
||||||
delete this.imageContainerWidth;
|
delete this.imageContainerWidth;
|
||||||
delete this.imageContainerHeight;
|
delete this.imageContainerHeight;
|
||||||
|
this.bounds = bounds; // setting bounds for ImageryView watcher
|
||||||
},
|
},
|
||||||
timeSystemChange() {
|
timeSystemChange() {
|
||||||
this.timeSystem = this.timeContext.timeSystem();
|
this.timeSystem = this.timeContext.timeSystem();
|
||||||
|
@ -27,10 +27,13 @@ export default function MissingObjectInterceptor(openmct) {
|
|||||||
},
|
},
|
||||||
invoke: (identifier, object) => {
|
invoke: (identifier, object) => {
|
||||||
if (object === undefined) {
|
if (object === undefined) {
|
||||||
|
const keyString = openmct.objects.makeKeyString(identifier);
|
||||||
|
openmct.notifications.error(`Failed to retrieve object ${keyString}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
identifier,
|
identifier,
|
||||||
type: 'unknown',
|
type: 'unknown',
|
||||||
name: 'Missing: ' + openmct.objects.makeKeyString(identifier)
|
name: 'Missing: ' + keyString
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,7 +83,6 @@ export default class LinkAction {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
this.openmct.forms.showForm(formStructure)
|
this.openmct.forms.showForm(formStructure)
|
||||||
.then(this.onSave.bind(this));
|
.then(this.onSave.bind(this));
|
||||||
}
|
}
|
||||||
@ -91,8 +90,8 @@ export default class LinkAction {
|
|||||||
validate(currentParent) {
|
validate(currentParent) {
|
||||||
return (data) => {
|
return (data) => {
|
||||||
|
|
||||||
// default current parent to ROOT, if it's undefined, then it's a root level item
|
// default current parent to ROOT, if it's null, then it's a root level item
|
||||||
if (currentParent === undefined) {
|
if (!currentParent) {
|
||||||
currentParent = {
|
currentParent = {
|
||||||
identifier: {
|
identifier: {
|
||||||
key: 'ROOT',
|
key: 'ROOT',
|
||||||
@ -101,24 +100,23 @@ export default class LinkAction {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentCandidate = data.value[0];
|
const parentCandidatePath = data.value;
|
||||||
const currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
|
const parentCandidate = parentCandidatePath[0];
|
||||||
const parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier);
|
|
||||||
const objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);
|
const objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);
|
||||||
|
|
||||||
if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) {
|
if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parentCandidateKeystring || !currentParentKeystring) {
|
// check if moving to same place
|
||||||
|
if (this.openmct.objects.areIdsEqual(parentCandidate.identifier, currentParent.identifier)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parentCandidateKeystring === currentParentKeystring) {
|
// check if moving to a child
|
||||||
return false;
|
if (parentCandidatePath.some(candidatePath => {
|
||||||
}
|
return this.openmct.objects.areIdsEqual(candidatePath.identifier, this.object.identifier);
|
||||||
|
})) {
|
||||||
if (parentCandidateKeystring === objectKeystring) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,26 +145,24 @@ export default class MoveAction {
|
|||||||
const parentCandidatePath = data.value;
|
const parentCandidatePath = data.value;
|
||||||
const parentCandidate = parentCandidatePath[0];
|
const parentCandidate = parentCandidatePath[0];
|
||||||
|
|
||||||
|
// check if moving to same place
|
||||||
|
if (this.openmct.objects.areIdsEqual(parentCandidate.identifier, currentParent.identifier)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if moving to a child
|
||||||
|
if (parentCandidatePath.some(candidatePath => {
|
||||||
|
return this.openmct.objects.areIdsEqual(candidatePath.identifier, this.object.identifier);
|
||||||
|
})) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) {
|
if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
|
|
||||||
let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier);
|
|
||||||
let objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);
|
let objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);
|
||||||
|
|
||||||
if (!parentCandidateKeystring || !currentParentKeystring) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parentCandidateKeystring === currentParentKeystring) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parentCandidateKeystring === objectKeystring) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentCandidateComposition = parentCandidate.composition;
|
const parentCandidateComposition = parentCandidate.composition;
|
||||||
if (parentCandidateComposition && parentCandidateComposition.indexOf(objectKeystring) !== -1) {
|
if (parentCandidateComposition && parentCandidateComposition.indexOf(objectKeystring) !== -1) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -69,27 +69,27 @@ describe("the plugin", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('adds an interceptor that returns a "My Items" model for', () => {
|
describe('adds an interceptor that returns a "My Items" model for', () => {
|
||||||
let myItemsMissing;
|
let myItemsObject;
|
||||||
let mockMissingProvider;
|
let mockNotFoundProvider;
|
||||||
let activeProvider;
|
let activeProvider;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockMissingProvider = {
|
mockNotFoundProvider = {
|
||||||
get: () => Promise.resolve(missingObj),
|
get: () => Promise.reject(new Error('Not found')),
|
||||||
create: () => Promise.resolve(missingObj),
|
create: () => Promise.resolve(missingObj),
|
||||||
update: () => Promise.resolve(missingObj)
|
update: () => Promise.resolve(missingObj)
|
||||||
};
|
};
|
||||||
|
|
||||||
activeProvider = mockMissingProvider;
|
activeProvider = mockNotFoundProvider;
|
||||||
spyOn(openmct.objects, 'getProvider').and.returnValue(activeProvider);
|
spyOn(openmct.objects, 'getProvider').and.returnValue(activeProvider);
|
||||||
myItemsMissing = await openmct.objects.get(myItemsIdentifier);
|
myItemsObject = await openmct.objects.get(myItemsIdentifier);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('missing objects', () => {
|
it('missing objects', () => {
|
||||||
let idsMatchMissing = openmct.objects.areIdsEqual(myItemsMissing.identifier, myItemsIdentifier);
|
let idsMatch = openmct.objects.areIdsEqual(myItemsObject.identifier, myItemsIdentifier);
|
||||||
|
|
||||||
expect(myItemsMissing).toBeDefined();
|
expect(myItemsObject).toBeDefined();
|
||||||
expect(idsMatchMissing).toBeTrue();
|
expect(idsMatch).toBeTrue();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
5
src/plugins/persistence/couch/.env.ci
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
OPENMCT_DATABASE_NAME=openmct
|
||||||
|
COUCH_ADMIN_USER=admin
|
||||||
|
COUCH_ADMIN_PASSWORD=password
|
||||||
|
COUCH_BASE_LOCAL=http://localhost:5984
|
||||||
|
COUCH_NODE_NAME=nonode@nohost
|
@ -1,52 +1,145 @@
|
|||||||
# Introduction
|
|
||||||
These instructions are for setting up CouchDB for a **development** environment. For a production environment, we recommend running Open MCT behind a proxy server (e.g., Nginx or Apache), and securing the CouchDB server properly:
|
|
||||||
https://docs.couchdb.org/en/main/intro/security.html
|
|
||||||
|
|
||||||
# Installing CouchDB
|
# Installing CouchDB
|
||||||
## macOS
|
|
||||||
### Installing with admin privileges to your computer
|
## Introduction
|
||||||
|
|
||||||
|
These instructions are for setting up CouchDB for a **development** environment. For a production environment, we recommend running Open MCT behind a proxy server (e.g., Nginx or Apache), and securing the CouchDB server properly:
|
||||||
|
<https://docs.couchdb.org/en/main/intro/security.html>
|
||||||
|
|
||||||
|
## Docker Quickstart
|
||||||
|
|
||||||
|
The following process is the preferred way of using CouchDB as it is automatic and closely resembles a production environment.
|
||||||
|
|
||||||
|
Requirement:
|
||||||
|
Get docker compose (or recent version of docker) installed on your machine. We recommend [Docker Desktop](https://www.docker.com/products/docker-desktop/)
|
||||||
|
|
||||||
|
1. Open a terminal to this current working directory (`cd openmct/src/plugins/persistence/couch`)
|
||||||
|
2. Create and start the `couchdb` container:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose -f ./couchdb-compose.yaml up --detach
|
||||||
|
```
|
||||||
|
3. Copy `.env.ci` file to file named `.env.local`
|
||||||
|
4. (Optional) Change the values of `.env.local` if desired
|
||||||
|
5. Set the environment variables in bash by sourcing the env file
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export $(cat .env.local | xargs)
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Execute the configuration script:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sh ./setup-couchdb.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
7. `cd` to the workspace root directory (the same directory as `index.html`)
|
||||||
|
8. Update `index.html` to use the CouchDB plugin as persistence store:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sh ./src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
|
||||||
|
```
|
||||||
|
9. ✅ Done!
|
||||||
|
|
||||||
|
Open MCT will now use your local CouchDB container as its persistence store. Access the CouchDB instance manager by visiting <http://localhost:5984/_utils>.
|
||||||
|
|
||||||
|
## macOS
|
||||||
|
|
||||||
|
While we highly recommend using the CouchDB docker-compose installation, it is still possible to install CouchDB through other means.
|
||||||
|
|
||||||
|
### Installing CouchDB
|
||||||
|
|
||||||
1. Install CouchDB using: `brew install couchdb`.
|
1. Install CouchDB using: `brew install couchdb`.
|
||||||
2. Edit `/usr/local/etc/local.ini` and add the following settings:
|
2. Edit `/usr/local/etc/local.ini` and add the following settings:
|
||||||
```
|
|
||||||
|
```txt
|
||||||
[admins]
|
[admins]
|
||||||
admin = youradminpassword
|
admin = youradminpassword
|
||||||
```
|
```
|
||||||
|
|
||||||
And set the server up for single node:
|
And set the server up for single node:
|
||||||
```
|
|
||||||
|
```txt
|
||||||
[couchdb]
|
[couchdb]
|
||||||
single_node=true
|
single_node=true
|
||||||
```
|
```
|
||||||
|
|
||||||
Enable CORS
|
Enable CORS
|
||||||
```
|
|
||||||
|
```txt
|
||||||
[chttpd]
|
[chttpd]
|
||||||
enable_cors = true
|
enable_cors = true
|
||||||
[cors]
|
[cors]
|
||||||
origins = http://localhost:8080
|
origins = http://localhost:8080
|
||||||
```
|
```
|
||||||
### Installing without admin privileges to your computer
|
|
||||||
1. Install CouchDB following these instructions: https://docs.brew.sh/Installation#untar-anywhere.
|
|
||||||
|
### Installing CouchDB without admin privileges to your computer
|
||||||
|
|
||||||
|
If `brew` is not available on your mac machine, you'll need to get the CouchDB installed using the official sourcefiles.
|
||||||
|
1. Install CouchDB following these instructions: <https://docs.brew.sh/Installation#untar-anywhere>.
|
||||||
1. Edit `local.ini` in Homebrew's `/etc/` directory as directed above in the 'Installing with admin privileges to your computer' section.
|
1. Edit `local.ini` in Homebrew's `/etc/` directory as directed above in the 'Installing with admin privileges to your computer' section.
|
||||||
|
|
||||||
## Other Operating Systems
|
## Other Operating Systems
|
||||||
Follow the installation instructions from the CouchDB installation guide: https://docs.couchdb.org/en/stable/install/index.html
|
|
||||||
|
Follow the installation instructions from the CouchDB installation guide: <https://docs.couchdb.org/en/stable/install/index.html>
|
||||||
|
|
||||||
# Configuring CouchDB
|
# Configuring CouchDB
|
||||||
|
|
||||||
|
## Configuration script
|
||||||
|
|
||||||
|
The simplest way to config a CouchDB instance is to use our provided tooling:
|
||||||
|
1. Copy `.env.ci` file to file named `.env.local`
|
||||||
|
2. Set the environment variables in bash by sourcing the env file
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export $(cat .env.local | xargs)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Execute the configuration script:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sh ./setup-couchdb.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Configuration
|
||||||
|
|
||||||
1. Start CouchDB by running: `couchdb`.
|
1. Start CouchDB by running: `couchdb`.
|
||||||
2. Add the `_global_changes` database using `curl` (note the `youradminpassword` should be changed to what you set above 👆): `curl -X PUT http://admin:youradminpassword@127.0.0.1:5984/_global_changes`
|
2. Add the `_global_changes` database using `curl` (note the `youradminpassword` should be changed to what you set above 👆): `curl -X PUT http://admin:youradminpassword@127.0.0.1:5984/_global_changes`
|
||||||
3. Navigate to http://localhost:5984/_utils
|
3. Navigate to <http://localhost:5984/_utils>
|
||||||
4. Create a database called `openmct`
|
4. Create a database called `openmct`
|
||||||
5. Navigate to http://127.0.0.1:5984/_utils/#/database/openmct/permissions
|
5. Navigate to <http://127.0.0.1:5984/_utils/#/database/openmct/permissions>
|
||||||
6. Remove permission restrictions in CouchDB from Open MCT by deleting `_admin` roles for both `Admin` and `Member`.
|
6. Remove permission restrictions in CouchDB from Open MCT by deleting `_admin` roles for both `Admin` and `Member`.
|
||||||
|
|
||||||
# Configuring Open MCT
|
# Configuring Open MCT to use CouchDB
|
||||||
|
|
||||||
|
## Configuration script
|
||||||
|
The simplest way to config a CouchDB instance is to use our provided tooling:
|
||||||
|
1. `cd` to the workspace root directory (the same directory as `index.html`)
|
||||||
|
2. Update `index.html` to use the CouchDB plugin as persistence store:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sh ./src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Configuration
|
||||||
|
|
||||||
1. Edit `openmct/index.html` comment out the following line:
|
1. Edit `openmct/index.html` comment out the following line:
|
||||||
```
|
|
||||||
openmct.install(openmct.plugins.LocalStorage());
|
```js
|
||||||
```
|
openmct.install(openmct.plugins.LocalStorage());
|
||||||
Add a line to install the CouchDB plugin for Open MCT:
|
```
|
||||||
```
|
|
||||||
openmct.install(openmct.plugins.CouchDB("http://localhost:5984/openmct"));
|
Add a line to install the CouchDB plugin for Open MCT:
|
||||||
```
|
|
||||||
2. Start Open MCT by running `npm start` in the `openmct` path.
|
```js
|
||||||
3. Navigate to http://localhost:8080/ and create a random object in Open MCT (e.g., a 'Clock') and save. You may get an error saying that the object failed to persist - this is a known error that you can ignore, and will only happen the first time you save - just try again.
|
openmct.install(openmct.plugins.CouchDB("http://localhost:5984/openmct"));
|
||||||
4. Navigate to: http://127.0.0.1:5984/_utils/#database/openmct/_all_docs
|
```
|
||||||
5. Look at the 'JSON' tab and ensure you can see the specific object you created above.
|
|
||||||
6. All done! 🏆
|
# Validating a successful Installation
|
||||||
|
|
||||||
|
1. Start Open MCT by running `npm start` in the `openmct` path.
|
||||||
|
2. Navigate to <http://localhost:8080/> and create a random object in Open MCT (e.g., a 'Clock') and save. You may get an error saying that the object failed to persist - this is a known error that you can ignore, and will only happen the first time you save - just try again.
|
||||||
|
3. Navigate to: <http://127.0.0.1:5984/_utils/#database/openmct/_all_docs>
|
||||||
|
4. Look at the 'JSON' tab and ensure you can see the specific object you created above.
|
||||||
|
5. All done! 🏆
|
||||||
|
14
src/plugins/persistence/couch/couchdb-compose.yaml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
couchdb:
|
||||||
|
image: couchdb:${COUCHDB_IMAGE_TAG:-3.2.1}
|
||||||
|
ports:
|
||||||
|
- "5984:5984"
|
||||||
|
- "5986:5986"
|
||||||
|
volumes:
|
||||||
|
- couchdb:/opt/couchdb/data
|
||||||
|
environment:
|
||||||
|
COUCHDB_USER: admin
|
||||||
|
COUCHDB_PASSWORD: password
|
||||||
|
volumes:
|
||||||
|
couchdb:
|
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash -e
|
||||||
|
|
||||||
|
sed -i'.bak' -e 's/LocalStorage()/CouchDB("http:\/\/localhost:5984\/openmct")/g' index.html
|
125
src/plugins/persistence/couch/setup-couchdb.sh
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
#!/bin/bash -e
|
||||||
|
|
||||||
|
# Do a couple checks for environment variables we expect to have a value.
|
||||||
|
|
||||||
|
if [ -z "${OPENMCT_DATABASE_NAME}" ] ; then
|
||||||
|
echo "OPENMCT_DATABASE_NAME has no value" 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${COUCH_ADMIN_USER}" ] ; then
|
||||||
|
echo "COUCH_ADMIN_USER has no value" 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${COUCH_BASE_LOCAL}" ] ; then
|
||||||
|
echo "COUCH_BASE_LOCAL has no value" 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Come up with what we'll be providing to curl's -u option. Always supply the username from the environment,
|
||||||
|
# and optionally supply the password from the environment, if it has a value.
|
||||||
|
CURL_USERPASS_ARG="${COUCH_ADMIN_USER}"
|
||||||
|
if [ "${COUCH_ADMIN_PASSWORD}" ] ; then
|
||||||
|
CURL_USERPASS_ARG+=":${COUCH_ADMIN_PASSWORD}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
system_tables_exist () {
|
||||||
|
resource_exists $COUCH_BASE_LOCAL/_users
|
||||||
|
}
|
||||||
|
|
||||||
|
create_users_db () {
|
||||||
|
curl -su "${CURL_USERPASS_ARG}" -X PUT $COUCH_BASE_LOCAL/_users
|
||||||
|
}
|
||||||
|
|
||||||
|
create_replicator_db () {
|
||||||
|
curl -su "${CURL_USERPASS_ARG}" -X PUT $COUCH_BASE_LOCAL/_replicator
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_system_tables () {
|
||||||
|
users_db_response=$(create_users_db)
|
||||||
|
if [ "{\"ok\":true}" == "${users_db_response}" ]; then
|
||||||
|
echo Successfully created users db
|
||||||
|
replicator_db_response=$(create_replicator_db)
|
||||||
|
if [ "{\"ok\":true}" == "${replicator_db_response}" ]; then
|
||||||
|
echo Successfully created replicator DB
|
||||||
|
else
|
||||||
|
echo Unable to create replicator DB
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo Unable to create users db
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
resource_exists () {
|
||||||
|
response=$(curl -u "${CURL_USERPASS_ARG}" -s -o /dev/null -I -w "%{http_code}" $1);
|
||||||
|
if [ "200" == "${response}" ]; then
|
||||||
|
echo "TRUE"
|
||||||
|
else
|
||||||
|
echo "FALSE";
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
db_exists () {
|
||||||
|
resource_exists $COUCH_BASE_LOCAL/$OPENMCT_DATABASE_NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
create_db () {
|
||||||
|
response=$(curl -su "${CURL_USERPASS_ARG}" -XPUT $COUCH_BASE_LOCAL/$OPENMCT_DATABASE_NAME);
|
||||||
|
echo $response
|
||||||
|
}
|
||||||
|
|
||||||
|
admin_user_exists () {
|
||||||
|
response=$(curl -su "${CURL_USERPASS_ARG}" -o /dev/null -I -w "%{http_code}" $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/admins/$COUCH_ADMIN_USER);
|
||||||
|
if [ "200" == "${response}" ]; then
|
||||||
|
echo "TRUE"
|
||||||
|
else
|
||||||
|
echo "FALSE";
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
create_admin_user () {
|
||||||
|
echo Creating admin user
|
||||||
|
curl -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/admins/$COUCH_ADMIN_USER -d \'"$COUCH_ADMIN_PASSWORD"\'
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$(admin_user_exists)" == "FALSE" ]; then
|
||||||
|
echo "Admin user does not exist, creating..."
|
||||||
|
create_admin_user
|
||||||
|
else
|
||||||
|
echo "Admin user exists"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "TRUE" == $(system_tables_exist) ]; then
|
||||||
|
echo System tables exist, skipping creation
|
||||||
|
else
|
||||||
|
echo Is fresh install, creating system tables
|
||||||
|
setup_system_tables
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "FALSE" == $(db_exists) ]; then
|
||||||
|
response=$(create_db)
|
||||||
|
if [ "{\"ok\":true}" == "${response}" ]; then
|
||||||
|
echo Database successfully created
|
||||||
|
else
|
||||||
|
echo Database creation failed
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo Database already exists, nothing to do
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Updating _replicator database permissions"
|
||||||
|
response=$(curl -su "${CURL_USERPASS_ARG}" --location --request PUT $COUCH_BASE_LOCAL/_replicator/_security --header 'Content-Type: application/json' --data-raw '{ "admins": {"roles": []},"members": {"roles": []}}');
|
||||||
|
if [ "{\"ok\":true}" == "${response}" ]; then
|
||||||
|
echo "Database permissions successfully updated"
|
||||||
|
else
|
||||||
|
echo "Database permissions not updated"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Updating ${OPENMCT_DATABASE_NAME} database permissions"
|
||||||
|
response=$(curl -su "${CURL_USERPASS_ARG}" --location --request PUT $COUCH_BASE_LOCAL/$OPENMCT_DATABASE_NAME/_security --header 'Content-Type: application/json' --data-raw '{ "admins": {"roles": []},"members": {"roles": []}}');
|
||||||
|
if [ "{\"ok\":true}" == "${response}" ]; then
|
||||||
|
echo "Database permissions successfully updated"
|
||||||
|
else
|
||||||
|
echo "Database permissions not updated"
|
||||||
|
fi
|
@ -32,7 +32,7 @@ define([
|
|||||||
'./autoflow/AutoflowTabularPlugin',
|
'./autoflow/AutoflowTabularPlugin',
|
||||||
'./timeConductor/plugin',
|
'./timeConductor/plugin',
|
||||||
'../../example/imagery/plugin',
|
'../../example/imagery/plugin',
|
||||||
'../../example/faultManagment/exampleFaultSource',
|
'../../example/faultManagement/exampleFaultSource',
|
||||||
'./imagery/plugin',
|
'./imagery/plugin',
|
||||||
'./summaryWidget/plugin',
|
'./summaryWidget/plugin',
|
||||||
'./URLIndicatorPlugin/URLIndicatorPlugin',
|
'./URLIndicatorPlugin/URLIndicatorPlugin',
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
const DEFAULT_DURATION_FORMATTER = 'duration';
|
const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||||
const LOCAL_STORAGE_HISTORY_KEY_FIXED = 'tcHistory';
|
const LOCAL_STORAGE_HISTORY_KEY_FIXED = 'tcHistory';
|
||||||
const LOCAL_STORAGE_HISTORY_KEY_REALTIME = 'tcHistoryRealtime';
|
const LOCAL_STORAGE_HISTORY_KEY_REALTIME = 'tcHistoryRealtime';
|
||||||
const DEFAULT_RECORDS = 10;
|
const DEFAULT_RECORDS_LENGTH = 10;
|
||||||
|
|
||||||
import { millisecondsToDHMS } from "utils/duration";
|
import { millisecondsToDHMS } from "utils/duration";
|
||||||
import UTCTimeFormat from "../utcTimeSystem/UTCTimeFormat.js";
|
import UTCTimeFormat from "../utcTimeSystem/UTCTimeFormat.js";
|
||||||
@ -79,16 +79,14 @@ export default {
|
|||||||
* @timespans {start, end} number representing timestamp
|
* @timespans {start, end} number representing timestamp
|
||||||
*/
|
*/
|
||||||
fixedHistory: {},
|
fixedHistory: {},
|
||||||
presets: []
|
presets: [],
|
||||||
|
isFixed: this.openmct.time.clock() === undefined
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
currentHistory() {
|
currentHistory() {
|
||||||
return this.mode + 'History';
|
return this.mode + 'History';
|
||||||
},
|
},
|
||||||
isFixed() {
|
|
||||||
return this.openmct.time.clock() === undefined;
|
|
||||||
},
|
|
||||||
historyForCurrentTimeSystem() {
|
historyForCurrentTimeSystem() {
|
||||||
const history = this[this.currentHistory][this.timeSystem.key];
|
const history = this[this.currentHistory][this.timeSystem.key];
|
||||||
|
|
||||||
@ -96,7 +94,7 @@ export default {
|
|||||||
},
|
},
|
||||||
storageKey() {
|
storageKey() {
|
||||||
let key = LOCAL_STORAGE_HISTORY_KEY_FIXED;
|
let key = LOCAL_STORAGE_HISTORY_KEY_FIXED;
|
||||||
if (this.mode !== 'fixed') {
|
if (!this.isFixed) {
|
||||||
key = LOCAL_STORAGE_HISTORY_KEY_REALTIME;
|
key = LOCAL_STORAGE_HISTORY_KEY_REALTIME;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,6 +106,7 @@ export default {
|
|||||||
handler() {
|
handler() {
|
||||||
// only for fixed time since we track offsets for realtime
|
// only for fixed time since we track offsets for realtime
|
||||||
if (this.isFixed) {
|
if (this.isFixed) {
|
||||||
|
this.updateMode();
|
||||||
this.addTimespan();
|
this.addTimespan();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -115,28 +114,35 @@ export default {
|
|||||||
},
|
},
|
||||||
offsets: {
|
offsets: {
|
||||||
handler() {
|
handler() {
|
||||||
|
this.updateMode();
|
||||||
this.addTimespan();
|
this.addTimespan();
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
},
|
},
|
||||||
timeSystem: {
|
timeSystem: {
|
||||||
handler(ts) {
|
handler(ts) {
|
||||||
|
this.updateMode();
|
||||||
this.loadConfiguration();
|
this.loadConfiguration();
|
||||||
this.addTimespan();
|
this.addTimespan();
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
},
|
},
|
||||||
mode: function () {
|
mode: function () {
|
||||||
this.getHistoryFromLocalStorage();
|
this.updateMode();
|
||||||
this.initializeHistoryIfNoHistory();
|
|
||||||
this.loadConfiguration();
|
this.loadConfiguration();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.updateMode();
|
||||||
this.getHistoryFromLocalStorage();
|
this.getHistoryFromLocalStorage();
|
||||||
this.initializeHistoryIfNoHistory();
|
this.initializeHistoryIfNoHistory();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
updateMode() {
|
||||||
|
this.isFixed = this.openmct.time.clock() === undefined;
|
||||||
|
this.getHistoryFromLocalStorage();
|
||||||
|
this.initializeHistoryIfNoHistory();
|
||||||
|
},
|
||||||
getHistoryMenuItems() {
|
getHistoryMenuItems() {
|
||||||
const history = this.historyForCurrentTimeSystem.map(timespan => {
|
const history = this.historyForCurrentTimeSystem.map(timespan => {
|
||||||
let name;
|
let name;
|
||||||
@ -203,8 +209,8 @@ export default {
|
|||||||
currentHistory = currentHistory.filter(ts => !(ts.start === timespan.start && ts.end === timespan.end));
|
currentHistory = currentHistory.filter(ts => !(ts.start === timespan.start && ts.end === timespan.end));
|
||||||
currentHistory.unshift(timespan); // add to front
|
currentHistory.unshift(timespan); // add to front
|
||||||
|
|
||||||
if (currentHistory.length > this.records) {
|
if (currentHistory.length > this.MAX_RECORDS_LENGTH) {
|
||||||
currentHistory.length = this.records;
|
currentHistory.length = this.MAX_RECORDS_LENGTH;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$set(this[this.currentHistory], key, currentHistory);
|
this.$set(this[this.currentHistory], key, currentHistory);
|
||||||
@ -231,7 +237,7 @@ export default {
|
|||||||
.filter(option => option.timeSystem === this.timeSystem.key);
|
.filter(option => option.timeSystem === this.timeSystem.key);
|
||||||
|
|
||||||
this.presets = this.loadPresets(configurations);
|
this.presets = this.loadPresets(configurations);
|
||||||
this.records = this.loadRecords(configurations);
|
this.MAX_RECORDS_LENGTH = this.loadRecords(configurations);
|
||||||
},
|
},
|
||||||
loadPresets(configurations) {
|
loadPresets(configurations) {
|
||||||
const configuration = configurations.find(option => {
|
const configuration = configurations.find(option => {
|
||||||
@ -243,9 +249,9 @@ export default {
|
|||||||
},
|
},
|
||||||
loadRecords(configurations) {
|
loadRecords(configurations) {
|
||||||
const configuration = configurations.find(option => option.records);
|
const configuration = configurations.find(option => option.records);
|
||||||
const records = configuration ? configuration.records : DEFAULT_RECORDS;
|
const maxRecordsLength = configuration ? configuration.records : DEFAULT_RECORDS_LENGTH;
|
||||||
|
|
||||||
return records;
|
return maxRecordsLength;
|
||||||
},
|
},
|
||||||
formatTime(time) {
|
formatTime(time) {
|
||||||
let format = this.timeSystem.timeFormat;
|
let format = this.timeSystem.timeFormat;
|
||||||
|
@ -131,15 +131,15 @@ describe('time conductor', () => {
|
|||||||
describe('duration functions', () => {
|
describe('duration functions', () => {
|
||||||
it('should transform milliseconds to DHMS', () => {
|
it('should transform milliseconds to DHMS', () => {
|
||||||
const functionResults = [millisecondsToDHMS(0), millisecondsToDHMS(86400000),
|
const functionResults = [millisecondsToDHMS(0), millisecondsToDHMS(86400000),
|
||||||
millisecondsToDHMS(129600000), millisecondsToDHMS(661824000)];
|
millisecondsToDHMS(129600000), millisecondsToDHMS(661824000), millisecondsToDHMS(213927028)];
|
||||||
const validResults = [' ', '+ 1d', '+ 1d 12h', '+ 7d 15h 50m 24s'];
|
const validResults = [' ', '+ 1d', '+ 1d 12h', '+ 7d 15h 50m 24s', '+ 2d 11h 25m 27s 28ms'];
|
||||||
expect(validResults).toEqual(functionResults);
|
expect(validResults).toEqual(functionResults);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get precise duration', () => {
|
it('should get precise duration', () => {
|
||||||
const functionResults = [getPreciseDuration(0), getPreciseDuration(643680000),
|
const functionResults = [getPreciseDuration(0), getPreciseDuration(643680000),
|
||||||
getPreciseDuration(1605312000)];
|
getPreciseDuration(1605312000), getPreciseDuration(213927028)];
|
||||||
const validResults = ['00:00:00:00', '07:10:48:00', '18:13:55:12'];
|
const validResults = ['00:00:00:00:000', '07:10:48:00:000', '18:13:55:12:000', '02:11:25:27:028'];
|
||||||
expect(validResults).toEqual(functionResults);
|
expect(validResults).toEqual(functionResults);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -188,7 +188,8 @@ export default {
|
|||||||
if (domainObject.type === 'plan') {
|
if (domainObject.type === 'plan') {
|
||||||
this.getPlanDataAndSetConfig({
|
this.getPlanDataAndSetConfig({
|
||||||
...this.domainObject,
|
...this.domainObject,
|
||||||
selectFile: domainObject.selectFile
|
selectFile: domainObject.selectFile,
|
||||||
|
sourceMap: domainObject.sourceMap
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="canEdit"
|
v-if="canEdit"
|
||||||
class="c-inspect-properties__hint span-all"
|
class="c-inspect-properties__hint span-all"
|
||||||
>These settings are not previewed and will be applied after editing is completed.</div>
|
>These settings don't affect the view while editing, but will be applied after editing is finished.</div>
|
||||||
<div
|
<div
|
||||||
class="c-inspect-properties__label"
|
class="c-inspect-properties__label"
|
||||||
title="Sort order of the timelist."
|
title="Sort order of the timelist."
|
||||||
|
@ -33,28 +33,16 @@ export default function () {
|
|||||||
description: 'A configurable, time-ordered list view of activities for a compatible mission plan file.',
|
description: 'A configurable, time-ordered list view of activities for a compatible mission plan file.',
|
||||||
creatable: true,
|
creatable: true,
|
||||||
cssClass: 'icon-timelist',
|
cssClass: 'icon-timelist',
|
||||||
form: [
|
|
||||||
{
|
|
||||||
name: 'Upload Plan (JSON File)',
|
|
||||||
key: 'selectFile',
|
|
||||||
control: 'file-input',
|
|
||||||
text: 'Select File...',
|
|
||||||
type: 'application/json',
|
|
||||||
property: [
|
|
||||||
"selectFile"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
initialize: function (domainObject) {
|
initialize: function (domainObject) {
|
||||||
domainObject.configuration = {
|
domainObject.configuration = {
|
||||||
sortOrderIndex: 0,
|
sortOrderIndex: 0,
|
||||||
futureEventsIndex: 0,
|
futureEventsIndex: 1,
|
||||||
futureEventsDurationIndex: 0,
|
futureEventsDurationIndex: 0,
|
||||||
futureEventsDuration: 20,
|
futureEventsDuration: 20,
|
||||||
currentEventsIndex: 1,
|
currentEventsIndex: 1,
|
||||||
currentEventsDurationIndex: 0,
|
currentEventsDurationIndex: 0,
|
||||||
currentEventsDuration: 20,
|
currentEventsDuration: 20,
|
||||||
pastEventsIndex: 0,
|
pastEventsIndex: 1,
|
||||||
pastEventsDurationIndex: 0,
|
pastEventsDurationIndex: 0,
|
||||||
pastEventsDuration: 20,
|
pastEventsDuration: 20,
|
||||||
filter: ''
|
filter: ''
|
||||||
|
@ -95,14 +95,12 @@ describe('the plugin', function () {
|
|||||||
originalRouterPath = openmct.router.path;
|
originalRouterPath = openmct.router.path;
|
||||||
|
|
||||||
mockComposition = new EventEmitter();
|
mockComposition = new EventEmitter();
|
||||||
mockComposition.load = () => {
|
// eslint-disable-next-line require-await
|
||||||
mockComposition.emit('add', planObject);
|
mockComposition.load = async () => {
|
||||||
|
return [planObject];
|
||||||
return Promise.resolve([planObject]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
|
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
|
||||||
|
|
||||||
openmct.on('start', done);
|
openmct.on('start', done);
|
||||||
openmct.start(appHolder);
|
openmct.start(appHolder);
|
||||||
});
|
});
|
||||||
@ -268,6 +266,8 @@ describe('the plugin', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('loads the plan from composition', () => {
|
it('loads the plan from composition', () => {
|
||||||
|
mockComposition.emit('add', planObject);
|
||||||
|
|
||||||
return Vue.nextTick(() => {
|
return Vue.nextTick(() => {
|
||||||
const items = element.querySelectorAll(LIST_ITEM_CLASS);
|
const items = element.querySelectorAll(LIST_ITEM_CLASS);
|
||||||
expect(items.length).toEqual(2);
|
expect(items.length).toEqual(2);
|
||||||
@ -319,6 +319,8 @@ describe('the plugin', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('activities', () => {
|
it('activities', () => {
|
||||||
|
mockComposition.emit('add', planObject);
|
||||||
|
|
||||||
return Vue.nextTick(() => {
|
return Vue.nextTick(() => {
|
||||||
const items = element.querySelectorAll(LIST_ITEM_CLASS);
|
const items = element.querySelectorAll(LIST_ITEM_CLASS);
|
||||||
expect(items.length).toEqual(1);
|
expect(items.length).toEqual(1);
|
||||||
@ -370,6 +372,8 @@ describe('the plugin', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('hides past events', () => {
|
it('hides past events', () => {
|
||||||
|
mockComposition.emit('add', planObject);
|
||||||
|
|
||||||
return Vue.nextTick(() => {
|
return Vue.nextTick(() => {
|
||||||
const items = element.querySelectorAll(LIST_ITEM_CLASS);
|
const items = element.querySelectorAll(LIST_ITEM_CLASS);
|
||||||
expect(items.length).toEqual(1);
|
expect(items.length).toEqual(1);
|
||||||
|
@ -32,6 +32,12 @@
|
|||||||
.c-list-item {
|
.c-list-item {
|
||||||
/* Time Lists */
|
/* Time Lists */
|
||||||
|
|
||||||
|
td {
|
||||||
|
$p: $interiorMarginSm;
|
||||||
|
padding-top: $p;
|
||||||
|
padding-bottom: $p;
|
||||||
|
}
|
||||||
|
|
||||||
&.--is-current {
|
&.--is-current {
|
||||||
background-color: $colorCurrentBg;
|
background-color: $colorCurrentBg;
|
||||||
border-top: 1px solid $colorCurrentBorder !important;
|
border-top: 1px solid $colorCurrentBorder !important;
|
||||||
|
@ -25,13 +25,14 @@
|
|||||||
/******************************************************** CONTROL-SPECIFIC MIXINS */
|
/******************************************************** CONTROL-SPECIFIC MIXINS */
|
||||||
@mixin menuOuter() {
|
@mixin menuOuter() {
|
||||||
border-radius: $basicCr;
|
border-radius: $basicCr;
|
||||||
box-shadow: $shdwMenuInner, $shdwMenu;
|
box-shadow: $shdwMenu;
|
||||||
|
@if $shdwMenuInner != none {
|
||||||
|
box-shadow: $shdwMenuInner, $shdwMenu;
|
||||||
|
}
|
||||||
background: $colorMenuBg;
|
background: $colorMenuBg;
|
||||||
color: $colorMenuFg;
|
color: $colorMenuFg;
|
||||||
//filter: $filterMenu; // 2022: causing all kinds of weird visual bugs in Chrome
|
|
||||||
text-shadow: $shdwMenuText;
|
text-shadow: $shdwMenuText;
|
||||||
padding: $interiorMarginSm;
|
padding: $interiorMarginSm;
|
||||||
//box-shadow: $shdwMenu;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -60,14 +61,13 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: nth($menuItemPad, 1) nth($menuItemPad, 2);
|
padding: nth($menuItemPad, 1) nth($menuItemPad, 2);
|
||||||
transition: $transIn;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
@include hover {
|
@include hover {
|
||||||
background: $colorMenuHovBg;
|
background: $colorMenuHovBg;
|
||||||
color: $colorMenuHovFg;
|
color: $colorMenuHovFg;
|
||||||
&:before {
|
&:before {
|
||||||
color: $colorMenuHovIc;
|
color: $colorMenuHovIc !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +53,7 @@
|
|||||||
type="horizontal"
|
type="horizontal"
|
||||||
>
|
>
|
||||||
<pane
|
<pane
|
||||||
|
id="tree-pane"
|
||||||
class="l-shell__pane-tree"
|
class="l-shell__pane-tree"
|
||||||
handle="after"
|
handle="after"
|
||||||
label="Browse"
|
label="Browse"
|
||||||
|
@ -41,6 +41,8 @@
|
|||||||
<div
|
<div
|
||||||
ref="mainTree"
|
ref="mainTree"
|
||||||
class="c-tree-and-search__tree c-tree"
|
class="c-tree-and-search__tree c-tree"
|
||||||
|
role="tree"
|
||||||
|
aria-expanded="true"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
@ -467,7 +469,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
scrollEndEvent() {
|
scrollEndEvent() {
|
||||||
if (!this.$refs.srcrollable) {
|
if (!this.$refs.scrollable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -576,14 +578,17 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
addTreeItemObserver(domainObject, parentObjectPath) {
|
addTreeItemObserver(domainObject, parentObjectPath) {
|
||||||
if (this.observers[domainObject.identifier.key]) {
|
const objectPath = [domainObject].concat(parentObjectPath);
|
||||||
this.observers[domainObject.identifier.key]();
|
const navigationPath = this.buildNavigationPath(objectPath);
|
||||||
|
|
||||||
|
if (this.observers[navigationPath]) {
|
||||||
|
this.observers[navigationPath]();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.observers[domainObject.identifier.key] = this.openmct.objects.observe(
|
this.observers[navigationPath] = this.openmct.objects.observe(
|
||||||
domainObject,
|
domainObject,
|
||||||
'name',
|
'name',
|
||||||
this.updateTreeItems.bind(this, parentObjectPath)
|
this.sortTreeItems.bind(this, parentObjectPath)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
async updateTreeItems(parentObjectPath) {
|
async updateTreeItems(parentObjectPath) {
|
||||||
@ -610,6 +615,44 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
sortTreeItems(parentObjectPath) {
|
||||||
|
const navigationPath = this.buildNavigationPath(parentObjectPath);
|
||||||
|
const parentItem = this.getTreeItemByPath(navigationPath);
|
||||||
|
|
||||||
|
// If the parent is not sortable, skip sorting
|
||||||
|
if (!this.isSortable(parentObjectPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the renamed object and its siblings (direct descendants of the parent)
|
||||||
|
const directDescendants = this.getChildrenInTreeFor(parentItem, false);
|
||||||
|
directDescendants.sort(this.sortNameAscending);
|
||||||
|
|
||||||
|
// Take a copy of the sorted descendants array
|
||||||
|
const sortedTreeItems = directDescendants.slice();
|
||||||
|
|
||||||
|
directDescendants.forEach(descendant => {
|
||||||
|
const parent = this.getTreeItemByPath(descendant.navigationPath);
|
||||||
|
|
||||||
|
// If descendant is not open, skip
|
||||||
|
if (!this.isTreeItemOpen(parent)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If descendant is open but has no children, skip
|
||||||
|
const children = this.getChildrenInTreeFor(parent, true);
|
||||||
|
if (children.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Splice in the children of the descendant
|
||||||
|
const parentIndex = sortedTreeItems.map(item => item.navigationPath).indexOf(parent.navigationPath);
|
||||||
|
sortedTreeItems.splice(parentIndex + 1, 0, ...children);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Splice in all of the sorted descendants
|
||||||
|
this.treeItems.splice(this.treeItems.indexOf(parentItem) + 1, sortedTreeItems.length, ...sortedTreeItems);
|
||||||
|
},
|
||||||
buildNavigationPath(objectPath) {
|
buildNavigationPath(objectPath) {
|
||||||
return '/browse/' + [...objectPath].reverse()
|
return '/browse/' + [...objectPath].reverse()
|
||||||
.map((object) => this.openmct.objects.makeKeyString(object.identifier))
|
.map((object) => this.openmct.objects.makeKeyString(object.identifier))
|
||||||
|
@ -77,7 +77,6 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.searchValue = value;
|
this.searchValue = value;
|
||||||
this.searchLoading = true;
|
|
||||||
// clear any previous search results
|
// clear any previous search results
|
||||||
this.annotationSearchResults = [];
|
this.annotationSearchResults = [];
|
||||||
this.objectSearchResults = [];
|
this.objectSearchResults = [];
|
||||||
@ -85,8 +84,13 @@ export default {
|
|||||||
if (this.searchValue) {
|
if (this.searchValue) {
|
||||||
await this.getSearchResults();
|
await this.getSearchResults();
|
||||||
} else {
|
} else {
|
||||||
this.searchLoading = false;
|
const dropdownOptions = {
|
||||||
this.$refs.searchResultsDropDown.showResults(this.annotationSearchResults, this.objectSearchResults);
|
searchLoading: this.searchLoading,
|
||||||
|
searchValue: this.searchValue,
|
||||||
|
annotationSearchResults: this.annotationSearchResults,
|
||||||
|
objectSearchResults: this.objectSearchResults
|
||||||
|
};
|
||||||
|
this.$refs.searchResultsDropDown.showResults(dropdownOptions);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getPathsForObjects(objectsNeedingPaths) {
|
getPathsForObjects(objectsNeedingPaths) {
|
||||||
@ -103,6 +107,8 @@ export default {
|
|||||||
async getSearchResults() {
|
async getSearchResults() {
|
||||||
// an abort controller will be passed in that will be used
|
// an abort controller will be passed in that will be used
|
||||||
// to cancel an active searches if necessary
|
// to cancel an active searches if necessary
|
||||||
|
this.searchLoading = true;
|
||||||
|
this.$refs.searchResultsDropDown.showSearchStarted();
|
||||||
this.abortSearchController = new AbortController();
|
this.abortSearchController = new AbortController();
|
||||||
const abortSignal = this.abortSearchController.signal;
|
const abortSignal = this.abortSearchController.signal;
|
||||||
try {
|
try {
|
||||||
@ -110,10 +116,15 @@ export default {
|
|||||||
const fullObjectSearchResults = await Promise.all(this.openmct.objects.search(this.searchValue, abortSignal));
|
const fullObjectSearchResults = await Promise.all(this.openmct.objects.search(this.searchValue, abortSignal));
|
||||||
const aggregatedObjectSearchResults = fullObjectSearchResults.flat();
|
const aggregatedObjectSearchResults = fullObjectSearchResults.flat();
|
||||||
const aggregatedObjectSearchResultsWithPaths = await this.getPathsForObjects(aggregatedObjectSearchResults);
|
const aggregatedObjectSearchResultsWithPaths = await this.getPathsForObjects(aggregatedObjectSearchResults);
|
||||||
const filterAnnotations = aggregatedObjectSearchResultsWithPaths.filter(result => {
|
const filterAnnotationsAndValidPaths = aggregatedObjectSearchResultsWithPaths.filter(result => {
|
||||||
return result.type !== 'annotation';
|
if (this.openmct.annotation.isAnnotation(result)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.openmct.objects.isReachable(result?.originalPath);
|
||||||
});
|
});
|
||||||
this.objectSearchResults = filterAnnotations;
|
this.objectSearchResults = filterAnnotationsAndValidPaths;
|
||||||
|
this.searchLoading = false;
|
||||||
this.showSearchResults();
|
this.showSearchResults();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`😞 Error searching`, error);
|
console.error(`😞 Error searching`, error);
|
||||||
@ -125,7 +136,13 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
showSearchResults() {
|
showSearchResults() {
|
||||||
this.$refs.searchResultsDropDown.showResults(this.annotationSearchResults, this.objectSearchResults);
|
const dropdownOptions = {
|
||||||
|
searchLoading: this.searchLoading,
|
||||||
|
searchValue: this.searchValue,
|
||||||
|
annotationSearchResults: this.annotationSearchResults,
|
||||||
|
objectSearchResults: this.objectSearchResults
|
||||||
|
};
|
||||||
|
this.$refs.searchResultsDropDown.showResults(dropdownOptions);
|
||||||
document.body.addEventListener('click', this.handleOutsideClick);
|
document.body.addEventListener('click', this.handleOutsideClick);
|
||||||
},
|
},
|
||||||
handleOutsideClick(event) {
|
handleOutsideClick(event) {
|
||||||
|
@ -39,7 +39,11 @@ describe("GrandSearch", () => {
|
|||||||
let mockAnnotationObject;
|
let mockAnnotationObject;
|
||||||
let mockDisplayLayout;
|
let mockDisplayLayout;
|
||||||
let mockFolderObject;
|
let mockFolderObject;
|
||||||
|
let mockAnotherFolderObject;
|
||||||
|
let mockTopObject;
|
||||||
let originalRouterPath;
|
let originalRouterPath;
|
||||||
|
let mockNewObject;
|
||||||
|
let mockObjectProvider;
|
||||||
|
|
||||||
beforeEach((done) => {
|
beforeEach((done) => {
|
||||||
openmct = createOpenMct();
|
openmct = createOpenMct();
|
||||||
@ -53,6 +57,7 @@ describe("GrandSearch", () => {
|
|||||||
mockDomainObject = {
|
mockDomainObject = {
|
||||||
type: 'notebook',
|
type: 'notebook',
|
||||||
name: 'fooRabbitNotebook',
|
name: 'fooRabbitNotebook',
|
||||||
|
location: 'fooNameSpace:topObject',
|
||||||
identifier: {
|
identifier: {
|
||||||
key: 'some-object',
|
key: 'some-object',
|
||||||
namespace: 'fooNameSpace'
|
namespace: 'fooNameSpace'
|
||||||
@ -70,17 +75,39 @@ describe("GrandSearch", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
mockTopObject = {
|
||||||
|
type: 'root',
|
||||||
|
name: 'Top Folder',
|
||||||
|
composition: [],
|
||||||
|
identifier: {
|
||||||
|
key: 'topObject',
|
||||||
|
namespace: 'fooNameSpace'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mockAnotherFolderObject = {
|
||||||
|
type: 'folder',
|
||||||
|
name: 'Another Test Folder',
|
||||||
|
composition: [],
|
||||||
|
location: 'fooNameSpace:topObject',
|
||||||
|
identifier: {
|
||||||
|
key: 'someParent',
|
||||||
|
namespace: 'fooNameSpace'
|
||||||
|
}
|
||||||
|
};
|
||||||
mockFolderObject = {
|
mockFolderObject = {
|
||||||
type: 'folder',
|
type: 'folder',
|
||||||
name: 'Test Folder',
|
name: 'Test Folder',
|
||||||
|
composition: [],
|
||||||
|
location: 'fooNameSpace:someParent',
|
||||||
identifier: {
|
identifier: {
|
||||||
key: 'some-folder',
|
key: 'someFolder',
|
||||||
namespace: 'fooNameSpace'
|
namespace: 'fooNameSpace'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
mockDisplayLayout = {
|
mockDisplayLayout = {
|
||||||
type: 'layout',
|
type: 'layout',
|
||||||
name: 'Bar Layout',
|
name: 'Bar Layout',
|
||||||
|
composition: [],
|
||||||
identifier: {
|
identifier: {
|
||||||
key: 'some-layout',
|
key: 'some-layout',
|
||||||
namespace: 'fooNameSpace'
|
namespace: 'fooNameSpace'
|
||||||
@ -105,9 +132,19 @@ describe("GrandSearch", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
mockNewObject = {
|
||||||
|
type: 'folder',
|
||||||
|
name: 'New Apple Test Folder',
|
||||||
|
composition: [],
|
||||||
|
location: 'fooNameSpace:topObject',
|
||||||
|
identifier: {
|
||||||
|
key: 'newApple',
|
||||||
|
namespace: 'fooNameSpace'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(false);
|
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(false);
|
||||||
const mockObjectProvider = jasmine.createSpyObj("mock object provider", [
|
mockObjectProvider = jasmine.createSpyObj("mock object provider", [
|
||||||
"create",
|
"create",
|
||||||
"update",
|
"update",
|
||||||
"get"
|
"get"
|
||||||
@ -122,6 +159,12 @@ describe("GrandSearch", () => {
|
|||||||
return mockDisplayLayout;
|
return mockDisplayLayout;
|
||||||
} else if (identifier.key === mockFolderObject.identifier.key) {
|
} else if (identifier.key === mockFolderObject.identifier.key) {
|
||||||
return mockFolderObject;
|
return mockFolderObject;
|
||||||
|
} else if (identifier.key === mockAnotherFolderObject.identifier.key) {
|
||||||
|
return mockAnotherFolderObject;
|
||||||
|
} else if (identifier.key === mockTopObject.identifier.key) {
|
||||||
|
return mockTopObject;
|
||||||
|
} else if (identifier.key === mockNewObject.identifier.key) {
|
||||||
|
return mockNewObject;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -144,6 +187,7 @@ describe("GrandSearch", () => {
|
|||||||
// use local worker
|
// use local worker
|
||||||
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
|
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
|
||||||
openmct.objects.inMemorySearchProvider.worker = null;
|
openmct.objects.inMemorySearchProvider.worker = null;
|
||||||
|
await openmct.objects.inMemorySearchProvider.index(mockTopObject);
|
||||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject);
|
await openmct.objects.inMemorySearchProvider.index(mockDomainObject);
|
||||||
await openmct.objects.inMemorySearchProvider.index(mockDisplayLayout);
|
await openmct.objects.inMemorySearchProvider.index(mockDisplayLayout);
|
||||||
await openmct.objects.inMemorySearchProvider.index(mockFolderObject);
|
await openmct.objects.inMemorySearchProvider.index(mockFolderObject);
|
||||||
@ -172,6 +216,7 @@ describe("GrandSearch", () => {
|
|||||||
openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;
|
openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;
|
||||||
openmct.router.path = originalRouterPath;
|
openmct.router.path = originalRouterPath;
|
||||||
grandSearchComponent.$destroy();
|
grandSearchComponent.$destroy();
|
||||||
|
document.body.removeChild(parent);
|
||||||
|
|
||||||
return resetApplicationState(openmct);
|
return resetApplicationState(openmct);
|
||||||
});
|
});
|
||||||
@ -179,25 +224,62 @@ describe("GrandSearch", () => {
|
|||||||
it("should render an object search result", async () => {
|
it("should render an object search result", async () => {
|
||||||
await grandSearchComponent.$children[0].searchEverything('foo');
|
await grandSearchComponent.$children[0].searchEverything('foo');
|
||||||
await Vue.nextTick();
|
await Vue.nextTick();
|
||||||
const searchResult = document.querySelector('[aria-label="fooRabbitNotebook notebook result"]');
|
const searchResults = document.querySelectorAll('[aria-label="fooRabbitNotebook notebook result"]');
|
||||||
expect(searchResult).toBeDefined();
|
expect(searchResults.length).toBe(1);
|
||||||
|
expect(searchResults[0].innerText).toContain('Rabbit');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render an object search result if new object added", async () => {
|
||||||
|
const composition = openmct.composition.get(mockFolderObject);
|
||||||
|
composition.add(mockNewObject);
|
||||||
|
await grandSearchComponent.$children[0].searchEverything('apple');
|
||||||
|
await Vue.nextTick();
|
||||||
|
const searchResults = document.querySelectorAll('[aria-label="New Apple Test Folder folder result"]');
|
||||||
|
expect(searchResults.length).toBe(1);
|
||||||
|
expect(searchResults[0].innerText).toContain('Apple');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not use InMemorySearch provider if object provider provides search", async () => {
|
||||||
|
// eslint-disable-next-line require-await
|
||||||
|
mockObjectProvider.search = async (query, abortSignal, searchType) => {
|
||||||
|
if (searchType === openmct.objects.SEARCH_TYPES.OBJECTS) {
|
||||||
|
return mockNewObject;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mockObjectProvider.supportsSearchType = (someType) => {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const composition = openmct.composition.get(mockFolderObject);
|
||||||
|
composition.add(mockNewObject);
|
||||||
|
await grandSearchComponent.$children[0].searchEverything('apple');
|
||||||
|
await Vue.nextTick();
|
||||||
|
const searchResults = document.querySelectorAll('[aria-label="New Apple Test Folder folder result"]');
|
||||||
|
// This will be of length 2 (doubles) if we're incorrectly searching with InMemorySearchProvider as well
|
||||||
|
expect(searchResults.length).toBe(1);
|
||||||
|
expect(searchResults[0].innerText).toContain('Apple');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render an annotation search result", async () => {
|
it("should render an annotation search result", async () => {
|
||||||
await grandSearchComponent.$children[0].searchEverything('S');
|
await grandSearchComponent.$children[0].searchEverything('S');
|
||||||
await Vue.nextTick();
|
await Vue.nextTick();
|
||||||
const annotationResult = document.querySelector('[aria-label="Search Result"]');
|
const annotationResults = document.querySelectorAll('[aria-label="Search Result"]');
|
||||||
expect(annotationResult).toBeDefined();
|
expect(annotationResults.length).toBe(2);
|
||||||
|
expect(annotationResults[1].innerText).toContain('Driving');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should preview object search results in edit mode if object clicked", async () => {
|
it("should preview object search results in edit mode if object clicked", async () => {
|
||||||
await grandSearchComponent.$children[0].searchEverything('Folder');
|
await grandSearchComponent.$children[0].searchEverything('Folder');
|
||||||
grandSearchComponent._provided.openmct.router.path = [mockDisplayLayout];
|
grandSearchComponent._provided.openmct.router.path = [mockDisplayLayout];
|
||||||
await Vue.nextTick();
|
await Vue.nextTick();
|
||||||
const searchResult = document.querySelector('[name="Test Folder"]');
|
const searchResults = document.querySelectorAll('[name="Test Folder"]');
|
||||||
expect(searchResult).toBeDefined();
|
expect(searchResults.length).toBe(1);
|
||||||
searchResult.click();
|
expect(searchResults[0].innerText).toContain('Folder');
|
||||||
|
searchResults[0].click();
|
||||||
const previewWindow = document.querySelector('.js-preview-window');
|
const previewWindow = document.querySelector('.js-preview-window');
|
||||||
expect(previewWindow).toBeDefined();
|
expect(previewWindow.innerText).toContain('Snapshot');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -22,8 +22,6 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="(annotationResults && annotationResults.length) ||
|
|
||||||
(objectResults && objectResults.length)"
|
|
||||||
class="c-gsearch__dropdown"
|
class="c-gsearch__dropdown"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -58,25 +56,40 @@
|
|||||||
@click.native="selectedResult"
|
@click.native="selectedResult"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="searchLoading"
|
||||||
|
> <progress-bar
|
||||||
|
:model="{progressText: 'Searching...',
|
||||||
|
progressPerc: undefined
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!searchLoading && (!annotationResults || !annotationResults.length) &&
|
||||||
|
(!objectResults || !objectResults.length)"
|
||||||
|
>No matching results.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div></template>
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import AnnotationSearchResult from './AnnotationSearchResult.vue';
|
import AnnotationSearchResult from './AnnotationSearchResult.vue';
|
||||||
import ObjectSearchResult from './ObjectSearchResult.vue';
|
import ObjectSearchResult from './ObjectSearchResult.vue';
|
||||||
|
import ProgressBar from '@/ui/components/ProgressBar.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SearchResultsDropDown',
|
name: 'SearchResultsDropDown',
|
||||||
components: {
|
components: {
|
||||||
AnnotationSearchResult,
|
AnnotationSearchResult,
|
||||||
ObjectSearchResult
|
ObjectSearchResult,
|
||||||
|
ProgressBar
|
||||||
},
|
},
|
||||||
inject: ['openmct'],
|
inject: ['openmct'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
resultsShown: false,
|
resultsShown: false,
|
||||||
|
searchLoading: false,
|
||||||
annotationResults: [],
|
annotationResults: [],
|
||||||
objectResults: [],
|
objectResults: [],
|
||||||
previewVisible: false
|
previewVisible: false
|
||||||
@ -91,12 +104,18 @@ export default {
|
|||||||
previewChanged(changedPreviewState) {
|
previewChanged(changedPreviewState) {
|
||||||
this.previewVisible = changedPreviewState;
|
this.previewVisible = changedPreviewState;
|
||||||
},
|
},
|
||||||
showResults(passedAnnotationResults, passedObjectResults) {
|
showSearchStarted() {
|
||||||
if ((passedAnnotationResults && passedAnnotationResults.length)
|
this.searchLoading = true;
|
||||||
|| (passedObjectResults && passedObjectResults.length)) {
|
this.resultsShown = true;
|
||||||
|
this.annotationResults = [];
|
||||||
|
this.objectResults = [];
|
||||||
|
},
|
||||||
|
showResults({searchLoading, searchValue, annotationSearchResults, objectSearchResults}) {
|
||||||
|
this.searchLoading = searchLoading;
|
||||||
|
this.annotationResults = annotationSearchResults;
|
||||||
|
this.objectResults = objectSearchResults;
|
||||||
|
if (searchValue?.length) {
|
||||||
this.resultsShown = true;
|
this.resultsShown = true;
|
||||||
this.annotationResults = passedAnnotationResults;
|
|
||||||
this.objectResults = passedObjectResults;
|
|
||||||
} else {
|
} else {
|
||||||
this.resultsShown = false;
|
this.resultsShown = false;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:style="treeItemStyles"
|
|
||||||
class="c-tree__item-h"
|
class="c-tree__item-h"
|
||||||
|
role="treeitem"
|
||||||
|
:style="treeItemStyles"
|
||||||
|
:aria-expanded="(!activeSearch && hasComposition) ? (isOpen || isLoading) ? 'true' : 'false' : undefined"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="c-tree__item"
|
class="c-tree__item"
|
||||||
|
@ -32,8 +32,16 @@ function normalizeAge(num) {
|
|||||||
return isWhole ? hundredtized / 100 : num;
|
return isWhole ? hundredtized / 100 : num;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function padLeadingZeros(num, numOfLeadingZeros) {
|
||||||
|
return num.toString().padStart(numOfLeadingZeros, '0');
|
||||||
|
}
|
||||||
|
|
||||||
function toDoubleDigits(num) {
|
function toDoubleDigits(num) {
|
||||||
return num >= 10 ? num : `0${num}`;
|
return padLeadingZeros(num, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toTripleDigits(num) {
|
||||||
|
return padLeadingZeros(num, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addTimeSuffix(value, suffix) {
|
function addTimeSuffix(value, suffix) {
|
||||||
@ -46,7 +54,8 @@ export function millisecondsToDHMS(numericDuration) {
|
|||||||
addTimeSuffix(Math.floor(normalizeAge(ms / ONE_DAY)), 'd'),
|
addTimeSuffix(Math.floor(normalizeAge(ms / ONE_DAY)), 'd'),
|
||||||
addTimeSuffix(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR)), 'h'),
|
addTimeSuffix(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR)), 'h'),
|
||||||
addTimeSuffix(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE)), 'm'),
|
addTimeSuffix(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE)), 'm'),
|
||||||
addTimeSuffix(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)), 's')
|
addTimeSuffix(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)), 's'),
|
||||||
|
addTimeSuffix(Math.floor(normalizeAge(ms % ONE_SECOND)), "ms")
|
||||||
].filter(Boolean).join(' ');
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
return `${ dhms ? '+' : ''} ${dhms}`;
|
return `${ dhms ? '+' : ''} ${dhms}`;
|
||||||
@ -59,7 +68,8 @@ export function getPreciseDuration(value) {
|
|||||||
toDoubleDigits(Math.floor(normalizeAge(ms / ONE_DAY))),
|
toDoubleDigits(Math.floor(normalizeAge(ms / ONE_DAY))),
|
||||||
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR))),
|
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR))),
|
||||||
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE))),
|
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE))),
|
||||||
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)))
|
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND))),
|
||||||
|
toTripleDigits(Math.floor(normalizeAge(ms % ONE_SECOND)))
|
||||||
].join(":");
|
].join(":");
|
||||||
|
|
||||||
}
|
}
|
||||||
|