Compare commits

...

31 Commits

Author SHA1 Message Date
7bccb73729 Merge branch 'release/2.0.8' of https://github.com/nasa/openmct into release/2.0.8 2022-08-24 09:23:37 -07:00
78df5fc2a5 Don't re-request historical data on ticks (#5701)
Don't rerequest telemetry on ticks.
2022-08-23 19:51:26 -07:00
b53cc810f5 Only index if provider does not support search - mct5690 (#5693)
* only index if provider does not support search

* add some tests

* fix tests

* [e2e] Add search couchdb test for duplicates

* [e2e] Modify existing search test instead

* lint

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2022-08-23 14:08:34 -07:00
5386ceb94c [Time Conductor] History not working correctly (#5687)
* the check for fixed time vs realtime was not updating, have fixed this

* merging in related changes from master pr #4414

* lint fixes

* Update src/plugins/timeConductor/ConductorHistory.vue

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>

* setting time mode directly on load

* fixing issue where realtime history was being wiped on reloads while viewing fixed time

* formatting

* stubbed in some tests

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-08-23 09:36:02 -07:00
5a4dd11955 Merge branch 'release/2.0.8' of https://github.com/nasa/openmct into release/2.0.8 2022-08-23 09:23:19 -07:00
affb7a5311 [Docs] Update CouchDB local install documentation (#5692)
* Update local CouchDB install docs to include docker workflow

* reformat to source configuration scripts where possible

* correct couchdb case

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-08-23 09:00:04 -07:00
1e1133046a Merge branch 'release/2.0.7' of https://github.com/nasa/openmct into release/2.0.8 2022-08-22 12:06:38 -07:00
06b321588e Handle couch db not found errors so that interceptors are still invoked. (#5654)
* Fix tests for interceptors
* [e2e] Add test for 'mine' folder initialization
* [e2e] don't fail on expected console errors

Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2022-08-22 10:15:52 -07:00
909901b0f3 [CI] Enable couchdb e2e testing in open source (#5655) 2022-08-19 13:14:27 -07:00
865f95c0b6 Time List 5534 for release/2.0.8 (#5678)
* Changes to Time List view. Closes #5534.
- Compacted table row spacing.
- Set all timeframes to display by default when creating a new Time List.
- Removed 'Upload plan' file button from properties.

* Changes to Time List view. Closes #5534.
- Better hint text for editing Timeframe Inspector section.

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-08-19 18:11:30 +00:00
cb1e04b0d6 Set Focused Image index after a imagery is selected from a timestrip - 5632 (#5664)
* Set focused image when timestamp prop is passed in

* Unused var

* Create timestrip with imagery child

* Add equality check for hovered image and view large image url

* Cleanup
2022-08-19 17:39:58 +00:00
b0c2e66613 Moves condition set fix into 2.0.8 (#5673) 2022-08-19 15:47:43 +00:00
d162b5dbd8 [e2e] Stabilize notebook tag tests (#5681)
* Use more deterministic selector

* Hover first to "slow down" e2e actions while in headless mode
2022-08-18 21:23:58 +02:00
64565b1bbb [Display Layout] Composition and configuration sync (#5669)
LGTM
2022-08-18 06:52:45 -07:00
f721980bf0 Mct5549 fix indexer composition error (#5610) 2022-08-18 05:58:54 -07:00
b47712a0f4 Sort tree items locally on rename (#5643)
* fix typo

* Sort the tree items locally on object rename

* Use the navigationPath as a key

- This ensures that objects AND linked objects will be sorted

* add 'tree' and 'treeitem' roles to mct-tree

* WIP tree item reordering test

* Select the first object that matches

* Test that all object links are also reordered

* Get the final uuid before queryParams as notebook sections have uuids

* Make `openObjectTreeContextMenu` more deterministic and update usage

* Add `expandPathToTreeItem` and `expandTreeItemByName` appActions

* add `#tree-pane` id for the tree view

* Add tree visual component test suite and bump percy-cli

* Remove tree appActions

* Better variable name

Co-authored-by: Scott Bell <scott@traclabs.com>
2022-08-17 18:16:40 +02:00
57f3d4eba0 [Fault Management] New Example Provider, Unit and e2e tests (#5579)
* added unit tests for fault management plugin

* modified the example fault provider to work out of the box

* updating for new e2e folder structure

* part of the e2e tests

* WIP

* Imagery thumbnail regression fixes - 5327 (#5569)

* Add an active class to thumbnail to indicate current focused image

* Differentiate bg color between real-time and fixed

* scrollIntoView inline: center

* Added watcher for bounds change to trigger thumbnail scroll

* Resolve merge conflict with requestHistory change to telemetry collection

* Split thumbnail into sub component

* Monitor isFixed value to unpause playback status

* updated search to include name, namespace and description added some more e2e tests

* added rest of e2e tests

* fixed my init script, had to disable lint for no-force because it was not working without it, saw online this may be a pw bug

* fix: removing maelstrom theme from application (#5600)

* added some tests for no faults

* visual tests

* added visual tests for fault management

* created utils file for shared functionality between function and visual tests

* updating to 2.0.8

* tryin to remove imagery changes from master

* trying to trigger a refresh

* tryin to refresh

* updated search to include name, namespace and description added some more e2e tests

* added rest of e2e tests

* fix: removing maelstrom theme from application (#5600)

* fixed my init script, had to disable lint for no-force because it was not working without it, saw online this may be a pw bug

* added some tests for no faults

* visual tests

* added visual tests for fault management

* created utils file for shared functionality between function and visual tests

* updating to 2.0.8

* no clue

* still no clue

* removing imports and chaning to requires

* updating utils file to work with require

* fixing paths

* fixing a test I had messed up when adding static exmaple faults

* ONE LAST PATH FIX... hopefully

* typo in files fix

* fix folder typo

* thought I got this one, but apparently not, well I did now! who is laughing now!?

Co-authored-by: Michael Rogers <contact@mhrogers.com>
Co-authored-by: Vitor Henckel <vitor@henckel.com.br>
2022-08-15 14:09:23 -07:00
064a865c9b [Condition Set] Add check for empty string being passed to the makeKeyString util by TelemetryCriterion (#5636) (#5663)
* Check telemetry is defined before using makeKeyString util

* Add optional chaining in the check

* Add e2e test

* Add check for undefined

Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
2022-08-15 14:21:21 -05:00
61bf60783c Prevent cyclic references in link & move actions (#5635)
* do not create circular refs

* add negative validation test

* move to plugin

* add link test too

* fix docs

* refactored per john request

* fix path

* use appAction lib

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-08-10 19:05:38 +00:00
5dc718b78d Update version number 2022-08-10 11:34:12 -07:00
41f8cb404d Check for circular references in originalPath - 5615 (#5619)
* check for circular references

* add test

* fix test

* address PR comments by making comments better

* fix docs...again
2022-08-09 12:11:03 +02:00
c6c58af12c [e2e] Tests for Display Layout and LAD Tables and telemetry (#5607) 2022-08-08 13:30:20 -07:00
15a0a87251 Revert "Have in-memory search indexer use composition API (#5578)" (#5609)
This reverts commit 7cf11e177c.
2022-08-04 19:15:54 +00:00
59a8614f1c Add parsing for areIdsEqual util to consistently remove folders (#5589)
* Add parsing util to identifier for ID comparison

* Moved firstIdentifier to top of function

* Lint fix

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-08-04 11:42:43 -07:00
7cf11e177c Have in-memory search indexer use composition API (#5578)
* need to remove tags and objects on composition removal
* had to separate out emits from load as it was causing memory indexer to loop upon itself
2022-08-04 18:20:38 +00:00
1a44652470 Search should indicate in progress and no results states, filter orphaned results (#5599)
* no matching result implemented

* now filtering annotations that are orphaned

* filter object results without valid paths

* add progress bar

* added e2e tests

* removed extraneous click

* fix typos

* fix unit tests

* lint

* address pr comments

* fix tests

* fix tests, centralize logic to object api, check for root instead

* remove debug statement

* lint

* fix documentation

* lint

* fix doc

* made some optimizations after talking with akhenry

* fix test

* update docs

* fix docs
2022-08-04 11:06:16 -07:00
51d16f812a Include the plan source map when generating the time list/plan hybrid object (#5604) 2022-08-03 18:17:16 -07:00
25de5653e8 Fix menu style in Snow theme (#5557) 2022-08-03 11:24:04 -07:00
cb6014d69f Update package.json (#5601) 2022-08-03 11:12:31 -07:00
36736eb8a0 [e2e] Improve appActions (#5592)
* update selectors to use aria labels

* Update appActions

- Create new function `getHashUrlToDomainObject` to get the browse url to a given object given its uuid

- Create new function `getFocusedObjectUuid`... self explanatory :)

- Update `createDomainObjectWIthDefaults` to make use of the new url generation

- Update `createDomainObject...`'s arguments to be more organized, and accept a parent object

- Update some docs, still need to clarify some

* Update appActions e2e tests

- Refactor for organization

- Test our new appActions in one go

* Update existing usages of `createDomainObject...` to match the new API

* fix accidental renamed export

* Fix jsdoc return types

* refactor telemetryTable test to use appActions

* Improve selectors

* Refactor test

* improve selector

* add clock mode appActions

* lint

* Fix jsdoc

* Code review comments

* mark failing visual tests as fixme temporarily
2022-08-03 00:48:47 +00:00
a13a6002c5 Imagery thumbnail regression fixes - 5327 (#5591)
* Add an active class to thumbnail to indicate current focused image

* Differentiate bg color between real-time and fixed

* scrollIntoView inline: center

* Added watcher for bounds change to trigger thumbnail scroll

* Resolve merge conflict with requestHistory change to telemetry collection

* Split thumbnail into sub component

* Monitor isFixed value to unpause playback status

Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
2022-08-02 13:44:01 -05:00
80 changed files with 3107 additions and 659 deletions

37
.github/workflows/e2e-couchdb.yml vendored Normal file
View 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

View File

@ -30,18 +30,37 @@
*/ */
/** /**
* This common function creates a `domainObject` with default options. It is the preferred way of creating objects * Defines parameters to be used in the creation of a domain object.
* in the e2e suite when uninterested in properties of the objects themselves. * @typedef {Object} CreateObjectOptions
* @param {import('@playwright/test').Page} page * @property {string} type the type of domain object to create (e.g.: "Sine Wave Generator").
* @param {string} type * @property {string} [name] the desired name of the created domain object.
* @param {string | undefined} name * @property {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the Identifier or uuid of the parent object.
*/ */
async function createDomainObjectWithDefaults(page, type, name) {
// Navigate to focus the 'My Items' folder, and hide the object tree /**
// This is necessary so that subsequent objects can be created without a parent * Contains information about the newly created domain object.
// TODO: Ideally this would navigate to a common `e2e` folder * @typedef {Object} CreatedObjectInfo
await page.goto('./#/browse/mine?hideTree=true'); * @property {string} name the name of the created object
* @property {string} uuid the uuid of the created object
* @property {string} url the relative url to the object (for use with `page.goto()`)
*/
/**
* This common function creates a domain object with the default options. It is the preferred way of creating objects
* in the e2e suite when uninterested in properties of the objects themselves.
*
* @param {import('@playwright/test').Page} page
* @param {CreateObjectOptions} options
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
*/
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) {
const parentUrl = await getHashUrlToDomainObject(page, parent);
// Navigate to the parent object. This is necessary to create the object
// in the correct location, such as a folder, layout, or plot.
await page.goto(`${parentUrl}?hideTree=true`);
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
//Click the Create button //Click the Create button
await page.click('button:has-text("Create")'); await page.click('button:has-text("Create")');
@ -50,7 +69,7 @@ async function createDomainObjectWithDefaults(page, type, name) {
// Modify the name input field of the domain object to accept 'name' // Modify the name input field of the domain object to accept 'name'
if (name) { if (name) {
const nameInput = page.locator('input[type="text"]').nth(2); const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
await nameInput.fill(""); await nameInput.fill("");
await nameInput.fill(name); await nameInput.fill(name);
} }
@ -63,30 +82,187 @@ async function createDomainObjectWithDefaults(page, type, name) {
page.waitForSelector('.c-message-banner__message') page.waitForSelector('.c-message-banner__message')
]); ]);
return name || `Unnamed ${type}`; // Wait until the URL is updated
await page.waitForURL(`**/${parent}/*`);
const uuid = await getFocusedObjectUuid(page);
const objectUrl = await getHashUrlToDomainObject(page, uuid);
if (await _isInEditMode(page, uuid)) {
// Save (exit edit mode)
await page.locator('button[title="Save"]').click();
await page.locator('li[title="Save and Finish Editing"]').click();
}
return {
name: name || `Unnamed ${type}`,
uuid: uuid,
url: objectUrl
};
} }
/** /**
* Open the given `domainObject`'s context menu from the object tree. * Open the given `domainObject`'s context menu from the object tree.
* Expands the 'My Items' folder if it is not already expanded. * Expands the path to the object and scrolls to it if necessary.
*
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {string} myItemsFolderName the name of the "My Items" folder * @param {string} url the url to the object
* @param {string} domainObjectName the display name of the `domainObject`
*/ */
async function openObjectTreeContextMenu(page, myItemsFolderName, domainObjectName) { async function openObjectTreeContextMenu(page, url) {
const myItemsFolder = page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3); await page.goto(url);
const className = await myItemsFolder.getAttribute('class'); await page.click('button[title="Show selected item in tree"]');
if (!className.includes('c-disclosure-triangle--expanded')) { await page.locator('.is-navigated-object').click({
await myItemsFolder.click();
}
await page.locator(`a:has-text("${domainObjectName}")`).click({
button: 'right' button: 'right'
}); });
} }
/**
* Gets the UUID of the currently focused object by parsing the current URL
* and returning the last UUID in the path.
* @param {import('@playwright/test').Page} page
* @returns {Promise<string>} the uuid of the focused object
*/
async function getFocusedObjectUuid(page) {
const UUIDv4Regexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi;
const focusedObjectUuid = await page.evaluate((regexp) => {
return window.location.href.split('?')[0].match(regexp).at(-1);
}, UUIDv4Regexp);
return focusedObjectUuid;
}
/**
* Returns the hashUrl to the domainObject given its uuid.
* Useful for directly navigating to the given domainObject.
*
* URLs returned will be of the form `'./browse/#/mine/<uuid0>/<uuid1>/...'`
*
* @param {import('@playwright/test').Page} page
* @param {string} uuid the uuid of the object to get the url for
* @returns {Promise<string>} the url of the object
*/
async function getHashUrlToDomainObject(page, uuid) {
const hashUrl = await page.evaluate(async (objectUuid) => {
const path = await window.openmct.objects.getOriginalPath(objectUuid);
let url = './#/browse/' + [...path].reverse()
.map((object) => window.openmct.objects.makeKeyString(object.identifier))
.join('/');
// Drop the vestigial '/ROOT' if it exists
if (url.includes('/ROOT')) {
url = url.split('/ROOT').join('');
}
return url;
}, uuid);
return hashUrl;
}
/**
* Utilizes the OpenMCT API to detect if the given object has an active transaction (is in Edit mode).
* @private
* @param {import('@playwright/test').Page} page
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} identifier
* @return {Promise<boolean>} true if the object has an active transaction, false otherwise
*/
async function _isInEditMode(page, identifier) {
// eslint-disable-next-line no-return-await
return await page.evaluate((objectIdentifier) => window.openmct.objects.isTransactionActive(objectIdentifier), identifier);
}
/**
* Set the time conductor mode to either fixed timespan or realtime mode.
* @param {import('@playwright/test').Page} page
* @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true
*/
async function setTimeConductorMode(page, isFixedTimespan = true) {
// Click 'mode' button
await page.locator('.c-mode-button').click();
// Switch time conductor mode
if (isFixedTimespan) {
await page.locator('data-testid=conductor-modeOption-fixed').click();
} else {
await page.locator('data-testid=conductor-modeOption-realtime').click();
}
}
/**
* Set the time conductor to fixed timespan mode
* @param {import('@playwright/test').Page} page
*/
async function setFixedTimeMode(page) {
await setTimeConductorMode(page, true);
}
/**
* Set the time conductor to realtime mode
* @param {import('@playwright/test').Page} page
*/
async function setRealTimeMode(page) {
await setTimeConductorMode(page, false);
}
/**
* @typedef {Object} OffsetValues
* @property {string | undefined} hours
* @property {string | undefined} mins
* @property {string | undefined} secs
*/
/**
* Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
* @param {import('@playwright/test').Locator} offsetButton
*/
async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) {
await offsetButton.click();
if (hours) {
await page.fill('.pr-time-controls__hrs', hours);
}
if (mins) {
await page.fill('.pr-time-controls__mins', mins);
}
if (secs) {
await page.fill('.pr-time-controls__secs', secs);
}
// Click the check button
await page.locator('.pr-time__buttons .icon-check').click();
}
/**
* Set the values (hours, mins, secs) for the start time offset when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
*/
async function setStartOffset(page, offset) {
const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
await setTimeConductorOffset(page, offset, startOffsetButton);
}
/**
* Set the values (hours, mins, secs) for the end time offset when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
*/
async function setEndOffset(page, offset) {
const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
await setTimeConductorOffset(page, offset, endOffsetButton);
}
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
module.exports = { module.exports = {
createDomainObjectWithDefaults, createDomainObjectWithDefaults,
openObjectTreeContextMenu openObjectTreeContextMenu,
getHashUrlToDomainObject,
getFocusedObjectUuid,
setFixedTimeMode,
setRealTimeMode,
setStartOffset,
setEndOffset
}; };

View 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());
});

View 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));
});

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

View File

@ -23,19 +23,66 @@
const { test, expect } = require('../../baseFixtures.js'); const { test, expect } = require('../../baseFixtures.js');
const { createDomainObjectWithDefaults } = require('../../appActions.js'); const { createDomainObjectWithDefaults } = require('../../appActions.js');
test.describe('appActions tests', () => { test.describe('AppActions', () => {
test('createDomainObjectsWithDefaults can create multiple objects in a row', async ({ page }) => { test('createDomainObjectsWithDefaults', async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
await createDomainObjectWithDefaults(page, 'Timer', 'Timer Foo');
await createDomainObjectWithDefaults(page, 'Timer', 'Timer Bar');
await createDomainObjectWithDefaults(page, 'Timer', 'Timer Baz');
// Expand the tree const e2eFolder = await createDomainObjectWithDefaults(page, {
await page.click('.c-disclosure-triangle'); type: 'Folder',
name: 'e2e folder'
});
// Verify the objects were created await test.step('Create multiple flat objects in a row', async () => {
await expect(page.locator('a :text("Timer Foo")')).toBeVisible(); const timer1 = await createDomainObjectWithDefaults(page, {
await expect(page.locator('a :text("Timer Bar")')).toBeVisible(); type: 'Timer',
await expect(page.locator('a :text("Timer Baz")')).toBeVisible(); name: 'Timer Foo',
parent: e2eFolder.uuid
});
const timer2 = await createDomainObjectWithDefaults(page, {
type: 'Timer',
name: 'Timer Bar',
parent: e2eFolder.uuid
});
const timer3 = await createDomainObjectWithDefaults(page, {
type: 'Timer',
name: 'Timer Baz',
parent: e2eFolder.uuid
});
await page.goto(timer1.url, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Foo');
await page.goto(timer2.url, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Bar');
await page.goto(timer3.url, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Baz');
});
await test.step('Create multiple nested objects in a row', async () => {
const folder1 = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Folder Foo',
parent: e2eFolder.uuid
});
const folder2 = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Folder Bar',
parent: folder1.uuid
});
const folder3 = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Folder Baz',
parent: folder2.uuid
});
await page.goto(folder1.url, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Foo');
await page.goto(folder2.url, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Bar');
await page.goto(folder3.url, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Baz');
expect(folder1.url).toBe(`${e2eFolder.url}/${folder1.uuid}`);
expect(folder2.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}`);
expect(folder3.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}/${folder3.uuid}`);
});
}); });
}); });

View File

@ -58,7 +58,7 @@ test.describe('Renaming Timer Object', () => {
//Open a browser, navigate to the main page, and wait until all networkevents to resolve //Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
//We provide some helper functions in appActions like createDomainObjectWithDefaults. This example will create a Timer object //We provide some helper functions in appActions like createDomainObjectWithDefaults. This example will create a Timer object
await createDomainObjectWithDefaults(page, 'Timer'); await createDomainObjectWithDefaults(page, { type: 'Timer' });
//Assert the object to be created and check it's name in the title //Assert the object to be created and check it's name in the title
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer'); await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer');
@ -73,7 +73,7 @@ test.describe('Renaming Timer Object', () => {
//Open a browser, navigate to the main page, and wait until all networkevents to resolve //Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
//We provide some helper functions in appActions like createDomainObjectWithDefaults. This example will create a Timer object //We provide some helper functions in appActions like createDomainObjectWithDefaults. This example will create a Timer object
await createDomainObjectWithDefaults(page, 'Timer'); await createDomainObjectWithDefaults(page, { type: 'Timer' });
//Expect the object to be created and check it's name in the title //Expect the object to be created and check it's name in the title
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer'); await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer');

View File

@ -31,29 +31,13 @@ TODO: Provide additional validation of object properties as it grows.
*/ */
const { createDomainObjectWithDefaults } = require('../../appActions.js');
const { test, expect } = require('../../pluginFixtures.js'); const { test, expect } = require('../../pluginFixtures.js');
test('Generate Visual Test Data @localStorage', async ({ page, context, openmctConfig }) => { test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
const { myItemsFolderName } = openmctConfig;
//Go to baseURL //Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
const overlayPlot = await createDomainObjectWithDefaults(page, { type: 'Overlay Plot' });
await page.locator('button:has-text("Create")').click();
// add overlay plot with defaults
await page.locator('li:has-text("Overlay Plot")').click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear1
page.waitForSelector('.c-message-banner__message')
]);
// save (exit edit mode)
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
await page.locator('text=Save and Finish Editing').click();
// click create button // click create button
await page.locator('button:has-text("Create")').click(); await page.locator('button:has-text("Create")').click();
@ -67,16 +51,12 @@ test('Generate Visual Test Data @localStorage', async ({ page, context, openmctC
await Promise.all([ await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),
page.locator('text=OK').click(), page.locator('text=OK').click(),
//Wait for Save Banner to appear1 //Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message') page.waitForSelector('.c-message-banner__message')
]); ]);
// focus the overlay plot // focus the overlay plot
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click(); await page.goto(overlayPlot.url);
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Overlay Plot').first().click()
]);
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot'); await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
//Save localStorage for future test execution //Save localStorage for future test execution

View 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);
});
});

View File

@ -35,7 +35,10 @@ test.describe('Example Event Generator CRUD Operations', () => {
//Create a name for the object //Create a name for the object
const newObjectName = 'Test Event Generator'; const newObjectName = 'Test Event Generator';
await createDomainObjectWithDefaults(page, 'Event Message Generator', newObjectName); await createDomainObjectWithDefaults(page, {
type: 'Event Message Generator',
name: newObjectName
});
//Assertions against newly created object which define standard behavior //Assertions against newly created object which define standard behavior
await expect(page.waitForURL(/.*&view=table/)).toBeTruthy(); await expect(page.waitForURL(/.*&view=table/)).toBeTruthy();

View 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
});

View File

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

View File

@ -27,6 +27,7 @@ demonstrate some playwright for test developers. This pattern should not be re-u
*/ */
const { test, expect } = require('../../../../pluginFixtures.js'); const { test, expect } = require('../../../../pluginFixtures.js');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
let conditionSetUrl; let conditionSetUrl;
let getConditionSetIdentifierFromUrl; let getConditionSetIdentifierFromUrl;
@ -178,3 +179,24 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
}); });
}); });
test.describe('Basic Condition Set Use', () => {
test('Can add a condition', async ({ page }) => {
//Navigate to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
// Create a new condition set
await createDomainObjectWithDefaults(page, {
type: 'Condition Set',
name: "Test Condition Set"
});
// Change the object to edit mode
await page.locator('[title="Edit"]').click();
// Click Add Condition button
await page.locator('#addCondition').click();
// Check that the new Unnamed Condition section appears
const numOfUnnamedConditions = await page.locator('text=Unnamed Condition').count();
expect(numOfUnnamedConditions).toEqual(1);
});
});

