From 90662ce4a77f31774c39289e85cdf36285843e5e Mon Sep 17 00:00:00 2001 From: Shefali Joshi Date: Wed, 24 Aug 2022 11:08:17 -0700 Subject: [PATCH] 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 * [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 * 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 * [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 * [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 Co-authored-by: Vitor Henckel * 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 * 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 * [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 Co-authored-by: Scott Bell Co-authored-by: John Hill Co-authored-by: Jesse Mazzella * [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 * [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 * 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 * 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 * 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 Co-authored-by: Khalid Adil Co-authored-by: Jesse Mazzella Co-authored-by: John Hill Co-authored-by: Charles Hacskaylo Co-authored-by: Andrew Henry Co-authored-by: Scott Bell Co-authored-by: Alize Nguyen Co-authored-by: Jamie V Co-authored-by: Vitor Henckel Co-authored-by: Jesse Mazzella --- .github/workflows/e2e-couchdb.yml | 37 +++ e2e/appActions.js | 19 +- e2e/helper/addInitExampleFaultProvider.js | 28 ++ .../addInitExampleFaultProviderStatic.js | 30 ++ e2e/helper/addInitFaultManagementPlugin.js | 28 ++ e2e/helper/faultUtils.js | 277 ++++++++++++++++++ e2e/tests/functional/couchdb.e2e.spec.js | 108 +++++++ .../functional/moveAndLinkObjects.e2e.spec.js | 212 ++++++++++++++ e2e/tests/functional/moveObjects.e2e.spec.js | 148 ---------- .../displayLayout/displayLayout.e2e.spec.js | 64 ++++ .../faultManagement.e2e.spec.js | 237 +++++++++++++++ .../imagery/exampleImagery.e2e.spec.js | 36 ++- .../notebook/restrictedNotebook.e2e.spec.js | 27 +- .../plugins/notebook/tags.e2e.spec.js | 19 +- .../autoscale-canvas-panned-chrome-darwin | Bin 18929 -> 16116 bytes .../autoscale-canvas-panned-chrome-linux | Bin 18524 -> 15770 bytes .../autoscale-canvas-prepan-chrome-darwin | Bin 21763 -> 18406 bytes .../autoscale-canvas-prepan-chrome-linux | Bin 21375 -> 18071 bytes .../timeConductor/timeConductor.e2e.spec.js | 20 ++ .../plugins/timer/timer.e2e.spec.js | 17 +- e2e/tests/functional/search.e2e.spec.js | 13 +- e2e/tests/functional/tree.e2e.spec.js | 138 +++++++++ .../visual/components/tree.visual.spec.js | 101 +++++++ .../visual/faultManagement.visual.spec.js | 78 +++++ .../exampleFaultSource.js | 47 +-- .../pluginSpec.js | 0 example/faultManagement/utils.js | 76 +++++ package.json | 5 +- src/api/objects/InMemorySearchProvider.js | 58 ++-- src/api/objects/ObjectAPI.js | 7 +- .../charts/scatter/ScatterPlotView.vue | 9 +- .../components/DisplayLayout.vue | 14 +- .../components/TelemetryView.vue | 8 +- src/plugins/displayLayout/pluginSpec.js | 54 ++++ .../FaultManagementListView.vue | 28 +- src/plugins/faultManagement/pluginSpec.js | 57 +++- .../imagery/components/ImageryView.vue | 15 +- .../interceptors/missingObjectInterceptor.js | 5 +- src/plugins/linkAction/LinkAction.js | 22 +- src/plugins/move/MoveAction.js | 26 +- src/plugins/myItems/pluginSpec.js | 18 +- src/plugins/persistence/couch/.env.ci | 5 + src/plugins/persistence/couch/README.md | 145 +++++++-- .../persistence/couch/couchdb-compose.yaml | 14 + ...ace-localstorage-with-couchdb-indexhtml.sh | 3 + .../persistence/couch/setup-couchdb.sh | 125 ++++++++ src/plugins/plugins.js | 2 +- .../timeConductor/ConductorHistory.vue | 32 +- .../inspector/TimelistPropertiesView.vue | 2 +- src/plugins/timelist/plugin.js | 16 +- src/plugins/timelist/pluginSpec.js | 14 +- src/plugins/timelist/timelist.scss | 6 + src/ui/layout/Layout.vue | 1 + src/ui/layout/mct-tree.vue | 53 +++- src/ui/layout/search/GrandSearchSpec.js | 76 ++++- src/ui/layout/tree-item.vue | 4 +- 56 files changed, 2181 insertions(+), 403 deletions(-) create mode 100644 .github/workflows/e2e-couchdb.yml create mode 100644 e2e/helper/addInitExampleFaultProvider.js create mode 100644 e2e/helper/addInitExampleFaultProviderStatic.js create mode 100644 e2e/helper/addInitFaultManagementPlugin.js create mode 100644 e2e/helper/faultUtils.js create mode 100644 e2e/tests/functional/couchdb.e2e.spec.js create mode 100644 e2e/tests/functional/moveAndLinkObjects.e2e.spec.js delete mode 100644 e2e/tests/functional/moveObjects.e2e.spec.js create mode 100644 e2e/tests/functional/plugins/faultManagement/faultManagement.e2e.spec.js create mode 100644 e2e/tests/functional/tree.e2e.spec.js create mode 100644 e2e/tests/visual/components/tree.visual.spec.js create mode 100644 e2e/tests/visual/faultManagement.visual.spec.js rename example/{faultManagment => faultManagement}/exampleFaultSource.js (55%) rename example/{faultManagment => faultManagement}/pluginSpec.js (100%) create mode 100644 example/faultManagement/utils.js create mode 100644 src/plugins/persistence/couch/.env.ci create mode 100644 src/plugins/persistence/couch/couchdb-compose.yaml create mode 100644 src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh create mode 100644 src/plugins/persistence/couch/setup-couchdb.sh diff --git a/.github/workflows/e2e-couchdb.yml b/.github/workflows/e2e-couchdb.yml new file mode 100644 index 0000000000..c6ecf3eab1 --- /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 e4fbf75899..c7a4428d5d 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 0000000000..1bf7b02096 --- /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 0000000000..fc7ec53979 --- /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 0000000000..4f1c396fa4 --- /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 0000000000..819c4b42b9 --- /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 0000000000..7e8d539de1 --- /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 0000000000..78f20cb65f --- /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 f11f8c7b23..0000000000 --- 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 83090fc0e7..3d6456e2e0 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 0000000000..8bf08e0c9b --- /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 42e532b44c..7757c93192 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 c076329d40..568061c89d 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 c54233cee2..ada74ccad0 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 bea4d7c4089da224339623c8d77c92585ce347ef..01850a3bc4bdfbe4c9eeb71284b62423d00cad43 100644 GIT binary patch literal 16116 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

)#w62)zH@7|e?{lv-1 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 z zWn3X&OF;k9p5`@+uEm40@KR@39Z#lSs^+E)Ingc1RHUpaSa!`XQ>*5zywnJP!sB+U z0B7>7qyBnz0mkI*7eH39y^80$4g*8WaSPoVll7?*t%NH{YyQPE<*MiX$~1&D>v&`O zEcLy})>^$JIUWiHm+!Gn>>I81;kx@?+q_K;-#LXH)pFE*>h5#%v&e1EfbK|E{fZG1 zar1_rPX#9<&%j1Qcby-(M&(|a_PAj4g6!sAeHnK+exq%YjfbX`Cw1$&X-{Z3`-FX! zW^tljG;u~*;b3m2C`IICj^R?x^0a&PWk&%(N2Sh;Ragn@5GVe&vf0BX!IKp3)acH> z7Dnq7z7qZ}8my(~#QSuI&_?wdpO_FIbn;v`@ z&E4M<>OCjNmz5gU&1`J)mN>4J-bYJFxOw}@{+2tAQPI1PW=?KQvOVAQ{2}+niQv}K ziT>he;`yE?GEtieXjT4HNnP*lf!Kn6e-|IbIv*=V@-$ zm@xhYYnzP(qzT8*25prk5mO!$m)oqYQ$#-InyuLqB0OWQXr7nxC#fl~s!dJ$+H=%+ zjGfd6)#0+?W6!XgmNcsLxYZ{{TbgzK-TU6#LHZZGz44%|iE1;0nyQOelj352DQynZ zYzB(%L|&>Jof9#a^&y!nfg!{d$wxX?<9C8LRuwVN#b6dk=kKH40mtnXOU@OI2e;lw zmlt~5>%|4R=v*n(RAPTV^N#OVd!&kzH_{3evgUKRbScMYJ}|eRZ%`$G1v=;rcAd%Q zNqTT)%?henT^#QxFy!U{%eAWb_Chg)%o%^8gx;}}kMovEkl-hHMA8bp`*_O_4pge7w#PmUn2#>=^D4kKiLAE+k6a@afmbz$`JDwr&8r9hd76Z9zOn}IvG11UF zjV=p&McZ9EQO3VKZq;ULEjk6Susp;#NSG@rwqiirsNnD5G#rGTxA@6c{l|`?K1jsEJ8JI~ce{16qSAEu$*(0qG2ZZ!GqYS; zNw_T|JjdjbX9!Ha&sY5I4}mu>nzF9jJw`M*Fn>5f zR3w14svP#U$G^!Q$~kcuNt4g`F^%zPB(`2(VcTqqno`V>ttt)gid-D1*YieB8XxMj zGQ=5WF0QJk|2Y9uhYzzHRHr@v6fQQ`T)_NM0REtvx%T-PLj-p%=Q=IoKKRs?QZQW~ zU%h`f!ji2hr_*;~@3juMlU3%TM(^X{if!v0JuEJZ0dUi_7dv5*m2eCjK8nJVFwu!zpN5?4OCR){kq+sS{UYk^)hoSrx$_q zC0c25R2~}c$x@aw?q4ttA69ZMFG^arVlV9k3t6HoygGABT?Mv1f zouB4}Y~ay@1YIw9Nd8pFRhQCJz5n~#to^D5fvs|U&`_2ogzQu|ri!T) z7S1;LtXC;7N0c4@*f$6-?1FyCTDp53v6Uf_0JH%2Vf9PA2$g-~tfPT3x-3hImqpE4 z=Tr;o<@iF$oYGjL?Ukv}9ETor&_4pyHKH8;fW4^GL;tcZc*;@|O3u*hPm2NcvfGly zQP+DmZ5Bj z#J*9h&YTnc9ZOX7Dm7!>P%S8z;{!_=ClGDjI&-sJ4_HdA?WP><8r*Hz;kK|hd;mZi zZ$Z=HwXd@o5>@s3T_4=Nye>l&zJWieF}%xXa2T0DRICy;W9+-aMaVTiprFhmQZTpxvfQ=~TsWbh{mQ}&0fo#s<3`3-eT0wC(InVy-AA*0qz>b~q3Jjc(1{3@`}wfq4NvVm2vy$Y2+x9l+~nz?r5Gdi{g8 z;erBW1D2vr1vR8fYOD=9d6~B|I4u@F{Ol>bKY3Zn@RD`f+`PC-+*7!~i#wK;!Y`hJ zXR$T{Da%$TN;@NkHrvDPtD}Y<(pk{c!XO(~l1KJH`Y_O}B9B~OfOcFZ<-v#3z5GZ5> z)6>)r3;tf1*QILzHNbh&zW^%9sZYWM1_cV^+i7v_NhK*8jLNsVzvjO(jflknlV6?nQ(k~}+ni3|^KQ}lb%idlcL*)Ab`Fj;8x zKr(_9WfD;smq`ac9gMe+j50YEP}0qJ6!Y#C8QH~n@0K5+V5wPn{Eq~G~S|>2GRaR_$_smrmAS zeK6%W$>*h~#BPD*D}%qax8H=x9-%Pu!g^*`X~C9n_8lHjwNI1dThF&WL!2AWz}y)A zV*Ja!$9Mo&zpg7O(ReZ6Z$GGFD8XH8esJO_Elm7EFB2zh2J#~f#=lI}xdk-$tOL6^ zaPgrc4r+u0!*YDt;eBa$4d23PEpsY({}?AeX8fD{K}Tjeuo$JaqCd*yc7SsofwOtv z-v0Gu!f7D>n4=QHueY&`oc+LIKfW@B*g}r<2A4i;!)Ol=5d4K$kK52i5YpzGk2rF1 z0j5w|-NH4!6xU^;#Su8NTXiiwMN{$8DijmEz)OOTsiMr+BUYEU_-l=QPJkkkU_xqw86bASE?%OD^q*H9%PsBpAJy!gbxF5 z`C6bZ(seC1u_p!kcD>eB{ph^Z(WkQ0T_A3tJAvJVc&h>K)~Su z-~|^)1b+`yE#R&_PdDMq1_$Tk6BLo78g)d0*&&p`=*;pAP#swHddytf5a6FLg*}}B zjv#UH$$e{DGTT*oKGSqs`Y#z7B1S&6sZMd@G)N+`v9Ve{vXdhMxDNH=E%!PPlB4gF z>>;m4wc>Hi_Dx>AB1qd(y^YOfmsqp?4pA_IseSq6IoX(jHx;0x@Vo>+o{qcLN7v?h z)H5@*vaCRW`=-}iW$<=W6sPkP(>1VZINs1>+1+&P!x3Z8@D|m~n%*wef))k~Fmda3 zyIJuToJ-x9eHBdDx;$fx!RBTm+7S71Z_DwXuUp)eSGaUtV}(+WwH(3t%2d)zc#t6j zkSRgiz8ki}p^vCN?|YpvO(h@dth_b@xwZto4XYjdci*m~PDobe4c32t^bpf2unb~XI+l_9bZ>n9LCNg_@SOwH6 zt$5WMQ?^XZ@87R)xK+L2>}@)^GFa9`r;Oy6?v8M`4D=*!aF(@z^rdL``mNRetfo`!O*@)PJn29U+PK zT(lqKtm&-EYLKDQ$X(X-875q%FmOyfjx|1SzuL zB|X}AQ@y2Nu&Q|5t5-8!-_7M?5TS1EvxI_z>EsJ{)X3`AYyzh1&e9EBnE(Q=muD%M z7co1HF23S=Ly3K}JtnIE^JmgppSr|(FPKPxGoOq-f6={EeC0fgn(80JHJ#+e>h<04611LDxry4#5zeN(az|iqONgQlCfWHVBiwT{RPssXs!eD#j|96~HwdM~c6Qib_7kM? z5{+B5Ystrfa+k|?R3xrgKBM%477waE*zX*QxxfVFIW@T}+X&7T)Ar)S0;P^q*oDNq zzgrC5jk90Ec^N2NFT*e&_PZr)Rxz{ONmY{BUId@g%MytJo&c#Kfk+M}{e%L9XFjj` z_KjE=#iVIf%z01r=gMPRGVZTeJl5u$^7CBtDZ3(w)awN*U>s*B=3j9yG2BZ4RXHS)aj08&-Ft+MaD$>m~Gwr(__o z6vq}*XU>ng3JQRI=4l)DqpDjut6NA@;gbvzxz;H{M$!?@84K|MX*xf() zW+yjoZ+#{uDk=;aEe)vQ#A>xaN&tri`mz4eoIy)Pg_!2?ni>Hz78G+dx7j030Th`d zC+O#Nyj@FfYZ-|U0zIJEFlm9z)Ke-H5#S3F0bInr$%M~8;b-&Kh;2(fs^973HfSF# zYZJY=({M#yzLj&a~JLojKQpesq;Z7=NzC zw(6E3uWRQi7JLh{R;3#(osQY7x=nKA8y}2gn+d_I&c5P?HUj8MZCx1`!4b#kS_v1A29yZV z(a}MXEqr`ti6tvG`uq{^>WJM`UvPM?xACF#H88p7$qN8S$N6ye^R?N_gO4vH`Ggu+ zLRA4I5gn$P;nX9g0zU-@VFG!!I%)a*gmrlw5^9~KedDll(W()6emFjn71t7|!yL4+ z>M6O_>Gz}i2u6M*W+(~>39RGcqIrM!ZuwiO%$ASB#OB+t5_e@>HWd9Wpd^fk3&?kz z57wjN)`ZdG0~K##yUO6Ovc=M3b6n3 zir^`a%Q34gFYEi7-gd8(j2`rSKSdx$*p{t5XsDF>F|t_#y>6d&aH3S1PDx3L>O#4w zJ5MGZ{*@#B`_w3#h{KQr97`PkwdFBS(yj&+#tiW&qxVEFYpTS>-H|HZ&rN+n#cAcP zmDP0xg*+L(fjZ6dp;fv_pHUTFf;;FY6T^j0bo#X_BcbAD!ZyM0=tfN}+NoC`_7%Iv){-kA#2m zogdwtJ*t2$<4_Yj1qxDv)YGl$;Rj*6f!S~w3Uc*Tm)hS~0H8bv??Q%*U1e--+_Rx& zY+VMr+`kUdt!E?)S)U!usFe%fvW^QwqCQ=f1{!bK=AL%Yf+7FobBA}Hzq5*iDCfTa z>1(RqA*u$(}tCJI8nur(H?QpLmG8#fK2rhHq=@tFsI3=V3% z4|Ti2$54UJgid50Bhb+wJ@th5jIEXi)o!++2`F83$t*OZx>RC6P_gS$hT{9G)SMx0 zG)y3`zZ773S1*aamD|zLp*=) zVjcdId$E%6UKzVudYZzPCYuhh7H1@O^23)eH>llrda0s96Us=p%y@ygh#SfYCACzv z!eDGVz!bn9t=uCWFUrcMUb9ZqNg#vCq%GMXQ~AdWK!w8toDJT2N7!28Q7kRX8J_x( znw#d{Hc&$}W6DLtYGurW_Z2&6SbtX80Qt}NcD&9nv3;+XGs@(`!43_`>j?rZz4h_* zzfZ;iQ_YLG!NB@UhtLmz@cwf1L#)Gdx)y#e1^9|A) zA;c{z$xw4!Y)=se0}MkTsq06Z(&bFBAYfCBK#Ag%;bq@uvQr_%sbUoT-Ey!mbQ%zcUWfRdTFjP63k0-^9q&)+#L`O84!@V#};a%{%A{NY_X z3x?HOe1A0#T!%DvO4*WF7;lJQlYcL$RpJcHa5H zgsJK&wNZ=f)YN8=@ug~mS}#|qr3?et=bF-FQA2>>ATvUMyLRgxRY9E5P(=xYQaS80 z{AHgU%~dY(l=YY%^^iwL_aPnJPL_?#E0$M|&urwmw^y3<1OfeG=@T~0UMH?4w598U)*7CQeF^UXKU%gNPC~EavF`B#ouS_m98B*>CQ?s31%dV!%Ni*` z%!okv3bOvvf)S5{Z(KA~80N1H`|c40$yGy1d$v0K=A2r9HLpBF?SnGG+VTE^KF)&{ zQ$AmV=ns~Y66{I1U%6VhDnksw(7!rS_pld&H9Fss#BYX$KBkO%&Df@y)R}`lWI|E~ zg@!j6c;AU$zgnulsa!VJ@Y=}2^qGJ)uuGW^u|vuTD4{DL7hf!_1I~uzvXJ?Q#{3{+ zLV_f0)&O`$?bkt>IWv?fQo3f;L!gW=(q0zg815@7Is)Q*mo?;43QXc4I;X`9&4br- zF{xtIFamTt@Z1kI-7b6o2YOI#xNlMd8dfN1$9z^z?i2X^7rQjw~5mfCao4D|D{9$C4NW+K#A zY;-!rgzC`1xnzB%Mn$^r>}(k#4zT3J5r7x9rjVPpxkXd^1;Vizahz`()2&OGDboGf zNBeKro~a!0V!i5d%DW@i7p4?h-?t0o+FUq~WnsuTNs+)fhh4mnaRF`)f`UIvMGL&^ zMirTBqL75<{@i}5Z}pn0{=(D<+13i2Cuzn-%1Z3=<>?}^{fK^Gr`PTb3Vzqa#VPal zAo!njNKBf4wVK10r5O5$Q<=%`eE0Oe#;2o~H zEL6o);ZGEIvqbTDtNQU(g4z|Uy@7>FTF1oqzh*U3(t2iMcmJ0sP(zKK9pp%Fbpiy* zR+ji-myXw~KZu?4m9J$B7@GdLEMcI1>m|s-_cN6{zvD1#Rz^wfn6+Ui zTpkT{prRe!*n6?}<@DZ>21L2*J^#~@2PJ{!4qS`8l?%G4WXkrPIolE!#pe_G;-5At z^)fyls%b`xf&gqf(&Z}zXPq_xBe+|rMsu!3Vhm`#Go7juLKk7soz-*t$yu_~(l5!8 zRJ@KUB{+5FYDVI+fM%CYsC{1(+&Uy>i|s$7iX6BimvxqkvD7;^hLLdjR*2#c09gy4 zqg1A~6Rz-cg*i-^lrniiYzx&$*Bz*BEkNmbfNQ!rFvATn|$IhHq^jzm9rm< z&mGPKPpWn%`brzZigrdG6ku_jy1>6Pi-h(&< zEeQIYC!MX^HRp)k4n4uGSOG|Dy0ZzwhQ}p-z~%hu<#>9RPUvy(>HQ7Q>3@Hy^br!} zZiEs7gj|n$(qLG8|I|23dl(RUHDs-)u(n5`3K}mAsOs_71EnlwRUV7fp@3iOdCbXW ztlzEw`xOz;LG&JgtpPq4|L3!AK|}i6wJLAAshFrswu%R`&`9h4?(4dEVdDEfW%YfC zgVscXa@-XI^!K<-2#}U6`02%m`cLoQl~(65(wshQ02xVjyT^j@ojd!#{<^$}YDs0E zL1^@@l*-;7ewaFyRx#GHDu>}~X+3Ei-|1-)lY){FD<~{+v=r-C`@1=nz{QeDoblj1 zQ2huJh|ItY6V| z4p()}_w=7Q1mdy9PHjJMx8<(?@4!VUm)znbmgRPjb2eo4{a}dj$f;MKemFdRq|KpH zf53_o8|P7}c!8jsB~&~MC0iu|w#{D~r9ztZ7d^uXMJMzR{|qKJEO}6H{yQ`PH`HPC zH3&Xu6$^H{XmHtAMafN8#~YMd5EoOa(lx?XfUv`;~n5V&l0}PM0Q4MyXktskJ5rI-KfxSx^oEuwZwXK{Du*cd-6;j4A{% za|p6-p#sH*8Ulft_#jyz?^>-bYbAE&%Bgo^3)x_Gi`q$z?-QbnL3kZF1Qtn`)dj|hwh8dcj{YI*xx?E5V6B0wDI3>oCGF_|nt zZJK-DOz)E;mLax!a~*+ko}{_URHEosDCbc4Fc}h{oGaDC>MY^M50q;wL-CbPiCKB` z`5#tFwI4vUM^Et3_s0MZl^Zot=?b)bM*WaH+-b zNVXt**UW)ZD(}xxLMoOa4+_lG)Ik2x&J^Gr1`2M9aK}Tc{eSM*3%#KRu32g{xr7ozmo73mrv$U5(O zzcBUPPPk%J{hYyuw}vFwU7~nO@>IwTj!Xw}o27Ad4iHN2w){TCq1*w7u<5?no4vhv z=01G=8XXw8U{pMD9W%(o)lY~i6Dhd963R7=ZF0726Y%*KC9J(#Q@*ljL}*K9^9&9Q zwAn;#-+o%RY~NiLHZhQ5=&>mq6xa-Iyqq)4*f#LC8-D6-O0BZ{gwfj5wPkJ$5J~Q+ z&V~ZD`L$QyY%NJfpiupqbFyVxIyDj;P@Aqeb6Bi5r_rar6UM@akDRD1J})bC##(0G zQXeRTpx>7JH3?dtQTGztO}lcwr#Li!w?pAV0FIHN$4FlXY_TMD!(-C?y+2@z{^p8> zwkGXs_n2css*c+-z7nir+o@Y8QIjEE2BN{>l1*QYX9+)>Y=Z$`I&t*aDGz#9Ha4i? zQtKTa#P|1D1vQJG4ieHwH99M3#XdD!Nr9XyLa z)#IXz2sF3D%B((UI5YqH8Jhoim^25ZNwa+={AI#{hPM6g;MPCXo(X;-fU0mM{@~s} zs49gRoIL(fUua;=Nix>pWK4@G)Ha|KR(r^xPo(rgoVJM{&-VpD$14*9gA(Ep3${oN zIDTmUw^+Y>YWG%6^hqDLr67d|SKUsO3ctNQ-dA}pjOlu+X&yljpv%uW!BD=U76gfb1$ z(g%9Sn@m8LeJEY8-H67a2SUDr*!&FkW& zOKaGh$pZDD^u~M!2>Jqa)cq*BJCzh!VA~B3rd@Gd5PM3+ZkwZ}uPz&&eOA#`oN2~a@V>ilz~5zfr;YD+#x^z>8 zz*M=Z0#Kgu(jI8}$v%p9TF-l(QJaXBEzm1|I?tt*JDsHl5S{RZWRlhQThH=fgkp0~l{s0s-})JqXKCHL-IQcUI;ZpWHumj( zXBlfxt-RE$+Rp+(lzj&J0OBgcjN0wrnfo@27^~# z=^zWizYv4Q^llplkdsq?q=k)c!q01)r?0pQ{P@83+#YVTv#^z+3u(9$mI{pNx>u8W zPoIbb%UyhEcnMZ#E++$DwInyn)++lhY7!y{$wqt})$Tbk+uH?N*`@H%Te%*k(=~HM z#m#03U$h_>&$aR%>b@2qx+~JpQ9#e=Ca#v^da_jzwLB1hqm5p*C^|-ZH7T;FJD6gX zY6X5$z>dt4uIu;^eee9)_Kqp}u@N7>kKmagL71T4|cU*g-oi%JSK;sHXb13$PS zf#B=e`Vv!buzEQ<{6txhd@gGEZXx3;Ve}gvO-jQRLl^boE0?un{mQNJS|NOjE}6PF zf&&N2=888*1SV=EZkA}!^~MVaC4RsC>}V}vs8C*BhKfUdn8r-$_G32)VgXBj&Ny%c zq(k{Mc$-LImlm`>$Y<}r=4~&_!{s?xp|qUcwcG7Y$g;J2PcEd@E}wz+SCxS%7gATK zqO3R!|A^g7ZXU##*v)^NnZf54*hR$4PCY`leesm$DOJ8W8-r{?iSOXqq znTG*=CWBZsQ{!F5;msm%%@&6(>=`0QGJA{#tp9W{3M2S@tJmkYRx|zhTj#93vo=RV zer#efhe;8kl+@Bwt^pK2*D$MA5o{yq`r0Pkre0Rb&{;C4$rx(^K)~`6MN=r-jGya9 zbp3#>lhz2A`wvcEz8ALpsL(!yOOVflwEY1k$DnE%C=;s{R{~f}zKjO#V76+JLjJm! z&s74uhY;^%y6$EDOEUp}AqG<*?|T$}AxZkd*X!JNWua$Jl-#3nI-$W>6i>yhg>`Rui7ebMBQnC)R?@KBB5Rzn2*=bC+k(e3T_npCz zj4fNTufubWuIsv=`*%FY@%#UI?!V^n`JCtbe6O$fa(+bL)7PRu&2gHHjEr7KTiu9^ zjG~*2jC`Gz8az3u)wTyeq zyFjVU|3&rsJKk|hn{#xWPiQ!?BQaf%SvyUtZHGnN$}DuQa3ae~7LRvKr%MhH?w|DK5G30>% zIvN2W6IodJ-Fzb$R7N!IuI7!jyIwrQf2@a`yjb{#oE{Vn72Us*5~w<*{d6b;gxx?=ohZGQ9iXpMn?9DSXxldCG&ub8G1sBeHx;DaJ**0l`PzG*p(w@gQ8mLACYrtP zcbl5VdY%w>*xt+)^1ajwTH!;Xm>~Fy7GG;vx%S~dT_4z-*EY_~ZLIIrUNx`Kj^|cn z?Kn6^Mkb)MrQFT4TSqhVyJq!mF%lE-GYjoFAY}ISnN7jY*Q5QVrDJr^o`)GW1a0Zj zCRpY%+y12rHx$DnIupU^`dab#*W1d(1%nQkstF;Sbs!=xF0OIEn$7ar^Pu(QtT!J& ze$<}yg+lNC#vJ_KKgbL{+Vj|%@QsW&kjT!?F2$%MY5W(nrdv^-E^1>yN4F-7dsS)x zAJvOR#kM=YC-g&613R@_2)7hqXgb!buf3OsrGs52F~RG98qRhM4&MI|xU}uh$H!*n zl{oG<4NK~opG~vROw1xK+po77gc!-Vj$cl-^oUm4{Tc4D`)8_E89A{tv~3u_NVJ%& zn(P5ar{$k~H_D^nrLDdx&<&eh-M5d3h{$<(b@*8E*SyPEh0c$o?JC7kSE1yE{hORJ z(AT=Td;2p4Ot`cRK*YzEEXHWJm!kUmX=;;Vlu0A zN{Nd4Y*wzRo0|mp9ya6S^O$175i7WQW0 zoW8%H05%_~)t){{I{>GN5k))xbQ zeT;{!ZXQ+r2yh+<`CiG?v$cfl&|tH=k39l{wDTt@^=~K#1Y2k8rOQ+zav6+^*4jmp zgU5kwNrJ$4Q(HHBR3!NL+%S82%cJF5;@CqE|II$Fz>tWJc`LuESNfBI->KMIGj9go z3bmg8E{Mb)edAK}xx2kODZ%GCD?Oj#YfW6L%u;MUaGUt{KwW*f#7b0l!Yesws~D2< zxb!kukQp??&y#rYyT(92LqRY__f---85v^@D%w+6T*veEsq`5@Ej;${DY4M~uBhe_#JMkEe z7Xavf>|l0=AS#=^xAkfqKU<^CU2E5x_m^D3F_n`#E)`!NUezsq_?gGf=t>IT9@_g8 zag*=m?f(ij+1{v)ALziRd+W0|C%qm&{pBMITqsXNumwecNMwM#q1n zOLhs@($d0j732f{-s|?o@besuwAl82($EVn!Dp>(ZHzx_p!Yc3b@xA9Yr%&E1EMz+ z6-CR-%h4e(QCWB=C#Rx?9pBj$)AmB+eA}MPo9p#-60I#NNA6~&k6*#%-1#-f@ ztmC~b-$ThB<%7>k2g~L9tM~~B?gQVo_3=T4)XI>N zsajINw)NHoAO%CO6f( zNTT+$848+a>E6V$-hW3zW8I9;Q}Al#D;OVNLqkKE|Aw7E9{)qgKzYv=Z71K}1FdkK zP}u8@R2JA7_cRdT|6Vojac5vD0oYp)sj~b1Y)q>qY-93hWisMO2_UrVeK#%KZgBWW z{O{h6x7SzPLM>dYXIafE91=HvGiG4sqlGhHlN?w}W#4cV6C4<93Im|lR`X*^TqH%e zEcjp>%AUakHl+mMe}=w525 z2%l$-W9>02eZW0&`@JEvUU9Nx7Fa)Ko3G=bZ?fiXa+obYdWaw^WtFnZvutDZ|ro^-~cBVAW?-H!BdPwISG@ zhD>P!$l5>Nj+{9QU$%6>qY`3?0kGaCKM&`h&kQWFEjN>Y_EWXs;)ALl#b-am z#(gJ)J@#f(JKjq>+uGWy=jG+i0C0N4X6^S8Ihnnb1y+YCfjH>K%HnpiKz43x(-BGS zFhZC-1s<~CZCN34PgSlFC;r%(bFES@Fz7*ooXYmdljvZd-hr(EdA~m&xRm^^_3td_ z+`rQHd!ptkNH?eUYTp+Yiuli^+4F@?5)XDdWHD>7{UzBSe%TAsp!DD^xaht;yYZQ* z1Q2rj5vZz^dEK{v5)q-9!;NMe_nGSXF+NNdplWd$n@)EL`Y8R}{egI@M$XPr9p=I& zDOb|~^=RT7t9?{82alZ(;c`c&c$<&XYt!?6d;Z}^L@H?og)v-KIM4r9`N*U=-M_-p zW>@Yp)5-)=!D1BSTwdU|?82|*>?ZJj;YJD`O5eFC6QtYMxm0g8)Og6irw(&D0` zYPZLngA*vWjcSgEaqfpoqs!xA=#(t`-mKKdNx)86$kWrWZsFrdMTt;yp|EUX(8Gp6 zjkL={d2=68t`+=07RiPBG6jR5yDI= zPV2~{CBe_nu%_qWC+w6a92`RiD!xk&5|6ZiJwYk>A3y$E2cG)Z#@JIQOXL2YsSUr~ zYh4%r3|QiSa{`@zlRF{KN<#sD87};r*~ky*Fz%`UUUVk?o7@TUt0#hRT_cf0H{=+7~%S($c!9zlvy-r(B_0 zCKZ;N|AphJ|0Z|B$0K)Mu;XVK)4w$6NjpY2!o;vf3ip3kqZ0qp>)(xa1XZx3j{JWN z@_)BM_L@22aQOMex%4>>+*eQ{x_9L}OLP2w<=mL+{Jmtz;cKc7uE3qa#>=bX=+3K~ z|Ht@p`ghP=swi%I&i#K}^FJ~8KQZ`k6#E}g{C_S>@lHN`c~f3qgssO3@+qB6`1 zPjrXvdcbz`*34JFMFdW9tj(H#ajsnA_?c)@0dp`Wqh=)-d&2FvlwL>jd~Zif0b8H}h& z$EUNQYn@9A*&gfZSng50eK%U2^{Cx#7-AZ)*S>jtgl_#2dao8H5wI?g;^!d-w}~s3 zGcF7s6VDZ5);7S)zLsNcTU&qPRvQiGvWIP`?Wzi`atj+840c)?<*|0zS8GG=5Vtm; z>}bN}+>#QPlpll!7fDT95zaWgvA~!|;8+%s#VH;+t64l&lbc5=go@N?$q#*hEZkl% z_i7Wp-M!G~`R~Fb#nIL`MlFe3o$EtJ+gIJGlhg}Ib->Rskomie|DjFzZ<)UeQ>^K% z=jlE7)t2?@&5aDn77Y$-8ic)Z9en-!bC=}nm&5Pp{8tKd(Nuql5O+RoP15l=XFgXe!pT)3 z97Eh|W?D`gaZFySru)HKH0N)vx6uGeT;Gk~KbW6f*I#wYtRz))5CXm=Z?ExqABuK) zkbC9M>iF2^7Vp@QRMz1V1c?o`8?7$eeFAaX|8_RITw+b*nQ{}V5^V6r@K&cdtE<}v z;7!c76;)0@*cdk-2s#XPP5f9|ytIlv+)!C7nH$aD<&#@kTlp$?PhCL8(63vaQJzoA4o1*7h^?k@ z6$hZjE*|%g6%qE)@uMk*523kh+*WDWTex=HK?`LnEv08iX&)?p1G$7HD zr1(4fcLcdq;mts{iEaB0&79c4qrSahbFDYd{M{J}!(`u<9!`&&hjV$uJP~>R4p7(A ze%pII_omNmW_wT0t$(&<%@W&VnZL8PHP451cm4dG%i8pH+I*-wOtLqBm}9bZ9wN_`ivvk+(d6(Dio9-gq0cjleykYTLt`dIp`L|%#cKTf@1MCjaiD;GA zCe@`?ksLO^pZ=irax{r2PAT#ShVpj3CDyDYb!9F5qUN>%aZ2muA7pR4diG(yo#MCU z7`i0Pt2#|Bt-#Q8wHiSawl|!P*#CLCJ2vYH4R$dIpYaUp17q4E$B)%1Y(l|wVgFS3 z-J#j{SNiq**O_lO>Gvn;>U_D`Xx<9>v=v57h_|1s;UoU+?@&oJ&Shs}Y7>Wu?QJ;J zs-C|h%y7p$z^Mddrf$3Mkh2PTAEait^Sc1+I$p$}dc6LGDiMk*3{DY(L~TmO^c;V& z-hf-UV^Yh}s!0mNa%ab4B$O^`VZ6NitH}M_+PHUGH&KYK3~7Q1o;?zQ2zv%7u#eVi zdj52JFLdzc^kH3hK{MmvLJp?)iMDyqA6PVCf5n7$kSioac4 znChTFw{kzJvj~cF3;E2Oiw@U&pI_y&52e4OAP#C7j9oTtz+*q(sjSGr( zb7{O1y>G|EgTXYh{f=_EmWx)b<*48zO_q^GjiW7*5+5{-08#LK4mCi*sEwEqs4Pfr zY7A3+5M3SxIScvQHc%UL?BuV+FhV+lVpo7H(LzYzxYqRMd#n)3?3BJi9HI7kTK?%{7z8k<)zQCPONU)<2d3buNX*^3tay*bfSeA zpoI^F8RaaW<~0!vZN$iSOKM}<$(&)4*`5h|5u@4<^sP@vsg$z$=?8~C6XkfpU3l86U!>c)cByBz| z#l$ZIIE!0r$%&AFCu6kqm~ZpiCCsn0rSw_{A6URl!ZT>&2^Am-&Xssg3DKt;Jp4nI z^r;e4tqB&q7TAg|w<9%j@!PvjmWh+v2TYi;>DVL31jN}4>#lGU@W(T$7GL1L#Ar!; zKlz3uPiavfiN*(SP6aabEYAO4(Iwz!4zmqWEMa@K3!66_sH6(L1JK&=%6`kb_&*iR zxO^tjuCoxtnug!|Crifprb{9(-cQGZH&w$6TYT=rfF5;MDzLx`P*^wttWaO|@mFPb za`vg!hHH2!2T23;S3;}})&4BuMp#%l!X)YkI4mX=(B?FVie|RH-V2lF`BhTC4GSZN zBPFhB!(ccza~=%Y_@8BsF<>CHF5G{YZ$)aDlj4`&f#dUxXED6JoyCo2oCvDrsEFa+ zeW@XfYD(tL%0A6wWGa%b(^0~|J`4p87f4xV`Cb6>yW zKD_;B=wV&_FNZj!f#^D?qf));lOQH~Sm{>ffaoW;US04Px67K#%I1EmoM_-KML1yz zxCRPvc^Tt!8yBCaCi>{Lxgy$Hu$xaQDA8Iu;UT%>TyVZG-h>+QT6aYO{X?+(y@Ti@ ztqQ3oTzymu%4I>SiLI}n)q*a=S`XNW5bNL=<5r`O^o=ej(WS+JICBU9M?LyDK$r92 z@0-?f3K9CV5Emft+q~TydK@?P)-(Xg;k)^!eIHWtcKHI#3HZW+C40j1-WxW?mkIe) zKA#q*=y>1tTU#kwkh?DqE^OU!pPP!6>bq9MEO-&4O$RZcTUN)v{i4cTdl&AU{_>jn0V;dR)Fg$ zx|{s@Lq)K4X)MxgXkd#kl_kg$Do!)PrmI?ba*D@|&n~yj$7yh~YK*V3Cho zYLs{609IWXL)D?NYdxDm;FW zya~w1&gx4%vO?-3ana@4WoIGuK$QmlYflxVtaV!0)^rq5bBgGH&YBAXdZKN+-@F3Q z25|D&s~M~#I{jxMWJJ-GiSKbto$d>p0a_1&c|sIKL5OSp7)9) zdnxnBmHJF6;77{~XA~`u#gpYC>nN!3QErDxO07}5V=4Ec1A_}JX%_IFayG*|WyEh+ z7b}zH%1@EUeZlZySwMgvJ#J)Y-Bb|!k{Er0fl%6hKk{Em-}EK&Y3mtrXk3>-x6Y&I zFI;q*WL;>41u7*OR&XVatZcbo2GScpu(;q*0tC*S0cRn1I}3$}uTdbX?c#na0&G5H zIeE;=e%O#obs8neG$|GTQ1I2Zvng|_o=d*$b2ZVi5|{9Jw_bf@A;3Gc_gbIdU4OCK zZ@JVV^5T!#$>X$0iJ-T`fT%vxOQciAkBV;A+^zWtdNoCK9A z59PEC^8b0}h?jp1X{A#!A>|vOK^+*w3nWt`r5$6+sY|$QWyBK;g*wO?vN~a+Pe9j6 z>`t+fq)ge~p99>fo4>mYr+EbgrN4o3oQNnH;`MF*NF0;yZPM@xSfZA8(7;!^+*@0z z8c0u=vSyY=yA*%ktv_cpe&9zmpZz0hl%eqErxm$pQS~LWd%N~qIDXT~E5gS;spo3> zt?WY9ab>%xW4(T;e;=O$$IGa zinhuVFRwS5NE`Pi+44;;vrSR#kQa=pSkj|d{Ow^=x~&6IQA0f^VDTLn?2>yPag4C{Ta zu#{=>1NnU#tN;KNZBR8DpA9lJ3Htp!51B9Gei5^*Y34VL#6e6AxRVQ!q55yf{S)B3 z`||?6M^C%3*mq8u&wb3D-x~xU_t*wZ4s5aP#i|^&bU}LPJ33V+QX3IMkEgn@;7f?S zqy1J><*TB~J7JxNyGu8K-(Ef5NstY`8UNn?Lk4K|`oE(+HWPk`-TD31-qotFoi067 zEH^yx=^~cs)UzXPQDi9TL3}$R^K^cb4^N0+ZQZgM*{O(>t3rJO{u79wxQkyA>~;}! z1dhQzd3mg4p)oDv^at((S>NVWYV$SdLOhCYrM|*OGM6Kz0ja%^l9f^Ynn~n$uw{fy ztwq_qHD5|$xE=(l&wcl&c%p8(c zJHymA7rBUxlt8Q5Wd*9QFP**-)P>lAEcxoz_nD-lSF)iY)?Jd)UH5F77X78Ww2ptKGS^M6gO+x(jhP)d_U;WQLdA8J+{s0iS4q z%bn6@ne^CI^wdif>Eo97h3>9H4>#bDKo;q)-X}hqIYw`t98Ge}^{w$F@={tMYVGGSa0%|>?X<4FwVbWVjKkBi;~}|M}w!$uUSy;!ay22 zA6;JaRTB~RVK?x?85cIzTp{=PCxg1AOPOd<%-yX;RlpAlrD*a<7Dh&RzmmH$#;O&R z-Fe)fEH|+6isJ4TwHBU=>?PBzt6ZTDeEK9KBN^Ob=HN2xv4v$L)bLR*y3dW~4!%+> z9&obr+)&h^?&24s&2!$QW~gE4?heCnj;OnxJ-t9&W3`G`5P4{yPh!Po`&gr7xma}- zrzTR8CtX67$UW|mNg_s1GxCE}YS<#XIQl8*iI~oc9l}}^00&DdM521Z^c4G1H((lw)ZdxgN?H^|c zVDEmsSD>pq$a-15GNaew_WG&7Pw}Ka6#!4`U~pFS^34I;CbrDi{E%-7Wjom*rnR8i zM+d1w01&i*ZD7#GOq8?5Ah`Big?!VME=X2?v0Fp48;#;*=&^p7glSBpdZA4lDg5$W z3(6!+)Mt?F@cfocQlTzf;}fwZp^y(qD{d&!$%paL;I{B-_(yn=$(|%|sxy)8C)ga6J9 z!SjV~YmP7ISrIx5$bSlti6_-V4gIb%sxxcD`5CtgSbn}}IQQv$AO3=rT`ZZDt`&hx zkoEEit9OtNydlVmjNArf!)!!NtV{B}> z5AI{{0F8nh=CMZh3zSX**)0@mXHIAQ_;np0s}|+S*)}NDeU*`t%&UIm(nY1fsTwcg z?n_#Thd`$DYqNh|`l**9SU_o5@VEn-s1HK)QbjN+S+WL6&zL1reoRu3EIjko{}rkWVXo`tCifxytSgl7BgP36qHQ`lSit8VViNykTv_M%(Y>LU(Rbt&=qo z1hvK)r_lTh?(t_`*vPIq6dJ&vf4#@gE)oAj)AHek3o1Uts$A69E#B-m%6Ar|klOuo zLYP6>vkWV2h+9=w3h2`5(OCToQZZ>L+8Ea8e+`TRQw&AP!lKUDp)P}-T>IEq6W9{m zt%3{L)99~J`fkhhT#wc9m59k*1}9WhquE2-m%j}8Jong8^zWpGL~l_R1?882s1(?z|8DyZL5Vlc{E{wL=Cy~U z-%eKY5RUuN0@0qdCk*U=oHP_AIGYwUX~fB#1^BnJ3DodbJ4r69xq#(UeZ6X(fk1b7 z6I3Edoe;!Pyun1pO3h&X4wy(F!Cs=C!!|z^44FZkXa*5`S5@8ukixLBUAK^WM*J&K z7VuZ$M_@3kiD2X~Mz?(!JZ*E`g0hb4FAIoOs?!49r*H;^z?;M$FP$9|H{aS?=Kcv~ ziuODO_OQ}fk*SozY;Qc|jru};(?Xpv9!B*dih{p8oP@h)DE`tBa|kL-ysNL0ZBhZL z**CXrtgmaN%s4@7DNJ~#7@xcAy`a7P8(w|VI+N5&DG*=S{X6A4lbTKsVkDCwS_c}> zyaJM&;+X1NZ9R<;Z)mpt1-j|Md{H3qJ6`>UeEzCEh$6M=bV!Y%9V%2TuSQLx9KV`! zI0oyUGytyj3@RBO{O6NdrIAm_@0ea61l{?`yZU8pwR(UEq2?p2K)0CgwBo|vCyDUi zTc2QLXCWUVCY~id08{bX0EKwgon>|XAMdEDBsW%T_&+xNU?{4p&b+V%NJ@1yum!W5 zn|@dky8H_&aWA!wq@7sa6E70e4od#D^=Y(o8>u6%nx6_9uHKQkVOLw!Wg9iSKu~2- z1#AmlBPs5M2>+&_Y-hbHRnaVSwh66Ja zw5G2|Krn0N%1uj=`lznG(ez%KB!O<1Yfi6J<&@OuQI)rNJ6Q{vHyQbJER>(`+c7cE zqbyJGDcu>Q05vw8ktk^Jx?NI7`GUC=NDyN=ojyp?8z%?^S_XsOS5z;aHOnLwJ&R(3 z3(Ar7LrzCI71MCe3rvAZMl_95KSW-*gieA3=lNatbT9_gUGhz-4d({k#VP@Q&2m@( zhNsK8uL97?-xc~xBHQE{$V~f`@{P12&Go7&?^a&L3fRYL!xf*gpMaJ)fKSSdD^2W) zZCjy#e7fv_dC^kliI&3CGI?hECbwd?ebBoe5ZDjMol zi6r_u23w!1fZzIjd8gGtGU(X}&6C80+x75!5r9UKj5AbBN(T_fAp4e;M<52gW04Ug zW-kH?iaKtQr(^*aqIR)|_cBMC_W3%qZ2;<4)1-EdBr2c0eq(NP5tX?4qT`aLHoIWC zhCnxi0?fQr^*hRSZK!ui!)-#B6cpaEfZP3#_)D5VBMMEX9e|;7fLp!lI!HFOq0=i9 zDF4D)RJca+_%j0CDGhCShS%TZl|d-BQRMo%fI7*e@uj%~ODXy%xlp0Wcjj9SGvvbM zTZHA8hN4paU$_~@LB4gWxVu$PUygRj$yHBk93|Is%TA0~dG@mek229_2i%?pPxSjp zEM)7t+x__K24Fyrw*I=Xc~%VM>-gB%5zT(lj1yp}j78=FrPyl$Bzrn|x+nns%vcP3 z!DUcL4Hn5lX<`*LNRnUr0>t)N8yZ}Cm7cZ&Nv3iE0R^d1XnH%!cU)n?s|^y@O1bID z!Tg#CUOukOpCYJAigJyR$Ip4yy}CeJRa}&EdgV3shf>9TQ|6u6`Z+qvOP`#9(&fX3 zPvU=A90RuH`XaALF2Ge*IyWKgf6v2-yir*N^ z48=Z@YrH&BLE@ICt`EPE=cvdRWzaF0CZe)m)N(>>5d5>yPdxpp* zrJT8@w=M!`6U3WzR5)l6hR;J@op5#CDaCp$08JZVRJ{3^QwN*U?E8fqs_RGs6@~)$ z6Ht+EGPOaneXn?X!Y_>EPza^cVn@Y?Kz6;g2P9zOV$3#K$SK9eJ{lRmk_)UuX%{%4 zGfm=0^(y0Ck*D~n4=M<}`FCYhV#V|9s(-(O{pIyKW(@Ee%29O^8{fW0xd;$(a(I9R zh>MV15~slqHv>4gORx1(yBw48hd}uqPnDw=vb{-RakkQ&gwQ4l5_G4!9L7(O;0Y+5 z`Xic8f=gOCP`VpvZRfdpv6qd_2X3fyigG9G=y!_mUVREAIEx(a8nLmAph2>bc$Bk? zy=&8Z#R^RKz^0FXLBMHkXlzL%)ak_jizM7OcA}#Mp*ut<{>w$2d2UCq0<-;{3zfP| zs6;6M;3A*=NDLa}u4R-tO~XKHe=Mb-P~@Y?wTUE=<*#uneafk0;e`JVN<2<(?guB} zmcQO8URZ)rTEMGzR=7rn9|1F6G|vy{!#Y(9JTPKAIN!s%R)sVe(oJhlr`D}%P&;3DAoJ|C?cU!QsARP(GLSS0V7^ zE_m_)U{>gCw3SED!b9+HDHrtg^@~oh+IZS-`OBRpA354cAWfZ2l29o(^z`K{l6zdL zITn$5A`1ndEqf!`h{Y!DWqxgCIc}$}vvYHU=Jqnf^)k1ilvP8LOkB5I*BvzxXNq-A zNQ+7B;NqUCKrm-iU+*^x7u*LRa^isEblv6IlaRhIs3`kX{gh1MHxzFQE>jl>6TV&? zm{Ydzk@)eI&gALG_qr0a5{h7?FJ#uaH9kIhDU7sCKO7R7k^)cw*mNjvEzI2V(SctlKftD04(i z_-GLndJKH1-EHja@%N&$jSwQou?%O$a#fX%kjho9 zU09A=z8Lt*^1mHaQ$0<;Qrrz%m;ZZsx$RrK{4f+^*P!S?q9S}s|VH7W~=3o)VAe02pWH>)c-hjs#(E@qo- z$65NO&iIB(l41;$z;ZLq7a*jh&kxpJPlJOq+VslgdKEMg%WF18Y<-VVxOd}UPKt63 z&~scG)PG|$*#5S9e=cKdw?40}tqru>6%bB+gRP4}>wP(^l-G-h_)aKNj2|~4D2d1B z9gTt!Ke;9XYGd>0+ZRL%HyGdWadd8v5lC6tleV(FgT1os>F=qU;st2Hh;gh0-kd!t zE$|P3hT^8wQS00KJ*<0wmO)oan{DE=+rOmo(r@>D9n|pKHN11A0V!^n08dd7))M_6 z&EL;XIjg&%XctHfA=+e7v*AZkCBhGmHGb8-Zt$Jj3O!p(#1QK2vwL|j1^YoCvc zbDRVnrHcbxtp7)d4xGRKO(Ih!G&01c{?p-8}xZxmoyQFE%kL?j|baB_ew$T6={Sb z$@9;0Z-7?o<%P<1E@O0IQ$flxaE4*~*w+_3xA&1gzurlJ@0=aP9@!7JyCLvX4v-IF z&)&0Lxb8Gt(|mFaVhwTLUfrYNp9%VUrwL7?zyqKkkc~1o92B|KV_gtREl|Uv|Eg0} zvx4xNNsaO^hbepjOT%MJLm0q(ryKT-`Cbt~a!}Uccl#H%OqRy@R#rg!cq`6)udB*h z;*&;UO3R4mjUl+y_4R{U5A2(6D+yi&;?7jgj0!rBXC;@Wfor6lyAaBTE{appzRN zZkL7f|6_x~-N_{J+7c$hfBfYdBOhTx<>xyrOrSq{)cSbWoKFW}b3TPnUYydvM_h66 zbBcP$nz{lh(KMx84rRwGm;ksb(5Kr-TiLoBISMp_6LCNK6%oGPvKnX@QI_&`wS_V- z?vUUN_)i}_k~UK*jA#2{K0((NOQ{U&qVefP6nX&P$!bra-j5{+heX8ZV z?EXTs-yu49ks5HoqB zIAUH97|w;TSN|552t-HGWSbm4LeawBt!&-t9vBFy5$eW1>(;4uB-&I5IIETXFn_&@ zdGj0@xp`k?pz-N> zgRYv)j+n#dN@R0@G3zy_S(?0nz8J6(m;nKf3`c^BTS1_E5BeY^c);0n@-cwI z$PLfq(221p9xJa}sJ&tXyhTawf6fXtg(O$?vUt>pGP(Hch6QONV-W-_bGybxz`3Go z5*586EFOA$_-a<#LB!|QCee9aNpbn5yV1>V(D$#Qd1Jp5Zi7&J)q}QfjEqEvbh3Wr zwhJWw+<^AgHu{ggNDA67BI@D9Exe7^M`J#_X~S_@H^q3WHy<7P{t^Ld>qX#VHwjZq zR0U=IpspCPOLpa08PyP)_W4>q%+S~cI3~RN+@1iO;KKWCRAQasWuaSi9d<=<9}L^=AK_n2mxX5fK~F1%yt?h>ktk8eG9 zr}pY3I=Bj6`7Gpxl5~3Vl%*hwbCW&Wq}~6=l1A-?%6?I-Pj*$LFmFib_Tt<|y(&{; z-dOBp;3oLTG%X3;Me9ySbKZwrI}*_kNI3uu28Dt-?-j{7Js3L}0A&E;Ms+-3en{8s zhCC~6k?jZdvZc1?zq)ft2#pTd`Wy0SLPtT0H5!$e1M2ob8|8Aec{S)^(ZnRVmbgfy z5p@7Mip}{vseJcp@c9zMK0<;w%2W!Uix`FKf8hq21ev)HkS7Wk2gqzHoO-*Zq6)%1 z|8DKigLYM*%*TVeB6hJq!$z$$u0r_%EJ&g%`=bl3zg{+2o|`H@qvdkyXb}a=kvLKb YrBKJ-IyCDd-TCNf=&P69di3Id0iF7f#{d8T literal 18524 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 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 fc3db7c140610addeac5f114d79b08c8e31268ea..75b1c4d9534e7ac3b621405f321304fec7ba7c05 100644 GIT binary patch literal 18406 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 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 88e71e7895b800e4ba7eda51e5994613796679bb..031025d8e0f68efc0c8df7ee0bee1067eb4a7f41 100644 GIT binary patch literal 18071 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 ORJzdHnx)rmpZ*VUQ|PMz literal 21375 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 diff --git a/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js b/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js index 59d3171706..ea50e85301 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 0235a1d2c1..16c6b5defc 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 fe488697c6..b654f44d10 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 0000000000..691f7f1277 --- /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} 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 0000000000..0ad2aca75f --- /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 0000000000..ab6b34e34b --- /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 similarity index 55% rename from example/faultManagment/exampleFaultSource.js rename to example/faultManagement/exampleFaultSource.js index 338f0903b5..9e296ad7f6 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 similarity index 100% rename from example/faultManagment/pluginSpec.js rename to example/faultManagement/pluginSpec.js diff --git a/example/faultManagement/utils.js b/example/faultManagement/utils.js new file mode 100644 index 0000000000..1287d570b4 --- /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 9041e15d33..462d4d0d2c 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 840b316187..6feadaf444 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]))); + // The object comes in as a mutable domain object, which has functions, + // which the index function cannot handle as it will eventually be serialized + // using structuredClone. Thus we're using JSON.parse/JSON.stringify to discard + // those functions. + const nonMutableDomainObject = JSON.parse(JSON.stringify(newDomainObjectToIndex)); - identifiersToIndex.forEach(identifier => { - this.openmct.objects.get(identifier).then(objectToIndex => provider.index(objectToIndex)); - }); + const objectProvider = this.openmct.objects.getProvider(nonMutableDomainObject.identifier); + if (objectProvider === undefined || objectProvider.search === undefined) { + provider.index(nonMutableDomainObject); + } + } + + onCompositionRemoved(domainObjectToRemoveIdentifier) { + const keyString = this.openmct.objects.makeKeyString(domainObjectToRemoveIdentifier); + if (this.indexedIds[keyString]) { + // we store the unobserve function in the indexedId map + this.indexedIds[keyString](); + delete this.indexedIds[keyString]; + } + + const composition = this.indexedCompositions[keyString]; + if (composition) { + composition.off('add', this.onCompositionAdded); + composition.off('remove', this.onCompositionRemoved); + delete this.indexedCompositions[keyString]; + } } /** @@ -305,6 +329,7 @@ class InMemorySearchProvider { async index(domainObject) { const provider = this; const keyString = this.openmct.objects.makeKeyString(domainObject.identifier); + const composition = this.openmct.composition.get(domainObject); if (!this.indexedIds[keyString]) { this.indexedIds[keyString] = this.openmct.objects.observe( @@ -312,11 +337,12 @@ class InMemorySearchProvider { 'name', this.onNameMutation.bind(this, domainObject) ); - this.indexedCompositions[keyString] = this.openmct.objects.observe( - domainObject, - 'composition', - this.onCompositionMutation.bind(this, domainObject) - ); + if (composition) { + composition.on('add', this.onCompositionAdded); + composition.on('remove', this.onCompositionRemoved); + this.indexedCompositions[keyString] = composition; + } + if (domainObject.type === 'annotation') { this.indexedTags[keyString] = this.openmct.objects.observe( domainObject, @@ -338,8 +364,6 @@ class InMemorySearchProvider { } } - const composition = this.openmct.composition.get(domainObject); - if (composition !== undefined) { const children = await composition.load(); diff --git a/src/api/objects/ObjectAPI.js b/src/api/objects/ObjectAPI.js index b82bc61591..64167f3c75 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 f6a69228e9..129a3bca98 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 bc29b615a9..98afcba651 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 3c5e5eba2d..19036b26e2 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 8e6a56e5f2..e70e754b6e 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 f07dc839a2..be19cbfe50 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; - } + SEARCH_KEYS.forEach((key) => { + if (fault[key]?.toString().toLowerCase().includes(this.searchTerm)) { + match = true; + } + }); - if (fault?.triggerTime.toString().toLowerCase().includes(this.searchTerm)) { - return true; - } - - if (fault?.severity.toString().toLowerCase().includes(this.searchTerm)) { - return true; - } - - return false; + return match; }, isSelected(fault) { return Boolean(this.selectedFaults[fault.id]); diff --git a/src/plugins/faultManagement/pluginSpec.js b/src/plugins/faultManagement/pluginSpec.js index 07ad9664c3..29169c05c7 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 8814153342..b6bed994b5 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 4a21670809..9eb2134d1d 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 9390b3e5a4..576e53d35a 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) { + // check if moving to same place + if (this.openmct.objects.areIdsEqual(parentCandidate.identifier, currentParent.identifier)) { return false; } - if (parentCandidateKeystring === currentParentKeystring) { - return false; - } - - if (parentCandidateKeystring === objectKeystring) { + // check if moving to a child + if (parentCandidatePath.some(candidatePath => { + return this.openmct.objects.areIdsEqual(candidatePath.identifier, this.object.identifier); + })) { return false; } diff --git a/src/plugins/move/MoveAction.js b/src/plugins/move/MoveAction.js index d9a4d144ea..594317c3b0 100644 --- a/src/plugins/move/MoveAction.js +++ b/src/plugins/move/MoveAction.js @@ -145,26 +145,24 @@ export default class MoveAction { const parentCandidatePath = data.value; const parentCandidate = parentCandidatePath[0]; + // check if moving to same place + if (this.openmct.objects.areIdsEqual(parentCandidate.identifier, currentParent.identifier)) { + return false; + } + + // check if moving to a child + if (parentCandidatePath.some(candidatePath => { + return this.openmct.objects.areIdsEqual(candidatePath.identifier, this.object.identifier); + })) { + return false; + } + if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) { return false; } - let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier); - let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier); let objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier); - if (!parentCandidateKeystring || !currentParentKeystring) { - return false; - } - - if (parentCandidateKeystring === currentParentKeystring) { - return false; - } - - if (parentCandidateKeystring === objectKeystring) { - return false; - } - const parentCandidateComposition = parentCandidate.composition; if (parentCandidateComposition && parentCandidateComposition.indexOf(objectKeystring) !== -1) { return false; diff --git a/src/plugins/myItems/pluginSpec.js b/src/plugins/myItems/pluginSpec.js index b463b0a5e1..867fbf2432 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 0000000000..104d70d6e7 --- /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 fc5cf795b4..8b17e14781 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: + + +## 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 . + +## 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: . 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: + # 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 4. Create a database called `openmct` -5. Navigate to http://127.0.0.1:5984/_utils/#/database/openmct/permissions +5. Navigate to 6. Remove permission restrictions in CouchDB from Open MCT by deleting `_admin` roles for both `Admin` and `Member`. -# Configuring Open MCT +# Configuring Open MCT to use CouchDB + +## Configuration script +The simplest way to config a CouchDB instance is to use our provided tooling: +1. `cd` to the workspace root directory (the same directory as `index.html`) +2. Update `index.html` to use the CouchDB plugin as persistence store: + +```sh +sh ./src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh +``` + +## Manual Configuration + 1. Edit `openmct/index.html` comment out the following line: -``` -openmct.install(openmct.plugins.LocalStorage()); -``` -Add a line to install the CouchDB plugin for Open MCT: -``` -openmct.install(openmct.plugins.CouchDB("http://localhost:5984/openmct")); -``` -2. Start Open MCT by running `npm start` in the `openmct` path. -3. Navigate to http://localhost:8080/ and create a random object in Open MCT (e.g., a 'Clock') and save. You may get an error saying that the object failed to persist - this is a known error that you can ignore, and will only happen the first time you save - just try again. -4. Navigate to: http://127.0.0.1:5984/_utils/#database/openmct/_all_docs -5. Look at the 'JSON' tab and ensure you can see the specific object you created above. -6. All done! 🏆 + + ```js + openmct.install(openmct.plugins.LocalStorage()); + ``` + + Add a line to install the CouchDB plugin for Open MCT: + + ```js + openmct.install(openmct.plugins.CouchDB("http://localhost:5984/openmct")); + ``` + +# Validating a successful Installation + +1. Start Open MCT by running `npm start` in the `openmct` path. +2. Navigate to 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: +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 0000000000..40e58ff0ab --- /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 0000000000..4fbc50d4ec --- /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 0000000000..f07fc9470f --- /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 48449f6915..c39e6c322c 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 a91accfa2d..8df1925e85 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 57a1747b7d..986325f99f 100644 --- a/src/plugins/timelist/inspector/TimelistPropertiesView.vue +++ b/src/plugins/timelist/inspector/TimelistPropertiesView.vue @@ -32,7 +32,7 @@

+ >These settings don't affect the view while editing, but will be applied after editing is finished.
{ - 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 eea87d114d..6ee7a50abc 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 ee52964363..87db0617c0 100644 --- a/src/ui/layout/Layout.vue +++ b/src/ui/layout/Layout.vue @@ -53,6 +53,7 @@ type="horizontal" >
@@ -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 32d719b11f..5235fc2b34 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 7283e3bd30..5c0712f957 100644 --- a/src/ui/layout/tree-item.vue +++ b/src/ui/layout/tree-item.vue @@ -1,7 +1,9 @@