Compare commits
124 Commits
enable-dis
...
fix-gauge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a73719e809 | ||
|
|
798e2d4337 | ||
|
|
e3770dc701 | ||
|
|
fcfa95c2df | ||
|
|
0f12aa1eae | ||
|
|
d673cc6bcc | ||
|
|
c17efcc157 | ||
|
|
90662ce4a7 | ||
|
|
84c1526f5e | ||
|
|
b6d8d794be | ||
|
|
b53cc810f5 | ||
|
|
d73cb2ef70 | ||
|
|
5386ceb94c | ||
|
|
affb7a5311 | ||
|
|
07bdbe5108 | ||
|
|
f92bc0122c | ||
|
|
291e62687e | ||
|
|
313390f1fe | ||
|
|
e7804dd25e | ||
|
|
ec1de92d4b | ||
|
|
06b321588e | ||
|
|
909901b0f3 | ||
|
|
865f95c0b6 | ||
|
|
cb1e04b0d6 | ||
|
|
b0c2e66613 | ||
|
|
d162b5dbd8 | ||
|
|
efadf9036f | ||
|
|
ca928370a4 | ||
|
|
6820e0d044 | ||
|
|
64565b1bbb | ||
|
|
f721980bf0 | ||
|
|
b47712a0f4 | ||
|
|
b8fa89af6e | ||
|
|
57f3d4eba0 | ||
|
|
bbb84c695d | ||
|
|
f979e170ee | ||
|
|
1d875cb8ca | ||
|
|
064a865c9b | ||
|
|
a584766618 | ||
|
|
a671be726b | ||
|
|
61bf60783c | ||
|
|
5dc718b78d | ||
|
|
115912da31 | ||
|
|
10ff4e1781 | ||
|
|
41f8cb404d | ||
|
|
c6c58af12c | ||
|
|
15a0a87251 | ||
|
|
59a8614f1c | ||
|
|
7cf11e177c | ||
|
|
1a44652470 | ||
|
|
51d16f812a | ||
|
|
25de5653e8 | ||
|
|
cb6014d69f | ||
|
|
36736eb8a0 | ||
|
|
a13a6002c5 | ||
|
|
f1c85933c3 | ||
|
|
01aac89be0 | ||
|
|
3c7ecc8561 | ||
|
|
dee92d893c | ||
|
|
0e707150e0 | ||
|
|
2540d96617 | ||
|
|
8ca0f13cd9 | ||
|
|
8ec7fbb74c | ||
|
|
143fb2dcdc | ||
|
|
1c8784fec5 | ||
|
|
a692245644 | ||
|
|
2943d2b6ec | ||
|
|
4246a597a9 | ||
|
|
0af7965021 | ||
|
|
d6bd2793d7 | ||
|
|
e9c0909415 | ||
|
|
0f0a3dc48f | ||
|
|
4c82680b87 | ||
|
|
450d3a5575 | ||
|
|
4f28e3bdf1 | ||
|
|
e6d25b22c1 | ||
|
|
acc60f5e4e | ||
|
|
9911d9ed6a | ||
|
|
c4734b8ad6 | ||
|
|
9786ff5de4 | ||
|
|
437154a5c0 | ||
|
|
2bd38dab9f | ||
|
|
063df721ae | ||
|
|
a09db30b32 | ||
|
|
9d89bdd6d3 | ||
|
|
ed9ca2829b | ||
|
|
eacbac6aad | ||
|
|
69153fe8f0 | ||
|
|
51196530fd | ||
|
|
fefa46ce7e | ||
|
|
82e685d4df | ||
|
|
e08ab8ef24 | ||
|
|
0060a6e20b | ||
|
|
7011877e64 | ||
|
|
8890cd9b22 | ||
|
|
34ecc08238 | ||
|
|
a07c043a29 | ||
|
|
2999a5135e | ||
|
|
2766452b38 | ||
|
|
f3cdf69288 | ||
|
|
a040bb30c2 | ||
|
|
0a2e0a4e65 | ||
|
|
e8df2bd437 | ||
|
|
ccd2a8b64c | ||
|
|
2bd35bb2a5 | ||
|
|
28dbd724d6 | ||
|
|
5a1c329c66 | ||
|
|
00a5cbd2fd | ||
|
|
a2d698d5c1 | ||
|
|
5685a5b393 | ||
|
|
164f39695e | ||
|
|
c384cf67da | ||
|
|
417b225505 | ||
|
|
e5e93f311c | ||
|
|
39e6d9c90c | ||
|
|
60d021ef82 | ||
|
|
59880955a2 | ||
|
|
b51ed7e844 | ||
|
|
7bbaec4006 | ||
|
|
c0f24b3925 | ||
|
|
4e79725897 | ||
|
|
0674c9fc33 | ||
|
|
de1b877954 | ||
|
|
4db2f547d9 |
5
.github/dependabot.yml
vendored
@@ -7,12 +7,13 @@ updates:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
- "pr:e2e"
|
||||
- "type:maintenance"
|
||||
- "dependencies"
|
||||
- "pr:e2e"
|
||||
- "pr:daveit"
|
||||
- "pr:visual"
|
||||
- "pr:platform"
|
||||
ignore:
|
||||
- dependency-name: "@playwright/test" #we source the container instead of the dependency in CI
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
|
||||
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
|
||||
6
API.md
@@ -390,7 +390,7 @@ A telemetry object is a domain object with a telemetry property. To take an exa
|
||||
{
|
||||
"key": "value",
|
||||
"name": "Value",
|
||||
"units": "kilograms",
|
||||
"unit": "kilograms",
|
||||
"format": "float",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
@@ -425,7 +425,7 @@ attribute | type | flags | notes
|
||||
`name` | string | optional | a human readable label for this field. If omitted, defaults to `key`.
|
||||
`source` | string | optional | identifies the property of a datum where this value is stored. If omitted, defaults to `key`.
|
||||
`format` | string | optional | a specific format identifier, mapping to a formatter. If omitted, uses a default formatter. For enumerations, use `enum`. For timestamps, use `utc` if you are using utc dates, otherwise use a key mapping to your custom date format.
|
||||
`units` | string | optional | the units of this value, e.g. `km`, `seconds`, `parsecs`
|
||||
`unit` | string | optional | the unit of this value, e.g. `km`, `seconds`, `parsecs`
|
||||
`min` | number | optional | the minimum possible value of this measurement. Will be used by plots, gauges, etc to automatically set a min value.
|
||||
`max` | number | optional | the maximum possible value of this measurement. Will be used by plots, gauges, etc to automatically set a max value.
|
||||
`enumerations` | array | optional | for objects where `format` is `"enum"`, this array tracks all possible enumerations of the value. Each entry in this array is an object, with a `value` property that is the numerical value of the enumeration, and a `string` property that is the text value of the enumeration. ex: `{"value": 0, "string": "OFF"}`. If you use an enumerations array, `min` and `max` will be set automatically for you.
|
||||
@@ -1082,4 +1082,4 @@ View provider Example:
|
||||
return openmct.priority.HIGH;
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@@ -23,21 +23,23 @@ If this is your first time ever using the Playwright framework, we recommend goi
|
||||
Once you've got an understanding of Playwright, you'll need a baseline understanding of Open MCT:
|
||||
|
||||
1. Follow the steps [Building and Running Open MCT Locally](../README.md#building-and-running-open-mct-locally)
|
||||
2. Once you're serving Open MCT locally, create an Example Telemetry Object (e.g.: 'Sine Wave Generator')
|
||||
2. Once you're serving Open MCT locally, create a 'Display Layout' object. Save it.
|
||||
3. Create a 'Plot' Object (e.g.: 'Stacked Plot')
|
||||
4. Expand the Tree on the left-hand nav and drag and drop the Example Telemetry Object into the Plot Object
|
||||
5. Create a 'Display Layout' object
|
||||
6. From the Tree, Drag the Plot object into the Display Layout
|
||||
4. Create an Example Telemetry Object (e.g.: 'Sine Wave Generator')
|
||||
5. Expand the Tree and note the hierarchy of objects which were created.
|
||||
6. Navigate to the Demo Display Layout Object to edit and modify the embedded plot.
|
||||
7. Modify the embedded plot with Telemetry Data.
|
||||
|
||||
What you've created is a display which mimics the display that a mission control operator might use to understand and model telemetry data.
|
||||
|
||||
Recreate the steps above with Playwright's codegen tool:
|
||||
|
||||
1. `npm run start` in a terminal window
|
||||
2. Open another terminal window and start the Playwright codegen application `npx playwright codegen`
|
||||
3. Navigate the browser to `http://localhost:8080`
|
||||
4. Click the Create button and notice how your actions in the browser are being recorded in the Playwright Inspector
|
||||
5. Continue through the steps 2-6 above
|
||||
1. `npm run start` in a terminal window to serve Open MCT locally
|
||||
2. `npx @playwright/test install` to install playwright and dependencies
|
||||
3. Open another terminal window and start the Playwright codegen application `npx playwright codegen`
|
||||
4. Navigate the browser to `http://localhost:8080`
|
||||
5. Click the Create button and notice how your actions in the browser are being recorded in the Playwright Inspector
|
||||
6. Continue through the steps 2-6 above
|
||||
|
||||
What you've created is an automated test which mimics the creation of a mission control display.
|
||||
|
||||
|
||||
@@ -30,18 +30,37 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* This common function creates a `domainObject` with 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 {string} type
|
||||
* @param {string | undefined} name
|
||||
* Defines parameters to be used in the creation of a domain object.
|
||||
* @typedef {Object} CreateObjectOptions
|
||||
* @property {string} type the type of domain object to create (e.g.: "Sine Wave Generator").
|
||||
* @property {string} [name] the desired name of the created domain object.
|
||||
* @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
|
||||
// TODO: Ideally this would navigate to a common `e2e` folder
|
||||
await page.goto('./#/browse/mine?hideTree=true');
|
||||
|
||||
/**
|
||||
* Contains information about the newly created domain object.
|
||||
* @typedef {Object} CreatedObjectInfo
|
||||
* @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');
|
||||
|
||||
//Click the Create button
|
||||
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'
|
||||
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(name);
|
||||
}
|
||||
@@ -63,30 +82,187 @@ async function createDomainObjectWithDefaults(page, type, name) {
|
||||
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.
|
||||
* 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 {string} myItemsFolderName the name of the "My Items" folder
|
||||
* @param {string} domainObjectName the display name of the `domainObject`
|
||||
* @param {string} url the url to the object
|
||||
*/
|
||||
async function openObjectTreeContextMenu(page, myItemsFolderName, domainObjectName) {
|
||||
const myItemsFolder = page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3);
|
||||
const className = await myItemsFolder.getAttribute('class');
|
||||
if (!className.includes('c-disclosure-triangle--expanded')) {
|
||||
await myItemsFolder.click();
|
||||
}
|
||||
|
||||
await page.locator(`a:has-text("${domainObjectName}")`).click({
|
||||
async function openObjectTreeContextMenu(page, url) {
|
||||
await page.goto(url);
|
||||
await page.click('button[title="Show selected item in tree"]');
|
||||
await page.locator('.is-navigated-object').click({
|
||||
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
|
||||
module.exports = {
|
||||
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 { createDomainObjectWithDefaults } = require('../../appActions.js');
|
||||
|
||||
test.describe('appActions tests', () => {
|
||||
test('createDomainObjectsWithDefaults can create multiple objects in a row', async ({ page }) => {
|
||||
test.describe('AppActions', () => {
|
||||
test('createDomainObjectsWithDefaults', async ({ page }) => {
|
||||
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
|
||||
await page.click('.c-disclosure-triangle');
|
||||
const e2eFolder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'e2e folder'
|
||||
});
|
||||
|
||||
// Verify the objects were created
|
||||
await expect(page.locator('a :text("Timer Foo")')).toBeVisible();
|
||||
await expect(page.locator('a :text("Timer Bar")')).toBeVisible();
|
||||
await expect(page.locator('a :text("Timer Baz")')).toBeVisible();
|
||||
await test.step('Create multiple flat objects in a row', async () => {
|
||||
const timer1 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Timer',
|
||||
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
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
//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
|
||||
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
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
//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
|
||||
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');
|
||||
|
||||
test('Generate Visual Test Data @localStorage', async ({ page, context, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
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();
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, { type: 'Overlay Plot' });
|
||||
|
||||
// click create button
|
||||
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([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear1
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
// focus the overlay plot
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Overlay Plot').first().click()
|
||||
]);
|
||||
await page.goto(overlayPlot.url);
|
||||
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
|
||||
//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
|
||||
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
|
||||
await expect(page.waitForURL(/.*&view=table/)).toBeTruthy();
|
||||
|
||||
@@ -28,7 +28,8 @@ const { test, expect } = require('../../../../baseFixtures');
|
||||
|
||||
test.describe('Sine Wave Generator', () => {
|
||||
test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ page, browserName }) => {
|
||||
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
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
|
||||
});
|
||||
@@ -50,7 +50,7 @@ test.describe('Clock Generator CRUD Operations', () => {
|
||||
await page.locator('.icon-arrow-down').click();
|
||||
|
||||
// Verify clicking on the autocomplete arrow collapses the dropdown
|
||||
await expect(page.locator(".c-input--autocomplete__options")).not.toBeVisible();
|
||||
await expect(page.locator(".c-input--autocomplete__options")).toBeHidden();
|
||||
|
||||
// Click timezone input to open dropdown
|
||||
await page.locator('.c-input--autocomplete__input').click();
|
||||
@@ -60,7 +60,7 @@ test.describe('Clock Generator CRUD Operations', () => {
|
||||
// Verify clicking outside the autocomplete dropdown collapses it
|
||||
await page.locator('text=Timezone').click();
|
||||
// Verify clicking on the autocomplete arrow collapses the dropdown
|
||||
await expect(page.locator(".c-input--autocomplete__options")).not.toBeVisible();
|
||||
await expect(page.locator(".c-input--autocomplete__options")).toBeHidden();
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ demonstrate some playwright for test developers. This pattern should not be re-u
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures.js');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
|
||||
let conditionSetUrl;
|
||||
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.
|
||||
*/
|
||||
/* globals process */
|
||||
|
||||
const { v4: uuid } = require('uuid');
|
||||
const { waitForAnimations } = require('../../../../baseFixtures');
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
@@ -41,7 +41,7 @@ test.describe('Example Imagery Object', () => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Create a default 'Example Imagery' object
|
||||
createDomainObjectWithDefaults(page, 'Example Imagery');
|
||||
createDomainObjectWithDefaults(page, { type: 'Example Imagery' });
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
@@ -77,7 +77,8 @@ test.describe('Example Imagery Object', () => {
|
||||
});
|
||||
|
||||
test('Can adjust image brightness/contrast by dragging the sliders', async ({ page, browserName }) => {
|
||||
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
// Open the image filter menu
|
||||
await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click();
|
||||
|
||||
@@ -422,16 +423,12 @@ test.describe('Example imagery thumbnails resize in display layouts', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
// test.fixme('Can use alt+drag to move around image once zoomed in');
|
||||
// test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
// test.fixme('If the imagery view is in pause mode, images still come in');
|
||||
// test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
test.describe('Example Imagery in Flexible layout', () => {
|
||||
test('Example Imagery in Flexible layout @unstable', async ({ page, browserName, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5326'
|
||||
@@ -573,6 +570,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.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
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -86,6 +86,23 @@ test.describe('Notebook section tests', () => {
|
||||
//Delete 3rd section
|
||||
//1st is selected and there is no default notebook
|
||||
});
|
||||
test.fixme('Section rename operations', async ({ page }) => {
|
||||
// Create a new notebook
|
||||
// Add a section
|
||||
// Rename the section but do not confirm
|
||||
// Keyboard press 'Escape'
|
||||
// Verify that the section name reverts to the default name
|
||||
// Rename the section but do not confirm
|
||||
// Keyboard press 'Enter'
|
||||
// Verify that the section name is updated
|
||||
// Rename the section to "" (empty string)
|
||||
// Keyboard press 'Enter' to confirm
|
||||
// Verify that the section name reverts to the default name
|
||||
// Rename the section to something long that overflows the text box
|
||||
// Verify that the section name is not truncated while input is active
|
||||
// Confirm the section name edit
|
||||
// Verify that the section name is truncated now that input is not active
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Notebook page tests', () => {
|
||||
@@ -107,6 +124,23 @@ test.describe('Notebook page tests', () => {
|
||||
//Delete 3rd page
|
||||
//First is now selected and there is no default notebook
|
||||
});
|
||||
test.fixme('Page rename operations', async ({ page }) => {
|
||||
// Create a new notebook
|
||||
// Add a page
|
||||
// Rename the page but do not confirm
|
||||
// Keyboard press 'Escape'
|
||||
// Verify that the page name reverts to the default name
|
||||
// Rename the page but do not confirm
|
||||
// Keyboard press 'Enter'
|
||||
// Verify that the page name is updated
|
||||
// Rename the page to "" (empty string)
|
||||
// Keyboard press 'Enter' to confirm
|
||||
// Verify that the page name reverts to the default name
|
||||
// Rename the page to something long that overflows the text box
|
||||
// Verify that the page name is not truncated while input is active
|
||||
// Confirm the page name edit
|
||||
// Verify that the page name is truncated now that input is not active
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Notebook search tests', () => {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { openObjectTreeContextMenu } = require('../../../../appActions');
|
||||
const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const path = require('path');
|
||||
|
||||
const TEST_TEXT = 'Testing text for entries.';
|
||||
@@ -30,8 +30,9 @@ const CUSTOM_NAME = 'CUSTOM_NAME';
|
||||
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
|
||||
|
||||
test.describe('Restricted Notebook', () => {
|
||||
let notebook;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await startAndAddRestrictedNotebookObject(page);
|
||||
notebook = await startAndAddRestrictedNotebookObject(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 }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
await openObjectTreeContextMenu(page, myItemsFolderName, `Unnamed ${CUSTOM_NAME}`);
|
||||
await openObjectTreeContextMenu(page, notebook.url);
|
||||
|
||||
const menuOptions = page.locator('.c-menu ul');
|
||||
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', () => {
|
||||
|
||||
let notebook;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await startAndAddRestrictedNotebookObject(page);
|
||||
notebook = await startAndAddRestrictedNotebookObject(page);
|
||||
await enterTextEntry(page);
|
||||
await lockPage(page);
|
||||
|
||||
@@ -86,9 +85,9 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
|
||||
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.fixme(testInfo.project === 'chrome-beta', "Test is unreliable on chrome-beta");
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
test('Locked page should now be in a locked state @addInit @unstable', async ({ page }, testInfo) => {
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip(testInfo.project === 'chrome-beta', "Test is unreliable on chrome-beta");
|
||||
// main lock message on page
|
||||
const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed');
|
||||
expect.soft(await lockMessage.count()).toEqual(1);
|
||||
@@ -98,7 +97,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
|
||||
expect.soft(await pageLockIcon.count()).toEqual(1);
|
||||
|
||||
// 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');
|
||||
|
||||
await expect(menuOptions).not.toContainText('Remove');
|
||||
@@ -178,13 +177,8 @@ async function startAndAddRestrictedNotebookObject(page) {
|
||||
// eslint-disable-next-line no-undef
|
||||
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitRestrictedNotebook.js') });
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.click('button:has-text("Create")');
|
||||
await page.click(`text=${CUSTOM_NAME}`); // secondarily tests renamability also
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('text=OK')
|
||||
]);
|
||||
|
||||
return createDomainObjectWithDefaults(page, { type: CUSTOM_NAME });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,7 +36,7 @@ async function createNotebookAndEntry(page, iterations = 1) {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
createDomainObjectWithDefaults(page, 'Notebook');
|
||||
createDomainObjectWithDefaults(page, { type: 'Notebook' });
|
||||
|
||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||
// 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);
|
||||
|
||||
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();
|
||||
|
||||
// Click [placeholder="Type to select tag"]
|
||||
// Click inside the tag search input
|
||||
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();
|
||||
|
||||
// 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();
|
||||
// Click [placeholder="Type to select tag"]
|
||||
// Click inside the tag search input
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -122,15 +126,16 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toBeHidden();
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toBeHidden();
|
||||
});
|
||||
|
||||
test('Can delete tags', async ({ page }) => {
|
||||
await createNotebookEntryAndTags(page);
|
||||
await page.locator('[aria-label="Notebook Entries"]').click();
|
||||
// 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"]')).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 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 }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
await createDomainObjectWithDefaults(page, 'Clock');
|
||||
await createDomainObjectWithDefaults(page, { type: 'Clock' });
|
||||
|
||||
const ITERATIONS = 4;
|
||||
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 |
@@ -28,9 +28,10 @@ const { test, expect } = require('../../../../pluginFixtures');
|
||||
|
||||
test.describe('Handle missing object for plots', () => {
|
||||
test('Displays empty div for missing stacked plot item @unstable', async ({ page, browserName, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip(browserName === 'firefox', 'Firefox failing due to console events being missed');
|
||||
|
||||
test.fixme(browserName === 'firefox', 'Firefox failing due to console events being missed');
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
const errorLogs = [];
|
||||
|
||||
page.on("console", (message) => {
|
||||
|
||||
@@ -20,55 +20,26 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
|
||||
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({
|
||||
type: 'issue',
|
||||
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' });
|
||||
|
||||
// Click create button
|
||||
await page.locator(createButton).click();
|
||||
await page.locator('li:has-text("Telemetry Table")').click();
|
||||
|
||||
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)
|
||||
]);
|
||||
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: table.uuid
|
||||
});
|
||||
|
||||
// focus the Telemetry Table
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Telemetry Table').first().click()
|
||||
]);
|
||||
page.goto(table.url);
|
||||
|
||||
// Click pause button
|
||||
const pauseButton = page.locator('button.c-button.icon-pause');
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../../../baseFixtures');
|
||||
const { setFixedTimeMode, setRealTimeMode, setStartOffset, setEndOffset } = require('../../../../appActions');
|
||||
|
||||
test.describe('Time conductor operations', () => {
|
||||
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(`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');
|
||||
|
||||
test.describe('Timer', () => {
|
||||
let timer;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
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 }) => {
|
||||
@@ -35,13 +36,13 @@ test.describe('Timer', () => {
|
||||
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 triggerTimerContextMenuAction(page, myItemsFolderName, 'Start');
|
||||
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Pause');
|
||||
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Restart at 0');
|
||||
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Stop');
|
||||
await triggerTimerContextMenuAction(page, timerUrl, 'Start');
|
||||
await triggerTimerContextMenuAction(page, timerUrl, 'Pause');
|
||||
await triggerTimerContextMenuAction(page, timerUrl, 'Restart at 0');
|
||||
await triggerTimerContextMenuAction(page, timerUrl, 'Stop');
|
||||
});
|
||||
|
||||
await test.step("From the 3dot menu", async () => {
|
||||
@@ -74,9 +75,9 @@ test.describe('Timer', () => {
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {TimerAction} action
|
||||
*/
|
||||
async function triggerTimerContextMenuAction(page, myItemsFolderName, action) {
|
||||
async function triggerTimerContextMenuAction(page, timerUrl, action) {
|
||||
const menuAction = `.c-menu ul li >> text="${action}"`;
|
||||
await openObjectTreeContextMenu(page, myItemsFolderName, "Unnamed Timer");
|
||||
await openObjectTreeContextMenu(page, timerUrl);
|
||||
await page.locator(menuAction).click();
|
||||
assertTimerStateAfterAction(page, action);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
const { v4: uuid } = require('uuid');
|
||||
|
||||
test.describe('Grand Search', () => {
|
||||
test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page, openmctConfig }) => {
|
||||
@@ -41,7 +43,7 @@ test.describe('Grand Search', () => {
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=3')).toContainText(`Clock D ${myItemsFolderName} Red Folder Blue Folder`);
|
||||
// Click text=Elements >> nth=0
|
||||
await page.locator('text=Elements').first().click();
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toBeVisible();
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
|
||||
await page.locator('[aria-label="Clock A clock result"] >> text=Clock A').click();
|
||||
@@ -54,11 +56,11 @@ test.describe('Grand Search', () => {
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] a >> nth=0
|
||||
await page.locator('[aria-label="OpenMCT Search"] a').first().click();
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toBeVisible();
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
|
||||
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('foo');
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toBeVisible();
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
|
||||
|
||||
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
@@ -107,15 +109,21 @@ test.describe("Search Tests @unstable", () => {
|
||||
|
||||
// Verify that no results are found
|
||||
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
|
||||
await page.goto("./", { waitUntil: "networkidle" });
|
||||
|
||||
// Create a folder object
|
||||
const folderName = 'testFolder';
|
||||
await createFolderObject(page, folderName);
|
||||
const folderName = uuid();
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'folder',
|
||||
name: folderName
|
||||
});
|
||||
|
||||
// Full search for object
|
||||
await page.type("input[type=search]", folderName);
|
||||
@@ -124,7 +132,7 @@ test.describe("Search Tests @unstable", () => {
|
||||
await waitForSearchCompletion(page);
|
||||
|
||||
// Get the search results
|
||||
const searchResults = await page.locator(searchResultSelector);
|
||||
const searchResults = page.locator(searchResultSelector);
|
||||
|
||||
// Verify that one result is found
|
||||
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"]');
|
||||
}
|
||||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
161
e2e/tests/plugins/plot/missingPlotObj.e2e.spec.js
Normal file
@@ -0,0 +1,161 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Tests to verify log plot functionality when objects are missing
|
||||
*/
|
||||
|
||||
const { test } = require('../../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Handle missing object for plots', () => {
|
||||
test('Displays empty div for missing stacked plot item', async ({ page }) => {
|
||||
const errorLogs = [];
|
||||
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === 'warning') {
|
||||
errorLogs.push(message.text());
|
||||
}
|
||||
});
|
||||
|
||||
//Make stacked plot
|
||||
await makeStackedPlot(page);
|
||||
|
||||
//Gets local storage and deletes the last sine wave generator in the stacked plot
|
||||
const localStorage = await page.evaluate(() => window.localStorage);
|
||||
const parsedData = JSON.parse(localStorage.mct);
|
||||
const keys = Object.keys(parsedData);
|
||||
const lastKey = keys[keys.length - 1];
|
||||
|
||||
delete parsedData[lastKey];
|
||||
|
||||
//Sets local storage with missing object
|
||||
await page.evaluate(
|
||||
`window.localStorage.setItem('mct', '${JSON.stringify(parsedData)}')`
|
||||
);
|
||||
|
||||
//Reloads page and clicks on stacked plot
|
||||
await Promise.all([
|
||||
page.reload(),
|
||||
page.waitForLoadState('networkidle')
|
||||
]);
|
||||
|
||||
//Verify Main section is there on load
|
||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Stacked Plot');
|
||||
|
||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Stacked Plot').first().click()
|
||||
]);
|
||||
|
||||
//Check that there is only one stacked item plot with a plot, the missing one will be empty
|
||||
await expect(page.locator(".c-plot--stacked-container:has(.gl-plot)")).toHaveCount(1);
|
||||
//Verify that console.warn is thrown
|
||||
await expect(errorLogs).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This is used the create a stacked plot object
|
||||
* @private
|
||||
*/
|
||||
async function makeStackedPlot(page) {
|
||||
// fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// create stacked plot
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li:has-text("Stacked Plot")').click();
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// save the stacked plot
|
||||
await saveStackedPlot(page);
|
||||
|
||||
// create a sinewave generator
|
||||
await createSineWaveGenerator(page);
|
||||
|
||||
// click on stacked plot
|
||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Stacked Plot').first().click()
|
||||
]);
|
||||
|
||||
// create a second sinewave generator
|
||||
await createSineWaveGenerator(page);
|
||||
|
||||
// click on stacked plot
|
||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Stacked Plot').first().click()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to save a stacked plot object
|
||||
* @private
|
||||
*/
|
||||
async function saveStackedPlot(page) {
|
||||
// save stacked plot
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
|
||||
await Promise.all([
|
||||
page.locator('text=Save and Finish Editing').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to create a sine wave generator object
|
||||
* @private
|
||||
*/
|
||||
async function createSineWaveGenerator(page) {
|
||||
//Create sine wave generator
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
}
|
||||
41
e2e/tests/plugins/remoteClock/remoteClock.e2e.spec.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test } = require('../../../fixtures.js');
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Remote Clock', () => {
|
||||
// eslint-disable-next-line require-await
|
||||
test.fixme('blocks historical requests until first tick is received', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5221'
|
||||
});
|
||||
// addInitScript to with remote clock
|
||||
// Switch time conductor mode to 'remote clock'
|
||||
// Navigate to telemetry
|
||||
// Verify that the plot renders historical data within the correct bounds
|
||||
// Refresh the page
|
||||
// Verify again that the plot renders historical data within the correct bounds
|
||||
});
|
||||
});
|
||||
184
e2e/tests/plugins/timer/timer.e2e.spec.js
Normal file
@@ -0,0 +1,184 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test } = require('../../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Timer', () => {
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click 'Timer'
|
||||
await page.click('text=Timer');
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('text=OK')
|
||||
]);
|
||||
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer');
|
||||
});
|
||||
|
||||
test('Can perform actions on the Timer', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/4313'
|
||||
});
|
||||
|
||||
await test.step("From the tree context menu", async () => {
|
||||
await triggerTimerContextMenuAction(page, 'Start');
|
||||
await triggerTimerContextMenuAction(page, 'Pause');
|
||||
await triggerTimerContextMenuAction(page, 'Restart at 0');
|
||||
await triggerTimerContextMenuAction(page, 'Stop');
|
||||
});
|
||||
|
||||
await test.step("From the 3dot menu", async () => {
|
||||
await triggerTimer3dotMenuAction(page, 'Start');
|
||||
await triggerTimer3dotMenuAction(page, 'Pause');
|
||||
await triggerTimer3dotMenuAction(page, 'Restart at 0');
|
||||
await triggerTimer3dotMenuAction(page, 'Stop');
|
||||
});
|
||||
|
||||
await test.step("From the object view", async () => {
|
||||
await triggerTimerViewAction(page, 'Start');
|
||||
await triggerTimerViewAction(page, 'Pause');
|
||||
await triggerTimerViewAction(page, 'Restart at 0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Actions that can be performed on a timer from context menus.
|
||||
* @typedef {'Start' | 'Stop' | 'Pause' | 'Restart at 0'} TimerAction
|
||||
*/
|
||||
|
||||
/**
|
||||
* Actions that can be performed on a timer from the object view.
|
||||
* @typedef {'Start' | 'Pause' | 'Restart at 0'} TimerViewAction
|
||||
*/
|
||||
|
||||
/**
|
||||
* Open the timer context menu from the object tree.
|
||||
* Expands the 'My Items' folder if it is not already expanded.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function openTimerContextMenu(page) {
|
||||
const myItemsFolder = page.locator('text=Open MCT My Items >> span').nth(3);
|
||||
const className = await myItemsFolder.getAttribute('class');
|
||||
if (!className.includes('c-disclosure-triangle--expanded')) {
|
||||
await myItemsFolder.click();
|
||||
}
|
||||
|
||||
await page.locator(`a:has-text("Unnamed Timer")`).click({
|
||||
button: 'right'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a timer action from the tree context menu
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {TimerAction} action
|
||||
*/
|
||||
async function triggerTimerContextMenuAction(page, action) {
|
||||
const menuAction = `.c-menu ul li >> text="${action}"`;
|
||||
await openTimerContextMenu(page);
|
||||
await page.locator(menuAction).click();
|
||||
assertTimerStateAfterAction(page, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a timer action from the 3dot menu
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {TimerAction} action
|
||||
*/
|
||||
async function triggerTimer3dotMenuAction(page, action) {
|
||||
const menuAction = `.c-menu ul li >> text="${action}"`;
|
||||
const threeDotMenuButton = 'button[title="More options"]';
|
||||
let isActionAvailable = false;
|
||||
let iterations = 0;
|
||||
// Dismiss/open the 3dot menu until the action is available
|
||||
// or a maxiumum number of iterations is reached
|
||||
while (!isActionAvailable && iterations <= 20) {
|
||||
await page.click('.c-object-view');
|
||||
await page.click(threeDotMenuButton);
|
||||
isActionAvailable = await page.locator(menuAction).isVisible();
|
||||
iterations++;
|
||||
}
|
||||
|
||||
await page.locator(menuAction).click();
|
||||
assertTimerStateAfterAction(page, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a timer action from the object view
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {TimerViewAction} action
|
||||
*/
|
||||
async function triggerTimerViewAction(page, action) {
|
||||
const buttonTitle = buttonTitleFromAction(action);
|
||||
await page.click(`button[title="${buttonTitle}"]`);
|
||||
assertTimerStateAfterAction(page, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes in a TimerViewAction and returns the button title
|
||||
* @param {TimerViewAction} action
|
||||
*/
|
||||
function buttonTitleFromAction(action) {
|
||||
switch (action) {
|
||||
case 'Start':
|
||||
return 'Start';
|
||||
case 'Pause':
|
||||
return 'Pause';
|
||||
case 'Restart at 0':
|
||||
return 'Reset';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the timer state after a timer action has been performed.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {TimerAction} action
|
||||
*/
|
||||
async function assertTimerStateAfterAction(page, action) {
|
||||
let timerStateClass;
|
||||
switch (action) {
|
||||
case 'Start':
|
||||
case 'Restart at 0':
|
||||
timerStateClass = "is-started";
|
||||
break;
|
||||
case 'Stop':
|
||||
timerStateClass = 'is-stopped';
|
||||
break;
|
||||
case 'Pause':
|
||||
timerStateClass = 'is-paused';
|
||||
break;
|
||||
}
|
||||
|
||||
await expect.soft(page.locator('.c-timer')).toHaveClass(new RegExp(timerStateClass));
|
||||
}
|
||||
111
e2e/tests/ui/layout/search/grandsearch.e2e.spec.js
Normal file
@@ -0,0 +1,111 @@
|
||||
/*****************************************************************************
|
||||
* 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 search functionality.
|
||||
*/
|
||||
|
||||
const { expect } = require('@playwright/test');
|
||||
const { test } = require('../../../../fixtures');
|
||||
|
||||
/**
|
||||
* Creates a notebook object and adds an entry.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function createClockAndDisplayLayout(page) {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click button:has-text("Create")
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
// Click li:has-text("Notebook")
|
||||
await page.locator('li:has-text("Clock")').click();
|
||||
// Click button:has-text("OK")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
|
||||
// Click a:has-text("My Items")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('a:has-text("My Items") >> nth=0').click()
|
||||
]);
|
||||
// Click button:has-text("Create")
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
// Click li:has-text("Notebook")
|
||||
await page.locator('li:has-text("Display Layout")').click();
|
||||
// Click button:has-text("OK")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
}
|
||||
|
||||
test.describe('Grand Search', () => {
|
||||
test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page }) => {
|
||||
await createClockAndDisplayLayout(page);
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cl');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toContainText('Clock');
|
||||
// Click text=Elements >> nth=0
|
||||
await page.locator('text=Elements').first().click();
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
|
||||
// Click [aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock
|
||||
await page.locator('[aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock').click();
|
||||
await expect(page.locator('.js-preview-window')).toBeVisible();
|
||||
|
||||
// Click [aria-label="Close"]
|
||||
await page.locator('[aria-label="Close"]').click();
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toBeVisible();
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toContainText('Cloc');
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] a >> nth=0
|
||||
await page.locator('[aria-label="OpenMCT Search"] a').first().click();
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
|
||||
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('foo');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
|
||||
|
||||
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
// Click text=Save and Finish Editing
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
|
||||
// Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl');
|
||||
// Click text=Unnamed Clock
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Clock').click()
|
||||
]);
|
||||
await expect(page.locator('.is-object-type-clock')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -53,7 +53,7 @@ test.describe('Visual - addInit', () => {
|
||||
//Go to baseURL
|
||||
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
|
||||
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,21 +67,21 @@ test.describe('Visual - Default', () => {
|
||||
await percySnapshot(page, `About (theme: '${theme}')`);
|
||||
});
|
||||
|
||||
test('Visual - Default Condition Set', async ({ page, theme }) => {
|
||||
test('Visual - Default Condition Set @unstable', async ({ page, theme }) => {
|
||||
|
||||
await createDomainObjectWithDefaults(page, 'Condition Set');
|
||||
await createDomainObjectWithDefaults(page, { type: 'Condition Set' });
|
||||
|
||||
// Take a snapshot of the newly created Condition Set object
|
||||
await percySnapshot(page, `Default Condition Set (theme: '${theme}')`);
|
||||
});
|
||||
|
||||
test.fixme('Visual - Default Condition Widget', async ({ page, theme }) => {
|
||||
test('Visual - Default Condition Widget @unstable', async ({ page, theme }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
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
|
||||
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}')`);
|
||||
});
|
||||
|
||||
test('Visual - Save Successful Banner', async ({ page, theme }) => {
|
||||
await createDomainObjectWithDefaults(page, 'Timer');
|
||||
test('Visual - Save Successful Banner @unstable', async ({ page, theme }) => {
|
||||
await createDomainObjectWithDefaults(page, { type: 'Timer' });
|
||||
|
||||
await page.locator('.c-message-banner__message').hover({ trial: true });
|
||||
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 }) => {
|
||||
await createDomainObjectWithDefaults(page, 'Gauge');
|
||||
test('Visual - Default Gauge is correct @unstable', async ({ page, theme }) => {
|
||||
await createDomainObjectWithDefaults(page, { type: 'Gauge' });
|
||||
|
||||
// Take a snapshot of the newly created Gauge object
|
||||
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}')`);
|
||||
});
|
||||
});
|
||||
95
e2e/tests/visual/generateVisualTestData.e2e.spec.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/*****************************************************************************
|
||||
* 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 generating LocalStorage via Session Storage to be used
|
||||
in some visual test suites like controlledClock.visual.spec.js. This suite should run to completion
|
||||
and generate an artifact named ./e2e/test-data/VisualTestData_storage.json . This will run
|
||||
on every Commit to ensure that this object still loads into tests correctly and will retain the
|
||||
.e2e.spec.js suffix.
|
||||
|
||||
TODO: Provide additional validation of object properties as it grows.
|
||||
|
||||
*/
|
||||
|
||||
const { test } = require('../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// add overlay plot with defaults
|
||||
await page.locator('li:has-text("Overlay Plot")').click();
|
||||
|
||||
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
|
||||
await page.click('form[name="mctForm"] a:has-text("My Items")');
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear1
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// save (exit edit mode)
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// click create button
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// add sine wave generator with defaults
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
|
||||
//Add a 5000 ms Delay
|
||||
await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000');
|
||||
|
||||
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
|
||||
await page.click('form[name="mctForm"] a:has-text("Overlay Plot")');
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear1
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// focus the overlay plot
|
||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
||||
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');
|
||||
//Save localStorage for future test execution
|
||||
await context.storageState({ path: './e2e/test-data/VisualTestData_storage.json' });
|
||||
});
|
||||
@@ -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=Save and Finish Editing').click();
|
||||
const folder1 = 'Folder1';
|
||||
await createDomainObjectWithDefaults(page, 'Folder', folder1);
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: folder1
|
||||
});
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
|
||||
@@ -20,59 +20,36 @@
|
||||
* 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) {
|
||||
openmct.install(openmct.plugins.FaultManagement());
|
||||
|
||||
const faultsData = utils.randomFaults(staticFaults);
|
||||
|
||||
openmct.faults.addProvider({
|
||||
request(domainObject, options) {
|
||||
const faults = JSON.parse(localStorage.getItem('faults'));
|
||||
|
||||
return Promise.resolve(faults.alarms);
|
||||
return Promise.resolve(faultsData);
|
||||
},
|
||||
subscribe(domainObject, callback) {
|
||||
const faultsData = JSON.parse(localStorage.getItem('faults')).alarms;
|
||||
|
||||
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);
|
||||
};
|
||||
return () => {};
|
||||
},
|
||||
supportsRequest(domainObject) {
|
||||
const faults = localStorage.getItem('faults');
|
||||
|
||||
return faults && domainObject.type === 'faultManagement';
|
||||
return domainObject.type === 'faultManagement';
|
||||
},
|
||||
supportsSubscribe(domainObject) {
|
||||
const faults = localStorage.getItem('faults');
|
||||
|
||||
return faults && domainObject.type === 'faultManagement';
|
||||
return domainObject.type === 'faultManagement';
|
||||
},
|
||||
acknowledgeFault(fault, { comment = '' }) {
|
||||
console.log('acknowledgeFault', fault);
|
||||
console.log('comment', comment);
|
||||
utils.acknowledgeFault(fault);
|
||||
|
||||
return Promise.resolve({
|
||||
success: true
|
||||
});
|
||||
},
|
||||
shelveFault(fault, shelveData) {
|
||||
console.log('shelveFault', fault);
|
||||
console.log('shelveData', shelveData);
|
||||
shelveFault(fault, duration) {
|
||||
utils.shelveFault(fault, duration);
|
||||
|
||||
return Promise.resolve({
|
||||
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
|
||||
};
|
||||
27
package.json
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "2.1.0-SNAPSHOT",
|
||||
"version": "2.0.8",
|
||||
"description": "The Open MCT core platform",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.18.9",
|
||||
"@braintree/sanitize-url": "6.0.0",
|
||||
"@percy/cli": "1.7.2",
|
||||
"@percy/cli": "1.10.0",
|
||||
"@percy/playwright": "1.0.4",
|
||||
"@playwright/test": "1.23.0",
|
||||
"@types/eventemitter3": "^1.0.0",
|
||||
@@ -23,9 +23,9 @@
|
||||
"d3-axis": "3.0.0",
|
||||
"d3-scale": "3.3.0",
|
||||
"d3-selection": "3.0.0",
|
||||
"eslint": "8.18.0",
|
||||
"eslint": "8.22.0",
|
||||
"eslint-plugin-compat": "4.0.2",
|
||||
"eslint-plugin-playwright": "0.10.0",
|
||||
"eslint-plugin-playwright": "0.11.1",
|
||||
"eslint-plugin-vue": "9.3.0",
|
||||
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
|
||||
"eventemitter3": "1.2.0",
|
||||
@@ -33,9 +33,8 @@
|
||||
"file-saver": "2.0.5",
|
||||
"git-rev-sync": "3.0.2",
|
||||
"html2canvas": "1.4.1",
|
||||
"imports-loader": "0.8.0",
|
||||
"imports-loader": "4.0.1",
|
||||
"jasmine-core": "4.3.0",
|
||||
"jsdoc": "3.6.11",
|
||||
"karma": "6.3.20",
|
||||
"karma-chrome-launcher": "3.1.1",
|
||||
"karma-cli": "2.0.0",
|
||||
@@ -46,22 +45,20 @@
|
||||
"karma-sourcemap-loader": "0.3.8",
|
||||
"karma-spec-reporter": "0.0.34",
|
||||
"karma-webpack": "5.0.0",
|
||||
"lighthouse": "9.6.1",
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"mini-css-extract-plugin": "2.6.1",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.3.2",
|
||||
"moment-timezone": "0.5.34",
|
||||
"node-bourbon": "4.2.3",
|
||||
"moment-timezone": "0.5.37",
|
||||
"nyc":"15.1.0",
|
||||
"painterro": "1.2.78",
|
||||
"plotly.js-basic-dist": "2.12.0",
|
||||
"plotly.js-gl2d-dist": "2.12.0",
|
||||
"plotly.js-basic-dist": "2.14.0",
|
||||
"plotly.js-gl2d-dist": "2.14.0",
|
||||
"printj": "1.3.1",
|
||||
"request": "2.88.2",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sass": "1.52.2",
|
||||
"sass": "1.54.4",
|
||||
"sass-loader": "13.0.2",
|
||||
"sinon": "14.0.0",
|
||||
"style-loader": "^1.0.1",
|
||||
@@ -90,7 +87,8 @@
|
||||
"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: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: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",
|
||||
@@ -98,11 +96,8 @@
|
||||
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js",
|
||||
"test:perf": "npx playwright test --config=e2e/playwright-performance.config.js",
|
||||
"test:watch": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
|
||||
"jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
|
||||
"update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2022/gm' ./src/ui/layout/AboutDialog.vue",
|
||||
"update-copyright-date": "npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\s\\(c\\)\\s20\\d\\d\\-20\\d\\d/Copyright \\(c\\)\\ 2014\\-2022/gm'",
|
||||
"otherdoc": "node docs/gendocs.js --in docs/src --out dist/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'",
|
||||
"docs": "npm run jsdoc ; npm run otherdoc",
|
||||
"cov:e2e:report":"nyc report --reporter=lcovonly --report-dir=./coverage/e2e",
|
||||
"cov:e2e:full:publish":"codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-full",
|
||||
"cov:e2e:stable:publish":"codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-stable",
|
||||
|
||||
@@ -40,6 +40,8 @@ const ANNOTATION_TYPES = Object.freeze({
|
||||
PLOT_SPATIAL: 'PLOT_SPATIAL'
|
||||
});
|
||||
|
||||
const ANNOTATION_TYPE = 'annotation';
|
||||
|
||||
/**
|
||||
* @typedef {Object} 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.openmct.types.addType('annotation', {
|
||||
this.openmct.types.addType(ANNOTATION_TYPE, {
|
||||
name: 'Annotation',
|
||||
description: 'A user created note or comment about time ranges, pixel space, and geospatial features.',
|
||||
creatable: false,
|
||||
@@ -136,6 +138,10 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
this.availableTags[tagKey] = tagsDefinition;
|
||||
}
|
||||
|
||||
isAnnotation(domainObject) {
|
||||
return domainObject && (domainObject.type === ANNOTATION_TYPE);
|
||||
}
|
||||
|
||||
getAvailableTags() {
|
||||
if (this.availableTags) {
|
||||
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 appliedTagSearchResults = this.#addTagMetaInformationToResults(searchResults, matchingTagKeys);
|
||||
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 mockObjectProvider;
|
||||
let mockDomainObject;
|
||||
let mockFolderObject;
|
||||
let mockAnnotationObject;
|
||||
|
||||
beforeEach((done) => {
|
||||
openmct = createOpenMct();
|
||||
openmct.install(new ExampleTagsPlugin());
|
||||
const availableTags = openmct.annotation.getAvailableTags();
|
||||
mockFolderObject = {
|
||||
type: 'root',
|
||||
name: 'folderFoo',
|
||||
location: '',
|
||||
identifier: {
|
||||
key: 'someParent',
|
||||
namespace: 'fooNameSpace'
|
||||
}
|
||||
};
|
||||
mockDomainObject = {
|
||||
type: 'notebook',
|
||||
name: 'fooRabbitNotebook',
|
||||
location: 'fooNameSpace:someParent',
|
||||
identifier: {
|
||||
key: 'some-object',
|
||||
namespace: 'fooNameSpace'
|
||||
@@ -68,6 +79,8 @@ describe("The Annotation API", () => {
|
||||
return mockDomainObject;
|
||||
} else if (identifier.key === mockAnnotationObject.identifier.key) {
|
||||
return mockAnnotationObject;
|
||||
} else if (identifier.key === mockFolderObject.identifier.key) {
|
||||
return mockFolderObject;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@@ -150,6 +163,7 @@ describe("The Annotation API", () => {
|
||||
// use local worker
|
||||
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
|
||||
openmct.objects.inMemorySearchProvider.worker = null;
|
||||
await openmct.objects.inMemorySearchProvider.index(mockFolderObject);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockAnnotationObject);
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ import FormProperties from './components/FormProperties.vue';
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import Vue from 'vue';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default class FormsAPI extends EventEmitter {
|
||||
constructor(openmct) {
|
||||
@@ -158,7 +159,8 @@ export default class FormsAPI extends EventEmitter {
|
||||
key = property.join('.');
|
||||
}
|
||||
|
||||
changes[key] = data.value;
|
||||
_.set(changes, `${key}`, data.value);
|
||||
self.emit('formPropertiesChanged', changes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,8 @@ class InMemorySearchProvider {
|
||||
this.localSearchForTags = this.localSearchForTags.bind(this);
|
||||
this.localSearchForNotebookAnnotations = this.localSearchForNotebookAnnotations.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.startIndexing = this.startIndexing.bind(this);
|
||||
|
||||
@@ -75,6 +77,12 @@ class InMemorySearchProvider {
|
||||
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.indexedCompositions);
|
||||
});
|
||||
@@ -259,7 +267,6 @@ class InMemorySearchProvider {
|
||||
}
|
||||
|
||||
onAnnotationCreation(annotationObject) {
|
||||
|
||||
const objectProvider = this.openmct.objects.getProvider(annotationObject.identifier);
|
||||
if (objectProvider === undefined || objectProvider.search === undefined) {
|
||||
const provider = this;
|
||||
@@ -281,17 +288,34 @@ class InMemorySearchProvider {
|
||||
provider.index(domainObject);
|
||||
}
|
||||
|
||||
onCompositionMutation(domainObject, composition) {
|
||||
onCompositionAdded(newDomainObjectToIndex) {
|
||||
const provider = this;
|
||||
const indexedComposition = domainObject.composition;
|
||||
const identifiersToIndex = composition
|
||||
.filter(identifier => !indexedComposition
|
||||
.some(indexedIdentifier => this.openmct.objects
|
||||
.areIdsEqual([identifier, indexedIdentifier])));
|
||||
// The object comes in as a mutable domain object, which has functions,
|
||||
// which the index function cannot handle as it will eventually be serialized
|
||||
// using structuredClone. Thus we're using JSON.parse/JSON.stringify to discard
|
||||
// those functions.
|
||||
const nonMutableDomainObject = JSON.parse(JSON.stringify(newDomainObjectToIndex));
|
||||
|
||||
identifiersToIndex.forEach(identifier => {
|
||||
this.openmct.objects.get(identifier).then(objectToIndex => provider.index(objectToIndex));
|
||||
});
|
||||
const objectProvider = this.openmct.objects.getProvider(nonMutableDomainObject.identifier);
|
||||
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) {
|
||||
const provider = this;
|
||||
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
const composition = this.openmct.composition.get(domainObject);
|
||||
|
||||
if (!this.indexedIds[keyString]) {
|
||||
this.indexedIds[keyString] = this.openmct.objects.observe(
|
||||
@@ -312,11 +337,12 @@ class InMemorySearchProvider {
|
||||
'name',
|
||||
this.onNameMutation.bind(this, domainObject)
|
||||
);
|
||||
this.indexedCompositions[keyString] = this.openmct.objects.observe(
|
||||
domainObject,
|
||||
'composition',
|
||||
this.onCompositionMutation.bind(this, domainObject)
|
||||
);
|
||||
if (composition) {
|
||||
composition.on('add', this.onCompositionAdded);
|
||||
composition.on('remove', this.onCompositionRemoved);
|
||||
this.indexedCompositions[keyString] = composition;
|
||||
}
|
||||
|
||||
if (domainObject.type === 'annotation') {
|
||||
this.indexedTags[keyString] = this.openmct.objects.observe(
|
||||
domainObject,
|
||||
@@ -338,8 +364,6 @@ class InMemorySearchProvider {
|
||||
}
|
||||
}
|
||||
|
||||
const composition = this.openmct.composition.get(domainObject);
|
||||
|
||||
if (composition !== undefined) {
|
||||
const children = await composition.load();
|
||||
|
||||
|
||||
@@ -34,11 +34,11 @@ import InMemorySearchProvider from './InMemorySearchProvider';
|
||||
* Uniquely identifies a domain object.
|
||||
*
|
||||
* @typedef Identifier
|
||||
* @memberof module:openmct.ObjectAPI~
|
||||
* @property {string} namespace the namespace to/from which this domain
|
||||
* object should be loaded/stored.
|
||||
* @property {string} key a unique identifier for the domain object
|
||||
* within that namespace
|
||||
* @memberof module:openmct.ObjectAPI~
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -230,15 +230,10 @@ export default class ObjectAPI {
|
||||
return result;
|
||||
}).catch((result) => {
|
||||
console.warn(`Failed to retrieve ${keystring}:`, result);
|
||||
this.openmct.notifications.error(`Failed to retrieve object ${keystring}`);
|
||||
|
||||
delete this.cache[keystring];
|
||||
|
||||
if (!result) {
|
||||
//no result means resource either doesn't exist or is missing
|
||||
//otherwise it's an error, and we shouldn't apply interceptors
|
||||
result = this.applyGetInterceptors(identifier);
|
||||
}
|
||||
result = this.applyGetInterceptors(identifier);
|
||||
|
||||
return result;
|
||||
});
|
||||
@@ -615,27 +610,60 @@ export default class ObjectAPI {
|
||||
* @param {module:openmct.ObjectAPI~Identifier[]} identifiers
|
||||
*/
|
||||
areIdsEqual(...identifiers) {
|
||||
const firstIdentifier = utils.parseKeyString(identifiers[0]);
|
||||
|
||||
return identifiers.map(utils.parseKeyString)
|
||||
.every(identifier => {
|
||||
return identifier === identifiers[0]
|
||||
|| (identifier.namespace === identifiers[0].namespace
|
||||
&& identifier.key === identifiers[0].key);
|
||||
return identifier === firstIdentifier
|
||||
|| (identifier.namespace === firstIdentifier.namespace
|
||||
&& identifier.key === firstIdentifier.key);
|
||||
});
|
||||
}
|
||||
|
||||
getOriginalPath(identifier, path = []) {
|
||||
return this.get(identifier).then((domainObject) => {
|
||||
path.push(domainObject);
|
||||
let location = domainObject.location;
|
||||
/**
|
||||
* Given an original path check if the path is reachable via root
|
||||
* @param {Array<Object>} originalPath an array of path objects to check
|
||||
* @returns {boolean} whether the domain object is reachable
|
||||
*/
|
||||
isReachable(originalPath) {
|
||||
if (originalPath && originalPath.length) {
|
||||
return (originalPath[originalPath.length - 1].type === 'root');
|
||||
}
|
||||
|
||||
if (location) {
|
||||
return this.getOriginalPath(utils.parseKeyString(location), path);
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#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) {
|
||||
return objectPath !== undefined
|
||||
&& 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", () => {
|
||||
beforeEach(() => {
|
||||
spyOn(openmct.editor, 'isEditing').and.returnValue(true);
|
||||
|
||||
@@ -91,6 +91,10 @@ define([
|
||||
* @returns keyString
|
||||
*/
|
||||
function makeKeyString(identifier) {
|
||||
if (!identifier) {
|
||||
throw new Error("Cannot make key string from null identifier");
|
||||
}
|
||||
|
||||
if (isKeyString(identifier)) {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import AutoflowTabularPlugin from './AutoflowTabularPlugin';
|
||||
import AutoflowTabularConstants from './AutoflowTabularConstants';
|
||||
import DOMObserver from './dom-observer';
|
||||
import {
|
||||
@@ -28,376 +29,344 @@ import {
|
||||
} from 'utils/testing';
|
||||
import Vue from 'vue';
|
||||
|
||||
describe("AutoflowTabularPlugin", () => {
|
||||
let testTypeObject;
|
||||
let autoflowObject;
|
||||
let otherObject;
|
||||
let openmct;
|
||||
let viewProviders;
|
||||
let autoflowViewProvider;
|
||||
// TODO lots of its without expects
|
||||
xdescribe("AutoflowTabularPlugin", () => {
|
||||
let testType;
|
||||
let testObject;
|
||||
let mockmct;
|
||||
|
||||
beforeEach(done => {
|
||||
testTypeObject = {
|
||||
type: 'some-type'
|
||||
};
|
||||
autoflowObject = {
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'some-type-key'
|
||||
},
|
||||
type: 'some-type'
|
||||
};
|
||||
otherObject = {
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'other-type-key'
|
||||
},
|
||||
type: 'other-type'
|
||||
};
|
||||
beforeEach(() => {
|
||||
testType = "some-type";
|
||||
testObject = { type: testType };
|
||||
mockmct = createOpenMct();
|
||||
spyOn(mockmct.composition, 'get');
|
||||
spyOn(mockmct.objectViews, 'addProvider');
|
||||
spyOn(mockmct.telemetry, 'getMetadata');
|
||||
spyOn(mockmct.telemetry, 'getValueFormatter');
|
||||
spyOn(mockmct.telemetry, 'limitEvaluator');
|
||||
spyOn(mockmct.telemetry, 'request');
|
||||
spyOn(mockmct.telemetry, 'subscribe');
|
||||
|
||||
openmct = createOpenMct();
|
||||
openmct.install(openmct.plugins.AutoflowView(testTypeObject));
|
||||
|
||||
spyOn(openmct.composition, 'get');
|
||||
spyOn(openmct.telemetry, 'getMetadata');
|
||||
spyOn(openmct.telemetry, 'getValueFormatter');
|
||||
spyOn(openmct.telemetry, 'limitEvaluator');
|
||||
spyOn(openmct.telemetry, 'request');
|
||||
spyOn(openmct.telemetry, 'subscribe');
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
|
||||
viewProviders = openmct.objectViews.get(autoflowObject, []);
|
||||
autoflowViewProvider = viewProviders.filter(provider => provider?.key === 'autoflow')?.[0];
|
||||
const plugin = new AutoflowTabularPlugin({ type: testType });
|
||||
plugin(mockmct);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return resetApplicationState(openmct);
|
||||
return resetApplicationState(mockmct);
|
||||
});
|
||||
|
||||
it("installs a view provider", () => {
|
||||
expect(autoflowViewProvider).toBeDefined();
|
||||
expect(mockmct.objectViews.addProvider).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("applies its view to the type from options", () => {
|
||||
expect(autoflowViewProvider.canView(autoflowObject, [])).toBeTrue();
|
||||
});
|
||||
|
||||
it("does not apply to other types", () => {
|
||||
expect(autoflowViewProvider.canView(otherObject, [])).toBeFalse();
|
||||
});
|
||||
|
||||
describe("provides a view which", () => {
|
||||
let testKeys;
|
||||
let testChildren;
|
||||
let testContainer;
|
||||
let testHistories;
|
||||
let mockComposition;
|
||||
let mockMetadata;
|
||||
let mockEvaluator;
|
||||
let mockUnsubscribes;
|
||||
let callbacks;
|
||||
let view;
|
||||
let domObserver;
|
||||
|
||||
function waitsForChange() {
|
||||
return new Promise(function (resolve) {
|
||||
window.requestAnimationFrame(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function emitEvent(mockEmitter, type, event) {
|
||||
mockEmitter.on.calls.all().forEach((call) => {
|
||||
if (call.args[0] === type) {
|
||||
call.args[1](event);
|
||||
}
|
||||
});
|
||||
}
|
||||
describe("installs a view provider which", () => {
|
||||
let provider;
|
||||
|
||||
beforeEach(() => {
|
||||
callbacks = {};
|
||||
provider =
|
||||
mockmct.objectViews.addProvider.calls.mostRecent().args[0];
|
||||
});
|
||||
|
||||
spyOnBuiltins(['requestAnimationFrame']);
|
||||
window.requestAnimationFrame.and.callFake((callBack) => {
|
||||
callBack();
|
||||
});
|
||||
it("applies its view to the type from options", () => {
|
||||
expect(provider.canView(testObject, [])).toBe(true);
|
||||
});
|
||||
|
||||
testKeys = ['abc', 'def', 'xyz'];
|
||||
testChildren = testKeys.map((key) => {
|
||||
return {
|
||||
identifier: {
|
||||
namespace: "test",
|
||||
key: key
|
||||
},
|
||||
name: "Object " + key
|
||||
};
|
||||
});
|
||||
testContainer = document.createElement('div');
|
||||
domObserver = new DOMObserver(testContainer);
|
||||
it("does not apply to other types", () => {
|
||||
expect(provider.canView({ type: 'foo' }, [])).toBe(false);
|
||||
});
|
||||
|
||||
testHistories = testKeys.reduce((histories, key, index) => {
|
||||
histories[key] = {
|
||||
key: key,
|
||||
range: index + 10,
|
||||
domain: key + index
|
||||
};
|
||||
describe("provides a view which", () => {
|
||||
let testKeys;
|
||||
let testChildren;
|
||||
let testContainer;
|
||||
let testHistories;
|
||||
let mockComposition;
|
||||
let mockMetadata;
|
||||
let mockEvaluator;
|
||||
let mockUnsubscribes;
|
||||
let callbacks;
|
||||
let view;
|
||||
let domObserver;
|
||||
|
||||
return histories;
|
||||
}, {});
|
||||
|
||||
mockComposition =
|
||||
jasmine.createSpyObj('composition', ['load', 'on', 'off']);
|
||||
mockMetadata =
|
||||
jasmine.createSpyObj('metadata', ['valuesForHints']);
|
||||
|
||||
mockEvaluator = jasmine.createSpyObj('evaluator', ['evaluate']);
|
||||
mockUnsubscribes = testKeys.reduce((map, key) => {
|
||||
map[key] = jasmine.createSpy('unsubscribe-' + key);
|
||||
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
openmct.composition.get.and.returnValue(mockComposition);
|
||||
mockComposition.load.and.callFake(() => {
|
||||
testChildren.forEach(emitEvent.bind(null, mockComposition, 'add'));
|
||||
|
||||
return Promise.resolve(testChildren);
|
||||
});
|
||||
|
||||
openmct.telemetry.getMetadata.and.returnValue(mockMetadata);
|
||||
openmct.telemetry.getValueFormatter.and.callFake((metadatum) => {
|
||||
const mockFormatter = jasmine.createSpyObj('formatter', ['format']);
|
||||
mockFormatter.format.and.callFake((datum) => {
|
||||
return datum[metadatum.hint];
|
||||
function waitsForChange() {
|
||||
return new Promise(function (resolve) {
|
||||
window.requestAnimationFrame(resolve);
|
||||
});
|
||||
|
||||
return mockFormatter;
|
||||
});
|
||||
openmct.telemetry.limitEvaluator.and.returnValue(mockEvaluator);
|
||||
openmct.telemetry.subscribe.and.callFake((obj, callback) => {
|
||||
const key = obj.identifier.key;
|
||||
callbacks[key] = callback;
|
||||
|
||||
return mockUnsubscribes[key];
|
||||
});
|
||||
openmct.telemetry.request.and.callFake((obj, request) => {
|
||||
const key = obj.identifier.key;
|
||||
|
||||
return Promise.resolve([testHistories[key]]);
|
||||
});
|
||||
mockMetadata.valuesForHints.and.callFake((hints) => {
|
||||
return [{ hint: hints[0] }];
|
||||
});
|
||||
|
||||
view = autoflowViewProvider.view(autoflowObject);
|
||||
view.show(testContainer);
|
||||
|
||||
return Vue.nextTick();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
domObserver.destroy();
|
||||
});
|
||||
|
||||
it("populates its container", () => {
|
||||
expect(testContainer.children.length > 0).toBe(true);
|
||||
});
|
||||
|
||||
describe("when rows have been populated", () => {
|
||||
function rowsMatch() {
|
||||
const rows = testContainer.querySelectorAll(".l-autoflow-row").length;
|
||||
|
||||
return rows === testChildren.length;
|
||||
}
|
||||
|
||||
it("shows one row per child object", async () => {
|
||||
const success = await domObserver.when(rowsMatch);
|
||||
function emitEvent(mockEmitter, type, event) {
|
||||
mockEmitter.on.calls.all().forEach((call) => {
|
||||
if (call.args[0] === type) {
|
||||
call.args[1](event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
expect(success).toBeTrue();
|
||||
beforeEach(() => {
|
||||
callbacks = {};
|
||||
|
||||
spyOnBuiltins(['requestAnimationFrame']);
|
||||
window.requestAnimationFrame.and.callFake((callBack) => {
|
||||
callBack();
|
||||
});
|
||||
|
||||
testObject = { type: 'some-type' };
|
||||
testKeys = ['abc', 'def', 'xyz'];
|
||||
testChildren = testKeys.map((key) => {
|
||||
return {
|
||||
identifier: {
|
||||
namespace: "test",
|
||||
key: key
|
||||
},
|
||||
name: "Object " + key
|
||||
};
|
||||
});
|
||||
testContainer = document.createElement('div');
|
||||
domObserver = new DOMObserver(testContainer);
|
||||
|
||||
testHistories = testKeys.reduce((histories, key, index) => {
|
||||
histories[key] = {
|
||||
key: key,
|
||||
range: index + 10,
|
||||
domain: key + index
|
||||
};
|
||||
|
||||
return histories;
|
||||
}, {});
|
||||
|
||||
mockComposition =
|
||||
jasmine.createSpyObj('composition', ['load', 'on', 'off']);
|
||||
mockMetadata =
|
||||
jasmine.createSpyObj('metadata', ['valuesForHints']);
|
||||
|
||||
mockEvaluator = jasmine.createSpyObj('evaluator', ['evaluate']);
|
||||
mockUnsubscribes = testKeys.reduce((map, key) => {
|
||||
map[key] = jasmine.createSpy('unsubscribe-' + key);
|
||||
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
mockmct.composition.get.and.returnValue(mockComposition);
|
||||
mockComposition.load.and.callFake(() => {
|
||||
testChildren.forEach(emitEvent.bind(null, mockComposition, 'add'));
|
||||
|
||||
return Promise.resolve(testChildren);
|
||||
});
|
||||
|
||||
mockmct.telemetry.getMetadata.and.returnValue(mockMetadata);
|
||||
mockmct.telemetry.getValueFormatter.and.callFake((metadatum) => {
|
||||
const mockFormatter = jasmine.createSpyObj('formatter', ['format']);
|
||||
mockFormatter.format.and.callFake((datum) => {
|
||||
return datum[metadatum.hint];
|
||||
});
|
||||
|
||||
return mockFormatter;
|
||||
});
|
||||
mockmct.telemetry.limitEvaluator.and.returnValue(mockEvaluator);
|
||||
mockmct.telemetry.subscribe.and.callFake((obj, callback) => {
|
||||
const key = obj.identifier.key;
|
||||
callbacks[key] = callback;
|
||||
|
||||
return mockUnsubscribes[key];
|
||||
});
|
||||
mockmct.telemetry.request.and.callFake((obj, request) => {
|
||||
const key = obj.identifier.key;
|
||||
|
||||
return Promise.resolve([testHistories[key]]);
|
||||
});
|
||||
mockMetadata.valuesForHints.and.callFake((hints) => {
|
||||
return [{ hint: hints[0] }];
|
||||
});
|
||||
|
||||
view = provider.view(testObject);
|
||||
view.show(testContainer);
|
||||
|
||||
return Vue.nextTick();
|
||||
});
|
||||
|
||||
it("adds rows on composition change", async () => {
|
||||
const child = {
|
||||
identifier: {
|
||||
namespace: "test",
|
||||
key: "123"
|
||||
},
|
||||
name: "Object 123"
|
||||
};
|
||||
testChildren.push(child);
|
||||
emitEvent(mockComposition, 'add', child);
|
||||
|
||||
const success = await domObserver.when(rowsMatch);
|
||||
|
||||
expect(success).toBeTrue();
|
||||
afterEach(() => {
|
||||
domObserver.destroy();
|
||||
});
|
||||
|
||||
it("removes rows on composition change", async () => {
|
||||
const child = testChildren.pop();
|
||||
|
||||
emitEvent(mockComposition, 'remove', child.identifier);
|
||||
|
||||
const success = await domObserver.when(rowsMatch);
|
||||
|
||||
expect(success).toBeTrue();
|
||||
it("populates its container", () => {
|
||||
expect(testContainer.children.length > 0).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("removes subscriptions when destroyed", () => {
|
||||
testKeys.forEach((key) => {
|
||||
expect(mockUnsubscribes[key]).not.toHaveBeenCalled();
|
||||
});
|
||||
view.destroy();
|
||||
testKeys.forEach((key) => {
|
||||
expect(mockUnsubscribes[key]).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe("when rows have been populated", () => {
|
||||
function rowsMatch() {
|
||||
const rows = testContainer.querySelectorAll(".l-autoflow-row").length;
|
||||
|
||||
it("provides a button to change column width", async () => {
|
||||
let buttonClicked;
|
||||
|
||||
const initialWidth = testContainer.querySelector('.l-autoflow-col').style.width;
|
||||
|
||||
expect(initialWidth.length).toBeGreaterThan(0);
|
||||
|
||||
function widthHasChanged() {
|
||||
if (!buttonClicked) {
|
||||
buttonClicked = true;
|
||||
testContainer.querySelector('.change-column-width').click();
|
||||
return rows === testChildren.length;
|
||||
}
|
||||
|
||||
const changedWidth = testContainer.querySelector('.l-autoflow-col').style.width;
|
||||
|
||||
return changedWidth !== initialWidth;
|
||||
}
|
||||
|
||||
const success = await domObserver.when(widthHasChanged);
|
||||
|
||||
expect(success).toBeTrue();
|
||||
});
|
||||
|
||||
it("subscribes to all child objects", () => {
|
||||
testKeys.forEach((key) => {
|
||||
expect(callbacks[key]).toEqual(jasmine.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
it("displays historical telemetry", () => {
|
||||
function rowTextDefined() {
|
||||
return testContainer.querySelector(".l-autoflow-item.r").textContent !== "";
|
||||
}
|
||||
|
||||
return domObserver.when(rowTextDefined).then(() => {
|
||||
const rows = testContainer.querySelectorAll(".l-autoflow-row");
|
||||
|
||||
testKeys.forEach((key, index) => {
|
||||
const datum = testHistories[key];
|
||||
const $cell = rows[index].querySelector(".l-autoflow-item.r");
|
||||
|
||||
expect($cell.textContent).toEqual(String(datum.range));
|
||||
it("shows one row per child object", () => {
|
||||
return domObserver.when(rowsMatch);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("displays incoming telemetry", () => {
|
||||
const testData = testKeys.map((key, index) => {
|
||||
return {
|
||||
key: key,
|
||||
range: index * 100,
|
||||
domain: key + index
|
||||
};
|
||||
});
|
||||
// it("adds rows on composition change", () => {
|
||||
// const child = {
|
||||
// identifier: {
|
||||
// namespace: "test",
|
||||
// key: "123"
|
||||
// },
|
||||
// name: "Object 123"
|
||||
// };
|
||||
// testChildren.push(child);
|
||||
// emitEvent(mockComposition, 'add', child);
|
||||
|
||||
testData.forEach((datum) => {
|
||||
callbacks[datum.key](datum);
|
||||
});
|
||||
// return domObserver.when(rowsMatch);
|
||||
// });
|
||||
|
||||
return waitsForChange().then(() => {
|
||||
const rows = testContainer.querySelectorAll(".l-autoflow-row");
|
||||
it("removes rows on composition change", () => {
|
||||
const child = testChildren.pop();
|
||||
emitEvent(mockComposition, 'remove', child.identifier);
|
||||
|
||||
testData.forEach((datum, index) => {
|
||||
const $cell = rows[index].querySelector(".l-autoflow-item.r");
|
||||
|
||||
expect($cell.textContent).toEqual(String(datum.range));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("updates classes for limit violations", () => {
|
||||
const testClass = "some-limit-violation";
|
||||
|
||||
mockEvaluator.evaluate.and.returnValue({ cssClass: testClass });
|
||||
|
||||
testKeys.forEach((key) => {
|
||||
callbacks[key]({
|
||||
range: 'foo',
|
||||
domain: 'bar'
|
||||
return domObserver.when(rowsMatch);
|
||||
});
|
||||
});
|
||||
|
||||
return waitsForChange().then(() => {
|
||||
const rows = testContainer.querySelectorAll(".l-autoflow-row");
|
||||
|
||||
testKeys.forEach((datum, index) => {
|
||||
const $cell = rows[index].querySelector(".l-autoflow-item.r");
|
||||
|
||||
expect($cell.classList.contains(testClass)).toBe(true);
|
||||
it("removes subscriptions when destroyed", () => {
|
||||
testKeys.forEach((key) => {
|
||||
expect(mockUnsubscribes[key]).not.toHaveBeenCalled();
|
||||
});
|
||||
view.destroy();
|
||||
testKeys.forEach((key) => {
|
||||
expect(mockUnsubscribes[key]).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("automatically flows to new columns", () => {
|
||||
const rowHeight = AutoflowTabularConstants.ROW_HEIGHT;
|
||||
const sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT;
|
||||
const count = testKeys.length;
|
||||
const $container = testContainer;
|
||||
let promiseChain = Promise.resolve();
|
||||
it("provides a button to change column width", () => {
|
||||
const initialWidth = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH;
|
||||
const nextWidth =
|
||||
initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP;
|
||||
|
||||
function columnsHaveAutoflowed() {
|
||||
const itemsHeight = $container.querySelector('.l-autoflow-items').style.height;
|
||||
const availableHeight = itemsHeight - sliderHeight;
|
||||
const availableRows = Math.max(Math.floor(availableHeight / rowHeight), 1);
|
||||
const columns = Math.ceil(count / availableRows);
|
||||
expect(testContainer.querySelector('.l-autoflow-col').css('width'))
|
||||
.toEqual(initialWidth + 'px');
|
||||
|
||||
return $container.querySelectorAll('.l-autoflow-col').length === columns;
|
||||
}
|
||||
testContainer.querySelector('.change-column-width').click();
|
||||
|
||||
const absElement = $container.querySelector('.abs');
|
||||
absElement.style.position = 'absolute';
|
||||
absElement.style.left = 0;
|
||||
absElement.style.right = 0;
|
||||
absElement.style.top = 0;
|
||||
absElement.style.bottom = 0;
|
||||
function widthHasChanged() {
|
||||
const width = testContainer.querySelector('.l-autoflow-col').css('width');
|
||||
|
||||
$container.style.position = 'absolute';
|
||||
return width !== initialWidth + 'px';
|
||||
}
|
||||
|
||||
document.body.append($container);
|
||||
|
||||
function setHeight(height) {
|
||||
$container.style.height = `${height}px`;
|
||||
|
||||
return domObserver.when(columnsHaveAutoflowed);
|
||||
}
|
||||
|
||||
for (let height = 0; height < rowHeight * count * 2; height += rowHeight / 2) {
|
||||
// eslint-disable-next-line no-invalid-this
|
||||
promiseChain = promiseChain.then(setHeight.bind(this, height));
|
||||
}
|
||||
|
||||
return promiseChain.then(success => {
|
||||
expect(success).toBeTrue();
|
||||
|
||||
$container.remove();
|
||||
return domObserver.when(widthHasChanged)
|
||||
.then(() => {
|
||||
expect(testContainer.querySelector('.l-autoflow-col').css('width'))
|
||||
.toEqual(nextWidth + 'px');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("loads composition exactly once", () => {
|
||||
const testObj = testChildren.pop();
|
||||
emitEvent(mockComposition, 'remove', testObj.identifier);
|
||||
testChildren.push(testObj);
|
||||
emitEvent(mockComposition, 'add', testObj);
|
||||
expect(mockComposition.load.calls.count()).toEqual(1);
|
||||
it("subscribes to all child objects", () => {
|
||||
testKeys.forEach((key) => {
|
||||
expect(callbacks[key]).toEqual(jasmine.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
it("displays historical telemetry", () => {
|
||||
function rowTextDefined() {
|
||||
return testContainer.querySelector(".l-autoflow-item").filter(".r").text() !== "";
|
||||
}
|
||||
|
||||
return domObserver.when(rowTextDefined).then(() => {
|
||||
testKeys.forEach((key, index) => {
|
||||
const datum = testHistories[key];
|
||||
const $cell = testContainer.querySelector(".l-autoflow-row").eq(index).find(".r");
|
||||
expect($cell.text()).toEqual(String(datum.range));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("displays incoming telemetry", () => {
|
||||
const testData = testKeys.map((key, index) => {
|
||||
return {
|
||||
key: key,
|
||||
range: index * 100,
|
||||
domain: key + index
|
||||
};
|
||||
});
|
||||
|
||||
testData.forEach((datum) => {
|
||||
callbacks[datum.key](datum);
|
||||
});
|
||||
|
||||
return waitsForChange().then(() => {
|
||||
testData.forEach((datum, index) => {
|
||||
const $cell = testContainer.querySelector(".l-autoflow-row").eq(index).find(".r");
|
||||
expect($cell.text()).toEqual(String(datum.range));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("updates classes for limit violations", () => {
|
||||
const testClass = "some-limit-violation";
|
||||
mockEvaluator.evaluate.and.returnValue({ cssClass: testClass });
|
||||
testKeys.forEach((key) => {
|
||||
callbacks[key]({
|
||||
range: 'foo',
|
||||
domain: 'bar'
|
||||
});
|
||||
});
|
||||
|
||||
return waitsForChange().then(() => {
|
||||
testKeys.forEach((datum, index) => {
|
||||
const $cell = testContainer.querySelector(".l-autoflow-row").eq(index).find(".r");
|
||||
expect($cell.hasClass(testClass)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("automatically flows to new columns", () => {
|
||||
const rowHeight = AutoflowTabularConstants.ROW_HEIGHT;
|
||||
const sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT;
|
||||
const count = testKeys.length;
|
||||
const $container = testContainer;
|
||||
let promiseChain = Promise.resolve();
|
||||
|
||||
function columnsHaveAutoflowed() {
|
||||
const itemsHeight = $container.querySelector('.l-autoflow-items').height();
|
||||
const availableHeight = itemsHeight - sliderHeight;
|
||||
const availableRows = Math.max(Math.floor(availableHeight / rowHeight), 1);
|
||||
const columns = Math.ceil(count / availableRows);
|
||||
|
||||
return $container.querySelector('.l-autoflow-col').length === columns;
|
||||
}
|
||||
|
||||
$container.find('.abs').css({
|
||||
position: 'absolute',
|
||||
left: '0px',
|
||||
right: '0px',
|
||||
top: '0px',
|
||||
bottom: '0px'
|
||||
});
|
||||
$container.css({ position: 'absolute' });
|
||||
|
||||
$container.appendTo(document.body);
|
||||
|
||||
function setHeight(height) {
|
||||
$container.css('height', height + 'px');
|
||||
|
||||
return domObserver.when(columnsHaveAutoflowed);
|
||||
}
|
||||
|
||||
for (let height = 0; height < rowHeight * count * 2; height += rowHeight / 2) {
|
||||
// eslint-disable-next-line no-invalid-this
|
||||
promiseChain = promiseChain.then(setHeight.bind(this, height));
|
||||
}
|
||||
|
||||
return promiseChain.then(() => {
|
||||
$container.remove();
|
||||
});
|
||||
});
|
||||
|
||||
it("loads composition exactly once", () => {
|
||||
const testObj = testChildren.pop();
|
||||
emitEvent(mockComposition, 'remove', testObj.identifier);
|
||||
testChildren.push(testObj);
|
||||
emitEvent(mockComposition, 'add', testObj);
|
||||
expect(mockComposition.load.calls.count()).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ define([], function () {
|
||||
return new Promise(function (resolve, reject) {
|
||||
//Test latch function at least once
|
||||
if (latchFunction()) {
|
||||
resolve(true);
|
||||
resolve();
|
||||
} else {
|
||||
//Latch condition not true yet, create observer on DOM and test again on change.
|
||||
const config = {
|
||||
@@ -40,7 +40,7 @@ define([], function () {
|
||||
};
|
||||
const observer = new MutationObserver(function () {
|
||||
if (latchFunction()) {
|
||||
resolve(true);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
observer.observe(this.element, config);
|
||||
|
||||
@@ -97,11 +97,11 @@ export default {
|
||||
|
||||
},
|
||||
followTimeContext() {
|
||||
this.timeContext.on('bounds', this.reloadTelemetry);
|
||||
this.timeContext.on('bounds', this.reloadTelemetryOnBoundsChange);
|
||||
},
|
||||
stopFollowingTimeContext() {
|
||||
if (this.timeContext) {
|
||||
this.timeContext.off('bounds', this.reloadTelemetry);
|
||||
this.timeContext.off('bounds', this.reloadTelemetryOnBoundsChange);
|
||||
}
|
||||
},
|
||||
addToComposition(telemetryObject) {
|
||||
@@ -181,6 +181,11 @@ export default {
|
||||
this.composition.on('remove', this.removeTelemetryObject);
|
||||
this.composition.load();
|
||||
},
|
||||
reloadTelemetryOnBoundsChange(bounds, isTick) {
|
||||
if (!isTick) {
|
||||
this.reloadTelemetry();
|
||||
}
|
||||
},
|
||||
reloadTelemetry() {
|
||||
this.valuesByTimestamp = {};
|
||||
|
||||
|
||||
@@ -51,7 +51,11 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
}
|
||||
|
||||
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);
|
||||
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
|
||||
this.subscribeForStaleData();
|
||||
|
||||
@@ -517,7 +517,19 @@ export default {
|
||||
initializeItems() {
|
||||
this.telemetryViewMap = {};
|
||||
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) {
|
||||
let found = false;
|
||||
|
||||
@@ -232,10 +232,12 @@ export default {
|
||||
this.removeSelectable();
|
||||
}
|
||||
|
||||
this.telemetryCollection.off('add', this.setLatestValues);
|
||||
this.telemetryCollection.off('clear', this.refreshData);
|
||||
if (this.telemetryCollection) {
|
||||
this.telemetryCollection.off('add', this.setLatestValues);
|
||||
this.telemetryCollection.off('clear', this.refreshData);
|
||||
|
||||
this.telemetryCollection.destroy();
|
||||
this.telemetryCollection.destroy();
|
||||
}
|
||||
|
||||
if (this.mutablePromise) {
|
||||
this.mutablePromise.then(() => {
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
import { createOpenMct, resetApplicationState } from 'utils/testing';
|
||||
import Vue from 'vue';
|
||||
import DisplayLayoutPlugin from './plugin';
|
||||
|
||||
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', () => {
|
||||
let displayLayoutItem;
|
||||
let telemetryItem;
|
||||
|
||||
@@ -71,6 +71,8 @@ import FaultManagementToolbar from './FaultManagementToolbar.vue';
|
||||
|
||||
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 {
|
||||
components: {
|
||||
FaultManagementListHeader,
|
||||
@@ -125,27 +127,19 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
filterUsingSearchTerm(fault) {
|
||||
if (fault?.id?.toString().toLowerCase().includes(this.searchTerm)) {
|
||||
return true;
|
||||
if (!fault) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fault?.triggerValueInfo?.toString().toLowerCase().includes(this.searchTerm)) {
|
||||
return true;
|
||||
}
|
||||
let match = false;
|
||||
|
||||
if (fault?.currentValueInfo?.toString().toLowerCase().includes(this.searchTerm)) {
|
||||
return true;
|
||||
}
|
||||
SEARCH_KEYS.forEach((key) => {
|
||||
if (fault[key]?.toString().toLowerCase().includes(this.searchTerm)) {
|
||||
match = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (fault?.triggerTime.toString().toLowerCase().includes(this.searchTerm)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (fault?.severity.toString().toLowerCase().includes(this.searchTerm)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return match;
|
||||
},
|
||||
isSelected(fault) {
|
||||
return Boolean(this.selectedFaults[fault.id]);
|
||||
|
||||
@@ -24,10 +24,22 @@ import {
|
||||
createOpenMct,
|
||||
resetApplicationState
|
||||
} 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", () => {
|
||||
let openmct;
|
||||
const faultDomainObject = {
|
||||
name: 'it is not your fault',
|
||||
type: FAULT_MANAGEMENT_TYPE,
|
||||
identifier: {
|
||||
key: 'nobodies',
|
||||
namespace: 'fault'
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
openmct = createOpenMct();
|
||||
@@ -38,15 +50,54 @@ describe("The Fault Management Plugin", () => {
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
it('can be installed', () => {
|
||||
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');
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ import PropertiesAction from './PropertiesAction';
|
||||
import CreateWizard from './CreateWizard';
|
||||
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default class CreateAction extends PropertiesAction {
|
||||
constructor(openmct, type, parentDomainObject) {
|
||||
@@ -50,19 +51,15 @@ export default class CreateAction extends PropertiesAction {
|
||||
return;
|
||||
}
|
||||
|
||||
const properties = key.split('.');
|
||||
let object = this.domainObject;
|
||||
const propertiesLength = properties.length;
|
||||
properties.forEach((property, index) => {
|
||||
const isComplexProperty = propertiesLength > 1 && index !== propertiesLength - 1;
|
||||
if (isComplexProperty && object[property] !== null) {
|
||||
object = object[property];
|
||||
} else {
|
||||
object[property] = value;
|
||||
}
|
||||
});
|
||||
const existingValue = this.domainObject[`${key}`];
|
||||
if (!(existingValue instanceof Array) && (typeof existingValue === 'object')) {
|
||||
value = {
|
||||
...existingValue,
|
||||
...value
|
||||
};
|
||||
}
|
||||
|
||||
object = value;
|
||||
_.set(this.domainObject, `${key}`, value);
|
||||
});
|
||||
|
||||
const parentDomainObject = parentDomainObjectPath[0];
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
import PropertiesAction from './PropertiesAction';
|
||||
import CreateWizard from './CreateWizard';
|
||||
|
||||
export default class EditPropertiesAction extends PropertiesAction {
|
||||
constructor(openmct) {
|
||||
super(openmct);
|
||||
@@ -54,23 +55,26 @@ export default class EditPropertiesAction extends PropertiesAction {
|
||||
_onSave(changes) {
|
||||
try {
|
||||
Object.entries(changes).forEach(([key, value]) => {
|
||||
const properties = key.split('.');
|
||||
let object = this.domainObject;
|
||||
const propertiesLength = properties.length;
|
||||
properties.forEach((property, index) => {
|
||||
const isComplexProperty = propertiesLength > 1 && index !== propertiesLength - 1;
|
||||
if (isComplexProperty && object[property] !== null) {
|
||||
object = object[property];
|
||||
} else {
|
||||
object[property] = value;
|
||||
}
|
||||
});
|
||||
const existingValue = this.domainObject[`${key}`];
|
||||
if (!(existingValue instanceof Array) && (typeof existingValue === 'object')) {
|
||||
value = {
|
||||
...existingValue,
|
||||
...value
|
||||
};
|
||||
}
|
||||
|
||||
object = value;
|
||||
this.openmct.objects.mutate(this.domainObject, key, value);
|
||||
this.openmct.notifications.info('Save successful');
|
||||
this.openmct.objects.mutate(this.domainObject, `${key}`, value);
|
||||
});
|
||||
if (this.openmct.editor.isEditing()) {
|
||||
this.openmct.editor.save();
|
||||
}
|
||||
|
||||
this.openmct.notifications.info('Save successful');
|
||||
} catch (error) {
|
||||
if (this.openmct.editor.isEditing()) {
|
||||
this.openmct.editor.cancel();
|
||||
}
|
||||
|
||||
this.openmct.notifications.error('Error saving objects');
|
||||
console.error(error);
|
||||
}
|
||||
@@ -81,6 +85,9 @@ export default class EditPropertiesAction extends PropertiesAction {
|
||||
*/
|
||||
_onCancel() {
|
||||
//noop
|
||||
if (this.openmct.editor.isEditing()) {
|
||||
this.openmct.editor.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,6 +99,12 @@ export default class EditPropertiesAction extends PropertiesAction {
|
||||
const createWizard = new CreateWizard(this.openmct, this.domainObject, objectPath[1]);
|
||||
const formStructure = createWizard.getFormStructure(false);
|
||||
formStructure.title = 'Edit ' + this.domainObject.name;
|
||||
//If we're in edit mode AND want to edit properties for the same domain object
|
||||
//In this case, saving will put the object in view-only mode
|
||||
//TODO: Maybe we should block editing properties if someone is in Edit mode?
|
||||
if (!this.openmct.editor.isEditing()) {
|
||||
this.openmct.editor.edit();
|
||||
}
|
||||
|
||||
return this.openmct.forms.showForm(formStructure)
|
||||
.then(this._onSave.bind(this))
|
||||
|
||||
@@ -151,6 +151,7 @@ describe('EditPropertiesAction plugin', () => {
|
||||
function callback(newObject) {
|
||||
expect(newObject.name).not.toEqual(oldName);
|
||||
expect(newObject.name).toEqual(newName);
|
||||
expect(openmct.editor.isEditing()).toBeFalse();
|
||||
|
||||
unObserve();
|
||||
done();
|
||||
@@ -164,6 +165,7 @@ describe('EditPropertiesAction plugin', () => {
|
||||
openmct.forms.on('onFormPropertyChange', deBouncedFormChange);
|
||||
|
||||
function handleFormPropertyChange(data) {
|
||||
expect(openmct.editor.isEditing()).toBeTrue();
|
||||
const form = document.querySelector('.js-form');
|
||||
const title = form.querySelector('input');
|
||||
const notes = form.querySelector('textArea');
|
||||
@@ -213,10 +215,12 @@ describe('EditPropertiesAction plugin', () => {
|
||||
editPropertiesAction.invoke([domainObject])
|
||||
.then(() => {
|
||||
expect(domainObject.name).toEqual(name);
|
||||
expect(openmct.editor.isEditing()).toBeFalse();
|
||||
done();
|
||||
})
|
||||
.catch(() => {
|
||||
expect(domainObject.name).toEqual(name);
|
||||
expect(openmct.editor.isEditing()).toBeFalse();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -100,6 +100,7 @@ export default {
|
||||
components: {
|
||||
ToggleSwitch
|
||||
},
|
||||
inject: ["openmct"],
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
@@ -107,6 +108,8 @@ export default {
|
||||
}
|
||||
},
|
||||
data() {
|
||||
this.changes = {};
|
||||
|
||||
return {
|
||||
isUseTelemetryLimits: this.model.value.isUseTelemetryLimits,
|
||||
isDisplayMinMax: this.model.value.isDisplayMinMax,
|
||||
@@ -118,21 +121,30 @@ export default {
|
||||
min: this.model.value.min
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.openmct.forms.on('formPropertiesChanged', this.onFormPropertyChange);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.openmct.forms.off('formPropertiesChanged', this.onFormPropertyChange);
|
||||
},
|
||||
methods: {
|
||||
onFormPropertyChange(data) {
|
||||
this.changes = data?.configuration?.gaugeController || {};
|
||||
},
|
||||
onChange(event) {
|
||||
const data = {
|
||||
model: this.model,
|
||||
value: {
|
||||
gaugeType: this.model.value.gaugeType,
|
||||
isDisplayMinMax: this.isDisplayMinMax,
|
||||
isDisplayCurVal: this.isDisplayCurVal,
|
||||
isDisplayUnits: this.isDisplayUnits,
|
||||
gaugeType: this.changes.gaugeType || this.model.value.gaugeType,
|
||||
isDisplayMinMax: this.changes.isDisplayMinMax === undefined ? this.isDisplayMinMax : this.changes.isDisplayMinMax,
|
||||
isDisplayCurVal: this.changes.isDisplayCurVal === undefined ? this.isDisplayCurVal : this.changes.isDisplayCurVal,
|
||||
isDisplayUnits: this.changes.isDisplayUnits === undefined ? this.isDisplayUnits : this.changes.isDisplayUnits,
|
||||
isUseTelemetryLimits: this.isUseTelemetryLimits,
|
||||
limitLow: this.limitLow,
|
||||
limitHigh: this.limitHigh,
|
||||
max: this.max,
|
||||
min: this.min,
|
||||
precision: this.model.value.precision
|
||||
precision: this.changes.precision === undefined ? this.model.value.precision : this.changes.precision
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
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"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div
|
||||
<ImageThumbnail
|
||||
v-for="(image, index) in imageHistory"
|
||||
:key="image.url + image.time"
|
||||
class="c-imagery__thumb c-thumb"
|
||||
:class="{ selected: focusedImageIndex === index && isPaused }"
|
||||
:title="image.formattedTime"
|
||||
@click="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>
|
||||
:image="image"
|
||||
:active="focusedImageIndex === index"
|
||||
:selected="focusedImageIndex === index && isPaused"
|
||||
:real-time="!isFixed"
|
||||
@click.native="thumbnailClicked(index)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@@ -205,6 +194,7 @@ import moment from 'moment';
|
||||
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
|
||||
import Compass from './Compass/Compass.vue';
|
||||
import ImageControls from './ImageControls.vue';
|
||||
import ImageThumbnail from './ImageThumbnail.vue';
|
||||
import imageryData from "../../imagery/mixins/imageryData";
|
||||
|
||||
const REFRESH_CSS_MS = 500;
|
||||
@@ -229,9 +219,11 @@ const SHOW_THUMBS_THRESHOLD_HEIGHT = 200;
|
||||
const SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT = 600;
|
||||
|
||||
export default {
|
||||
name: 'ImageryView',
|
||||
components: {
|
||||
Compass,
|
||||
ImageControls
|
||||
ImageControls,
|
||||
ImageThumbnail
|
||||
},
|
||||
mixins: [imageryData],
|
||||
inject: ['openmct', 'domainObject', 'objectPath', 'currentView', 'imageFreshnessOptions'],
|
||||
@@ -254,6 +246,7 @@ export default {
|
||||
visibleLayers: [],
|
||||
durationFormatter: undefined,
|
||||
imageHistory: [],
|
||||
bounds: {},
|
||||
timeSystem: timeSystem,
|
||||
keyString: undefined,
|
||||
autoScroll: true,
|
||||
@@ -526,20 +519,17 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
imageHistory: {
|
||||
handler(newHistory, oldHistory) {
|
||||
handler(newHistory, _oldHistory) {
|
||||
const newSize = newHistory.length;
|
||||
let imageIndex;
|
||||
let imageIndex = newSize > 0 ? newSize - 1 : undefined;
|
||||
if (this.focusedImageTimestamp !== undefined) {
|
||||
const foundImageIndex = newHistory.findIndex(img => img.time === this.focusedImageTimestamp);
|
||||
imageIndex = foundImageIndex > -1
|
||||
? foundImageIndex
|
||||
: newSize - 1;
|
||||
} else {
|
||||
imageIndex = newSize > 0
|
||||
? newSize - 1
|
||||
: undefined;
|
||||
if (foundImageIndex > -1) {
|
||||
imageIndex = foundImageIndex;
|
||||
}
|
||||
}
|
||||
|
||||
this.setFocusedImage(imageIndex);
|
||||
this.nextImageIndex = imageIndex;
|
||||
|
||||
if (this.previousFocusedImage && newHistory.length) {
|
||||
@@ -569,6 +559,16 @@ export default {
|
||||
this.resetAgeCSS();
|
||||
this.updateRelatedTelemetryForFocusedImage();
|
||||
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() {
|
||||
@@ -610,6 +610,7 @@ export default {
|
||||
this.handleScroll = _.debounce(this.handleScroll, SCROLL_LATENCY);
|
||||
this.handleThumbWindowResizeEnded = _.debounce(this.handleThumbWindowResizeEnded, SCROLL_LATENCY);
|
||||
this.handleThumbWindowResizeStart = _.debounce(this.handleThumbWindowResizeStart, SCROLL_LATENCY);
|
||||
this.scrollToFocused = _.debounce(this.scrollToFocused, 400);
|
||||
|
||||
if (this.$refs.thumbsWrapper) {
|
||||
this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);
|
||||
@@ -845,7 +846,8 @@ export default {
|
||||
if (domThumb) {
|
||||
domThumb.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
block: 'center',
|
||||
inline: 'center'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -258,13 +258,22 @@
|
||||
min-width: $w;
|
||||
width: $w;
|
||||
|
||||
&.active {
|
||||
background: $colorSelectedBg;
|
||||
color: $colorSelectedFg;
|
||||
}
|
||||
&:hover {
|
||||
background: $colorThumbHoverBg;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: $colorPausedBg !important;
|
||||
color: $colorPausedFg !important;
|
||||
// fixed time - selected bg will match active bg color
|
||||
background: $colorSelectedBg;
|
||||
color: $colorSelectedFg;
|
||||
&.real-time {
|
||||
// real time - bg orange when selected
|
||||
background: $colorPausedBg !important;
|
||||
color: $colorPausedFg !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__image {
|
||||
|
||||
@@ -139,6 +139,7 @@ export default {
|
||||
// forcibly reset the imageContainer size to prevent an aspect ratio distortion
|
||||
delete this.imageContainerWidth;
|
||||
delete this.imageContainerHeight;
|
||||
this.bounds = bounds; // setting bounds for ImageryView watcher
|
||||
},
|
||||
timeSystemChange() {
|
||||
this.timeSystem = this.timeContext.timeSystem();
|
||||
|
||||
@@ -493,7 +493,6 @@ describe("The Imagery View Layouts", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
//Note this is a WIP test. Coverage was added in e2e suite
|
||||
xit('should change the image zoom factor when using the zoom buttons', async (done) => {
|
||||
await Vue.nextTick();
|
||||
let imageSizeBefore;
|
||||
@@ -515,7 +514,6 @@ describe("The Imagery View Layouts", () => {
|
||||
expect(imageSizeAfter.width).toBeLessThan(imageSizeBefore.width);
|
||||
done();
|
||||
});
|
||||
//Note this is a WIP test. Coverage was added in e2e suite
|
||||
xit('should reset the zoom factor on the image when clicking the zoom button', async (done) => {
|
||||
await Vue.nextTick();
|
||||
// test clicking the zoom reset button
|
||||
|
||||
@@ -27,10 +27,13 @@ export default function MissingObjectInterceptor(openmct) {
|
||||
},
|
||||
invoke: (identifier, object) => {
|
||||
if (object === undefined) {
|
||||
const keyString = openmct.objects.makeKeyString(identifier);
|
||||
openmct.notifications.error(`Failed to retrieve object ${keyString}`);
|
||||
|
||||
return {
|
||||
identifier,
|
||||
type: 'unknown',
|
||||
name: 'Missing: ' + openmct.objects.makeKeyString(identifier)
|
||||
name: 'Missing: ' + keyString
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,6 @@ export default class LinkAction {
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
this.openmct.forms.showForm(formStructure)
|
||||
.then(this.onSave.bind(this));
|
||||
}
|
||||
@@ -91,8 +90,8 @@ export default class LinkAction {
|
||||
validate(currentParent) {
|
||||
return (data) => {
|
||||
|
||||
// default current parent to ROOT, if it's undefined, then it's a root level item
|
||||
if (currentParent === undefined) {
|
||||
// default current parent to ROOT, if it's null, then it's a root level item
|
||||
if (!currentParent) {
|
||||
currentParent = {
|
||||
identifier: {
|
||||
key: 'ROOT',
|
||||
@@ -101,24 +100,23 @@ export default class LinkAction {
|
||||
};
|
||||
}
|
||||
|
||||
const parentCandidate = data.value[0];
|
||||
const currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
|
||||
const parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier);
|
||||
const parentCandidatePath = data.value;
|
||||
const parentCandidate = parentCandidatePath[0];
|
||||
const objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);
|
||||
|
||||
if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!parentCandidateKeystring || !currentParentKeystring) {
|
||||
// check if moving to same place
|
||||
if (this.openmct.objects.areIdsEqual(parentCandidate.identifier, currentParent.identifier)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parentCandidateKeystring === currentParentKeystring) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parentCandidateKeystring === objectKeystring) {
|
||||
// check if moving to a child
|
||||
if (parentCandidatePath.some(candidatePath => {
|
||||
return this.openmct.objects.areIdsEqual(candidatePath.identifier, this.object.identifier);
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -145,26 +145,24 @@ export default class MoveAction {
|
||||
const parentCandidatePath = data.value;
|
||||
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)) {
|
||||
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);
|
||||
|
||||
if (!parentCandidateKeystring || !currentParentKeystring) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parentCandidateKeystring === currentParentKeystring) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parentCandidateKeystring === objectKeystring) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parentCandidateComposition = parentCandidate.composition;
|
||||
if (parentCandidateComposition && parentCandidateComposition.indexOf(objectKeystring) !== -1) {
|
||||
return false;
|
||||
|
||||
@@ -69,27 +69,27 @@ describe("the plugin", () => {
|
||||
});
|
||||
|
||||
describe('adds an interceptor that returns a "My Items" model for', () => {
|
||||
let myItemsMissing;
|
||||
let mockMissingProvider;
|
||||
let myItemsObject;
|
||||
let mockNotFoundProvider;
|
||||
let activeProvider;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockMissingProvider = {
|
||||
get: () => Promise.resolve(missingObj),
|
||||
mockNotFoundProvider = {
|
||||
get: () => Promise.reject(new Error('Not found')),
|
||||
create: () => Promise.resolve(missingObj),
|
||||
update: () => Promise.resolve(missingObj)
|
||||
};
|
||||
|
||||
activeProvider = mockMissingProvider;
|
||||
activeProvider = mockNotFoundProvider;
|
||||
spyOn(openmct.objects, 'getProvider').and.returnValue(activeProvider);
|
||||
myItemsMissing = await openmct.objects.get(myItemsIdentifier);
|
||||
myItemsObject = await openmct.objects.get(myItemsIdentifier);
|
||||
});
|
||||
|
||||
it('missing objects', () => {
|
||||
let idsMatchMissing = openmct.objects.areIdsEqual(myItemsMissing.identifier, myItemsIdentifier);
|
||||
let idsMatch = openmct.objects.areIdsEqual(myItemsObject.identifier, myItemsIdentifier);
|
||||
|
||||
expect(myItemsMissing).toBeDefined();
|
||||
expect(idsMatchMissing).toBeTrue();
|
||||
expect(myItemsObject).toBeDefined();
|
||||
expect(idsMatch).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -12,8 +12,10 @@
|
||||
<template v-if="!page.isLocked">
|
||||
<div
|
||||
class="c-list__item__name js-list__item__name"
|
||||
:class="[{ 'c-input-inline': isSelected }]"
|
||||
:data-id="page.id"
|
||||
:contenteditable="true"
|
||||
:contenteditable="isSelected"
|
||||
@keydown.escape="updateName"
|
||||
@keydown.enter="updateName"
|
||||
@blur="updateName"
|
||||
>{{ pageName }}</div>
|
||||
@@ -32,8 +34,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PopupMenu from './PopupMenu.vue';
|
||||
import { KEY_ENTER, KEY_ESCAPE } from '../utils/notebook-key-code';
|
||||
import RemoveDialog from '../utils/removeDialog';
|
||||
import PopupMenu from './PopupMenu.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -107,36 +110,39 @@ export default {
|
||||
removeDialog.show();
|
||||
},
|
||||
selectPage(event) {
|
||||
const target = event.target;
|
||||
const id = target.dataset.id;
|
||||
const { target: { dataset: { id } } } = event;
|
||||
|
||||
if (!this.page.isLocked) {
|
||||
const page = target.closest('.js-list__item');
|
||||
const input = page.querySelector('.js-list__item__name');
|
||||
|
||||
if (page.className.indexOf('is-selected') > -1) {
|
||||
input.classList.add('c-input-inline');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
if (this.isSelected || !id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('selectPage', id);
|
||||
},
|
||||
updateName(event) {
|
||||
const target = event.target;
|
||||
const name = target.textContent.toString();
|
||||
target.classList.remove('c-input-inline');
|
||||
|
||||
if (name === '' || this.page.name === name) {
|
||||
renamePage(target) {
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('renamePage', Object.assign(this.page, { name }));
|
||||
target.textContent = target.textContent ? target.textContent.trim() : `Unnamed ${this.pageTitle}`;
|
||||
|
||||
if (this.page.name === target.textContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('renamePage', Object.assign(this.page, { name: target.textContent }));
|
||||
},
|
||||
updateName(event) {
|
||||
const { target, keyCode, type } = event;
|
||||
|
||||
if (keyCode === KEY_ESCAPE) {
|
||||
target.textContent = this.page.name;
|
||||
} else if (keyCode === KEY_ENTER || type === 'blur') {
|
||||
this.renamePage(target);
|
||||
}
|
||||
|
||||
target.scrollLeft = '0';
|
||||
|
||||
target.blur();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
>
|
||||
<span
|
||||
class="c-list__item__name js-list__item__name"
|
||||
:class="[{ 'c-input-inline': isSelected && !section.isLocked }]"
|
||||
:data-id="section.id"
|
||||
contenteditable="true"
|
||||
:contenteditable="isSelected && !section.isLocked"
|
||||
@keydown.escape="updateName"
|
||||
@keydown.enter="updateName"
|
||||
@blur="updateName"
|
||||
>{{ sectionName }}</span>
|
||||
@@ -20,8 +22,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PopupMenu from './PopupMenu.vue';
|
||||
import { KEY_ENTER, KEY_ESCAPE } from '../utils/notebook-key-code';
|
||||
import RemoveDialog from '../utils/removeDialog';
|
||||
import PopupMenu from './PopupMenu.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -96,36 +99,39 @@ export default {
|
||||
removeDialog.show();
|
||||
},
|
||||
selectSection(event) {
|
||||
const target = event.target;
|
||||
const id = target.dataset.id;
|
||||
const { target: { dataset: { id } } } = event;
|
||||
|
||||
if (!this.section.isLocked) {
|
||||
const section = target.closest('.js-list__item');
|
||||
const input = section.querySelector('.js-list__item__name');
|
||||
|
||||
if (section.className.indexOf('is-selected') > -1) {
|
||||
input.classList.add('c-input-inline');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
if (this.isSelected || !id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('selectSection', id);
|
||||
},
|
||||
updateName(event) {
|
||||
const target = event.target;
|
||||
target.classList.remove('c-input-inline');
|
||||
const name = target.textContent.trim();
|
||||
|
||||
if (name === '' || this.section.name === name) {
|
||||
renameSection(target) {
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('renameSection', Object.assign(this.section, { name }));
|
||||
target.textContent = target.textContent ? target.textContent.trim() : `Unnamed ${this.sectionTitle}`;
|
||||
|
||||
if (this.section.name === target.textContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('renameSection', Object.assign(this.section, { name: target.textContent }));
|
||||
},
|
||||
updateName(event) {
|
||||
const { target, keyCode, type } = event;
|
||||
|
||||
if (keyCode === KEY_ESCAPE) {
|
||||
target.textContent = this.section.name;
|
||||
} else if (keyCode === KEY_ENTER || type === 'blur') {
|
||||
this.renameSection(target);
|
||||
}
|
||||
|
||||
target.scrollLeft = '0';
|
||||
|
||||
target.blur();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
}
|
||||
|
||||
&__name {
|
||||
flex: 0 1 auto;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
&__menu-indicator {
|
||||
|
||||
3
src/plugins/notebook/utils/notebook-key-code.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// Key codes for `KeyboardEvent.keyCode`.
|
||||
export const KEY_ENTER = 13;
|
||||
export const KEY_ESCAPE = 27;
|
||||
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
|
||||
## 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`.
|
||||
2. Edit `/usr/local/etc/local.ini` and add the following settings:
|
||||
```
|
||||
|
||||
```txt
|
||||
[admins]
|
||||
admin = youradminpassword
|
||||
```
|
||||
|
||||
And set the server up for single node:
|
||||
```
|
||||
|
||||
```txt
|
||||
[couchdb]
|
||||
single_node=true
|
||||
```
|
||||
|
||||
Enable CORS
|
||||
```
|
||||
|
||||
```txt
|
||||
[chttpd]
|
||||
enable_cors = true
|
||||
[cors]
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
## 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`.
|
||||
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`
|
||||
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`.
|
||||
|
||||
# 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:
|
||||
```
|
||||
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"));
|
||||
```
|
||||
2. Start Open MCT by running `npm start` in the `openmct` path.
|
||||
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.
|
||||
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! 🏆
|
||||
|
||||
```js
|
||||
openmct.install(openmct.plugins.LocalStorage());
|
||||
```
|
||||
|
||||
Add a line to install the CouchDB plugin for Open MCT:
|
||||
|
||||
```js
|
||||
openmct.install(openmct.plugins.CouchDB("http://localhost:5984/openmct"));
|
||||
```
|
||||
|
||||
# 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
|
||||
144
src/plugins/persistence/couch/setup-couchdb.sh
Normal file
@@ -0,0 +1,144 @@
|
||||
#!/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"\'
|
||||
}
|
||||
|
||||
is_cors_enabled() {
|
||||
resource_exists $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/httpd/enable_cors
|
||||
}
|
||||
|
||||
enable_cors () {
|
||||
curl -su "${CURL_USERPASS_ARG}" -o /dev/null -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/httpd/enable_cors -d '"true"'
|
||||
curl -su "${CURL_USERPASS_ARG}" -o /dev/null -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/cors/origins -d '"http://localhost:8080"'
|
||||
curl -su "${CURL_USERPASS_ARG}" -o /dev/null -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/cors/credentials -d '"true"'
|
||||
curl -su "${CURL_USERPASS_ARG}" -o /dev/null -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/cors/methods -d '"GET, PUT, POST, HEAD, DELETE"'
|
||||
curl -su "${CURL_USERPASS_ARG}" -o /dev/null -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/cors/headers -d '"accept, authorization, content-type, origin, referer, x-csrf-token"'
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
if [ "FALSE" == $(is_cors_enabled) ]; then
|
||||
echo "Enabling CORS"
|
||||
enable_cors
|
||||
else
|
||||
echo "CORS enabled, nothing to do"
|
||||
fi
|
||||
@@ -136,7 +136,7 @@
|
||||
<span v-if="showValueWhenExpanded">Value</span>
|
||||
<span v-if="showMinimumWhenExpanded">Min</span>
|
||||
<span v-if="showMaximumWhenExpanded">Max</span>
|
||||
<span v-if="showUnitsWhenExpanded">Units</span>
|
||||
<span v-if="showUnitsWhenExpanded">Unit</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<option value="nearestValue">Nearest value</option>
|
||||
<option value="min">Minimum value</option>
|
||||
<option value="max">Maximum value</option>
|
||||
<option value="units">Units</option>
|
||||
<option value="unit">Unit</option>
|
||||
</select>
|
||||
</div>
|
||||
</li>
|
||||
@@ -89,7 +89,7 @@
|
||||
v-model="showUnitsWhenExpanded"
|
||||
type="checkbox"
|
||||
@change="updateForm('showUnitsWhenExpanded')"
|
||||
> Units</li>
|
||||
> Unit</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<span class="plot-series-name">{{ nameWithUnit }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-show="!!highlights.length && (valueToShowWhenCollapsed !== 'none' && valueToShowWhenCollapsed !== 'units')"
|
||||
v-show="!!highlights.length && (valueToShowWhenCollapsed !== 'none' && valueToShowWhenCollapsed !== 'unit')"
|
||||
class="plot-series-value hover-value-enabled"
|
||||
:class="[{ 'cursor-hover': notNearest }, valueToDisplayWhenCollapsedClass, mctLimitStateClass]"
|
||||
>
|
||||
|
||||
@@ -28,7 +28,7 @@ import EventEmitter from "EventEmitter";
|
||||
import PlotOptions from "./inspector/PlotOptions.vue";
|
||||
import PlotConfigurationModel from "./configuration/PlotConfigurationModel";
|
||||
|
||||
xdescribe("the plugin", function () {
|
||||
describe("the plugin", function () {
|
||||
let element;
|
||||
let child;
|
||||
let openmct;
|
||||
|
||||
@@ -32,7 +32,7 @@ define([
|
||||
'./autoflow/AutoflowTabularPlugin',
|
||||
'./timeConductor/plugin',
|
||||
'../../example/imagery/plugin',
|
||||
'../../example/faultManagment/exampleFaultSource',
|
||||
'../../example/faultManagement/exampleFaultSource',
|
||||
'./imagery/plugin',
|
||||
'./summaryWidget/plugin',
|
||||
'./URLIndicatorPlugin/URLIndicatorPlugin',
|
||||
@@ -58,7 +58,6 @@ define([
|
||||
'./condition/plugin',
|
||||
'./conditionWidget/plugin',
|
||||
'./themes/espresso',
|
||||
'./themes/maelstrom',
|
||||
'./themes/snow',
|
||||
'./URLTimeSettingsSynchronizer/plugin',
|
||||
'./notificationIndicator/plugin',
|
||||
@@ -122,7 +121,6 @@ define([
|
||||
ConditionPlugin,
|
||||
ConditionWidgetPlugin,
|
||||
Espresso,
|
||||
Maelstrom,
|
||||
Snow,
|
||||
URLTimeSettingsSynchronizer,
|
||||
NotificationIndicator,
|
||||
@@ -207,7 +205,6 @@ define([
|
||||
plugins.ClearData = ClearData;
|
||||
plugins.WebPage = WebPagePlugin.default;
|
||||
plugins.Espresso = Espresso.default;
|
||||
plugins.Maelstrom = Maelstrom.default;
|
||||
plugins.Snow = Snow.default;
|
||||
plugins.Condition = ConditionPlugin.default;
|
||||
plugins.ConditionWidget = ConditionWidgetPlugin.default;
|
||||
|
||||
@@ -26,7 +26,7 @@ define([
|
||||
SummaryWidgetTelemetryProvider
|
||||
) {
|
||||
|
||||
describe('SummaryWidgetTelemetryProvider', function () {
|
||||
xdescribe('SummaryWidgetTelemetryProvider', function () {
|
||||
let telemObjectA;
|
||||
let telemObjectB;
|
||||
let summaryWidgetObject;
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
define(['../src/ConditionManager'], function (ConditionManager) {
|
||||
describe('A Summary Widget Condition Manager', function () {
|
||||
xdescribe('A Summary Widget Condition Manager', function () {
|
||||
let conditionManager;
|
||||
let mockDomainObject;
|
||||
let mockCompObject1;
|
||||
@@ -360,7 +360,7 @@ define(['../src/ConditionManager'], function (ConditionManager) {
|
||||
});
|
||||
});
|
||||
|
||||
xit('populates its LAD cache with historial data on load, if available', function (done) {
|
||||
it('populates its LAD cache with historial data on load, if available', function (done) {
|
||||
expect(telemetryRequests.length).toBe(2);
|
||||
expect(telemetryRequests[0].object).toBe(mockCompObject1);
|
||||
expect(telemetryRequests[1].object).toBe(mockCompObject2);
|
||||
@@ -379,7 +379,7 @@ define(['../src/ConditionManager'], function (ConditionManager) {
|
||||
telemetryRequests[1].resolve([mockTelemetryValues.mockCompObject2]);
|
||||
});
|
||||
|
||||
xit('updates its LAD cache upon receiving telemetry and invokes the appropriate handlers', function () {
|
||||
it('updates its LAD cache upon receiving telemetry and invokes the appropriate handlers', function () {
|
||||
mockTelemetryAPI.triggerTelemetryCallback('mockCompObject1');
|
||||
expect(conditionManager.subscriptionCache.mockCompObject1.property1).toEqual('Its a different string');
|
||||
mockTelemetryAPI.triggerTelemetryCallback('mockCompObject2');
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
define(['../src/Condition'], function (Condition) {
|
||||
describe('A summary widget condition', function () {
|
||||
xdescribe('A summary widget condition', function () {
|
||||
let testCondition;
|
||||
let mockConfig;
|
||||
let mockConditionManager;
|
||||
|
||||