View File

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

View File

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

View File

@ -25,7 +25,7 @@ This test suite is dedicated to tests which verify the basic operations surround
but only assume that example imagery is present. but only assume that example imagery is present.
*/ */
/* globals process */ /* globals process */
const { v4: uuid } = require('uuid');
const { waitForAnimations } = require('../../../../baseFixtures'); const { waitForAnimations } = require('../../../../baseFixtures');
const { test, expect } = require('../../../../pluginFixtures'); const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions'); const { createDomainObjectWithDefaults } = require('../../../../appActions');
@ -41,7 +41,7 @@ test.describe('Example Imagery Object', () => {
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
// Create a default 'Example Imagery' object // Create a default 'Example Imagery' object
createDomainObjectWithDefaults(page, 'Example Imagery'); createDomainObjectWithDefaults(page, { type: 'Example Imagery' });
await Promise.all([ await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),
@ -573,6 +573,40 @@ test.describe('Example Imagery in Tabs view', () => {
test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in'); test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
}); });
test.describe('Example Imagery in Time Strip', () => {
test('ensure that clicking a thumbnail loads the image in large view', async ({ page, browserName }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5632'
});
await page.goto('./', { waitUntil: 'networkidle' });
const timeStripObject = await createDomainObjectWithDefaults(page, {
type: 'Time Strip',
name: 'Time Strip'.concat(' ', uuid())
});
await createDomainObjectWithDefaults(page, {
type: 'Example Imagery',
name: 'Example Imagery'.concat(' ', uuid()),
parent: timeStripObject.uuid
});
// Navigate to timestrip
await page.goto(timeStripObject.url);
await page.locator('.c-imagery-tsv-container').hover();
// get url of the hovered image
const hoveredImg = page.locator('.c-imagery-tsv div.c-imagery-tsv__image-wrapper:hover img');
const hoveredImgSrc = await hoveredImg.getAttribute('src');
expect(hoveredImgSrc).toBeTruthy();
await page.locator('.c-imagery-tsv-container').click();
// get image of view large container
const viewLargeImg = page.locator('img.c-imagery__main-image__image');
const viewLargeImgSrc = await viewLargeImg.getAttribute('src');
expect(viewLargeImgSrc).toBeTruthy();
expect(viewLargeImgSrc).toEqual(hoveredImgSrc);
});
});
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */

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

View File

