Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nasa/openmct.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShefali Joshi <simplyrender@gmail.com>2022-08-24 21:08:17 +0300
committerGitHub <noreply@github.com>2022-08-24 21:08:17 +0300
commit90662ce4a77f31774c39289e85cdf36285843e5e (patch)
tree4a232c9f7ec339c07ec2edaae921e95af2024664
parent84c1526f5eb2031b925144d11054aff1ace1fded (diff)
Merge `release/2.0.8` into `master` (#5709)
* 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> * [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 * Update package.json (#5601) * Fix menu style in Snow theme (#5557) * Include the plan source map when generating the time list/plan hybrid object (#5604) * 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 * 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 * 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> * Revert "Have in-memory search indexer use composition API (#5578)" (#5609) This reverts commit 7cf11e177c6c48093a6b37902ba3dfb36414ff10. * [e2e] Tests for Display Layout and LAD Tables and telemetry (#5607) * 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 * Update version number * 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> * [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> * [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> * 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> * Mct5549 fix indexer composition error (#5610) * [Display Layout] Composition and configuration sync (#5669) LGTM * [e2e] Stabilize notebook tag tests (#5681) * Use more deterministic selector * Hover first to "slow down" e2e actions while in headless mode * Moves condition set fix into 2.0.8 (#5673) * 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 * 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> * [CI] Enable couchdb e2e testing in open source (#5655) * 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> * [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> * [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> * 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> * Don't re-request historical data on ticks (#5701) Don't rerequest telemetry on ticks. * Fix duplicate declaration from merge Co-authored-by: Michael Rogers <contact@mhrogers.com> Co-authored-by: Khalid Adil <khalidadil29@gmail.com> Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com> Co-authored-by: John Hill <john.c.hill@nasa.gov> Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com> Co-authored-by: Andrew Henry <akhenry@gmail.com> Co-authored-by: Scott Bell <scott@traclabs.com> Co-authored-by: Alize Nguyen <alizenguyen@gmail.com> Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov> Co-authored-by: Vitor Henckel <vitor@henckel.com.br> Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
-rw-r--r--.github/workflows/e2e-couchdb.yml37
-rw-r--r--e2e/appActions.js19
-rw-r--r--e2e/helper/addInitExampleFaultProvider.js28
-rw-r--r--e2e/helper/addInitExampleFaultProviderStatic.js30
-rw-r--r--e2e/helper/addInitFaultManagementPlugin.js28
-rw-r--r--e2e/helper/faultUtils.js277
-rw-r--r--e2e/tests/functional/couchdb.e2e.spec.js108
-rw-r--r--e2e/tests/functional/moveAndLinkObjects.e2e.spec.js212
-rw-r--r--e2e/tests/functional/moveObjects.e2e.spec.js148
-rw-r--r--e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js64
-rw-r--r--e2e/tests/functional/plugins/faultManagement/faultManagement.e2e.spec.js237
-rw-r--r--e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js36
-rw-r--r--e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js27
-rw-r--r--e2e/tests/functional/plugins/notebook/tags.e2e.spec.js19
-rw-r--r--e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-darwinbin18929 -> 16116 bytes
-rw-r--r--e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-linuxbin18524 -> 15770 bytes
-rw-r--r--e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-darwinbin21763 -> 18406 bytes
-rw-r--r--e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-linuxbin21375 -> 18071 bytes
-rw-r--r--e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js20
-rw-r--r--e2e/tests/functional/plugins/timer/timer.e2e.spec.js17
-rw-r--r--e2e/tests/functional/search.e2e.spec.js13
-rw-r--r--e2e/tests/functional/tree.e2e.spec.js138
-rw-r--r--e2e/tests/visual/components/tree.visual.spec.js101
-rw-r--r--e2e/tests/visual/faultManagement.visual.spec.js78
-rw-r--r--example/faultManagement/exampleFaultSource.js (renamed from example/faultManagment/exampleFaultSource.js)47
-rw-r--r--example/faultManagement/pluginSpec.js (renamed from example/faultManagment/pluginSpec.js)0
-rw-r--r--example/faultManagement/utils.js76
-rw-r--r--package.json5
-rw-r--r--src/api/objects/InMemorySearchProvider.js60
-rw-r--r--src/api/objects/ObjectAPI.js7
-rw-r--r--src/plugins/charts/scatter/ScatterPlotView.vue9
-rw-r--r--src/plugins/displayLayout/components/DisplayLayout.vue14
-rw-r--r--src/plugins/displayLayout/components/TelemetryView.vue8
-rw-r--r--src/plugins/displayLayout/pluginSpec.js54
-rw-r--r--src/plugins/faultManagement/FaultManagementListView.vue28
-rw-r--r--src/plugins/faultManagement/pluginSpec.js57
-rw-r--r--src/plugins/imagery/components/ImageryView.vue15
-rw-r--r--src/plugins/interceptors/missingObjectInterceptor.js5
-rw-r--r--src/plugins/linkAction/LinkAction.js22
-rw-r--r--src/plugins/move/MoveAction.js18
-rw-r--r--src/plugins/myItems/pluginSpec.js18
-rw-r--r--src/plugins/persistence/couch/.env.ci5
-rw-r--r--src/plugins/persistence/couch/README.md145
-rw-r--r--src/plugins/persistence/couch/couchdb-compose.yaml14
-rw-r--r--src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh3
-rw-r--r--src/plugins/persistence/couch/setup-couchdb.sh125
-rw-r--r--src/plugins/plugins.js2
-rw-r--r--src/plugins/timeConductor/ConductorHistory.vue32
-rw-r--r--src/plugins/timelist/inspector/TimelistPropertiesView.vue2
-rw-r--r--src/plugins/timelist/plugin.js16
-rw-r--r--src/plugins/timelist/pluginSpec.js14
-rw-r--r--src/plugins/timelist/timelist.scss6
-rw-r--r--src/ui/layout/Layout.vue1
-rw-r--r--src/ui/layout/mct-tree.vue53
-rw-r--r--src/ui/layout/search/GrandSearchSpec.js76
-rw-r--r--src/ui/layout/tree-item.vue4
56 files changed, 2178 insertions, 400 deletions
diff --git a/.github/workflows/e2e-couchdb.yml b/.github/workflows/e2e-couchdb.yml
new file mode 100644
index 000000000..c6ecf3eab
--- /dev/null
+++ b/.github/workflows/e2e-couchdb.yml
@@ -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
diff --git a/e2e/appActions.js b/e2e/appActions.js
index e4fbf7589..c7a4428d5 100644
--- a/e2e/appActions.js
+++ b/e2e/appActions.js
@@ -102,20 +102,15 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
/**
* Open the given `domainObject`'s context menu from the object tree.
-* Expands the 'My Items' folder if it is not already expanded.
+* Expands the path to the object and scrolls to it if necessary.
*
* @param {import('@playwright/test').Page} page
-* @param {string} myItemsFolderName the name of the "My Items" folder
-* @param {string} domainObjectName the display name of the `domainObject`
+* @param {string} url the url to the object
*/
-async function openObjectTreeContextMenu(page, myItemsFolderName, domainObjectName) {
- const myItemsFolder = page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3);
- const className = await myItemsFolder.getAttribute('class');
- if (!className.includes('c-disclosure-triangle--expanded')) {
- await myItemsFolder.click();
- }
-
- await page.locator(`a:has-text("${domainObjectName}")`).click({
+async function openObjectTreeContextMenu(page, url) {
+ await page.goto(url);
+ await page.click('button[title="Show selected item in tree"]');
+ await page.locator('.is-navigated-object').click({
button: 'right'
});
}
@@ -129,7 +124,7 @@ async function openObjectTreeContextMenu(page, myItemsFolderName, domainObjectNa
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.match(regexp).at(-1);
+ return window.location.href.split('?')[0].match(regexp).at(-1);
}, UUIDv4Regexp);
return focusedObjectUuid;
diff --git a/e2e/helper/addInitExampleFaultProvider.js b/e2e/helper/addInitExampleFaultProvider.js
new file mode 100644
index 000000000..1bf7b0209
--- /dev/null
+++ b/e2e/helper/addInitExampleFaultProvider.js
@@ -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());
+});
diff --git a/e2e/helper/addInitExampleFaultProviderStatic.js b/e2e/helper/addInitExampleFaultProviderStatic.js
new file mode 100644
index 000000000..fc7ec5397
--- /dev/null
+++ b/e2e/helper/addInitExampleFaultProviderStatic.js
@@ -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));
+});
diff --git a/e2e/helper/addInitFaultManagementPlugin.js b/e2e/helper/addInitFaultManagementPlugin.js
new file mode 100644
index 000000000..4f1c396fa
--- /dev/null
+++ b/e2e/helper/addInitFaultManagementPlugin.js
@@ -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());
+});
diff --git a/e2e/helper/faultUtils.js b/e2e/helper/faultUtils.js
new file mode 100644
index 000000000..819c4b42b
--- /dev/null
+++ b/e2e/helper/faultUtils.js
@@ -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
+};
diff --git a/e2e/tests/functional/couchdb.e2e.spec.js b/e2e/tests/functional/couchdb.e2e.spec.js
new file mode 100644
index 000000000..7e8d539de
--- /dev/null
+++ b/e2e/tests/functional/couchdb.e2e.spec.js
@@ -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);
+ });
+});
diff --git a/e2e/tests/functional/moveAndLinkObjects.e2e.spec.js b/e2e/tests/functional/moveAndLinkObjects.e2e.spec.js
new file mode 100644
index 000000000..78f20cb65
--- /dev/null
+++ b/e2e/tests/functional/moveAndLinkObjects.e2e.spec.js
@@ -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
+});
diff --git a/e2e/tests/functional/moveObjects.e2e.spec.js b/e2e/tests/functional/moveObjects.e2e.spec.js
deleted file mode 100644
index f11f8c7b2..000000000
--- a/e2e/tests/functional/moveObjects.e2e.spec.js
+++ /dev/null
@@ -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
-});
diff --git a/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js b/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js
index 83090fc0e..3d6456e2e 100644
--- a/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js
+++ b/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js
@@ -93,6 +93,70 @@ test.describe('Testing Display Layout @unstable', () => {
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);
+ });
});
/**
diff --git a/e2e/tests/functional/plugins/faultManagement/faultManagement.e2e.spec.js b/e2e/tests/functional/plugins/faultManagement/faultManagement.e2e.spec.js
new file mode 100644
index 000000000..8bf08e0c9
--- /dev/null
+++ b/e2e/tests/functional/plugins/faultManagement/faultManagement.e2e.spec.js
@@ -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);
+ });
+});
diff --git a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js
index 42e532b44..7757c9319 100644
--- a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js
+++ b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js
@@ -25,7 +25,7 @@ This test suite is dedicated to tests which verify the basic operations surround
but only assume that example imagery is present.
*/
/* globals process */
-
+const { v4: uuid } = require('uuid');
const { waitForAnimations } = require('../../../../baseFixtures');
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
@@ -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.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
*/
diff --git a/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js b/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js
index c076329d4..568061c89 100644
--- a/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js
+++ b/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js
@@ -21,7 +21,7 @@
*****************************************************************************/
const { test, expect } = require('../../../../pluginFixtures');
-const { openObjectTreeContextMenu } = require('../../../../appActions');
+const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions');
const path = require('path');
const TEST_TEXT = 'Testing text for entries.';
@@ -30,8 +30,9 @@ const CUSTOM_NAME = 'CUSTOM_NAME';
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
test.describe('Restricted Notebook', () => {
+ let notebook;
test.beforeEach(async ({ page }) => {
- await startAndAddRestrictedNotebookObject(page);
+ notebook = await startAndAddRestrictedNotebookObject(page);
});
test('Can be renamed @addInit', async ({ page }) => {
@@ -39,9 +40,7 @@ test.describe('Restricted Notebook', () => {
});
test('Can be deleted if there are no locked pages @addInit', async ({ page, openmctConfig }) => {
- const { myItemsFolderName } = openmctConfig;
-
- await openObjectTreeContextMenu(page, myItemsFolderName, `Unnamed ${CUSTOM_NAME}`);
+ await openObjectTreeContextMenu(page, notebook.url);
const menuOptions = page.locator('.c-menu ul');
await expect.soft(menuOptions).toContainText('Remove');
@@ -76,9 +75,9 @@ test.describe('Restricted Notebook', () => {
});
test.describe('Restricted Notebook with at least one entry and with the page locked @addInit', () => {
-
+ let notebook;
test.beforeEach(async ({ page }) => {
- await startAndAddRestrictedNotebookObject(page);
+ notebook = await startAndAddRestrictedNotebookObject(page);
await enterTextEntry(page);
await lockPage(page);
@@ -86,9 +85,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();
});
- 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");
- const { myItemsFolderName } = openmctConfig;
// main lock message on page
const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed');
expect.soft(await lockMessage.count()).toEqual(1);
@@ -98,7 +96,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
expect.soft(await pageLockIcon.count()).toEqual(1);
// no way to remove a restricted notebook with a locked page
- await openObjectTreeContextMenu(page, myItemsFolderName, `Unnamed ${CUSTOM_NAME}`);
+ await openObjectTreeContextMenu(page, notebook.url);
const menuOptions = page.locator('.c-menu ul');
await expect(menuOptions).not.toContainText('Remove');
@@ -178,13 +176,8 @@ async function startAndAddRestrictedNotebookObject(page) {
// eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitRestrictedNotebook.js') });
await page.goto('./', { waitUntil: 'networkidle' });
- await page.click('button:has-text("Create")');
- await page.click(`text=${CUSTOM_NAME}`); // secondarily tests renamability also
- // Click text=OK
- await Promise.all([
- page.waitForNavigation({waitUntil: 'networkidle'}),
- page.click('text=OK')
- ]);
+
+ return createDomainObjectWithDefaults(page, { type: CUSTOM_NAME });
}
/**
diff --git a/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js b/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js
index c54233cee..ada74ccad 100644
--- a/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js
+++ b/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js
@@ -56,19 +56,23 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
await createNotebookAndEntry(page, iterations);
for (let iteration = 0; iteration < iterations; iteration++) {
- // Click text=To start a new entry, click here or drag and drop any object
+ // Hover and click "Add Tag" button
+ // Hover is needed here to "slow down" the actions while running in headless mode
+ await page.hover(`button:has-text("Add Tag") >> nth = ${iteration}`);
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
- // Click [placeholder="Type to select tag"]
+ // Click inside the tag search input
await page.locator('[placeholder="Type to select tag"]').click();
- // Click text=Driving
+ // Select the "Driving" tag
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
- // Click button:has-text("Add Tag")
+ // Hover and click "Add Tag" button
+ // Hover is needed here to "slow down" the actions while running in headless mode
+ await page.hover(`button:has-text("Add Tag") >> nth = ${iteration}`);
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
- // Click [placeholder="Type to select tag"]
+ // Click inside the tag search input
await page.locator('[placeholder="Type to select tag"]').click();
- // Click text=Science
+ // Select the "Science" tag
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
}
}
@@ -130,7 +134,8 @@ test.describe('Tagging in Notebooks @addInit', () => {
await createNotebookEntryAndTags(page);
await page.locator('[aria-label="Notebook Entries"]').click();
// Delete Driving
- await page.locator('text=Science Driving Add Tag >> button').nth(1).click();
+ await page.hover('.c-tag__label:has-text("Driving")');
+ await page.locator('.c-tag__label:has-text("Driving") ~ .c-completed-tag-deletion').click();
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving");
diff --git a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-darwin b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-darwin
index bea4d7c40..01850a3bc 100644
--- a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-darwin
+++ b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-darwin
Binary files differ
diff --git a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-linux b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-linux
index 345901fcc..7fb1ec390 100644
--- a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-linux
+++ b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-linux
Binary files differ
diff --git a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-darwin b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-darwin
index fc3db7c14..75b1c4d95 100644
--- a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-darwin
+++ b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-darwin
Binary files differ
diff --git a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-linux b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-linux
index 88e71e789..031025d8e 100644
--- a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-linux
+++ b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-linux
Binary files differ
diff --git a/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js b/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js
index 59d317170..ea50e8530 100644
--- a/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js
+++ b/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js
@@ -147,4 +147,24 @@ test.describe('Time conductor input fields real-time mode', () => {
expect(page.url()).toContain(`startDelta=${startDelta}`);
expect(page.url()).toContain(`endDelta=${endDelta}`);
});
+
+ test.fixme('time conductor history in fixed time mode will track changing start and end times', async ({ page }) => {
+ // change start time, verify it's tracked in history
+ // change end time, verify it's tracked in history
+ });
+
+ test.fixme('time conductor history in realtime mode will track changing start and end times', async ({ page }) => {
+ // change start offset, verify it's tracked in history
+ // change end offset, verify it's tracked in history
+ });
+
+ test.fixme('time conductor history allows you to set a historical timeframe', async ({ page }) => {
+ // make sure there are historical history options
+ // select an option and make sure the time conductor start and end bounds are updated correctly
+ });
+
+ test.fixme('time conductor history allows you to set a realtime offsets', async ({ page }) => {
+ // make sure there are realtime history options
+ // select an option and verify the offsets are updated correctly
+ });
});
diff --git a/e2e/tests/functional/plugins/timer/timer.e2e.spec.js b/e2e/tests/functional/plugins/timer/timer.e2e.spec.js
index 0235a1d2c..16c6b5def 100644
--- a/e2e/tests/functional/plugins/timer/timer.e2e.spec.js
+++ b/e2e/tests/functional/plugins/timer/timer.e2e.spec.js
@@ -24,9 +24,10 @@ const { test, expect } = require('../../../../pluginFixtures');
const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions');
test.describe('Timer', () => {
+ let timer;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
- await createDomainObjectWithDefaults(page, { type: 'timer' });
+ timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
});
test('Can perform actions on the Timer', async ({ page, openmctConfig }) => {
@@ -35,13 +36,13 @@ test.describe('Timer', () => {
description: 'https://github.com/nasa/openmct/issues/4313'
});
- const { myItemsFolderName } = await openmctConfig;
+ const timerUrl = timer.url;
await test.step("From the tree context menu", async () => {
- await triggerTimerContextMenuAction(page, myItemsFolderName, 'Start');
- await triggerTimerContextMenuAction(page, myItemsFolderName, 'Pause');
- await triggerTimerContextMenuAction(page, myItemsFolderName, 'Restart at 0');
- await triggerTimerContextMenuAction(page, myItemsFolderName, 'Stop');
+ await triggerTimerContextMenuAction(page, timerUrl, 'Start');
+ await triggerTimerContextMenuAction(page, timerUrl, 'Pause');
+ await triggerTimerContextMenuAction(page, timerUrl, 'Restart at 0');
+ await triggerTimerContextMenuAction(page, timerUrl, 'Stop');
});
await test.step("From the 3dot menu", async () => {
@@ -74,9 +75,9 @@ test.describe('Timer', () => {
* @param {import('@playwright/test').Page} page
* @param {TimerAction} action
*/
-async function triggerTimerContextMenuAction(page, myItemsFolderName, action) {
+async function triggerTimerContextMenuAction(page, timerUrl, action) {
const menuAction = `.c-menu ul li >> text="${action}"`;
- await openObjectTreeContextMenu(page, myItemsFolderName, "Unnamed Timer");
+ await openObjectTreeContextMenu(page, timerUrl);
await page.locator(menuAction).click();
assertTimerStateAfterAction(page, action);
}
diff --git a/e2e/tests/functional/search.e2e.spec.js b/e2e/tests/functional/search.e2e.spec.js
index fe488697c..b654f44d1 100644
--- a/e2e/tests/functional/search.e2e.spec.js
+++ b/e2e/tests/functional/search.e2e.spec.js
@@ -24,6 +24,8 @@
*/
const { test, expect } = require('../../pluginFixtures');
+const { createDomainObjectWithDefaults } = require('../../appActions');
+const { v4: uuid } = require('uuid');
test.describe('Grand Search', () => {
test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page, openmctConfig }) => {
@@ -112,13 +114,16 @@ test.describe("Search Tests @unstable", () => {
await expect(page.locator('text=No matching results.')).toBeVisible();
});
- test('Validate single object in search result', async ({ page }) => {
+ test('Validate single object in search result @couchdb', async ({ page }) => {
//Go to baseURL
await page.goto("./", { waitUntil: "networkidle" });
// Create a folder object
- const folderName = 'testFolder';
- await createFolderObject(page, folderName);
+ const folderName = uuid();
+ await createDomainObjectWithDefaults(page, {
+ type: 'folder',
+ name: folderName
+ });
// Full search for object
await page.type("input[type=search]", folderName);
@@ -127,7 +132,7 @@ test.describe("Search Tests @unstable", () => {
await waitForSearchCompletion(page);
// Get the search results
- const searchResults = await page.locator(searchResultSelector);
+ const searchResults = page.locator(searchResultSelector);
// Verify that one result is found
expect(await searchResults.count()).toBe(1);
diff --git a/e2e/tests/functional/tree.e2e.spec.js b/e2e/tests/functional/tree.e2e.spec.js
new file mode 100644
index 000000000..691f7f127
--- /dev/null
+++ b/e2e/tests/functional/tree.e2e.spec.js
@@ -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"]');
+}
diff --git a/e2e/tests/visual/components/tree.visual.spec.js b/e2e/tests/visual/components/tree.visual.spec.js
new file mode 100644
index 000000000..0ad2aca75
--- /dev/null
+++ b/e2e/tests/visual/components/tree.visual.spec.js
@@ -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();
+}
diff --git a/e2e/tests/visual/faultManagement.visual.spec.js b/e2e/tests/visual/faultManagement.visual.spec.js
new file mode 100644
index 000000000..ab6b34e34
--- /dev/null
+++ b/e2e/tests/visual/faultManagement.visual.spec.js
@@ -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}')`);
+ });
+});
diff --git a/example/faultManagment/exampleFaultSource.js b/example/faultManagement/exampleFaultSource.js
index 338f0903b..9e296ad7f 100644
--- a/example/faultManagment/exampleFaultSource.js
+++ b/example/faultManagement/exampleFaultSource.js
@@ -20,59 +20,36 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
-export default function () {
+import utils from './utils';
+
+export default function (staticFaults = false) {
return function install(openmct) {
openmct.install(openmct.plugins.FaultManagement());
+ const faultsData = utils.randomFaults(staticFaults);
+
openmct.faults.addProvider({
request(domainObject, options) {
- const faults = JSON.parse(localStorage.getItem('faults'));
-
- return Promise.resolve(faults.alarms);
+ return Promise.resolve(faultsData);
},
subscribe(domainObject, callback) {
- const faultsData = JSON.parse(localStorage.getItem('faults')).alarms;
-
- function getRandomIndex(start, end) {
- return Math.floor(start + (Math.random() * (end - start + 1)));
- }
-
- let id = setInterval(() => {
- const index = getRandomIndex(0, faultsData.length - 1);
- const randomFaultData = faultsData[index];
- const randomFault = randomFaultData.fault;
- randomFault.currentValueInfo.value = Math.random();
- callback({
- fault: randomFault,
- type: 'alarms'
- });
- }, 300);
-
- return () => {
- clearInterval(id);
- };
+ return () => {};
},
supportsRequest(domainObject) {
- const faults = localStorage.getItem('faults');
-
- return faults && domainObject.type === 'faultManagement';
+ return domainObject.type === 'faultManagement';
},
supportsSubscribe(domainObject) {
- const faults = localStorage.getItem('faults');
-
- return faults && domainObject.type === 'faultManagement';
+ return domainObject.type === 'faultManagement';
},
acknowledgeFault(fault, { comment = '' }) {
- console.log('acknowledgeFault', fault);
- console.log('comment', comment);
+ utils.acknowledgeFault(fault);
return Promise.resolve({
success: true
});
},
- shelveFault(fault, shelveData) {
- console.log('shelveFault', fault);
- console.log('shelveData', shelveData);
+ shelveFault(fault, duration) {
+ utils.shelveFault(fault, duration);
return Promise.resolve({
success: true
diff --git a/example/faultManagment/pluginSpec.js b/example/faultManagement/pluginSpec.js
index b7a0fa680..b7a0fa680 100644
--- a/example/faultManagment/pluginSpec.js
+++ b/example/faultManagement/pluginSpec.js
diff --git a/example/faultManagement/utils.js b/example/faultManagement/utils.js
new file mode 100644
index 000000000..1287d570b
--- /dev/null
+++ b/example/faultManagement/utils.js
@@ -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
+};
diff --git a/package.json b/package.json
index 9041e15d3..462d4d0d2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "openmct",
- "version": "2.0.8-SNAPSHOT",
+ "version": "2.0.8",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.18.9",
@@ -87,7 +87,8 @@
"test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
"test:e2e": "npx playwright test",
- "test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert @unstable",
+ "test:e2e:couchdb": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @couchdb",
+ "test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert \"@unstable|@couchdb\"",
"test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable",
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots",
diff --git a/src/api/objects/InMemorySearchProvider.js b/src/api/objects/InMemorySearchProvider.js
index 840b31618..6feadaf44 100644
--- a/src/api/objects/InMemorySearchProvider.js
+++ b/src/api/objects/InMemorySearchProvider.js
@@ -63,6 +63,8 @@ class InMemorySearchProvider {
this.localSearchForTags = this.localSearchForTags.bind(this);
this.localSearchForNotebookAnnotations = this.localSearchForNotebookAnnotations.bind(this);
this.onAnnotationCreation = this.onAnnotationCreation.bind(this);
+ this.onCompositionAdded = this.onCompositionAdded.bind(this);
+ this.onCompositionRemoved = this.onCompositionRemoved.bind(this);
this.onerror = this.onWorkerError.bind(this);
this.startIndexing = this.startIndexing.bind(this);
@@ -75,6 +77,12 @@ class InMemorySearchProvider {
this.worker.port.close();
}
+ Object.keys(this.indexedCompositions).forEach(keyString => {
+ const composition = this.indexedCompositions[keyString];
+ composition.off('add', this.onCompositionAdded);
+ composition.off('remove', this.onCompositionRemoved);
+ });
+
this.destroyObservers(this.indexedIds);
this.destroyObservers(this.indexedCompositions);
});
@@ -259,7 +267,6 @@ class InMemorySearchProvider {
}
onAnnotationCreation(annotationObject) {
-
const objectProvider = this.openmct.objects.getProvider(annotationObject.identifier);
if (objectProvider === undefined || objectProvider.search === undefined) {
const provider = this;
@@ -281,17 +288,34 @@ class InMemorySearchProvider {
provider.index(domainObject);
}
- onCompositionMutation(domainObject, composition) {
+ onCompositionAdded(newDomainObjectToIndex) {
const provider = this;
- const indexedComposition = domainObject.composition;
- const identifiersToIndex = composition
- .filter(identifier => !indexedComposition
- .some(indexedIdentifier => this.openmct.objects
- .areIdsEqual([identifier, indexedIdentifier])));
-
- identifiersToIndex.forEach(identifier => {
- this.openmct.objects.get(identifier).then(objectToIndex => provider.index(objectToIndex));
- });
+ // The object comes in as a mutable domain object, which has functions,
+ // which the index function cannot handle as it will eventually be serialized
+ // using structuredClone. Thus we're using JSON.parse/JSON.stringify to discard
+ // those functions.
+ const nonMutableDomainObject = JSON.parse(JSON.stringify(newDomainObjectToIndex));
+
+ const objectProvider = this.openmct.objects.getProvider(nonMutableDomainObject.identifier);
+ if (objectProvider === undefined || objectProvider.search === undefined) {
+ provider.index(nonMutableDomainObject);
+ }
+ }
+
+ onCompositionRemoved(domainObjectToRemoveIdentifier) {
+ const keyString = this.openmct.objects.makeKeyString(domainObjectToRemoveIdentifier);
+ if (this.indexedIds[keyString]) {
+ // we store the unobserve function in the indexedId map
+ this.indexedIds[keyString]();
+ delete this.indexedIds[keyString];
+ }
+
+ const composition = this.indexedCompositions[keyString];
+ if (composition) {
+ composition.off('add', this.onCompositionAdded);
+ composition.off('remove', this.onCompositionRemoved);
+ delete this.indexedCompositions[keyString];
+ }
}
/**
@@ -305,6 +329,7 @@ class InMemorySearchProvider {
async index(domainObject) {
const provider = this;
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
+ const composition = this.openmct.composition.get(domainObject);
if (!this.indexedIds[keyString]) {
this.indexedIds[keyString] = this.openmct.objects.observe(
@@ -312,11 +337,12 @@ class InMemorySearchProvider {
'name',
this.onNameMutation.bind(this, domainObject)
);
- this.indexedCompositions[keyString] = this.openmct.objects.observe(
- domainObject,
- 'composition',
- this.onCompositionMutation.bind(this, domainObject)
- );
+ if (composition) {
+ composition.on('add', this.onCompositionAdded);
+ composition.on('remove', this.onCompositionRemoved);
+ this.indexedCompositions[keyString] = composition;
+ }
+
if (domainObject.type === 'annotation') {
this.indexedTags[keyString] = this.openmct.objects.observe(
domainObject,
@@ -338,8 +364,6 @@ class InMemorySearchProvider {
}
}
- const composition = this.openmct.composition.get(domainObject);
-
if (composition !== undefined) {
const children = await composition.load();
diff --git a/src/api/objects/ObjectAPI.js b/src/api/objects/ObjectAPI.js
index b82bc6159..64167f3c7 100644
--- a/src/api/objects/ObjectAPI.js
+++ b/src/api/objects/ObjectAPI.js
@@ -230,15 +230,10 @@ export default class ObjectAPI {
return result;
}).catch((result) => {
console.warn(`Failed to retrieve ${keystring}:`, result);
- this.openmct.notifications.error(`Failed to retrieve object ${keystring}`);
delete this.cache[keystring];
- if (!result) {
- //no result means resource either doesn't exist or is missing
- //otherwise it's an error, and we shouldn't apply interceptors
- result = this.applyGetInterceptors(identifier);
- }
+ result = this.applyGetInterceptors(identifier);
return result;
});
diff --git a/src/plugins/charts/scatter/ScatterPlotView.vue b/src/plugins/charts/scatter/ScatterPlotView.vue
index f6a69228e..129a3bca9 100644
--- a/src/plugins/charts/scatter/ScatterPlotView.vue
+++ b/src/plugins/charts/scatter/ScatterPlotView.vue
@@ -97,11 +97,11 @@ export default {
},
followTimeContext() {
- this.timeContext.on('bounds', this.reloadTelemetry);
+ this.timeContext.on('bounds', this.reloadTelemetryOnBoundsChange);
},
stopFollowingTimeContext() {
if (this.timeContext) {
- this.timeContext.off('bounds', this.reloadTelemetry);
+ this.timeContext.off('bounds', this.reloadTelemetryOnBoundsChange);
}
},
addToComposition(telemetryObject) {
@@ -181,6 +181,11 @@ export default {
this.composition.on('remove', this.removeTelemetryObject);
this.composition.load();
},
+ reloadTelemetryOnBoundsChange(bounds, isTick) {
+ if (!isTick) {
+ this.reloadTelemetry();
+ }
+ },
reloadTelemetry() {
this.valuesByTimestamp = {};
diff --git a/src/plugins/displayLayout/components/DisplayLayout.vue b/src/plugins/displayLayout/components/DisplayLayout.vue
index bc29b615a..98afcba65 100644
--- a/src/plugins/displayLayout/components/DisplayLayout.vue
+++ b/src/plugins/displayLayout/components/DisplayLayout.vue
@@ -517,7 +517,19 @@ export default {
initializeItems() {
this.telemetryViewMap = {};
this.objectViewMap = {};
- this.layoutItems.forEach(this.trackItem);
+
+ let removedItems = [];
+ this.layoutItems.forEach((item) => {
+ if (item.identifier) {
+ if (this.containsObject(item.identifier)) {
+ this.trackItem(item);
+ } else {
+ removedItems.push(this.openmct.objects.makeKeyString(item.identifier));
+ }
+ }
+ });
+
+ removedItems.forEach(this.removeFromConfiguration);
},
isItemAlreadyTracked(child) {
let found = false;
diff --git a/src/plugins/displayLayout/components/TelemetryView.vue b/src/plugins/displayLayout/components/TelemetryView.vue
index 3c5e5eba2..19036b26e 100644
--- a/src/plugins/displayLayout/components/TelemetryView.vue
+++ b/src/plugins/displayLayout/components/TelemetryView.vue
@@ -232,10 +232,12 @@ export default {
this.removeSelectable();
}
- this.telemetryCollection.off('add', this.setLatestValues);
- this.telemetryCollection.off('clear', this.refreshData);
+ if (this.telemetryCollection) {
+ this.telemetryCollection.off('add', this.setLatestValues);
+ this.telemetryCollection.off('clear', this.refreshData);
- this.telemetryCollection.destroy();
+ this.telemetryCollection.destroy();
+ }
if (this.mutablePromise) {
this.mutablePromise.then(() => {
diff --git a/src/plugins/displayLayout/pluginSpec.js b/src/plugins/displayLayout/pluginSpec.js
index 8e6a56e5f..e70e754b6 100644
--- a/src/plugins/displayLayout/pluginSpec.js
+++ b/src/plugins/displayLayout/pluginSpec.js
@@ -21,6 +21,7 @@
*****************************************************************************/
import { createOpenMct, resetApplicationState } from 'utils/testing';
+import Vue from 'vue';
import DisplayLayoutPlugin from './plugin';
describe('the plugin', function () {
@@ -117,6 +118,59 @@ describe('the plugin', function () {
});
+ describe('on load', () => {
+ let displayLayoutItem;
+ let item;
+
+ beforeEach((done) => {
+ item = {
+ 'width': 32,
+ 'height': 18,
+ 'x': 78,
+ 'y': 8,
+ 'identifier': {
+ 'namespace': '',
+ 'key': 'bdeb91ab-3a7e-4a71-9dd2-39d73644e136'
+ },
+ 'hasFrame': true,
+ 'type': 'line-view', // so no telemetry functionality is triggered, just want to test the sync
+ 'id': 'c0ff485a-344c-4e70-8d83-a9d9998a69fc'
+
+ };
+ displayLayoutItem = {
+ 'composition': [
+ // no item in compostion, but item in configuration items
+ ],
+ 'configuration': {
+ 'items': [
+ item
+ ],
+ 'layoutGrid': [
+ 10,
+ 10
+ ]
+ },
+ 'name': 'Display Layout',
+ 'type': 'layout',
+ 'identifier': {
+ 'namespace': '',
+ 'key': 'c5e636c1-6771-4c9c-b933-8665cab189b3'
+ }
+ };
+
+ const applicableViews = openmct.objectViews.get(displayLayoutItem, []);
+ const displayLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'layout.view');
+ const view = displayLayoutViewProvider.view(displayLayoutItem);
+ view.show(child, false);
+
+ Vue.nextTick(done);
+ });
+
+ it('will sync compostion and layout items', () => {
+ expect(displayLayoutItem.configuration.items.length).toBe(0);
+ });
+ });
+
describe('the alpha numeric format view', () => {
let displayLayoutItem;
let telemetryItem;
diff --git a/src/plugins/faultManagement/FaultManagementListView.vue b/src/plugins/faultManagement/FaultManagementListView.vue
index f07dc839a..be19cbfe5 100644
--- a/src/plugins/faultManagement/FaultManagementListView.vue
+++ b/src/plugins/faultManagement/FaultManagementListView.vue
@@ -71,6 +71,8 @@ import FaultManagementToolbar from './FaultManagementToolbar.vue';
import { FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS, FILTER_ITEMS, SORT_ITEMS } from './constants';
+const SEARCH_KEYS = ['id', 'triggerValueInfo', 'currentValueInfo', 'triggerTime', 'severity', 'name', 'shortDescription', 'namespace'];
+
export default {
components: {
FaultManagementListHeader,
@@ -125,27 +127,19 @@ export default {
},
methods: {
filterUsingSearchTerm(fault) {
- if (fault?.id?.toString().toLowerCase().includes(this.searchTerm)) {
- return true;
+ if (!fault) {
+ return false;
}
- if (fault?.triggerValueInfo?.toString().toLowerCase().includes(this.searchTerm)) {
- return true;
- }
+ let match = false;
- if (fault?.currentValueInfo?.toString().toLowerCase().includes(this.searchTerm)) {
- return true;
- }
-
- if (fault?.triggerTime.toString().toLowerCase().includes(this.searchTerm)) {
- return true;
- }
-
- if (fault?.severity.toString().toLowerCase().includes(this.searchTerm)) {
- return true;
- }
+ SEARCH_KEYS.forEach((key) => {
+ if (fault[key]?.toString().toLowerCase().includes(this.searchTerm)) {
+ match = true;
+ }
+ });
- return false;
+ return match;
},
isSelected(fault) {
return Boolean(this.selectedFaults[fault.id]);
diff --git a/src/plugins/faultManagement/pluginSpec.js b/src/plugins/faultManagement/pluginSpec.js
index 07ad9664c..29169c05c 100644
--- a/src/plugins/faultManagement/pluginSpec.js
+++ b/src/plugins/faultManagement/pluginSpec.js
@@ -24,10 +24,22 @@ import {
createOpenMct,
resetApplicationState
} from '../../utils/testing';
-import { FAULT_MANAGEMENT_TYPE } from './constants';
+import {
+ FAULT_MANAGEMENT_TYPE,
+ FAULT_MANAGEMENT_VIEW,
+ FAULT_MANAGEMENT_NAMESPACE
+} from './constants';
describe("The Fault Management Plugin", () => {
let openmct;
+ const faultDomainObject = {
+ name: 'it is not your fault',
+ type: FAULT_MANAGEMENT_TYPE,
+ identifier: {
+ key: 'nobodies',
+ namespace: 'fault'
+ }
+ };
beforeEach(() => {
openmct = createOpenMct();
@@ -38,15 +50,54 @@ describe("The Fault Management Plugin", () => {
});
it('is not installed by default', () => {
- let typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;
+ const typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;
expect(typeDef.name).toBe('Unknown Type');
});
it('can be installed', () => {
openmct.install(openmct.plugins.FaultManagement());
- let typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;
+ const typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;
expect(typeDef.name).toBe('Fault Management');
});
+
+ describe('once it is installed', () => {
+ beforeEach(() => {
+ openmct.install(openmct.plugins.FaultManagement());
+ });
+
+ it('provides a view for fault management types', () => {
+ const applicableViews = openmct.objectViews.get(faultDomainObject, []);
+ const faultManagementView = applicableViews.find(
+ (viewProvider) => viewProvider.key === FAULT_MANAGEMENT_VIEW
+ );
+
+ expect(applicableViews.length).toEqual(1);
+ expect(faultManagementView).toBeDefined();
+ });
+
+ it('provides an inspector view for fault management types', () => {
+ const faultDomainObjectSelection = [[
+ {
+ context: {
+ item: faultDomainObject
+ }
+ }
+ ]];
+ const applicableInspectorViews = openmct.inspectorViews.get(faultDomainObjectSelection);
+
+ expect(applicableInspectorViews.length).toEqual(1);
+ });
+
+ it('creates a root object for fault management', async () => {
+ const root = await openmct.objects.getRoot();
+ const rootCompositionCollection = openmct.composition.get(root);
+ const rootComposition = await rootCompositionCollection.load();
+ const faultObject = rootComposition.find(obj => obj.identifier.namespace === FAULT_MANAGEMENT_NAMESPACE);
+
+ expect(faultObject).toBeDefined();
+ });
+
+ });
});
diff --git a/src/plugins/imagery/components/ImageryView.vue b/src/plugins/imagery/components/ImageryView.vue
index 881415334..b6bed994b 100644
--- a/src/plugins/imagery/components/ImageryView.vue
+++ b/src/plugins/imagery/components/ImageryView.vue
@@ -519,20 +519,17 @@ export default {
},
watch: {
imageHistory: {
- handler(newHistory, oldHistory) {
+ handler(newHistory, _oldHistory) {
const newSize = newHistory.length;
- let imageIndex;
+ let imageIndex = newSize > 0 ? newSize - 1 : undefined;
if (this.focusedImageTimestamp !== undefined) {
const foundImageIndex = newHistory.findIndex(img => img.time === this.focusedImageTimestamp);
- imageIndex = foundImageIndex > -1
- ? foundImageIndex
- : newSize - 1;
- } else {
- imageIndex = newSize > 0
- ? newSize - 1
- : undefined;
+ if (foundImageIndex > -1) {
+ imageIndex = foundImageIndex;
+ }
}
+ this.setFocusedImage(imageIndex);
this.nextImageIndex = imageIndex;
if (this.previousFocusedImage && newHistory.length) {
diff --git a/src/plugins/interceptors/missingObjectInterceptor.js b/src/plugins/interceptors/missingObjectInterceptor.js
index 4a2167080..9eb2134d1 100644
--- a/src/plugins/interceptors/missingObjectInterceptor.js
+++ b/src/plugins/interceptors/missingObjectInterceptor.js
@@ -27,10 +27,13 @@ export default function MissingObjectInterceptor(openmct) {
},
invoke: (identifier, object) => {
if (object === undefined) {
+ const keyString = openmct.objects.makeKeyString(identifier);
+ openmct.notifications.error(`Failed to retrieve object ${keyString}`);
+
return {
identifier,
type: 'unknown',
- name: 'Missing: ' + openmct.objects.makeKeyString(identifier)
+ name: 'Missing: ' + keyString
};
}
diff --git a/src/plugins/linkAction/LinkAction.js b/src/plugins/linkAction/LinkAction.js
index 9390b3e5a..576e53d35 100644
--- a/src/plugins/linkAction/LinkAction.js
+++ b/src/plugins/linkAction/LinkAction.js
@@ -83,7 +83,6 @@ export default class LinkAction {
}
]
};
-
this.openmct.forms.showForm(formStructure)
.then(this.onSave.bind(this));
}
@@ -91,8 +90,8 @@ export default class LinkAction {
validate(currentParent) {
return (data) => {
- // default current parent to ROOT, if it's undefined, then it's a root level item
- if (currentParent === undefined) {
+ // default current parent to ROOT, if it's null, then it's a root level item
+ if (!currentParent) {
currentParent = {
identifier: {
key: 'ROOT',
@@ -101,24 +100,23 @@ export default class LinkAction {
};
}
- const parentCandidate = data.value[0];
- const currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
- const parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier);
+ const parentCandidatePath = data.value;
+ const parentCandidate = parentCandidatePath[0];
const objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);
if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) {
return false;
}
- if (!parentCandidateKeystring || !currentParentKeystring) {
- return false;
- }
-
- if (parentCandidateKeystring === currentParentKeystring) {
+ // check if moving to same place
+ if (this.openmct.objects.areIdsEqual(parentCandidate.identifier, currentParent.identifier)) {
return false;
}
- if (parentCandidateKeystring === objectKeystring) {
+ // check if moving to a child
+ if (parentCandidatePath.some(candidatePath => {
+ return this.openmct.objects.areIdsEqual(candidatePath.identifier, this.object.identifier);
+ })) {
return false;
}
diff --git a/src/plugins/move/MoveAction.js b/src/plugins/move/MoveAction.js
index d9a4d144e..594317c3b 100644
--- a/src/plugins/move/MoveAction.js
+++ b/src/plugins/move/MoveAction.js
@@ -145,25 +145,23 @@ export default class MoveAction {
const parentCandidatePath = data.value;
const parentCandidate = parentCandidatePath[0];
- if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) {
+ // check if moving to same place
+ if (this.openmct.objects.areIdsEqual(parentCandidate.identifier, currentParent.identifier)) {
return false;
}
- let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
- let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier);
- let objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);
-
- if (!parentCandidateKeystring || !currentParentKeystring) {
+ // check if moving to a child
+ if (parentCandidatePath.some(candidatePath => {
+ return this.openmct.objects.areIdsEqual(candidatePath.identifier, this.object.identifier);
+ })) {
return false;
}
- if (parentCandidateKeystring === currentParentKeystring) {
+ if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) {
return false;
}
- if (parentCandidateKeystring === objectKeystring) {
- return false;
- }
+ let objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);
const parentCandidateComposition = parentCandidate.composition;
if (parentCandidateComposition && parentCandidateComposition.indexOf(objectKeystring) !== -1) {
diff --git a/src/plugins/myItems/pluginSpec.js b/src/plugins/myItems/pluginSpec.js
index b463b0a5e..867fbf243 100644
--- a/src/plugins/myItems/pluginSpec.js
+++ b/src/plugins/myItems/pluginSpec.js
@@ -69,27 +69,27 @@ describe("the plugin", () => {
});
describe('adds an interceptor that returns a "My Items" model for', () => {
- let myItemsMissing;
- let mockMissingProvider;
+ let myItemsObject;
+ let mockNotFoundProvider;
let activeProvider;
beforeEach(async () => {
- mockMissingProvider = {
- get: () => Promise.resolve(missingObj),
+ mockNotFoundProvider = {
+ get: () => Promise.reject(new Error('Not found')),
create: () => Promise.resolve(missingObj),
update: () => Promise.resolve(missingObj)
};
- activeProvider = mockMissingProvider;
+ activeProvider = mockNotFoundProvider;
spyOn(openmct.objects, 'getProvider').and.returnValue(activeProvider);
- myItemsMissing = await openmct.objects.get(myItemsIdentifier);
+ myItemsObject = await openmct.objects.get(myItemsIdentifier);
});
it('missing objects', () => {
- let idsMatchMissing = openmct.objects.areIdsEqual(myItemsMissing.identifier, myItemsIdentifier);
+ let idsMatch = openmct.objects.areIdsEqual(myItemsObject.identifier, myItemsIdentifier);
- expect(myItemsMissing).toBeDefined();
- expect(idsMatchMissing).toBeTrue();
+ expect(myItemsObject).toBeDefined();
+ expect(idsMatch).toBeTrue();
});
});
diff --git a/src/plugins/persistence/couch/.env.ci b/src/plugins/persistence/couch/.env.ci
new file mode 100644
index 000000000..104d70d6e
--- /dev/null
+++ b/src/plugins/persistence/couch/.env.ci
@@ -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 \ No newline at end of file
diff --git a/src/plugins/persistence/couch/README.md b/src/plugins/persistence/couch/README.md
index fc5cf795b..8b17e1478 100644
--- a/src/plugins/persistence/couch/README.md
+++ b/src/plugins/persistence/couch/README.md
@@ -1,52 +1,145 @@
-# Introduction
-These instructions are for setting up CouchDB for a **development** environment. For a production environment, we recommend running Open MCT behind a proxy server (e.g., Nginx or Apache), and securing the CouchDB server properly:
-https://docs.couchdb.org/en/main/intro/security.html
# Installing CouchDB
-## macOS
-### Installing with admin privileges to your computer
+
+## Introduction
+
+These instructions are for setting up CouchDB for a **development** environment. For a production environment, we recommend running Open MCT behind a proxy server (e.g., Nginx or Apache), and securing the CouchDB server properly:
+<https://docs.couchdb.org/en/main/intro/security.html>
+
+## Docker Quickstart
+
+The following process is the preferred way of using CouchDB as it is automatic and closely resembles a production environment.
+
+Requirement:
+Get docker compose (or recent version of docker) installed on your machine. We recommend [Docker Desktop](https://www.docker.com/products/docker-desktop/)
+
+1. Open a terminal to this current working directory (`cd openmct/src/plugins/persistence/couch`)
+2. Create and start the `couchdb` container:
+
+```sh
+docker compose -f ./couchdb-compose.yaml up --detach
+```
+3. Copy `.env.ci` file to file named `.env.local`
+4. (Optional) Change the values of `.env.local` if desired
+5. Set the environment variables in bash by sourcing the env file
+
+```sh
+export $(cat .env.local | xargs)
+```
+
+6. Execute the configuration script:
+
+```sh
+sh ./setup-couchdb.sh
+```
+
+7. `cd` to the workspace root directory (the same directory as `index.html`)
+8. Update `index.html` to use the CouchDB plugin as persistence store:
+
+```sh
+sh ./src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
+```
+9. ✅ Done!
+
+Open MCT will now use your local CouchDB container as its persistence store. Access the CouchDB instance manager by visiting <http://localhost:5984/_utils>.
+
+## macOS
+
+While we highly recommend using the CouchDB docker-compose installation, it is still possible to install CouchDB through other means.
+
+### Installing CouchDB
+
1. Install CouchDB using: `brew install couchdb`.
2. Edit `/usr/local/etc/local.ini` and add the following settings:
- ```
+
+ ```txt
[admins]
admin = youradminpassword
```
+
And set the server up for single node:
- ```
+
+ ```txt
[couchdb]
single_node=true
```
+
Enable CORS
- ```
+
+ ```txt
[chttpd]
enable_cors = true
[cors]
origins = http://localhost:8080
```
-### Installing without admin privileges to your computer
-1. Install CouchDB following these instructions: https://docs.brew.sh/Installation#untar-anywhere.
+
+
+### Installing CouchDB without admin privileges to your computer
+
+If `brew` is not available on your mac machine, you'll need to get the CouchDB installed using the official sourcefiles.
+1. Install CouchDB following these instructions: <https://docs.brew.sh/Installation#untar-anywhere>.
1. Edit `local.ini` in Homebrew's `/etc/` directory as directed above in the 'Installing with admin privileges to your computer' section.
+
## Other Operating Systems
-Follow the installation instructions from the CouchDB installation guide: https://docs.couchdb.org/en/stable/install/index.html
+
+Follow the installation instructions from the CouchDB installation guide: <https://docs.couchdb.org/en/stable/install/index.html>
+
# Configuring CouchDB
+
+## Configuration script
+
+The simplest way to config a CouchDB instance is to use our provided tooling:
+1. Copy `.env.ci` file to file named `.env.local`
+2. Set the environment variables in bash by sourcing the env file
+
+```sh
+export $(cat .env.local | xargs)
+```
+
+3. Execute the configuration script:
+
+```sh
+sh ./setup-couchdb.sh
+```
+
+## Manual Configuration
+
1. Start CouchDB by running: `couchdb`.
2. Add the `_global_changes` database using `curl` (note the `youradminpassword` should be changed to what you set above 👆): `curl -X PUT http://admin:youradminpassword@127.0.0.1:5984/_global_changes`
-3. Navigate to http://localhost:5984/_utils
+3. Navigate to <http://localhost:5984/_utils>
4. Create a database called `openmct`
-5. Navigate to http://127.0.0.1:5984/_utils/#/database/openmct/permissions
+5. Navigate to <http://127.0.0.1:5984/_utils/#/database/openmct/permissions>
6. Remove permission restrictions in CouchDB from Open MCT by deleting `_admin` roles for both `Admin` and `Member`.
-# Configuring Open MCT
-1. Edit `openmct/index.html` comment out the following line:
-```
-openmct.install(openmct.plugins.LocalStorage());
-```
-Add a line to install the CouchDB plugin for Open MCT:
-```
-openmct.install(openmct.plugins.CouchDB("http://localhost:5984/openmct"));
+# 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
```
-2. Start Open MCT by running `npm start` in the `openmct` path.
-3. Navigate to http://localhost:8080/ and create a random object in Open MCT (e.g., a 'Clock') and save. You may get an error saying that the object failed to persist - this is a known error that you can ignore, and will only happen the first time you save - just try again.
-4. Navigate to: http://127.0.0.1:5984/_utils/#database/openmct/_all_docs
-5. Look at the 'JSON' tab and ensure you can see the specific object you created above.
-6. All done! 🏆
+
+## Manual Configuration
+
+1. Edit `openmct/index.html` comment out the following line:
+
+ ```js
+ openmct.install(openmct.plugins.LocalStorage());
+ ```
+
+ Add a line to install the CouchDB plugin for Open MCT:
+
+ ```js
+ openmct.install(openmct.plugins.CouchDB("http://localhost:5984/openmct"));
+ ```
+
+# Validating a successful Installation
+
+1. Start Open MCT by running `npm start` in the `openmct` path.
+2. Navigate to <http://localhost:8080/> and create a random object in Open MCT (e.g., a 'Clock') and save. You may get an error saying that the object failed to persist - this is a known error that you can ignore, and will only happen the first time you save - just try again.
+3. Navigate to: <http://127.0.0.1:5984/_utils/#database/openmct/_all_docs>
+4. Look at the 'JSON' tab and ensure you can see the specific object you created above.
+5. All done! 🏆
diff --git a/src/plugins/persistence/couch/couchdb-compose.yaml b/src/plugins/persistence/couch/couchdb-compose.yaml
new file mode 100644
index 000000000..40e58ff0a
--- /dev/null
+++ b/src/plugins/persistence/couch/couchdb-compose.yaml
@@ -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:
diff --git a/src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh b/src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
new file mode 100644
index 000000000..4fbc50d4e
--- /dev/null
+++ b/src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
@@ -0,0 +1,3 @@
+#!/bin/bash -e
+
+sed -i'.bak' -e 's/LocalStorage()/CouchDB("http:\/\/localhost:5984\/openmct")/g' index.html
diff --git a/src/plugins/persistence/couch/setup-couchdb.sh b/src/plugins/persistence/couch/setup-couchdb.sh
new file mode 100644
index 000000000..f07fc9470
--- /dev/null
+++ b/src/plugins/persistence/couch/setup-couchdb.sh
@@ -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
diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js
index 48449f691..c39e6c322 100644
--- a/src/plugins/plugins.js
+++ b/src/plugins/plugins.js
@@ -32,7 +32,7 @@ define([
'./autoflow/AutoflowTabularPlugin',
'./timeConductor/plugin',
'../../example/imagery/plugin',
- '../../example/faultManagment/exampleFaultSource',
+ '../../example/faultManagement/exampleFaultSource',
'./imagery/plugin',
'./summaryWidget/plugin',
'./URLIndicatorPlugin/URLIndicatorPlugin',
diff --git a/src/plugins/timeConductor/ConductorHistory.vue b/src/plugins/timeConductor/ConductorHistory.vue
index a91accfa2..8df1925e8 100644
--- a/src/plugins/timeConductor/ConductorHistory.vue
+++ b/src/plugins/timeConductor/ConductorHistory.vue
@@ -39,7 +39,7 @@
const DEFAULT_DURATION_FORMATTER = 'duration';
const LOCAL_STORAGE_HISTORY_KEY_FIXED = 'tcHistory';
const LOCAL_STORAGE_HISTORY_KEY_REALTIME = 'tcHistoryRealtime';
-const DEFAULT_RECORDS = 10;
+const DEFAULT_RECORDS_LENGTH = 10;
import { millisecondsToDHMS } from "utils/duration";
import UTCTimeFormat from "../utcTimeSystem/UTCTimeFormat.js";
@@ -79,16 +79,14 @@ export default {
* @timespans {start, end} number representing timestamp
*/
fixedHistory: {},
- presets: []
+ presets: [],
+ isFixed: this.openmct.time.clock() === undefined
};
},
computed: {
currentHistory() {
return this.mode + 'History';
},
- isFixed() {
- return this.openmct.time.clock() === undefined;
- },
historyForCurrentTimeSystem() {
const history = this[this.currentHistory][this.timeSystem.key];
@@ -96,7 +94,7 @@ export default {
},
storageKey() {
let key = LOCAL_STORAGE_HISTORY_KEY_FIXED;
- if (this.mode !== 'fixed') {
+ if (!this.isFixed) {
key = LOCAL_STORAGE_HISTORY_KEY_REALTIME;
}
@@ -108,6 +106,7 @@ export default {
handler() {
// only for fixed time since we track offsets for realtime
if (this.isFixed) {
+ this.updateMode();
this.addTimespan();
}
},
@@ -115,28 +114,35 @@ export default {
},
offsets: {
handler() {
+ this.updateMode();
this.addTimespan();
},
deep: true
},
timeSystem: {
handler(ts) {
+ this.updateMode();
this.loadConfiguration();
this.addTimespan();
},
deep: true
},
mode: function () {
- this.getHistoryFromLocalStorage();
- this.initializeHistoryIfNoHistory();
+ this.updateMode();
this.loadConfiguration();
}
},
mounted() {
+ this.updateMode();
this.getHistoryFromLocalStorage();
this.initializeHistoryIfNoHistory();
},
methods: {
+ updateMode() {
+ this.isFixed = this.openmct.time.clock() === undefined;
+ this.getHistoryFromLocalStorage();
+ this.initializeHistoryIfNoHistory();
+ },
getHistoryMenuItems() {
const history = this.historyForCurrentTimeSystem.map(timespan => {
let name;
@@ -203,8 +209,8 @@ export default {
currentHistory = currentHistory.filter(ts => !(ts.start === timespan.start && ts.end === timespan.end));
currentHistory.unshift(timespan); // add to front
- if (currentHistory.length > this.records) {
- currentHistory.length = this.records;
+ if (currentHistory.length > this.MAX_RECORDS_LENGTH) {
+ currentHistory.length = this.MAX_RECORDS_LENGTH;
}
this.$set(this[this.currentHistory], key, currentHistory);
@@ -231,7 +237,7 @@ export default {
.filter(option => option.timeSystem === this.timeSystem.key);
this.presets = this.loadPresets(configurations);
- this.records = this.loadRecords(configurations);
+ this.MAX_RECORDS_LENGTH = this.loadRecords(configurations);
},
loadPresets(configurations) {
const configuration = configurations.find(option => {
@@ -243,9 +249,9 @@ export default {
},
loadRecords(configurations) {
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) {
let format = this.timeSystem.timeFormat;
diff --git a/src/plugins/timelist/inspector/TimelistPropertiesView.vue b/src/plugins/timelist/inspector/TimelistPropertiesView.vue
index 57a1747b7..986325f99 100644
--- a/src/plugins/timelist/inspector/TimelistPropertiesView.vue
+++ b/src/plugins/timelist/inspector/TimelistPropertiesView.vue
@@ -32,7 +32,7 @@
<div
v-if="canEdit"
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
class="c-inspect-properties__label"
title="Sort order of the timelist."
diff --git a/src/plugins/timelist/plugin.js b/src/plugins/timelist/plugin.js
index 8597f5a54..11818a009 100644
--- a/src/plugins/timelist/plugin.js
+++ b/src/plugins/timelist/plugin.js
@@ -33,28 +33,16 @@ export default function () {
description: 'A configurable, time-ordered list view of activities for a compatible mission plan file.',
creatable: true,
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) {
domainObject.configuration = {
sortOrderIndex: 0,
- futureEventsIndex: 0,
+ futureEventsIndex: 1,
futureEventsDurationIndex: 0,
futureEventsDuration: 20,
currentEventsIndex: 1,
currentEventsDurationIndex: 0,
currentEventsDuration: 20,
- pastEventsIndex: 0,
+ pastEventsIndex: 1,
pastEventsDurationIndex: 0,
pastEventsDuration: 20,
filter: ''
diff --git a/src/plugins/timelist/pluginSpec.js b/src/plugins/timelist/pluginSpec.js
index 1b117e00f..a7bc9e2e3 100644
--- a/src/plugins/timelist/pluginSpec.js
+++ b/src/plugins/timelist/pluginSpec.js
@@ -95,14 +95,12 @@ describe('the plugin', function () {
originalRouterPath = openmct.router.path;
mockComposition = new EventEmitter();
- mockComposition.load = () => {
- mockComposition.emit('add', planObject);
-
- return Promise.resolve([planObject]);
+ // eslint-disable-next-line require-await
+ mockComposition.load = async () => {
+ return [planObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
-
openmct.on('start', done);
openmct.start(appHolder);
});
@@ -268,6 +266,8 @@ describe('the plugin', function () {
});
it('loads the plan from composition', () => {
+ mockComposition.emit('add', planObject);
+
return Vue.nextTick(() => {
const items = element.querySelectorAll(LIST_ITEM_CLASS);
expect(items.length).toEqual(2);
@@ -319,6 +319,8 @@ describe('the plugin', function () {
});
it('activities', () => {
+ mockComposition.emit('add', planObject);
+
return Vue.nextTick(() => {
const items = element.querySelectorAll(LIST_ITEM_CLASS);
expect(items.length).toEqual(1);
@@ -370,6 +372,8 @@ describe('the plugin', function () {
});
it('hides past events', () => {
+ mockComposition.emit('add', planObject);
+
return Vue.nextTick(() => {
const items = element.querySelectorAll(LIST_ITEM_CLASS);
expect(items.length).toEqual(1);
diff --git a/src/plugins/timelist/timelist.scss b/src/plugins/timelist/timelist.scss
index eea87d114..6ee7a50ab 100644
--- a/src/plugins/timelist/timelist.scss
+++ b/src/plugins/timelist/timelist.scss
@@ -32,6 +32,12 @@
.c-list-item {
/* Time Lists */
+ td {
+ $p: $interiorMarginSm;
+ padding-top: $p;
+ padding-bottom: $p;
+ }
+
&.--is-current {
background-color: $colorCurrentBg;
border-top: 1px solid $colorCurrentBorder !important;
diff --git a/src/ui/layout/Layout.vue b/src/ui/layout/Layout.vue
index ee5296436..87db0617c 100644
--- a/src/ui/layout/Layout.vue
+++ b/src/ui/layout/Layout.vue
@@ -53,6 +53,7 @@
type="horizontal"
>
<pane
+ id="tree-pane"
class="l-shell__pane-tree"
handle="after"
label="Browse"
diff --git a/src/ui/layout/mct-tree.vue b/src/ui/layout/mct-tree.vue
index 220f82b42..9bf1b87a4 100644
--- a/src/ui/layout/mct-tree.vue
+++ b/src/ui/layout/mct-tree.vue
@@ -41,6 +41,8 @@
<div
ref="mainTree"
class="c-tree-and-search__tree c-tree"
+ role="tree"
+ aria-expanded="true"
>
<div>
@@ -467,7 +469,7 @@ export default {
}
},
scrollEndEvent() {
- if (!this.$refs.srcrollable) {
+ if (!this.$refs.scrollable) {
return;
}
@@ -576,14 +578,17 @@ export default {
};
},
addTreeItemObserver(domainObject, parentObjectPath) {
- if (this.observers[domainObject.identifier.key]) {
- this.observers[domainObject.identifier.key]();
+ const objectPath = [domainObject].concat(parentObjectPath);
+ 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,
'name',
- this.updateTreeItems.bind(this, parentObjectPath)
+ this.sortTreeItems.bind(this, 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) {
return '/browse/' + [...objectPath].reverse()
.map((object) => this.openmct.objects.makeKeyString(object.identifier))
diff --git a/src/ui/layout/search/GrandSearchSpec.js b/src/ui/layout/search/GrandSearchSpec.js
index 32d719b11..5235fc2b3 100644
--- a/src/ui/layout/search/GrandSearchSpec.js
+++ b/src/ui/layout/search/GrandSearchSpec.js
@@ -42,6 +42,8 @@ describe("GrandSearch", () => {
let mockAnotherFolderObject;
let mockTopObject;
let originalRouterPath;
+ let mockNewObject;
+ let mockObjectProvider;
beforeEach((done) => {
openmct = createOpenMct();
@@ -55,6 +57,7 @@ describe("GrandSearch", () => {
mockDomainObject = {
type: 'notebook',
name: 'fooRabbitNotebook',
+ location: 'fooNameSpace:topObject',
identifier: {
key: 'some-object',
namespace: 'fooNameSpace'
@@ -75,6 +78,7 @@ describe("GrandSearch", () => {
mockTopObject = {
type: 'root',
name: 'Top Folder',
+ composition: [],
identifier: {
key: 'topObject',
namespace: 'fooNameSpace'
@@ -83,6 +87,7 @@ describe("GrandSearch", () => {
mockAnotherFolderObject = {
type: 'folder',
name: 'Another Test Folder',
+ composition: [],
location: 'fooNameSpace:topObject',
identifier: {
key: 'someParent',
@@ -92,6 +97,7 @@ describe("GrandSearch", () => {
mockFolderObject = {
type: 'folder',
name: 'Test Folder',
+ composition: [],
location: 'fooNameSpace:someParent',
identifier: {
key: 'someFolder',
@@ -101,6 +107,7 @@ describe("GrandSearch", () => {
mockDisplayLayout = {
type: 'layout',
name: 'Bar Layout',
+ composition: [],
identifier: {
key: 'some-layout',
namespace: 'fooNameSpace'
@@ -125,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);
- const mockObjectProvider = jasmine.createSpyObj("mock object provider", [
+ mockObjectProvider = jasmine.createSpyObj("mock object provider", [
"create",
"update",
"get"
@@ -146,6 +163,8 @@ describe("GrandSearch", () => {
return mockAnotherFolderObject;
} else if (identifier.key === mockTopObject.identifier.key) {
return mockTopObject;
+ } else if (identifier.key === mockNewObject.identifier.key) {
+ return mockNewObject;
} else {
return null;
}
@@ -168,6 +187,7 @@ describe("GrandSearch", () => {
// use local worker
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
openmct.objects.inMemorySearchProvider.worker = null;
+ await openmct.objects.inMemorySearchProvider.index(mockTopObject);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject);
await openmct.objects.inMemorySearchProvider.index(mockDisplayLayout);
await openmct.objects.inMemorySearchProvider.index(mockFolderObject);
@@ -196,6 +216,7 @@ describe("GrandSearch", () => {
openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;
openmct.router.path = originalRouterPath;
grandSearchComponent.$destroy();
+ document.body.removeChild(parent);
return resetApplicationState(openmct);
});
@@ -203,25 +224,62 @@ describe("GrandSearch", () => {
it("should render an object search result", async () => {
await grandSearchComponent.$children[0].searchEverything('foo');
await Vue.nextTick();
- const searchResult = document.querySelector('[aria-label="fooRabbitNotebook notebook result"]');
- expect(searchResult).toBeDefined();
+ const searchResults = document.querySelectorAll('[aria-label="fooRabbitNotebook notebook result"]');
+ 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 () => {
await grandSearchComponent.$children[0].searchEverything('S');
await Vue.nextTick();
- const annotationResult = document.querySelector('[aria-label="Search Result"]');
- expect(annotationResult).toBeDefined();
+ const annotationResults = document.querySelectorAll('[aria-label="Search Result"]');
+ expect(annotationResults.length).toBe(2);
+ expect(annotationResults[1].innerText).toContain('Driving');
});
it("should preview object search results in edit mode if object clicked", async () => {
await grandSearchComponent.$children[0].searchEverything('Folder');
grandSearchComponent._provided.openmct.router.path = [mockDisplayLayout];
await Vue.nextTick();
- const searchResult = document.querySelector('[name="Test Folder"]');
- expect(searchResult).toBeDefined();
- searchResult.click();
+ const searchResults = document.querySelectorAll('[name="Test Folder"]');
+ expect(searchResults.length).toBe(1);
+ expect(searchResults[0].innerText).toContain('Folder');
+ searchResults[0].click();
const previewWindow = document.querySelector('.js-preview-window');
- expect(previewWindow).toBeDefined();
+ expect(previewWindow.innerText).toContain('Snapshot');
});
});
diff --git a/src/ui/layout/tree-item.vue b/src/ui/layout/tree-item.vue
index 7283e3bd3..5c0712f95 100644
--- a/src/ui/layout/tree-item.vue
+++ b/src/ui/layout/tree-item.vue
@@ -1,7 +1,9 @@
<template>
<div
- :style="treeItemStyles"
class="c-tree__item-h"
+ role="treeitem"
+ :style="treeItemStyles"
+ :aria-expanded="(!activeSearch && hasComposition) ? (isOpen || isLoading) ? 'true' : 'false' : undefined"
>
<div
class="c-tree__item"