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