@ -21,7 +21,7 @@
*****************************************************************************/ *****************************************************************************/
const { test, expect } = require('../../../../pluginFixtures'); const { test, expect } = require('../../../../pluginFixtures');
const { openObjectTreeContextMenu } = require('../../../../appActions'); const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions');
const path = require('path'); const path = require('path');
const TEST_TEXT = 'Testing text for entries.'; const TEST_TEXT = 'Testing text for entries.';
@ -30,8 +30,9 @@ const CUSTOM_NAME = 'CUSTOM_NAME';
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area'; const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
test.describe('Restricted Notebook', () => { test.describe('Restricted Notebook', () => {
let notebook;
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await startAndAddRestrictedNotebookObject(page); notebook = await startAndAddRestrictedNotebookObject(page);
}); });
test('Can be renamed @addInit', async ({ page }) => { test('Can be renamed @addInit', async ({ page }) => {
@ -39,9 +40,7 @@ test.describe('Restricted Notebook', () => {
}); });
test('Can be deleted if there are no locked pages @addInit', async ({ page, openmctConfig }) => { test('Can be deleted if there are no locked pages @addInit', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig; await openObjectTreeContextMenu(page, notebook.url);
await openObjectTreeContextMenu(page, myItemsFolderName, `Unnamed ${CUSTOM_NAME}`);
const menuOptions = page.locator('.c-menu ul'); const menuOptions = page.locator('.c-menu ul');
await expect.soft(menuOptions).toContainText('Remove'); await expect.soft(menuOptions).toContainText('Remove');
@ -76,9 +75,9 @@ test.describe('Restricted Notebook', () => {
}); });
test.describe('Restricted Notebook with at least one entry and with the page locked @addInit', () => { test.describe('Restricted Notebook with at least one entry and with the page locked @addInit', () => {
let notebook;
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await startAndAddRestrictedNotebookObject(page); notebook = await startAndAddRestrictedNotebookObject(page);
await enterTextEntry(page); await enterTextEntry(page);
await lockPage(page); await lockPage(page);
@ -86,9 +85,8 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
await page.locator('button.c-notebook__toggle-nav-button').click(); await page.locator('button.c-notebook__toggle-nav-button').click();
}); });
test('Locked page should now be in a locked state @addInit @unstable', async ({ page, openmctConfig }, testInfo) => { test('Locked page should now be in a locked state @addInit @unstable', async ({ page }, testInfo) => {
test.fixme(testInfo.project === 'chrome-beta', "Test is unreliable on chrome-beta"); test.fixme(testInfo.project === 'chrome-beta', "Test is unreliable on chrome-beta");
const { myItemsFolderName } = openmctConfig;
// main lock message on page // main lock message on page
const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed'); const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed');
expect.soft(await lockMessage.count()).toEqual(1); expect.soft(await lockMessage.count()).toEqual(1);
@ -98,7 +96,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
expect.soft(await pageLockIcon.count()).toEqual(1); expect.soft(await pageLockIcon.count()).toEqual(1);
// no way to remove a restricted notebook with a locked page // no way to remove a restricted notebook with a locked page
await openObjectTreeContextMenu(page, myItemsFolderName, `Unnamed ${CUSTOM_NAME}`); await openObjectTreeContextMenu(page, notebook.url);
const menuOptions = page.locator('.c-menu ul'); const menuOptions = page.locator('.c-menu ul');
await expect(menuOptions).not.toContainText('Remove'); await expect(menuOptions).not.toContainText('Remove');
@ -178,13 +176,8 @@ async function startAndAddRestrictedNotebookObject(page) {
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitRestrictedNotebook.js') }); await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitRestrictedNotebook.js') });
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
await page.click('button:has-text("Create")');
await page.click(`text=${CUSTOM_NAME}`); // secondarily tests renamability also return createDomainObjectWithDefaults(page, { type: CUSTOM_NAME });
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('text=OK')
]);
} }
/** /**

View File

@ -36,7 +36,7 @@ async function createNotebookAndEntry(page, iterations = 1) {
//Go to baseURL //Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
createDomainObjectWithDefaults(page, 'Notebook'); createDomainObjectWithDefaults(page, { type: 'Notebook' });
for (let iteration = 0; iteration < iterations; iteration++) { for (let iteration = 0; iteration < iterations; iteration++) {
// Click text=To start a new entry, click here or drag and drop any object // Click text=To start a new entry, click here or drag and drop any object
@ -56,19 +56,23 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
await createNotebookAndEntry(page, iterations); await createNotebookAndEntry(page, iterations);
for (let iteration = 0; iteration < iterations; iteration++) { for (let iteration = 0; iteration < iterations; iteration++) {
// Click text=To start a new entry, click here or drag and drop any object // Hover and click "Add Tag" button
// Hover is needed here to "slow down" the actions while running in headless mode
await page.hover(`button:has-text("Add Tag") >> nth = ${iteration}`);
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click(); await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
// Click [placeholder="Type to select tag"] // Click inside the tag search input
await page.locator('[placeholder="Type to select tag"]').click(); await page.locator('[placeholder="Type to select tag"]').click();
// Click text=Driving // Select the "Driving" tag
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click(); await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
// Click button:has-text("Add Tag") // Hover and click "Add Tag" button
// Hover is needed here to "slow down" the actions while running in headless mode
await page.hover(`button:has-text("Add Tag") >> nth = ${iteration}`);
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click(); await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
// Click [placeholder="Type to select tag"] // Click inside the tag search input
await page.locator('[placeholder="Type to select tag"]').click(); await page.locator('[placeholder="Type to select tag"]').click();
// Click text=Science // Select the "Science" tag
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click(); await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
} }
} }
@ -130,7 +134,8 @@ test.describe('Tagging in Notebooks @addInit', () => {
await createNotebookEntryAndTags(page); await createNotebookEntryAndTags(page);
await page.locator('[aria-label="Notebook Entries"]').click(); await page.locator('[aria-label="Notebook Entries"]').click();
// Delete Driving // Delete Driving
await page.locator('text=Science Driving Add Tag >> button').nth(1).click(); await page.hover('.c-tag__label:has-text("Driving")');
await page.locator('.c-tag__label:has-text("Driving") ~ .c-completed-tag-deletion').click();
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science"); await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving"); await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving");
@ -139,11 +144,28 @@ test.describe('Tagging in Notebooks @addInit', () => {
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving"); await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
}); });
test('Can delete objects with tags and neither return in search', async ({ page }) => {
await createNotebookEntryAndTags(page);
// Delete Notebook
await page.locator('button[title="More options"]').click();
await page.locator('li[title="Remove this object from its containing object."]').click();
await page.locator('button:has-text("OK")').click();
await page.goto('./', { waitUntil: 'networkidle' });
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed');
await expect(page.locator('text=No matching results.')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sci');
await expect(page.locator('text=No matching results.')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('dri');
await expect(page.locator('text=No matching results.')).toBeVisible();
});
test('Tags persist across reload', async ({ page }) => { test('Tags persist across reload', async ({ page }) => {
//Go to baseURL //Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
await createDomainObjectWithDefaults(page, 'Clock'); await createDomainObjectWithDefaults(page, { type: 'Clock' });
const ITERATIONS = 4; const ITERATIONS = 4;
await createNotebookEntryAndTags(page, ITERATIONS); await createNotebookEntryAndTags(page, ITERATIONS);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -20,55 +20,26 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const { test, expect } = require('../../../../pluginFixtures'); const { test, expect } = require('../../../../pluginFixtures');
test.describe('Telemetry Table', () => { test.describe('Telemetry Table', () => {
test('unpauses and filters data when paused by button and user changes bounds', async ({ page, openmctConfig }) => { test('unpauses and filters data when paused by button and user changes bounds', async ({ page }) => {
test.info().annotations.push({ test.info().annotations.push({
type: 'issue', type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5113' description: 'https://github.com/nasa/openmct/issues/5113'
}); });
const { myItemsFolderName } = openmctConfig;
const bannerMessage = '.c-message-banner__message';
const createButton = 'button:has-text("Create")';
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
// Click create button const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
await page.locator(createButton).click(); await createDomainObjectWithDefaults(page, {
await page.locator('li:has-text("Telemetry Table")').click(); type: 'Sine Wave Generator',
parent: table.uuid
await Promise.all([ });
page.waitForNavigation(),
page.locator('text=OK').click(),
// Wait for Save Banner to appear
page.waitForSelector(bannerMessage)
]);
// Save (exit edit mode)
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(3).click();
await page.locator('text=Save and Finish Editing').click();
// Click create button
await page.locator(createButton).click();
// add Sine Wave Generator with defaults
await page.locator('li:has-text("Sine Wave Generator")').click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
// Wait for Save Banner to appear
page.waitForSelector(bannerMessage)
]);
// focus the Telemetry Table // focus the Telemetry Table
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click(); page.goto(table.url);
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Telemetry Table').first().click()
]);
// Click pause button // Click pause button
const pauseButton = page.locator('button.c-button.icon-pause'); const pauseButton = page.locator('button.c-button.icon-pause');

View File

@ -21,6 +21,7 @@
*****************************************************************************/ *****************************************************************************/
const { test, expect } = require('../../../../baseFixtures'); const { test, expect } = require('../../../../baseFixtures');
const { setFixedTimeMode, setRealTimeMode, setStartOffset, setEndOffset } = require('../../../../appActions');
test.describe('Time conductor operations', () => { test.describe('Time conductor operations', () => {
test('validate start time does not exceeds end time', async ({ page }) => { test('validate start time does not exceeds end time', async ({ page }) => {
@ -146,89 +147,24 @@ test.describe('Time conductor input fields real-time mode', () => {
expect(page.url()).toContain(`startDelta=${startDelta}`); expect(page.url()).toContain(`startDelta=${startDelta}`);
expect(page.url()).toContain(`endDelta=${endDelta}`); expect(page.url()).toContain(`endDelta=${endDelta}`);
}); });
test.fixme('time conductor history in fixed time mode will track changing start and end times', async ({ page }) => {
// change start time, verify it's tracked in history
// change end time, verify it's tracked in history
});
test.fixme('time conductor history in realtime mode will track changing start and end times', async ({ page }) => {
// change start offset, verify it's tracked in history
// change end offset, verify it's tracked in history
});
test.fixme('time conductor history allows you to set a historical timeframe', async ({ page }) => {
// make sure there are historical history options
// select an option and make sure the time conductor start and end bounds are updated correctly
});
test.fixme('time conductor history allows you to set a realtime offsets', async ({ page }) => {
// make sure there are realtime history options
// select an option and verify the offsets are updated correctly
});
}); });
/**
* @typedef {Object} OffsetValues
* @property {string | undefined} hours
* @property {string | undefined} mins
* @property {string | undefined} secs
*/
/**
* Set the values (hours, mins, secs) for the start time offset when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
*/
async function setStartOffset(page, offset) {
const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
await setTimeConductorOffset(page, offset, startOffsetButton);
}
/**
* Set the values (hours, mins, secs) for the end time offset when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
*/
async function setEndOffset(page, offset) {
const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
await setTimeConductorOffset(page, offset, endOffsetButton);
}
/**
* Set the time conductor to fixed timespan mode
* @param {import('@playwright/test').Page} page
*/
async function setFixedTimeMode(page) {
await setTimeConductorMode(page, true);
}
/**
* Set the time conductor to realtime mode
* @param {import('@playwright/test').Page} page
*/
async function setRealTimeMode(page) {
await setTimeConductorMode(page, false);
}
/**
* Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
* @param {import('@playwright/test').Locator} offsetButton
*/
async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) {
await offsetButton.click();
if (hours) {
await page.fill('.pr-time-controls__hrs', hours);
}
if (mins) {
await page.fill('.pr-time-controls__mins', mins);
}
if (secs) {
await page.fill('.pr-time-controls__secs', secs);
}
// Click the check button
await page.locator('.icon-check').click();
}
/**
* Set the time conductor mode to either fixed timespan or realtime mode.
* @param {import('@playwright/test').Page} page
* @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true
*/
async function setTimeConductorMode(page, isFixedTimespan = true) {
// Click 'mode' button
await page.locator('.c-mode-button').click();
// Switch time conductor mode
if (isFixedTimespan) {
await page.locator('data-testid=conductor-modeOption-fixed').click();
} else {
await page.locator('data-testid=conductor-modeOption-realtime').click();
}
}

View File

@ -24,9 +24,10 @@ const { test, expect } = require('../../../../pluginFixtures');
const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions'); const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions');
test.describe('Timer', () => { test.describe('Timer', () => {
let timer;
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
await createDomainObjectWithDefaults(page, 'timer'); timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
}); });
test('Can perform actions on the Timer', async ({ page, openmctConfig }) => { test('Can perform actions on the Timer', async ({ page, openmctConfig }) => {
@ -35,13 +36,13 @@ test.describe('Timer', () => {
description: 'https://github.com/nasa/openmct/issues/4313' description: 'https://github.com/nasa/openmct/issues/4313'
}); });
const { myItemsFolderName } = await openmctConfig; const timerUrl = timer.url;
await test.step("From the tree context menu", async () => { await test.step("From the tree context menu", async () => {
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Start'); await triggerTimerContextMenuAction(page, timerUrl, 'Start');
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Pause'); await triggerTimerContextMenuAction(page, timerUrl, 'Pause');
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Restart at 0'); await triggerTimerContextMenuAction(page, timerUrl, 'Restart at 0');
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Stop'); await triggerTimerContextMenuAction(page, timerUrl, 'Stop');
}); });
await test.step("From the 3dot menu", async () => { await test.step("From the 3dot menu", async () => {
@ -74,9 +75,9 @@ test.describe('Timer', () => {
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {TimerAction} action * @param {TimerAction} action
*/ */
async function triggerTimerContextMenuAction(page, myItemsFolderName, action) { async function triggerTimerContextMenuAction(page, timerUrl, action) {
const menuAction = `.c-menu ul li >> text="${action}"`; const menuAction = `.c-menu ul li >> text="${action}"`;
await openObjectTreeContextMenu(page, myItemsFolderName, "Unnamed Timer"); await openObjectTreeContextMenu(page, timerUrl);
await page.locator(menuAction).click(); await page.locator(menuAction).click();
assertTimerStateAfterAction(page, action); assertTimerStateAfterAction(page, action);
} }

View File

@ -24,6 +24,8 @@
*/ */
const { test, expect } = require('../../pluginFixtures'); const { test, expect } = require('../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../appActions');
const { v4: uuid } = require('uuid');
test.describe('Grand Search', () => { test.describe('Grand Search', () => {
test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page, openmctConfig }) => { test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page, openmctConfig }) => {
@ -107,15 +109,21 @@ test.describe("Search Tests @unstable", () => {
// Verify that no results are found // Verify that no results are found
expect(await searchResults.count()).toBe(0); expect(await searchResults.count()).toBe(0);
// Verify proper message appears
await expect(page.locator('text=No matching results.')).toBeVisible();
}); });
test('Validate single object in search result', async ({ page }) => { test('Validate single object in search result @couchdb', async ({ page }) => {
//Go to baseURL //Go to baseURL
await page.goto("./", { waitUntil: "networkidle" }); await page.goto("./", { waitUntil: "networkidle" });
// Create a folder object // Create a folder object
const folderName = 'testFolder'; const folderName = uuid();
await createFolderObject(page, folderName); await createDomainObjectWithDefaults(page, {
type: 'folder',
name: folderName
});
// Full search for object // Full search for object
await page.type("input[type=search]", folderName); await page.type("input[type=search]", folderName);
@ -124,7 +132,7 @@ test.describe("Search Tests @unstable", () => {
await waitForSearchCompletion(page); await waitForSearchCompletion(page);
// Get the search results // Get the search results
const searchResults = await page.locator(searchResultSelector); const searchResults = page.locator(searchResultSelector);
// Verify that one result is found // Verify that one result is found
expect(await searchResults.count()).toBe(1); expect(await searchResults.count()).toBe(1);

View 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"]');
}

View File

@ -53,7 +53,7 @@ test.describe('Visual - addInit', () => {
//Go to baseURL //Go to baseURL
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' }); await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
await createDomainObjectWithDefaults(page, CUSTOM_NAME); await createDomainObjectWithDefaults(page, { type: CUSTOM_NAME });
// Take a snapshot of the newly created CUSTOM_NAME notebook // Take a snapshot of the newly created CUSTOM_NAME notebook
await percySnapshot(page, `Restricted Notebook with CUSTOM_NAME (theme: '${theme}')`); await percySnapshot(page, `Restricted Notebook with CUSTOM_NAME (theme: '${theme}')`);

View 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();
}

View File

@ -67,9 +67,9 @@ test.describe('Visual - Default', () => {
await percySnapshot(page, `About (theme: '${theme}')`); await percySnapshot(page, `About (theme: '${theme}')`);
}); });
test('Visual - Default Condition Set', async ({ page, theme }) => { test.fixme('Visual - Default Condition Set', async ({ page, theme }) => {
await createDomainObjectWithDefaults(page, 'Condition Set'); await createDomainObjectWithDefaults(page, { type: 'Condition Set' });
// Take a snapshot of the newly created Condition Set object // Take a snapshot of the newly created Condition Set object
await percySnapshot(page, `Default Condition Set (theme: '${theme}')`); await percySnapshot(page, `Default Condition Set (theme: '${theme}')`);
@ -81,7 +81,7 @@ test.describe('Visual - Default', () => {
description: 'https://github.com/nasa/openmct/issues/5349' description: 'https://github.com/nasa/openmct/issues/5349'
}); });
await createDomainObjectWithDefaults(page, 'Condition Widget'); await createDomainObjectWithDefaults(page, { type: 'Condition Widget' });
// Take a snapshot of the newly created Condition Widget object // Take a snapshot of the newly created Condition Widget object
await percySnapshot(page, `Default Condition Widget (theme: '${theme}')`); await percySnapshot(page, `Default Condition Widget (theme: '${theme}')`);
@ -137,8 +137,8 @@ test.describe('Visual - Default', () => {
await percySnapshot(page, `removed amplitude property value (theme: '${theme}')`); await percySnapshot(page, `removed amplitude property value (theme: '${theme}')`);
}); });
test('Visual - Save Successful Banner', async ({ page, theme }) => { test.fixme('Visual - Save Successful Banner', async ({ page, theme }) => {
await createDomainObjectWithDefaults(page, 'Timer'); await createDomainObjectWithDefaults(page, { type: 'Timer' });
await page.locator('.c-message-banner__message').hover({ trial: true }); await page.locator('.c-message-banner__message').hover({ trial: true });
await percySnapshot(page, `Banner message shown (theme: '${theme}')`); await percySnapshot(page, `Banner message shown (theme: '${theme}')`);
@ -159,8 +159,8 @@ test.describe('Visual - Default', () => {
}); });
test('Visual - Default Gauge is correct', async ({ page, theme }) => { test.fixme('Visual - Default Gauge is correct', async ({ page, theme }) => {
await createDomainObjectWithDefaults(page, 'Gauge'); await createDomainObjectWithDefaults(page, { type: 'Gauge' });
// Take a snapshot of the newly created Gauge object // Take a snapshot of the newly created Gauge object
await percySnapshot(page, `Default Gauge (theme: '${theme}')`); await percySnapshot(page, `Default Gauge (theme: '${theme}')`);

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

View File

@ -46,7 +46,10 @@ test.describe('Grand Search', () => {
// await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); // await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
// await page.locator('text=Save and Finish Editing').click(); // await page.locator('text=Save and Finish Editing').click();
const folder1 = 'Folder1'; const folder1 = 'Folder1';
await createDomainObjectWithDefaults(page, 'Folder', folder1); await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: folder1
});
// Click [aria-label="OpenMCT Search"] input[type="search"] // Click [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();

View File

@ -20,59 +20,36 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
export default function () { import utils from './utils';
export default function (staticFaults = false) {
return function install(openmct) { return function install(openmct) {
openmct.install(openmct.plugins.FaultManagement()); openmct.install(openmct.plugins.FaultManagement());
const faultsData = utils.randomFaults(staticFaults);
openmct.faults.addProvider({ openmct.faults.addProvider({
request(domainObject, options) { request(domainObject, options) {
const faults = JSON.parse(localStorage.getItem('faults')); return Promise.resolve(faultsData);
return Promise.resolve(faults.alarms);
}, },
subscribe(domainObject, callback) { subscribe(domainObject, callback) {
const faultsData = JSON.parse(localStorage.getItem('faults')).alarms; return () => {};
function getRandomIndex(start, end) {
return Math.floor(start + (Math.random() * (end - start + 1)));
}
let id = setInterval(() => {
const index = getRandomIndex(0, faultsData.length - 1);
const randomFaultData = faultsData[index];
const randomFault = randomFaultData.fault;
randomFault.currentValueInfo.value = Math.random();
callback({
fault: randomFault,
type: 'alarms'
});
}, 300);
return () => {
clearInterval(id);
};
}, },
supportsRequest(domainObject) { supportsRequest(domainObject) {
const faults = localStorage.getItem('faults'); return domainObject.type === 'faultManagement';
return faults && domainObject.type === 'faultManagement';
}, },
supportsSubscribe(domainObject) { supportsSubscribe(domainObject) {
const faults = localStorage.getItem('faults'); return domainObject.type === 'faultManagement';
return faults && domainObject.type === 'faultManagement';
}, },
acknowledgeFault(fault, { comment = '' }) { acknowledgeFault(fault, { comment = '' }) {
console.log('acknowledgeFault', fault); utils.acknowledgeFault(fault);
console.log('comment', comment);
return Promise.resolve({ return Promise.resolve({
success: true success: true
}); });
}, },
shelveFault(fault, shelveData) { shelveFault(fault, duration) {
console.log('shelveFault', fault); utils.shelveFault(fault, duration);
console.log('shelveData', shelveData);
return Promise.resolve({ return Promise.resolve({
success: true success: true

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

View File

@ -1,11 +1,11 @@
{ {
"name": "openmct", "name": "openmct",
"version": "2.1.0-SNAPSHOT", "version": "2.0.8",
"description": "The Open MCT core platform", "description": "The Open MCT core platform",
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "7.18.9", "@babel/eslint-parser": "7.18.9",
"@braintree/sanitize-url": "6.0.0", "@braintree/sanitize-url": "6.0.0",
"@percy/cli": "1.7.2", "@percy/cli": "1.8.1",
"@percy/playwright": "1.0.4", "@percy/playwright": "1.0.4",
"@playwright/test": "1.23.0", "@playwright/test": "1.23.0",
"@types/eventemitter3": "^1.0.0", "@types/eventemitter3": "^1.0.0",
@ -90,7 +90,8 @@
"test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run", "test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run", "test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
"test:e2e": "npx playwright test", "test:e2e": "npx playwright test",
"test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert @unstable", "test:e2e:couchdb": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @couchdb",
"test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert \"@unstable|@couchdb\"",
"test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable", "test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable",
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome", "test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots", "test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots",

View File

@ -40,6 +40,8 @@ const ANNOTATION_TYPES = Object.freeze({
PLOT_SPATIAL: 'PLOT_SPATIAL' PLOT_SPATIAL: 'PLOT_SPATIAL'
}); });
const ANNOTATION_TYPE = 'annotation';
/** /**
* @typedef {Object} Tag * @typedef {Object} Tag
* @property {String} key a unique identifier for the tag * @property {String} key a unique identifier for the tag
@ -54,7 +56,7 @@ export default class AnnotationAPI extends EventEmitter {
this.ANNOTATION_TYPES = ANNOTATION_TYPES; this.ANNOTATION_TYPES = ANNOTATION_TYPES;
this.openmct.types.addType('annotation', { this.openmct.types.addType(ANNOTATION_TYPE, {
name: 'Annotation', name: 'Annotation',
description: 'A user created note or comment about time ranges, pixel space, and geospatial features.', description: 'A user created note or comment about time ranges, pixel space, and geospatial features.',
creatable: false, creatable: false,
@ -136,6 +138,10 @@ export default class AnnotationAPI extends EventEmitter {
this.availableTags[tagKey] = tagsDefinition; this.availableTags[tagKey] = tagsDefinition;
} }
isAnnotation(domainObject) {
return domainObject && (domainObject.type === ANNOTATION_TYPE);
}
getAvailableTags() { getAvailableTags() {
if (this.availableTags) { if (this.availableTags) {
const rearrangedToArray = Object.keys(this.availableTags).map(tagKey => { const rearrangedToArray = Object.keys(this.availableTags).map(tagKey => {
@ -271,7 +277,10 @@ export default class AnnotationAPI extends EventEmitter {
const searchResults = (await Promise.all(this.openmct.objects.search(matchingTagKeys, abortController, this.openmct.objects.SEARCH_TYPES.TAGS))).flat(); const searchResults = (await Promise.all(this.openmct.objects.search(matchingTagKeys, abortController, this.openmct.objects.SEARCH_TYPES.TAGS))).flat();
const appliedTagSearchResults = this.#addTagMetaInformationToResults(searchResults, matchingTagKeys); const appliedTagSearchResults = this.#addTagMetaInformationToResults(searchResults, matchingTagKeys);
const appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults); const appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults);
const resultsWithValidPath = appliedTargetsModels.filter(result => {
return this.openmct.objects.isReachable(result.targetModels?.[0]?.originalPath);
});
return appliedTargetsModels; return resultsWithValidPath;
} }
} }

View File

@ -27,15 +27,26 @@ describe("The Annotation API", () => {
let openmct; let openmct;
let mockObjectProvider; let mockObjectProvider;
let mockDomainObject; let mockDomainObject;
let mockFolderObject;
let mockAnnotationObject; let mockAnnotationObject;
beforeEach((done) => { beforeEach((done) => {
openmct = createOpenMct(); openmct = createOpenMct();
openmct.install(new ExampleTagsPlugin()); openmct.install(new ExampleTagsPlugin());
const availableTags = openmct.annotation.getAvailableTags(); const availableTags = openmct.annotation.getAvailableTags();
mockFolderObject = {
type: 'root',
name: 'folderFoo',
location: '',
identifier: {
key: 'someParent',
namespace: 'fooNameSpace'
}
};
mockDomainObject = { mockDomainObject = {
type: 'notebook', type: 'notebook',
name: 'fooRabbitNotebook', name: 'fooRabbitNotebook',
location: 'fooNameSpace:someParent',
identifier: { identifier: {
key: 'some-object', key: 'some-object',
namespace: 'fooNameSpace' namespace: 'fooNameSpace'
@ -68,6 +79,8 @@ describe("The Annotation API", () => {
return mockDomainObject; return mockDomainObject;
} else if (identifier.key === mockAnnotationObject.identifier.key) { } else if (identifier.key === mockAnnotationObject.identifier.key) {
return mockAnnotationObject; return mockAnnotationObject;
} else if (identifier.key === mockFolderObject.identifier.key) {
return mockFolderObject;
} else { } else {
return null; return null;
} }
@ -150,6 +163,7 @@ describe("The Annotation API", () => {
// use local worker // use local worker
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker; sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
openmct.objects.inMemorySearchProvider.worker = null; openmct.objects.inMemorySearchProvider.worker = null;
await openmct.objects.inMemorySearchProvider.index(mockFolderObject);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject); await openmct.objects.inMemorySearchProvider.index(mockDomainObject);
await openmct.objects.inMemorySearchProvider.index(mockAnnotationObject); await openmct.objects.inMemorySearchProvider.index(mockAnnotationObject);
}); });

View File

@ -63,6 +63,8 @@ class InMemorySearchProvider {
this.localSearchForTags = this.localSearchForTags.bind(this); this.localSearchForTags = this.localSearchForTags.bind(this);
this.localSearchForNotebookAnnotations = this.localSearchForNotebookAnnotations.bind(this); this.localSearchForNotebookAnnotations = this.localSearchForNotebookAnnotations.bind(this);
this.onAnnotationCreation = this.onAnnotationCreation.bind(this); this.onAnnotationCreation = this.onAnnotationCreation.bind(this);
this.onCompositionAdded = this.onCompositionAdded.bind(this);
this.onCompositionRemoved = this.onCompositionRemoved.bind(this);
this.onerror = this.onWorkerError.bind(this); this.onerror = this.onWorkerError.bind(this);
this.startIndexing = this.startIndexing.bind(this); this.startIndexing = this.startIndexing.bind(this);
@ -75,6 +77,12 @@ class InMemorySearchProvider {
this.worker.port.close(); this.worker.port.close();
} }
Object.keys(this.indexedCompositions).forEach(keyString => {
const composition = this.indexedCompositions[keyString];
composition.off('add', this.onCompositionAdded);
composition.off('remove', this.onCompositionRemoved);
});
this.destroyObservers(this.indexedIds); this.destroyObservers(this.indexedIds);
this.destroyObservers(this.indexedCompositions); this.destroyObservers(this.indexedCompositions);
}); });
@ -259,7 +267,6 @@ class InMemorySearchProvider {
} }
onAnnotationCreation(annotationObject) { onAnnotationCreation(annotationObject) {
const objectProvider = this.openmct.objects.getProvider(annotationObject.identifier); const objectProvider = this.openmct.objects.getProvider(annotationObject.identifier);
if (objectProvider === undefined || objectProvider.search === undefined) { if (objectProvider === undefined || objectProvider.search === undefined) {
const provider = this; const provider = this;
@ -281,17 +288,34 @@ class InMemorySearchProvider {
provider.index(domainObject); provider.index(domainObject);
} }
onCompositionMutation(domainObject, composition) { onCompositionAdded(newDomainObjectToIndex) {
const provider = this; const provider = this;
const indexedComposition = domainObject.composition; // The object comes in as a mutable domain object, which has functions,
const identifiersToIndex = composition // which the index function cannot handle as it will eventually be serialized
.filter(identifier => !indexedComposition // using structuredClone. Thus we're using JSON.parse/JSON.stringify to discard
.some(indexedIdentifier => this.openmct.objects // those functions.
.areIdsEqual([identifier, indexedIdentifier]))); const nonMutableDomainObject = JSON.parse(JSON.stringify(newDomainObjectToIndex));
identifiersToIndex.forEach(identifier => { const objectProvider = this.openmct.objects.getProvider(nonMutableDomainObject.identifier);
this.openmct.objects.get(identifier).then(objectToIndex => provider.index(objectToIndex)); if (objectProvider === undefined || objectProvider.search === undefined) {
}); provider.index(nonMutableDomainObject);
}
}
onCompositionRemoved(domainObjectToRemoveIdentifier) {
const keyString = this.openmct.objects.makeKeyString(domainObjectToRemoveIdentifier);
if (this.indexedIds[keyString]) {
// we store the unobserve function in the indexedId map
this.indexedIds[keyString]();
delete this.indexedIds[keyString];
}
const composition = this.indexedCompositions[keyString];
if (composition) {
composition.off('add', this.onCompositionAdded);
composition.off('remove', this.onCompositionRemoved);
delete this.indexedCompositions[keyString];
}
} }
/** /**
@ -305,6 +329,7 @@ class InMemorySearchProvider {
async index(domainObject) { async index(domainObject) {
const provider = this; const provider = this;
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier); const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
const composition = this.openmct.composition.get(domainObject);
if (!this.indexedIds[keyString]) { if (!this.indexedIds[keyString]) {
this.indexedIds[keyString] = this.openmct.objects.observe( this.indexedIds[keyString] = this.openmct.objects.observe(
@ -312,11 +337,12 @@ class InMemorySearchProvider {
'name', 'name',
this.onNameMutation.bind(this, domainObject) this.onNameMutation.bind(this, domainObject)
); );
this.indexedCompositions[keyString] = this.openmct.objects.observe( if (composition) {
domainObject, composition.on('add', this.onCompositionAdded);
'composition', composition.on('remove', this.onCompositionRemoved);
this.onCompositionMutation.bind(this, domainObject) this.indexedCompositions[keyString] = composition;
); }
if (domainObject.type === 'annotation') { if (domainObject.type === 'annotation') {
this.indexedTags[keyString] = this.openmct.objects.observe( this.indexedTags[keyString] = this.openmct.objects.observe(
domainObject, domainObject,
@ -338,8 +364,6 @@ class InMemorySearchProvider {
} }
} }
const composition = this.openmct.composition.get(domainObject);
if (composition !== undefined) { if (composition !== undefined) {
const children = await composition.load(); const children = await composition.load();

View File

@ -34,11 +34,11 @@ import InMemorySearchProvider from './InMemorySearchProvider';
* Uniquely identifies a domain object. * Uniquely identifies a domain object.
* *
* @typedef Identifier * @typedef Identifier
* @memberof module:openmct.ObjectAPI~
* @property {string} namespace the namespace to/from which this domain * @property {string} namespace the namespace to/from which this domain
* object should be loaded/stored. * object should be loaded/stored.
* @property {string} key a unique identifier for the domain object * @property {string} key a unique identifier for the domain object
* within that namespace * within that namespace
* @memberof module:openmct.ObjectAPI~
*/ */
/** /**
@ -230,15 +230,10 @@ export default class ObjectAPI {
return result; return result;
}).catch((result) => { }).catch((result) => {
console.warn(`Failed to retrieve ${keystring}:`, result); console.warn(`Failed to retrieve ${keystring}:`, result);
this.openmct.notifications.error(`Failed to retrieve object ${keystring}`);
delete this.cache[keystring]; delete this.cache[keystring];
if (!result) { result = this.applyGetInterceptors(identifier);
//no result means resource either doesn't exist or is missing
//otherwise it's an error, and we shouldn't apply interceptors
result = this.applyGetInterceptors(identifier);
}
return result; return result;
}); });
@ -615,27 +610,60 @@ export default class ObjectAPI {
* @param {module:openmct.ObjectAPI~Identifier[]} identifiers * @param {module:openmct.ObjectAPI~Identifier[]} identifiers
*/ */
areIdsEqual(...identifiers) { areIdsEqual(...identifiers) {
const firstIdentifier = utils.parseKeyString(identifiers[0]);
return identifiers.map(utils.parseKeyString) return identifiers.map(utils.parseKeyString)
.every(identifier => { .every(identifier => {
return identifier === identifiers[0] return identifier === firstIdentifier
|| (identifier.namespace === identifiers[0].namespace || (identifier.namespace === firstIdentifier.namespace
&& identifier.key === identifiers[0].key); && identifier.key === firstIdentifier.key);
}); });
} }
getOriginalPath(identifier, path = []) { /**
return this.get(identifier).then((domainObject) => { * Given an original path check if the path is reachable via root
path.push(domainObject); * @param {Array<Object>} originalPath an array of path objects to check
let location = domainObject.location; * @returns {boolean} whether the domain object is reachable
*/
isReachable(originalPath) {
if (originalPath && originalPath.length) {
return (originalPath[originalPath.length - 1].type === 'root');
}
if (location) { return false;
return this.getOriginalPath(utils.parseKeyString(location), path); }
} else {
return path; #pathContainsDomainObject(keyStringToCheck, path) {
} if (!keyStringToCheck) {
return false;
}
return path.some(pathElement => {
const identifierToCheck = utils.parseKeyString(keyStringToCheck);
return this.areIdsEqual(identifierToCheck, pathElement.identifier);
}); });
} }
/**
* Given an identifier, constructs the original path by walking up its parents
* @param {module:openmct.ObjectAPI~Identifier} identifier
* @param {Array<module:openmct.DomainObject>} path an array of path objects
* @returns {Promise<Array<module:openmct.DomainObject>>} a promise containing an array of domain objects
*/
async getOriginalPath(identifier, path = []) {
const domainObject = await this.get(identifier);
path.push(domainObject);
const { location } = domainObject;
if (location && (!this.#pathContainsDomainObject(location, path))) {
// if we have a location, and we don't already have this in our constructed path,
// then keep walking up the path
return this.getOriginalPath(utils.parseKeyString(location), path);
} else {
return path;
}
}
isObjectPathToALink(domainObject, objectPath) { isObjectPathToALink(domainObject, objectPath) {
return objectPath !== undefined return objectPath !== undefined
&& objectPath.length > 1 && objectPath.length > 1

View File

@ -377,6 +377,73 @@ describe("The Object API", () => {
}); });
}); });
describe("getOriginalPath", () => {
let mockGrandParentObject;
let mockParentObject;
let mockChildObject;
beforeEach(() => {
const mockObjectProvider = jasmine.createSpyObj("mock object provider", [
"create",
"update",
"get"
]);
mockGrandParentObject = {
type: 'folder',
name: 'Grand Parent Folder',
location: 'fooNameSpace:child',
identifier: {
key: 'grandParent',
namespace: 'fooNameSpace'
}
};
mockParentObject = {
type: 'folder',
name: 'Parent Folder',
location: 'fooNameSpace:grandParent',
identifier: {
key: 'parent',
namespace: 'fooNameSpace'
}
};
mockChildObject = {
type: 'folder',
name: 'Child Folder',
location: 'fooNameSpace:parent',
identifier: {
key: 'child',
namespace: 'fooNameSpace'
}
};
// eslint-disable-next-line require-await
mockObjectProvider.get = async (identifier) => {
if (identifier.key === mockGrandParentObject.identifier.key) {
return mockGrandParentObject;
} else if (identifier.key === mockParentObject.identifier.key) {
return mockParentObject;
} else if (identifier.key === mockChildObject.identifier.key) {
return mockChildObject;
} else {
return null;
}
};
openmct.objects.addProvider('fooNameSpace', mockObjectProvider);
mockObjectProvider.create.and.returnValue(Promise.resolve(true));
mockObjectProvider.update.and.returnValue(Promise.resolve(true));
openmct.objects.addProvider('fooNameSpace', mockObjectProvider);
});
it('can construct paths even with cycles', async () => {
const objectPath = await objectAPI.getOriginalPath(mockChildObject.identifier);
expect(objectPath.length).toEqual(3);
});
});
describe("transactions", () => { describe("transactions", () => {
beforeEach(() => { beforeEach(() => {
spyOn(openmct.editor, 'isEditing').and.returnValue(true); spyOn(openmct.editor, 'isEditing').and.returnValue(true);

View File

@ -91,6 +91,10 @@ define([
* @returns keyString * @returns keyString
*/ */
function makeKeyString(identifier) { function makeKeyString(identifier) {
if (!identifier) {
throw new Error("Cannot make key string from null identifier");
}
if (isKeyString(identifier)) { if (isKeyString(identifier)) {
return identifier; return identifier;
} }

View File

@ -97,11 +97,11 @@ export default {
}, },
followTimeContext() { followTimeContext() {
this.timeContext.on('bounds', this.reloadTelemetry); this.timeContext.on('bounds', this.reloadTelemetryOnBoundsChange);
}, },
stopFollowingTimeContext() { stopFollowingTimeContext() {
if (this.timeContext) { if (this.timeContext) {
this.timeContext.off('bounds', this.reloadTelemetry); this.timeContext.off('bounds', this.reloadTelemetryOnBoundsChange);
} }
}, },
addToComposition(telemetryObject) { addToComposition(telemetryObject) {
@ -181,6 +181,11 @@ export default {
this.composition.on('remove', this.removeTelemetryObject); this.composition.on('remove', this.removeTelemetryObject);
this.composition.load(); this.composition.load();
}, },
reloadTelemetryOnBoundsChange(bounds, isTick) {
if (!isTick) {
this.reloadTelemetry();
}
},
reloadTelemetry() { reloadTelemetry() {
this.valuesByTimestamp = {}; this.valuesByTimestamp = {};

View File

@ -51,7 +51,11 @@ export default class TelemetryCriterion extends EventEmitter {
} }
initialize() { initialize() {
this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry); this.telemetryObjectIdAsString = "";
if (![undefined, null, ""].includes(this.telemetryDomainObjectDefinition?.telemetry)) {
this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry);
}
this.updateTelemetryObjects(this.telemetryDomainObjectDefinition.telemetryObjects); this.updateTelemetryObjects(this.telemetryDomainObjectDefinition.telemetryObjects);
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) { if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
this.subscribeForStaleData(); this.subscribeForStaleData();

View File

@ -517,7 +517,19 @@ export default {
initializeItems() { initializeItems() {
this.telemetryViewMap = {}; this.telemetryViewMap = {};
this.objectViewMap = {}; this.objectViewMap = {};
this.layoutItems.forEach(this.trackItem);
let removedItems = [];
this.layoutItems.forEach((item) => {
if (item.identifier) {
if (this.containsObject(item.identifier)) {
this.trackItem(item);
} else {
removedItems.push(this.openmct.objects.makeKeyString(item.identifier));
}
}
});
removedItems.forEach(this.removeFromConfiguration);
}, },
isItemAlreadyTracked(child) { isItemAlreadyTracked(child) {
let found = false; let found = false;

View File

@ -232,10 +232,12 @@ export default {
this.removeSelectable(); this.removeSelectable();
} }
this.telemetryCollection.off('add', this.setLatestValues); if (this.telemetryCollection) {
this.telemetryCollection.off('clear', this.refreshData); this.telemetryCollection.off('add', this.setLatestValues);
this.telemetryCollection.off('clear', this.refreshData);
this.telemetryCollection.destroy(); this.telemetryCollection.destroy();
}
if (this.mutablePromise) { if (this.mutablePromise) {
this.mutablePromise.then(() => { this.mutablePromise.then(() => {

View File

@ -21,6 +21,7 @@
*****************************************************************************/ *****************************************************************************/
import { createOpenMct, resetApplicationState } from 'utils/testing'; import { createOpenMct, resetApplicationState } from 'utils/testing';
import Vue from 'vue';
import DisplayLayoutPlugin from './plugin'; import DisplayLayoutPlugin from './plugin';
describe('the plugin', function () { describe('the plugin', function () {
@ -117,6 +118,59 @@ describe('the plugin', function () {
}); });
describe('on load', () => {
let displayLayoutItem;
let item;
beforeEach((done) => {
item = {
'width': 32,
'height': 18,
'x': 78,
'y': 8,
'identifier': {
'namespace': '',
'key': 'bdeb91ab-3a7e-4a71-9dd2-39d73644e136'
},
'hasFrame': true,
'type': 'line-view', // so no telemetry functionality is triggered, just want to test the sync
'id': 'c0ff485a-344c-4e70-8d83-a9d9998a69fc'
};
displayLayoutItem = {
'composition': [
// no item in compostion, but item in configuration items
],
'configuration': {
'items': [
item
],
'layoutGrid': [
10,
10
]
},
'name': 'Display Layout',
'type': 'layout',
'identifier': {
'namespace': '',
'key': 'c5e636c1-6771-4c9c-b933-8665cab189b3'
}
};
const applicableViews = openmct.objectViews.get(displayLayoutItem, []);
const displayLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'layout.view');
const view = displayLayoutViewProvider.view(displayLayoutItem);
view.show(child, false);
Vue.nextTick(done);
});
it('will sync compostion and layout items', () => {
expect(displayLayoutItem.configuration.items.length).toBe(0);
});
});
describe('the alpha numeric format view', () => { describe('the alpha numeric format view', () => {
let displayLayoutItem; let displayLayoutItem;
let telemetryItem; let telemetryItem;

View File

@ -71,6 +71,8 @@ import FaultManagementToolbar from './FaultManagementToolbar.vue';
import { FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS, FILTER_ITEMS, SORT_ITEMS } from './constants'; import { FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS, FILTER_ITEMS, SORT_ITEMS } from './constants';
const SEARCH_KEYS = ['id', 'triggerValueInfo', 'currentValueInfo', 'triggerTime', 'severity', 'name', 'shortDescription', 'namespace'];
export default { export default {
components: { components: {
FaultManagementListHeader, FaultManagementListHeader,
@ -125,27 +127,19 @@ export default {
}, },
methods: { methods: {
filterUsingSearchTerm(fault) { filterUsingSearchTerm(fault) {
if (fault?.id?.toString().toLowerCase().includes(this.searchTerm)) { if (!fault) {
return true; return false;
} }
if (fault?.triggerValueInfo?.toString().toLowerCase().includes(this.searchTerm)) { let match = false;
return true;
}
if (fault?.currentValueInfo?.toString().toLowerCase().includes(this.searchTerm)) { SEARCH_KEYS.forEach((key) => {
return true; if (fault[key]?.toString().toLowerCase().includes(this.searchTerm)) {
} match = true;
}
});
if (fault?.triggerTime.toString().toLowerCase().includes(this.searchTerm)) { return match;
return true;
}
if (fault?.severity.toString().toLowerCase().includes(this.searchTerm)) {
return true;
}
return false;
}, },
isSelected(fault) { isSelected(fault) {
return Boolean(this.selectedFaults[fault.id]); return Boolean(this.selectedFaults[fault.id]);

View File

@ -24,10 +24,22 @@ import {
createOpenMct, createOpenMct,
resetApplicationState resetApplicationState
} from '../../utils/testing'; } from '../../utils/testing';
import { FAULT_MANAGEMENT_TYPE } from './constants'; import {
FAULT_MANAGEMENT_TYPE,
FAULT_MANAGEMENT_VIEW,
FAULT_MANAGEMENT_NAMESPACE
} from './constants';
describe("The Fault Management Plugin", () => { describe("The Fault Management Plugin", () => {
let openmct; let openmct;
const faultDomainObject = {
name: 'it is not your fault',
type: FAULT_MANAGEMENT_TYPE,
identifier: {
key: 'nobodies',
namespace: 'fault'
}
};
beforeEach(() => { beforeEach(() => {
openmct = createOpenMct(); openmct = createOpenMct();
@ -38,15 +50,54 @@ describe("The Fault Management Plugin", () => {
}); });
it('is not installed by default', () => { it('is not installed by default', () => {
let typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition; const typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;
expect(typeDef.name).toBe('Unknown Type'); expect(typeDef.name).toBe('Unknown Type');
}); });
it('can be installed', () => { it('can be installed', () => {
openmct.install(openmct.plugins.FaultManagement()); openmct.install(openmct.plugins.FaultManagement());
let typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition; const typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;
expect(typeDef.name).toBe('Fault Management'); expect(typeDef.name).toBe('Fault Management');
}); });
describe('once it is installed', () => {
beforeEach(() => {
openmct.install(openmct.plugins.FaultManagement());
});
it('provides a view for fault management types', () => {
const applicableViews = openmct.objectViews.get(faultDomainObject, []);
const faultManagementView = applicableViews.find(
(viewProvider) => viewProvider.key === FAULT_MANAGEMENT_VIEW
);
expect(applicableViews.length).toEqual(1);
expect(faultManagementView).toBeDefined();
});
it('provides an inspector view for fault management types', () => {
const faultDomainObjectSelection = [[
{
context: {
item: faultDomainObject
}
}
]];
const applicableInspectorViews = openmct.inspectorViews.get(faultDomainObjectSelection);
expect(applicableInspectorViews.length).toEqual(1);
});
it('creates a root object for fault management', async () => {
const root = await openmct.objects.getRoot();
const rootCompositionCollection = openmct.composition.get(root);
const rootComposition = await rootCompositionCollection.load();
const faultObject = rootComposition.find(obj => obj.identifier.namespace === FAULT_MANAGEMENT_NAMESPACE);
expect(faultObject).toBeDefined();
});
});
}); });

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

View File

@ -166,26 +166,15 @@
class="c-imagery__thumbs-scroll-area" class="c-imagery__thumbs-scroll-area"
@scroll="handleScroll" @scroll="handleScroll"
> >
<div <ImageThumbnail
v-for="(image, index) in imageHistory" v-for="(image, index) in imageHistory"
:key="image.url + image.time" :key="image.url + image.time"
class="c-imagery__thumb c-thumb" :image="image"
:class="{ selected: focusedImageIndex === index && isPaused }" :active="focusedImageIndex === index"
:title="image.formattedTime" :selected="focusedImageIndex === index && isPaused"
@click="thumbnailClicked(index)" :real-time="!isFixed"
> @click.native="thumbnailClicked(index)"
<a />
href=""
:download="image.imageDownloadName"
@click.prevent
>
<img
class="c-thumb__image"
:src="image.url"
>
</a>
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
</div>
</div> </div>
<button <button
@ -205,6 +194,7 @@ import moment from 'moment';
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry'; import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
import Compass from './Compass/Compass.vue'; import Compass from './Compass/Compass.vue';
import ImageControls from './ImageControls.vue'; import ImageControls from './ImageControls.vue';
import ImageThumbnail from './ImageThumbnail.vue';
import imageryData from "../../imagery/mixins/imageryData"; import imageryData from "../../imagery/mixins/imageryData";
const REFRESH_CSS_MS = 500; const REFRESH_CSS_MS = 500;
@ -229,9 +219,11 @@ const SHOW_THUMBS_THRESHOLD_HEIGHT = 200;
const SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT = 600; const SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT = 600;
export default { export default {
name: 'ImageryView',
components: { components: {
Compass, Compass,
ImageControls ImageControls,
ImageThumbnail
}, },
mixins: [imageryData], mixins: [imageryData],
inject: ['openmct', 'domainObject', 'objectPath', 'currentView', 'imageFreshnessOptions'], inject: ['openmct', 'domainObject', 'objectPath', 'currentView', 'imageFreshnessOptions'],
@ -254,6 +246,7 @@ export default {
visibleLayers: [], visibleLayers: [],
durationFormatter: undefined, durationFormatter: undefined,
imageHistory: [], imageHistory: [],
bounds: {},
timeSystem: timeSystem, timeSystem: timeSystem,
keyString: undefined, keyString: undefined,
autoScroll: true, autoScroll: true,
@ -526,20 +519,17 @@ export default {
}, },
watch: { watch: {
imageHistory: { imageHistory: {
handler(newHistory, oldHistory) { handler(newHistory, _oldHistory) {
const newSize = newHistory.length; const newSize = newHistory.length;
let imageIndex; let imageIndex = newSize > 0 ? newSize - 1 : undefined;
if (this.focusedImageTimestamp !== undefined) { if (this.focusedImageTimestamp !== undefined) {
const foundImageIndex = newHistory.findIndex(img => img.time === this.focusedImageTimestamp); const foundImageIndex = newHistory.findIndex(img => img.time === this.focusedImageTimestamp);
imageIndex = foundImageIndex > -1 if (foundImageIndex > -1) {
? foundImageIndex imageIndex = foundImageIndex;
: newSize - 1; }
} else {
imageIndex = newSize > 0
? newSize - 1
: undefined;
} }
this.setFocusedImage(imageIndex);
this.nextImageIndex = imageIndex; this.nextImageIndex = imageIndex;
if (this.previousFocusedImage && newHistory.length) { if (this.previousFocusedImage && newHistory.length) {
@ -569,6 +559,16 @@ export default {
this.resetAgeCSS(); this.resetAgeCSS();
this.updateRelatedTelemetryForFocusedImage(); this.updateRelatedTelemetryForFocusedImage();
this.getImageNaturalDimensions(); this.getImageNaturalDimensions();
},
bounds() {
this.scrollToFocused();
},
isFixed(newValue) {
const isRealTime = !newValue;
// if realtime unpause which will focus on latest image
if (isRealTime) {
this.paused(false);
}
} }
}, },
async mounted() { async mounted() {
@ -610,6 +610,7 @@ export default {
this.handleScroll = _.debounce(this.handleScroll, SCROLL_LATENCY); this.handleScroll = _.debounce(this.handleScroll, SCROLL_LATENCY);
this.handleThumbWindowResizeEnded = _.debounce(this.handleThumbWindowResizeEnded, SCROLL_LATENCY); this.handleThumbWindowResizeEnded = _.debounce(this.handleThumbWindowResizeEnded, SCROLL_LATENCY);
this.handleThumbWindowResizeStart = _.debounce(this.handleThumbWindowResizeStart, SCROLL_LATENCY); this.handleThumbWindowResizeStart = _.debounce(this.handleThumbWindowResizeStart, SCROLL_LATENCY);
this.scrollToFocused = _.debounce(this.scrollToFocused, 400);
if (this.$refs.thumbsWrapper) { if (this.$refs.thumbsWrapper) {
this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart); this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);
@ -845,7 +846,8 @@ export default {
if (domThumb) { if (domThumb) {
domThumb.scrollIntoView({ domThumb.scrollIntoView({
behavior: 'smooth', behavior: 'smooth',
block: 'center' block: 'center',
inline: 'center'
}); });
} }
}, },

View File

@ -258,13 +258,22 @@
min-width: $w; min-width: $w;
width: $w; width: $w;
&.active {
background: $colorSelectedBg;
color: $colorSelectedFg;
}
&:hover { &:hover {
background: $colorThumbHoverBg; background: $colorThumbHoverBg;
} }
&.selected { &.selected {
background: $colorPausedBg !important; // fixed time - selected bg will match active bg color
color: $colorPausedFg !important; background: $colorSelectedBg;
color: $colorSelectedFg;
&.real-time {
// real time - bg orange when selected
background: $colorPausedBg !important;
color: $colorPausedFg !important;
}
} }
&__image { &__image {

View File

@ -139,6 +139,7 @@ export default {
// forcibly reset the imageContainer size to prevent an aspect ratio distortion // forcibly reset the imageContainer size to prevent an aspect ratio distortion
delete this.imageContainerWidth; delete this.imageContainerWidth;
delete this.imageContainerHeight; delete this.imageContainerHeight;
this.bounds = bounds; // setting bounds for ImageryView watcher
}, },
timeSystemChange() { timeSystemChange() {
this.timeSystem = this.timeContext.timeSystem(); this.timeSystem = this.timeContext.timeSystem();

View File

@ -27,10 +27,13 @@ export default function MissingObjectInterceptor(openmct) {
}, },
invoke: (identifier, object) => { invoke: (identifier, object) => {
if (object === undefined) { if (object === undefined) {
const keyString = openmct.objects.makeKeyString(identifier);
openmct.notifications.error(`Failed to retrieve object ${keyString}`);
return { return {
identifier, identifier,
type: 'unknown', type: 'unknown',
name: 'Missing: ' + openmct.objects.makeKeyString(identifier) name: 'Missing: ' + keyString
}; };
} }

View File

@ -83,7 +83,6 @@ export default class LinkAction {
} }
] ]
}; };
this.openmct.forms.showForm(formStructure) this.openmct.forms.showForm(formStructure)
.then(this.onSave.bind(this)); .then(this.onSave.bind(this));
} }
@ -91,8 +90,8 @@ export default class LinkAction {
validate(currentParent) { validate(currentParent) {
return (data) => { return (data) => {
// default current parent to ROOT, if it's undefined, then it's a root level item // default current parent to ROOT, if it's null, then it's a root level item
if (currentParent === undefined) { if (!currentParent) {
currentParent = { currentParent = {
identifier: { identifier: {
key: 'ROOT', key: 'ROOT',
@ -101,24 +100,23 @@ export default class LinkAction {
}; };
} }
const parentCandidate = data.value[0]; const parentCandidatePath = data.value;
const currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier); const parentCandidate = parentCandidatePath[0];
const parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier);
const objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier); const objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);
if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) { if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) {
return false; return false;
} }
if (!parentCandidateKeystring || !currentParentKeystring) { // check if moving to same place
if (this.openmct.objects.areIdsEqual(parentCandidate.identifier, currentParent.identifier)) {
return false; return false;
} }
if (parentCandidateKeystring === currentParentKeystring) { // check if moving to a child
return false; if (parentCandidatePath.some(candidatePath => {
} return this.openmct.objects.areIdsEqual(candidatePath.identifier, this.object.identifier);
})) {
if (parentCandidateKeystring === objectKeystring) {
return false; return false;
} }

View File

@ -145,26 +145,24 @@ export default class MoveAction {
const parentCandidatePath = data.value; const parentCandidatePath = data.value;
const parentCandidate = parentCandidatePath[0]; const parentCandidate = parentCandidatePath[0];
// check if moving to same place
if (this.openmct.objects.areIdsEqual(parentCandidate.identifier, currentParent.identifier)) {
return false;
}
// check if moving to a child
if (parentCandidatePath.some(candidatePath => {
return this.openmct.objects.areIdsEqual(candidatePath.identifier, this.object.identifier);
})) {
return false;
}
if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) { if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) {
return false; return false;
} }
let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier);
let objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier); let objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);
if (!parentCandidateKeystring || !currentParentKeystring) {
return false;
}
if (parentCandidateKeystring === currentParentKeystring) {
return false;
}
if (parentCandidateKeystring === objectKeystring) {
return false;
}
const parentCandidateComposition = parentCandidate.composition; const parentCandidateComposition = parentCandidate.composition;
if (parentCandidateComposition && parentCandidateComposition.indexOf(objectKeystring) !== -1) { if (parentCandidateComposition && parentCandidateComposition.indexOf(objectKeystring) !== -1) {
return false; return false;

View File

@ -69,27 +69,27 @@ describe("the plugin", () => {
}); });
describe('adds an interceptor that returns a "My Items" model for', () => { describe('adds an interceptor that returns a "My Items" model for', () => {
let myItemsMissing; let myItemsObject;
let mockMissingProvider; let mockNotFoundProvider;
let activeProvider; let activeProvider;
beforeEach(async () => { beforeEach(async () => {
mockMissingProvider = { mockNotFoundProvider = {
get: () => Promise.resolve(missingObj), get: () => Promise.reject(new Error('Not found')),
create: () => Promise.resolve(missingObj), create: () => Promise.resolve(missingObj),
update: () => Promise.resolve(missingObj) update: () => Promise.resolve(missingObj)
}; };
activeProvider = mockMissingProvider; activeProvider = mockNotFoundProvider;
spyOn(openmct.objects, 'getProvider').and.returnValue(activeProvider); spyOn(openmct.objects, 'getProvider').and.returnValue(activeProvider);
myItemsMissing = await openmct.objects.get(myItemsIdentifier); myItemsObject = await openmct.objects.get(myItemsIdentifier);
}); });
it('missing objects', () => { it('missing objects', () => {
let idsMatchMissing = openmct.objects.areIdsEqual(myItemsMissing.identifier, myItemsIdentifier); let idsMatch = openmct.objects.areIdsEqual(myItemsObject.identifier, myItemsIdentifier);
expect(myItemsMissing).toBeDefined(); expect(myItemsObject).toBeDefined();
expect(idsMatchMissing).toBeTrue(); expect(idsMatch).toBeTrue();
}); });
}); });

View 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

View File

@ -1,52 +1,145 @@
# Introduction
These instructions are for setting up CouchDB for a **development** environment. For a production environment, we recommend running Open MCT behind a proxy server (e.g., Nginx or Apache), and securing the CouchDB server properly:
https://docs.couchdb.org/en/main/intro/security.html
# Installing CouchDB # Installing CouchDB
## macOS
### Installing with admin privileges to your computer ## Introduction
These instructions are for setting up CouchDB for a **development** environment. For a production environment, we recommend running Open MCT behind a proxy server (e.g., Nginx or Apache), and securing the CouchDB server properly:
<https://docs.couchdb.org/en/main/intro/security.html>
## Docker Quickstart
The following process is the preferred way of using CouchDB as it is automatic and closely resembles a production environment.
Requirement:
Get docker compose (or recent version of docker) installed on your machine. We recommend [Docker Desktop](https://www.docker.com/products/docker-desktop/)
1. Open a terminal to this current working directory (`cd openmct/src/plugins/persistence/couch`)
2. Create and start the `couchdb` container:
```sh
docker compose -f ./couchdb-compose.yaml up --detach
```
3. Copy `.env.ci` file to file named `.env.local`
4. (Optional) Change the values of `.env.local` if desired
5. Set the environment variables in bash by sourcing the env file
```sh
export $(cat .env.local | xargs)
```
6. Execute the configuration script:
```sh
sh ./setup-couchdb.sh
```
7. `cd` to the workspace root directory (the same directory as `index.html`)
8. Update `index.html` to use the CouchDB plugin as persistence store:
```sh
sh ./src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
```
9. ✅ Done!
Open MCT will now use your local CouchDB container as its persistence store. Access the CouchDB instance manager by visiting <http://localhost:5984/_utils>.
## macOS
While we highly recommend using the CouchDB docker-compose installation, it is still possible to install CouchDB through other means.
### Installing CouchDB
1. Install CouchDB using: `brew install couchdb`. 1. Install CouchDB using: `brew install couchdb`.
2. Edit `/usr/local/etc/local.ini` and add the following settings: 2. Edit `/usr/local/etc/local.ini` and add the following settings:
```
```txt
[admins] [admins]
admin = youradminpassword admin = youradminpassword
``` ```
And set the server up for single node: And set the server up for single node:
```
```txt
[couchdb] [couchdb]
single_node=true single_node=true
``` ```
Enable CORS Enable CORS
```
```txt
[chttpd] [chttpd]
enable_cors = true enable_cors = true
[cors] [cors]
origins = http://localhost:8080 origins = http://localhost:8080
``` ```
### Installing without admin privileges to your computer
1. Install CouchDB following these instructions: https://docs.brew.sh/Installation#untar-anywhere.
### Installing CouchDB without admin privileges to your computer
If `brew` is not available on your mac machine, you'll need to get the CouchDB installed using the official sourcefiles.
1. Install CouchDB following these instructions: <https://docs.brew.sh/Installation#untar-anywhere>.
1. Edit `local.ini` in Homebrew's `/etc/` directory as directed above in the 'Installing with admin privileges to your computer' section. 1. Edit `local.ini` in Homebrew's `/etc/` directory as directed above in the 'Installing with admin privileges to your computer' section.
## Other Operating Systems ## Other Operating Systems
Follow the installation instructions from the CouchDB installation guide: https://docs.couchdb.org/en/stable/install/index.html
Follow the installation instructions from the CouchDB installation guide: <https://docs.couchdb.org/en/stable/install/index.html>
# Configuring CouchDB # Configuring CouchDB
## Configuration script
The simplest way to config a CouchDB instance is to use our provided tooling:
1. Copy `.env.ci` file to file named `.env.local`
2. Set the environment variables in bash by sourcing the env file
```sh
export $(cat .env.local | xargs)
```
3. Execute the configuration script:
```sh
sh ./setup-couchdb.sh
```
## Manual Configuration
1. Start CouchDB by running: `couchdb`. 1. Start CouchDB by running: `couchdb`.
2. Add the `_global_changes` database using `curl` (note the `youradminpassword` should be changed to what you set above 👆): `curl -X PUT http://admin:youradminpassword@127.0.0.1:5984/_global_changes` 2. Add the `_global_changes` database using `curl` (note the `youradminpassword` should be changed to what you set above 👆): `curl -X PUT http://admin:youradminpassword@127.0.0.1:5984/_global_changes`
3. Navigate to http://localhost:5984/_utils 3. Navigate to <http://localhost:5984/_utils>
4. Create a database called `openmct` 4. Create a database called `openmct`
5. Navigate to http://127.0.0.1:5984/_utils/#/database/openmct/permissions 5. Navigate to <http://127.0.0.1:5984/_utils/#/database/openmct/permissions>
6. Remove permission restrictions in CouchDB from Open MCT by deleting `_admin` roles for both `Admin` and `Member`. 6. Remove permission restrictions in CouchDB from Open MCT by deleting `_admin` roles for both `Admin` and `Member`.
# Configuring Open MCT # Configuring Open MCT to use CouchDB
## Configuration script
The simplest way to config a CouchDB instance is to use our provided tooling:
1. `cd` to the workspace root directory (the same directory as `index.html`)
2. Update `index.html` to use the CouchDB plugin as persistence store:
```sh
sh ./src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
```
## Manual Configuration
1. Edit `openmct/index.html` comment out the following line: 1. Edit `openmct/index.html` comment out the following line:
```
openmct.install(openmct.plugins.LocalStorage()); ```js
``` openmct.install(openmct.plugins.LocalStorage());
Add a line to install the CouchDB plugin for Open MCT: ```
```
openmct.install(openmct.plugins.CouchDB("http://localhost:5984/openmct")); Add a line to install the CouchDB plugin for Open MCT:
```
2. Start Open MCT by running `npm start` in the `openmct` path. ```js
3. Navigate to http://localhost:8080/ and create a random object in Open MCT (e.g., a 'Clock') and save. You may get an error saying that the object failed to persist - this is a known error that you can ignore, and will only happen the first time you save - just try again. openmct.install(openmct.plugins.CouchDB("http://localhost:5984/openmct"));
4. Navigate to: http://127.0.0.1:5984/_utils/#database/openmct/_all_docs ```
5. Look at the 'JSON' tab and ensure you can see the specific object you created above.
6. All done! 🏆 # Validating a successful Installation
1. Start Open MCT by running `npm start` in the `openmct` path.
2. Navigate to <http://localhost:8080/> and create a random object in Open MCT (e.g., a 'Clock') and save. You may get an error saying that the object failed to persist - this is a known error that you can ignore, and will only happen the first time you save - just try again.
3. Navigate to: <http://127.0.0.1:5984/_utils/#database/openmct/_all_docs>
4. Look at the 'JSON' tab and ensure you can see the specific object you created above.
5. All done! 🏆

View 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:

View File

@ -0,0 +1,3 @@
#!/bin/bash -e
sed -i'.bak' -e 's/LocalStorage()/CouchDB("http:\/\/localhost:5984\/openmct")/g' index.html

View File

@ -0,0 +1,125 @@
#!/bin/bash -e
# Do a couple checks for environment variables we expect to have a value.
if [ -z "${OPENMCT_DATABASE_NAME}" ] ; then
echo "OPENMCT_DATABASE_NAME has no value" 1>&2
exit 1
fi
if [ -z "${COUCH_ADMIN_USER}" ] ; then
echo "COUCH_ADMIN_USER has no value" 1>&2
exit 1
fi
if [ -z "${COUCH_BASE_LOCAL}" ] ; then
echo "COUCH_BASE_LOCAL has no value" 1>&2
exit 1
fi
# Come up with what we'll be providing to curl's -u option. Always supply the username from the environment,
# and optionally supply the password from the environment, if it has a value.
CURL_USERPASS_ARG="${COUCH_ADMIN_USER}"
if [ "${COUCH_ADMIN_PASSWORD}" ] ; then
CURL_USERPASS_ARG+=":${COUCH_ADMIN_PASSWORD}"
fi
system_tables_exist () {
resource_exists $COUCH_BASE_LOCAL/_users
}
create_users_db () {
curl -su "${CURL_USERPASS_ARG}" -X PUT $COUCH_BASE_LOCAL/_users
}
create_replicator_db () {
curl -su "${CURL_USERPASS_ARG}" -X PUT $COUCH_BASE_LOCAL/_replicator
}
setup_system_tables () {
users_db_response=$(create_users_db)
if [ "{\"ok\":true}" == "${users_db_response}" ]; then
echo Successfully created users db
replicator_db_response=$(create_replicator_db)
if [ "{\"ok\":true}" == "${replicator_db_response}" ]; then
echo Successfully created replicator DB
else
echo Unable to create replicator DB
fi
else
echo Unable to create users db
fi
}
resource_exists () {
response=$(curl -u "${CURL_USERPASS_ARG}" -s -o /dev/null -I -w "%{http_code}" $1);
if [ "200" == "${response}" ]; then
echo "TRUE"
else
echo "FALSE";
fi
}
db_exists () {
resource_exists $COUCH_BASE_LOCAL/$OPENMCT_DATABASE_NAME
}
create_db () {
response=$(curl -su "${CURL_USERPASS_ARG}" -XPUT $COUCH_BASE_LOCAL/$OPENMCT_DATABASE_NAME);
echo $response
}
admin_user_exists () {
response=$(curl -su "${CURL_USERPASS_ARG}" -o /dev/null -I -w "%{http_code}" $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/admins/$COUCH_ADMIN_USER);
if [ "200" == "${response}" ]; then
echo "TRUE"
else
echo "FALSE";
fi
}
create_admin_user () {
echo Creating admin user
curl -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/admins/$COUCH_ADMIN_USER -d \'"$COUCH_ADMIN_PASSWORD"\'
}
if [ "$(admin_user_exists)" == "FALSE" ]; then
echo "Admin user does not exist, creating..."
create_admin_user
else
echo "Admin user exists"
fi
if [ "TRUE" == $(system_tables_exist) ]; then
echo System tables exist, skipping creation
else
echo Is fresh install, creating system tables
setup_system_tables
fi
if [ "FALSE" == $(db_exists) ]; then
response=$(create_db)
if [ "{\"ok\":true}" == "${response}" ]; then
echo Database successfully created
else
echo Database creation failed
fi
else
echo Database already exists, nothing to do
fi
echo "Updating _replicator database permissions"
response=$(curl -su "${CURL_USERPASS_ARG}" --location --request PUT $COUCH_BASE_LOCAL/_replicator/_security --header 'Content-Type: application/json' --data-raw '{ "admins": {"roles": []},"members": {"roles": []}}');
if [ "{\"ok\":true}" == "${response}" ]; then
echo "Database permissions successfully updated"
else
echo "Database permissions not updated"
fi
echo "Updating ${OPENMCT_DATABASE_NAME} database permissions"
response=$(curl -su "${CURL_USERPASS_ARG}" --location --request PUT $COUCH_BASE_LOCAL/$OPENMCT_DATABASE_NAME/_security --header 'Content-Type: application/json' --data-raw '{ "admins": {"roles": []},"members": {"roles": []}}');
if [ "{\"ok\":true}" == "${response}" ]; then
echo "Database permissions successfully updated"
else
echo "Database permissions not updated"
fi

View File

@ -32,7 +32,7 @@ define([
'./autoflow/AutoflowTabularPlugin', './autoflow/AutoflowTabularPlugin',
'./timeConductor/plugin', './timeConductor/plugin',
'../../example/imagery/plugin', '../../example/imagery/plugin',
'../../example/faultManagment/exampleFaultSource', '../../example/faultManagement/exampleFaultSource',
'./imagery/plugin', './imagery/plugin',
'./summaryWidget/plugin', './summaryWidget/plugin',
'./URLIndicatorPlugin/URLIndicatorPlugin', './URLIndicatorPlugin/URLIndicatorPlugin',

View File

@ -39,7 +39,7 @@
const DEFAULT_DURATION_FORMATTER = 'duration'; const DEFAULT_DURATION_FORMATTER = 'duration';
const LOCAL_STORAGE_HISTORY_KEY_FIXED = 'tcHistory'; const LOCAL_STORAGE_HISTORY_KEY_FIXED = 'tcHistory';
const LOCAL_STORAGE_HISTORY_KEY_REALTIME = 'tcHistoryRealtime'; const LOCAL_STORAGE_HISTORY_KEY_REALTIME = 'tcHistoryRealtime';
const DEFAULT_RECORDS = 10; const DEFAULT_RECORDS_LENGTH = 10;
import { millisecondsToDHMS } from "utils/duration"; import { millisecondsToDHMS } from "utils/duration";
import UTCTimeFormat from "../utcTimeSystem/UTCTimeFormat.js"; import UTCTimeFormat from "../utcTimeSystem/UTCTimeFormat.js";
@ -79,16 +79,14 @@ export default {
* @timespans {start, end} number representing timestamp * @timespans {start, end} number representing timestamp
*/ */
fixedHistory: {}, fixedHistory: {},
presets: [] presets: [],
isFixed: this.openmct.time.clock() === undefined
}; };
}, },
computed: { computed: {
currentHistory() { currentHistory() {
return this.mode + 'History'; return this.mode + 'History';
}, },
isFixed() {
return this.openmct.time.clock() === undefined;
},
historyForCurrentTimeSystem() { historyForCurrentTimeSystem() {
const history = this[this.currentHistory][this.timeSystem.key]; const history = this[this.currentHistory][this.timeSystem.key];
@ -96,7 +94,7 @@ export default {
}, },
storageKey() { storageKey() {
let key = LOCAL_STORAGE_HISTORY_KEY_FIXED; let key = LOCAL_STORAGE_HISTORY_KEY_FIXED;
if (this.mode !== 'fixed') { if (!this.isFixed) {
key = LOCAL_STORAGE_HISTORY_KEY_REALTIME; key = LOCAL_STORAGE_HISTORY_KEY_REALTIME;
} }
@ -108,6 +106,7 @@ export default {
handler() { handler() {
// only for fixed time since we track offsets for realtime // only for fixed time since we track offsets for realtime
if (this.isFixed) { if (this.isFixed) {
this.updateMode();
this.addTimespan(); this.addTimespan();
} }
}, },
@ -115,28 +114,35 @@ export default {
}, },
offsets: { offsets: {
handler() { handler() {
this.updateMode();
this.addTimespan(); this.addTimespan();
}, },
deep: true deep: true
}, },
timeSystem: { timeSystem: {
handler(ts) { handler(ts) {
this.updateMode();
this.loadConfiguration(); this.loadConfiguration();
this.addTimespan(); this.addTimespan();
}, },
deep: true deep: true
}, },
mode: function () { mode: function () {
this.getHistoryFromLocalStorage(); this.updateMode();
this.initializeHistoryIfNoHistory();
this.loadConfiguration(); this.loadConfiguration();
} }
}, },
mounted() { mounted() {
this.updateMode();
this.getHistoryFromLocalStorage(); this.getHistoryFromLocalStorage();
this.initializeHistoryIfNoHistory(); this.initializeHistoryIfNoHistory();
}, },
methods: { methods: {
updateMode() {
this.isFixed = this.openmct.time.clock() === undefined;
this.getHistoryFromLocalStorage();
this.initializeHistoryIfNoHistory();
},
getHistoryMenuItems() { getHistoryMenuItems() {
const history = this.historyForCurrentTimeSystem.map(timespan => { const history = this.historyForCurrentTimeSystem.map(timespan => {
let name; let name;
@ -203,8 +209,8 @@ export default {
currentHistory = currentHistory.filter(ts => !(ts.start === timespan.start && ts.end === timespan.end)); currentHistory = currentHistory.filter(ts => !(ts.start === timespan.start && ts.end === timespan.end));
currentHistory.unshift(timespan); // add to front currentHistory.unshift(timespan); // add to front
if (currentHistory.length > this.records) { if (currentHistory.length > this.MAX_RECORDS_LENGTH) {
currentHistory.length = this.records; currentHistory.length = this.MAX_RECORDS_LENGTH;
} }
this.$set(this[this.currentHistory], key, currentHistory); this.$set(this[this.currentHistory], key, currentHistory);
@ -231,7 +237,7 @@ export default {
.filter(option => option.timeSystem === this.timeSystem.key); .filter(option => option.timeSystem === this.timeSystem.key);
this.presets = this.loadPresets(configurations); this.presets = this.loadPresets(configurations);
this.records = this.loadRecords(configurations); this.MAX_RECORDS_LENGTH = this.loadRecords(configurations);
}, },
loadPresets(configurations) { loadPresets(configurations) {
const configuration = configurations.find(option => { const configuration = configurations.find(option => {
@ -243,9 +249,9 @@ export default {
}, },
loadRecords(configurations) { loadRecords(configurations) {
const configuration = configurations.find(option => option.records); const configuration = configurations.find(option => option.records);
const records = configuration ? configuration.records : DEFAULT_RECORDS; const maxRecordsLength = configuration ? configuration.records : DEFAULT_RECORDS_LENGTH;
return records; return maxRecordsLength;
}, },
formatTime(time) { formatTime(time) {
let format = this.timeSystem.timeFormat; let format = this.timeSystem.timeFormat;

View File

@ -131,15 +131,15 @@ describe('time conductor', () => {
describe('duration functions', () => { describe('duration functions', () => {
it('should transform milliseconds to DHMS', () => { it('should transform milliseconds to DHMS', () => {
const functionResults = [millisecondsToDHMS(0), millisecondsToDHMS(86400000), const functionResults = [millisecondsToDHMS(0), millisecondsToDHMS(86400000),
millisecondsToDHMS(129600000), millisecondsToDHMS(661824000)]; millisecondsToDHMS(129600000), millisecondsToDHMS(661824000), millisecondsToDHMS(213927028)];
const validResults = [' ', '+ 1d', '+ 1d 12h', '+ 7d 15h 50m 24s']; const validResults = [' ', '+ 1d', '+ 1d 12h', '+ 7d 15h 50m 24s', '+ 2d 11h 25m 27s 28ms'];
expect(validResults).toEqual(functionResults); expect(validResults).toEqual(functionResults);
}); });
it('should get precise duration', () => { it('should get precise duration', () => {
const functionResults = [getPreciseDuration(0), getPreciseDuration(643680000), const functionResults = [getPreciseDuration(0), getPreciseDuration(643680000),
getPreciseDuration(1605312000)]; getPreciseDuration(1605312000), getPreciseDuration(213927028)];
const validResults = ['00:00:00:00', '07:10:48:00', '18:13:55:12']; const validResults = ['00:00:00:00:000', '07:10:48:00:000', '18:13:55:12:000', '02:11:25:27:028'];
expect(validResults).toEqual(functionResults); expect(validResults).toEqual(functionResults);
}); });
}); });

View File

@ -188,7 +188,8 @@ export default {
if (domainObject.type === 'plan') { if (domainObject.type === 'plan') {
this.getPlanDataAndSetConfig({ this.getPlanDataAndSetConfig({
...this.domainObject, ...this.domainObject,
selectFile: domainObject.selectFile selectFile: domainObject.selectFile,
sourceMap: domainObject.sourceMap
}); });
} }
}, },

View File

@ -32,7 +32,7 @@
<div <div
v-if="canEdit" v-if="canEdit"
class="c-inspect-properties__hint span-all" class="c-inspect-properties__hint span-all"
>These settings are not previewed and will be applied after editing is completed.</div> >These settings don't affect the view while editing, but will be applied after editing is finished.</div>
<div <div
class="c-inspect-properties__label" class="c-inspect-properties__label"
title="Sort order of the timelist." title="Sort order of the timelist."

View File

@ -33,28 +33,16 @@ export default function () {
description: 'A configurable, time-ordered list view of activities for a compatible mission plan file.', description: 'A configurable, time-ordered list view of activities for a compatible mission plan file.',
creatable: true, creatable: true,
cssClass: 'icon-timelist', cssClass: 'icon-timelist',
form: [
{
name: 'Upload Plan (JSON File)',
key: 'selectFile',
control: 'file-input',
text: 'Select File...',
type: 'application/json',
property: [
"selectFile"
]
}
],
initialize: function (domainObject) { initialize: function (domainObject) {
domainObject.configuration = { domainObject.configuration = {
sortOrderIndex: 0, sortOrderIndex: 0,
futureEventsIndex: 0, futureEventsIndex: 1,
futureEventsDurationIndex: 0, futureEventsDurationIndex: 0,
futureEventsDuration: 20, futureEventsDuration: 20,
currentEventsIndex: 1, currentEventsIndex: 1,
currentEventsDurationIndex: 0, currentEventsDurationIndex: 0,
currentEventsDuration: 20, currentEventsDuration: 20,
pastEventsIndex: 0, pastEventsIndex: 1,
pastEventsDurationIndex: 0, pastEventsDurationIndex: 0,
pastEventsDuration: 20, pastEventsDuration: 20,
filter: '' filter: ''

View File

@ -95,14 +95,12 @@ describe('the plugin', function () {
originalRouterPath = openmct.router.path; originalRouterPath = openmct.router.path;
mockComposition = new EventEmitter(); mockComposition = new EventEmitter();
mockComposition.load = () => { // eslint-disable-next-line require-await
mockComposition.emit('add', planObject); mockComposition.load = async () => {
return [planObject];
return Promise.resolve([planObject]);
}; };
spyOn(openmct.composition, 'get').and.returnValue(mockComposition); spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
openmct.on('start', done); openmct.on('start', done);
openmct.start(appHolder); openmct.start(appHolder);
}); });
@ -268,6 +266,8 @@ describe('the plugin', function () {
}); });
it('loads the plan from composition', () => { it('loads the plan from composition', () => {
mockComposition.emit('add', planObject);
return Vue.nextTick(() => { return Vue.nextTick(() => {
const items = element.querySelectorAll(LIST_ITEM_CLASS); const items = element.querySelectorAll(LIST_ITEM_CLASS);
expect(items.length).toEqual(2); expect(items.length).toEqual(2);
@ -319,6 +319,8 @@ describe('the plugin', function () {
}); });
it('activities', () => { it('activities', () => {
mockComposition.emit('add', planObject);
return Vue.nextTick(() => { return Vue.nextTick(() => {
const items = element.querySelectorAll(LIST_ITEM_CLASS); const items = element.querySelectorAll(LIST_ITEM_CLASS);
expect(items.length).toEqual(1); expect(items.length).toEqual(1);
@ -370,6 +372,8 @@ describe('the plugin', function () {
}); });
it('hides past events', () => { it('hides past events', () => {
mockComposition.emit('add', planObject);
return Vue.nextTick(() => { return Vue.nextTick(() => {
const items = element.querySelectorAll(LIST_ITEM_CLASS); const items = element.querySelectorAll(LIST_ITEM_CLASS);
expect(items.length).toEqual(1); expect(items.length).toEqual(1);

View File

@ -32,6 +32,12 @@
.c-list-item { .c-list-item {
/* Time Lists */ /* Time Lists */
td {
$p: $interiorMarginSm;
padding-top: $p;
padding-bottom: $p;
}
&.--is-current { &.--is-current {
background-color: $colorCurrentBg; background-color: $colorCurrentBg;
border-top: 1px solid $colorCurrentBorder !important; border-top: 1px solid $colorCurrentBorder !important;

View File

@ -25,13 +25,14 @@
/******************************************************** CONTROL-SPECIFIC MIXINS */ /******************************************************** CONTROL-SPECIFIC MIXINS */
@mixin menuOuter() { @mixin menuOuter() {
border-radius: $basicCr; border-radius: $basicCr;
box-shadow: $shdwMenuInner, $shdwMenu; box-shadow: $shdwMenu;
@if $shdwMenuInner != none {
box-shadow: $shdwMenuInner, $shdwMenu;
}
background: $colorMenuBg; background: $colorMenuBg;
color: $colorMenuFg; color: $colorMenuFg;
//filter: $filterMenu; // 2022: causing all kinds of weird visual bugs in Chrome
text-shadow: $shdwMenuText; text-shadow: $shdwMenuText;
padding: $interiorMarginSm; padding: $interiorMarginSm;
//box-shadow: $shdwMenu;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: absolute; position: absolute;
@ -60,14 +61,13 @@
cursor: pointer; cursor: pointer;
display: flex; display: flex;
padding: nth($menuItemPad, 1) nth($menuItemPad, 2); padding: nth($menuItemPad, 1) nth($menuItemPad, 2);
transition: $transIn;
white-space: nowrap; white-space: nowrap;
@include hover { @include hover {
background: $colorMenuHovBg; background: $colorMenuHovBg;
color: $colorMenuHovFg; color: $colorMenuHovFg;
&:before { &:before {
color: $colorMenuHovIc; color: $colorMenuHovIc !important;
} }
} }

View File

@ -53,6 +53,7 @@
type="horizontal" type="horizontal"
> >
<pane <pane
id="tree-pane"
class="l-shell__pane-tree" class="l-shell__pane-tree"
handle="after" handle="after"
label="Browse" label="Browse"

View File

@ -41,6 +41,8 @@
<div <div
ref="mainTree" ref="mainTree"
class="c-tree-and-search__tree c-tree" class="c-tree-and-search__tree c-tree"
role="tree"
aria-expanded="true"
> >
<div> <div>
@ -467,7 +469,7 @@ export default {
} }
}, },
scrollEndEvent() { scrollEndEvent() {
if (!this.$refs.srcrollable) { if (!this.$refs.scrollable) {
return; return;
} }
@ -576,14 +578,17 @@ export default {
}; };
}, },
addTreeItemObserver(domainObject, parentObjectPath) { addTreeItemObserver(domainObject, parentObjectPath) {
if (this.observers[domainObject.identifier.key]) { const objectPath = [domainObject].concat(parentObjectPath);
this.observers[domainObject.identifier.key](); const navigationPath = this.buildNavigationPath(objectPath);
if (this.observers[navigationPath]) {
this.observers[navigationPath]();
} }
this.observers[domainObject.identifier.key] = this.openmct.objects.observe( this.observers[navigationPath] = this.openmct.objects.observe(
domainObject, domainObject,
'name', 'name',
this.updateTreeItems.bind(this, parentObjectPath) this.sortTreeItems.bind(this, parentObjectPath)
); );
}, },
async updateTreeItems(parentObjectPath) { async updateTreeItems(parentObjectPath) {
@ -610,6 +615,44 @@ export default {
} }
} }
}, },
sortTreeItems(parentObjectPath) {
const navigationPath = this.buildNavigationPath(parentObjectPath);
const parentItem = this.getTreeItemByPath(navigationPath);
// If the parent is not sortable, skip sorting
if (!this.isSortable(parentObjectPath)) {
return;
}
// Sort the renamed object and its siblings (direct descendants of the parent)
const directDescendants = this.getChildrenInTreeFor(parentItem, false);
directDescendants.sort(this.sortNameAscending);
// Take a copy of the sorted descendants array
const sortedTreeItems = directDescendants.slice();
directDescendants.forEach(descendant => {
const parent = this.getTreeItemByPath(descendant.navigationPath);
// If descendant is not open, skip
if (!this.isTreeItemOpen(parent)) {
return;
}
// If descendant is open but has no children, skip
const children = this.getChildrenInTreeFor(parent, true);
if (children.length === 0) {
return;
}
// Splice in the children of the descendant
const parentIndex = sortedTreeItems.map(item => item.navigationPath).indexOf(parent.navigationPath);
sortedTreeItems.splice(parentIndex + 1, 0, ...children);
});
// Splice in all of the sorted descendants
this.treeItems.splice(this.treeItems.indexOf(parentItem) + 1, sortedTreeItems.length, ...sortedTreeItems);
},
buildNavigationPath(objectPath) { buildNavigationPath(objectPath) {
return '/browse/' + [...objectPath].reverse() return '/browse/' + [...objectPath].reverse()
.map((object) => this.openmct.objects.makeKeyString(object.identifier)) .map((object) => this.openmct.objects.makeKeyString(object.identifier))

View File

@ -77,7 +77,6 @@ export default {
} }
this.searchValue = value; this.searchValue = value;
this.searchLoading = true;
// clear any previous search results // clear any previous search results
this.annotationSearchResults = []; this.annotationSearchResults = [];
this.objectSearchResults = []; this.objectSearchResults = [];
@ -85,8 +84,13 @@ export default {
if (this.searchValue) { if (this.searchValue) {
await this.getSearchResults(); await this.getSearchResults();
} else { } else {
this.searchLoading = false; const dropdownOptions = {
this.$refs.searchResultsDropDown.showResults(this.annotationSearchResults, this.objectSearchResults); searchLoading: this.searchLoading,
searchValue: this.searchValue,
annotationSearchResults: this.annotationSearchResults,
objectSearchResults: this.objectSearchResults
};
this.$refs.searchResultsDropDown.showResults(dropdownOptions);
} }
}, },
getPathsForObjects(objectsNeedingPaths) { getPathsForObjects(objectsNeedingPaths) {
@ -103,6 +107,8 @@ export default {
async getSearchResults() { async getSearchResults() {
// an abort controller will be passed in that will be used // an abort controller will be passed in that will be used
// to cancel an active searches if necessary // to cancel an active searches if necessary
this.searchLoading = true;
this.$refs.searchResultsDropDown.showSearchStarted();
this.abortSearchController = new AbortController(); this.abortSearchController = new AbortController();
const abortSignal = this.abortSearchController.signal; const abortSignal = this.abortSearchController.signal;
try { try {
@ -110,10 +116,15 @@ export default {
const fullObjectSearchResults = await Promise.all(this.openmct.objects.search(this.searchValue, abortSignal)); const fullObjectSearchResults = await Promise.all(this.openmct.objects.search(this.searchValue, abortSignal));
const aggregatedObjectSearchResults = fullObjectSearchResults.flat(); const aggregatedObjectSearchResults = fullObjectSearchResults.flat();
const aggregatedObjectSearchResultsWithPaths = await this.getPathsForObjects(aggregatedObjectSearchResults); const aggregatedObjectSearchResultsWithPaths = await this.getPathsForObjects(aggregatedObjectSearchResults);
const filterAnnotations = aggregatedObjectSearchResultsWithPaths.filter(result => { const filterAnnotationsAndValidPaths = aggregatedObjectSearchResultsWithPaths.filter(result => {
return result.type !== 'annotation'; if (this.openmct.annotation.isAnnotation(result)) {
return false;
}
return this.openmct.objects.isReachable(result?.originalPath);
}); });
this.objectSearchResults = filterAnnotations; this.objectSearchResults = filterAnnotationsAndValidPaths;
this.searchLoading = false;
this.showSearchResults(); this.showSearchResults();
} catch (error) { } catch (error) {
console.error(`😞 Error searching`, error); console.error(`😞 Error searching`, error);
@ -125,7 +136,13 @@ export default {
} }
}, },
showSearchResults() { showSearchResults() {
this.$refs.searchResultsDropDown.showResults(this.annotationSearchResults, this.objectSearchResults); const dropdownOptions = {
searchLoading: this.searchLoading,
searchValue: this.searchValue,
annotationSearchResults: this.annotationSearchResults,
objectSearchResults: this.objectSearchResults
};
this.$refs.searchResultsDropDown.showResults(dropdownOptions);
document.body.addEventListener('click', this.handleOutsideClick); document.body.addEventListener('click', this.handleOutsideClick);
}, },
handleOutsideClick(event) { handleOutsideClick(event) {

View File

@ -39,7 +39,11 @@ describe("GrandSearch", () => {
let mockAnnotationObject; let mockAnnotationObject;
let mockDisplayLayout; let mockDisplayLayout;
let mockFolderObject; let mockFolderObject;
let mockAnotherFolderObject;
let mockTopObject;
let originalRouterPath; let originalRouterPath;
let mockNewObject;
let mockObjectProvider;
beforeEach((done) => { beforeEach((done) => {
openmct = createOpenMct(); openmct = createOpenMct();
@ -53,6 +57,7 @@ describe("GrandSearch", () => {
mockDomainObject = { mockDomainObject = {
type: 'notebook', type: 'notebook',
name: 'fooRabbitNotebook', name: 'fooRabbitNotebook',
location: 'fooNameSpace:topObject',
identifier: { identifier: {
key: 'some-object', key: 'some-object',
namespace: 'fooNameSpace' namespace: 'fooNameSpace'
@ -70,17 +75,39 @@ describe("GrandSearch", () => {
} }
} }
}; };
mockTopObject = {
type: 'root',
name: 'Top Folder',
composition: [],
identifier: {
key: 'topObject',
namespace: 'fooNameSpace'
}
};
mockAnotherFolderObject = {
type: 'folder',
name: 'Another Test Folder',
composition: [],
location: 'fooNameSpace:topObject',
identifier: {
key: 'someParent',
namespace: 'fooNameSpace'
}
};
mockFolderObject = { mockFolderObject = {
type: 'folder', type: 'folder',
name: 'Test Folder', name: 'Test Folder',
composition: [],
location: 'fooNameSpace:someParent',
identifier: { identifier: {
key: 'some-folder', key: 'someFolder',
namespace: 'fooNameSpace' namespace: 'fooNameSpace'
} }
}; };
mockDisplayLayout = { mockDisplayLayout = {
type: 'layout', type: 'layout',
name: 'Bar Layout', name: 'Bar Layout',
composition: [],
identifier: { identifier: {
key: 'some-layout', key: 'some-layout',
namespace: 'fooNameSpace' namespace: 'fooNameSpace'
@ -105,9 +132,19 @@ describe("GrandSearch", () => {
} }
} }
}; };
mockNewObject = {
type: 'folder',
name: 'New Apple Test Folder',
composition: [],
location: 'fooNameSpace:topObject',
identifier: {
key: 'newApple',
namespace: 'fooNameSpace'
}
};
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(false); openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(false);
const mockObjectProvider = jasmine.createSpyObj("mock object provider", [ mockObjectProvider = jasmine.createSpyObj("mock object provider", [
"create", "create",
"update", "update",
"get" "get"
@ -122,6 +159,12 @@ describe("GrandSearch", () => {
return mockDisplayLayout; return mockDisplayLayout;
} else if (identifier.key === mockFolderObject.identifier.key) { } else if (identifier.key === mockFolderObject.identifier.key) {
return mockFolderObject; return mockFolderObject;
} else if (identifier.key === mockAnotherFolderObject.identifier.key) {
return mockAnotherFolderObject;
} else if (identifier.key === mockTopObject.identifier.key) {
return mockTopObject;
} else if (identifier.key === mockNewObject.identifier.key) {
return mockNewObject;
} else { } else {
return null; return null;
} }
@ -144,6 +187,7 @@ describe("GrandSearch", () => {
// use local worker // use local worker
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker; sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
openmct.objects.inMemorySearchProvider.worker = null; openmct.objects.inMemorySearchProvider.worker = null;
await openmct.objects.inMemorySearchProvider.index(mockTopObject);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject); await openmct.objects.inMemorySearchProvider.index(mockDomainObject);
await openmct.objects.inMemorySearchProvider.index(mockDisplayLayout); await openmct.objects.inMemorySearchProvider.index(mockDisplayLayout);
await openmct.objects.inMemorySearchProvider.index(mockFolderObject); await openmct.objects.inMemorySearchProvider.index(mockFolderObject);
@ -172,6 +216,7 @@ describe("GrandSearch", () => {
openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore; openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;
openmct.router.path = originalRouterPath; openmct.router.path = originalRouterPath;
grandSearchComponent.$destroy(); grandSearchComponent.$destroy();
document.body.removeChild(parent);
return resetApplicationState(openmct); return resetApplicationState(openmct);
}); });
@ -179,25 +224,62 @@ describe("GrandSearch", () => {
it("should render an object search result", async () => { it("should render an object search result", async () => {
await grandSearchComponent.$children[0].searchEverything('foo'); await grandSearchComponent.$children[0].searchEverything('foo');
await Vue.nextTick(); await Vue.nextTick();
const searchResult = document.querySelector('[aria-label="fooRabbitNotebook notebook result"]'); const searchResults = document.querySelectorAll('[aria-label="fooRabbitNotebook notebook result"]');
expect(searchResult).toBeDefined(); expect(searchResults.length).toBe(1);
expect(searchResults[0].innerText).toContain('Rabbit');
});
it("should render an object search result if new object added", async () => {
const composition = openmct.composition.get(mockFolderObject);
composition.add(mockNewObject);
await grandSearchComponent.$children[0].searchEverything('apple');
await Vue.nextTick();
const searchResults = document.querySelectorAll('[aria-label="New Apple Test Folder folder result"]');
expect(searchResults.length).toBe(1);
expect(searchResults[0].innerText).toContain('Apple');
});
it("should not use InMemorySearch provider if object provider provides search", async () => {
// eslint-disable-next-line require-await
mockObjectProvider.search = async (query, abortSignal, searchType) => {
if (searchType === openmct.objects.SEARCH_TYPES.OBJECTS) {
return mockNewObject;
} else {
return [];
}
};
mockObjectProvider.supportsSearchType = (someType) => {
return true;
};
const composition = openmct.composition.get(mockFolderObject);
composition.add(mockNewObject);
await grandSearchComponent.$children[0].searchEverything('apple');
await Vue.nextTick();
const searchResults = document.querySelectorAll('[aria-label="New Apple Test Folder folder result"]');
// This will be of length 2 (doubles) if we're incorrectly searching with InMemorySearchProvider as well
expect(searchResults.length).toBe(1);
expect(searchResults[0].innerText).toContain('Apple');
}); });
it("should render an annotation search result", async () => { it("should render an annotation search result", async () => {
await grandSearchComponent.$children[0].searchEverything('S'); await grandSearchComponent.$children[0].searchEverything('S');
await Vue.nextTick(); await Vue.nextTick();
const annotationResult = document.querySelector('[aria-label="Search Result"]'); const annotationResults = document.querySelectorAll('[aria-label="Search Result"]');
expect(annotationResult).toBeDefined(); expect(annotationResults.length).toBe(2);
expect(annotationResults[1].innerText).toContain('Driving');
}); });
it("should preview object search results in edit mode if object clicked", async () => { it("should preview object search results in edit mode if object clicked", async () => {
await grandSearchComponent.$children[0].searchEverything('Folder'); await grandSearchComponent.$children[0].searchEverything('Folder');
grandSearchComponent._provided.openmct.router.path = [mockDisplayLayout]; grandSearchComponent._provided.openmct.router.path = [mockDisplayLayout];
await Vue.nextTick(); await Vue.nextTick();
const searchResult = document.querySelector('[name="Test Folder"]'); const searchResults = document.querySelectorAll('[name="Test Folder"]');
expect(searchResult).toBeDefined(); expect(searchResults.length).toBe(1);
searchResult.click(); expect(searchResults[0].innerText).toContain('Folder');
searchResults[0].click();
const previewWindow = document.querySelector('.js-preview-window'); const previewWindow = document.querySelector('.js-preview-window');
expect(previewWindow).toBeDefined(); expect(previewWindow.innerText).toContain('Snapshot');
}); });
}); });

View File

@ -22,8 +22,6 @@
<template> <template>
<div <div
v-if="(annotationResults && annotationResults.length) ||
(objectResults && objectResults.length)"
class="c-gsearch__dropdown" class="c-gsearch__dropdown"
> >
<div <div
@ -58,25 +56,40 @@
@click.native="selectedResult" @click.native="selectedResult"
/> />
</div> </div>
<div
v-if="searchLoading"
> <progress-bar
:model="{progressText: 'Searching...',
progressPerc: undefined
}"
/>
</div>
<div
v-if="!searchLoading && (!annotationResults || !annotationResults.length) &&
(!objectResults || !objectResults.length)"
>No matching results.
</div>
</div> </div>
</div> </div>
</div> </div></template>
</template>
<script> <script>
import AnnotationSearchResult from './AnnotationSearchResult.vue'; import AnnotationSearchResult from './AnnotationSearchResult.vue';
import ObjectSearchResult from './ObjectSearchResult.vue'; import ObjectSearchResult from './ObjectSearchResult.vue';
import ProgressBar from '@/ui/components/ProgressBar.vue';
export default { export default {
name: 'SearchResultsDropDown', name: 'SearchResultsDropDown',
components: { components: {
AnnotationSearchResult, AnnotationSearchResult,
ObjectSearchResult ObjectSearchResult,
ProgressBar
}, },
inject: ['openmct'], inject: ['openmct'],
data() { data() {
return { return {
resultsShown: false, resultsShown: false,
searchLoading: false,
annotationResults: [], annotationResults: [],
objectResults: [], objectResults: [],
previewVisible: false previewVisible: false
@ -91,12 +104,18 @@ export default {
previewChanged(changedPreviewState) { previewChanged(changedPreviewState) {
this.previewVisible = changedPreviewState; this.previewVisible = changedPreviewState;
}, },
showResults(passedAnnotationResults, passedObjectResults) { showSearchStarted() {
if ((passedAnnotationResults && passedAnnotationResults.length) this.searchLoading = true;
|| (passedObjectResults && passedObjectResults.length)) { this.resultsShown = true;
this.annotationResults = [];
this.objectResults = [];
},
showResults({searchLoading, searchValue, annotationSearchResults, objectSearchResults}) {
this.searchLoading = searchLoading;
this.annotationResults = annotationSearchResults;
this.objectResults = objectSearchResults;
if (searchValue?.length) {
this.resultsShown = true; this.resultsShown = true;
this.annotationResults = passedAnnotationResults;
this.objectResults = passedObjectResults;
} else { } else {
this.resultsShown = false; this.resultsShown = false;
} }

View File

@ -1,7 +1,9 @@
<template> <template>
<div <div
:style="treeItemStyles"
class="c-tree__item-h" class="c-tree__item-h"
role="treeitem"
:style="treeItemStyles"
:aria-expanded="(!activeSearch && hasComposition) ? (isOpen || isLoading) ? 'true' : 'false' : undefined"
> >
<div <div
class="c-tree__item" class="c-tree__item"

View File

@ -32,8 +32,16 @@ function normalizeAge(num) {
return isWhole ? hundredtized / 100 : num; return isWhole ? hundredtized / 100 : num;
} }
function padLeadingZeros(num, numOfLeadingZeros) {
return num.toString().padStart(numOfLeadingZeros, '0');
}
function toDoubleDigits(num) { function toDoubleDigits(num) {
return num >= 10 ? num : `0${num}`; return padLeadingZeros(num, 2);
}
function toTripleDigits(num) {
return padLeadingZeros(num, 3);
} }
function addTimeSuffix(value, suffix) { function addTimeSuffix(value, suffix) {
@ -46,7 +54,8 @@ export function millisecondsToDHMS(numericDuration) {
addTimeSuffix(Math.floor(normalizeAge(ms / ONE_DAY)), 'd'), addTimeSuffix(Math.floor(normalizeAge(ms / ONE_DAY)), 'd'),
addTimeSuffix(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR)), 'h'), addTimeSuffix(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR)), 'h'),
addTimeSuffix(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE)), 'm'), addTimeSuffix(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE)), 'm'),
addTimeSuffix(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)), 's') addTimeSuffix(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)), 's'),
addTimeSuffix(Math.floor(normalizeAge(ms % ONE_SECOND)), "ms")
].filter(Boolean).join(' '); ].filter(Boolean).join(' ');
return `${ dhms ? '+' : ''} ${dhms}`; return `${ dhms ? '+' : ''} ${dhms}`;
@ -59,7 +68,8 @@ export function getPreciseDuration(value) {
toDoubleDigits(Math.floor(normalizeAge(ms / ONE_DAY))), toDoubleDigits(Math.floor(normalizeAge(ms / ONE_DAY))),
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR))), toDoubleDigits(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR))),
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE))), toDoubleDigits(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE))),
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND))) toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND))),
toTripleDigits(Math.floor(normalizeAge(ms % ONE_SECOND)))
].join(":"); ].join(":");
} }