Compare commits
	
		
			14 Commits
		
	
	
		
			nb-embed-e
			...
			timelist-r
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | d76a8db5b4 | ||
|   | 61bf60783c | ||
|   | 5dc718b78d | ||
|   | 41f8cb404d | ||
|   | c6c58af12c | ||
|   | 15a0a87251 | ||
|   | 59a8614f1c | ||
|   | 7cf11e177c | ||
|   | 1a44652470 | ||
|   | 51d16f812a | ||
|   | 25de5653e8 | ||
|   | cb6014d69f | ||
|   | 36736eb8a0 | ||
|   | a13a6002c5 | 
| @@ -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,12 +82,28 @@ 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. | ||||
| * | ||||
| * @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` | ||||
| @@ -85,8 +120,154 @@ async function openObjectTreeContextMenu(page, myItemsFolderName, domainObjectNa | ||||
|     }); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 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.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 | ||||
| }; | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										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 | ||||
| }); | ||||
| @@ -0,0 +1,122 @@ | ||||
| /***************************************************************************** | ||||
|  * 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); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * 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; | ||||
| } | ||||
| @@ -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(), | ||||
|   | ||||
							
								
								
									
										120
									
								
								e2e/tests/functional/plugins/lad/lad.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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; | ||||
| } | ||||
| @@ -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 | ||||
| @@ -139,11 +139,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); | ||||
|   | ||||
| @@ -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 }) => { | ||||
| @@ -147,88 +148,3 @@ test.describe('Time conductor input fields real-time mode', () => { | ||||
|         expect(page.url()).toContain(`endDelta=${endDelta}`); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * @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(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -26,7 +26,7 @@ const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('. | ||||
| test.describe('Timer', () => { | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await page.goto('./', { waitUntil: 'networkidle' }); | ||||
|         await createDomainObjectWithDefaults(page, 'timer'); | ||||
|         await createDomainObjectWithDefaults(page, { type: 'timer' }); | ||||
|     }); | ||||
|  | ||||
|     test('Can perform actions on the Timer', async ({ page, openmctConfig }) => { | ||||
|   | ||||
| @@ -107,6 +107,9 @@ 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 }) => { | ||||
|   | ||||
| @@ -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}')`); | ||||
|   | ||||
| @@ -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}')`); | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "openmct", | ||||
|   "version": "2.1.0-SNAPSHOT", | ||||
|   "version": "2.0.8", | ||||
|   "description": "The Open MCT core platform", | ||||
|   "devDependencies": { | ||||
|     "@babel/eslint-parser": "7.18.9", | ||||
|   | ||||
| @@ -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); | ||||
|         }); | ||||
|   | ||||
| @@ -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~ | ||||
|  */ | ||||
|  | ||||
| /** | ||||
| @@ -615,27 +615,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; | ||||
|         } | ||||
|   | ||||
							
								
								
									
										68
									
								
								src/plugins/imagery/components/ImageThumbnail.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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, | ||||
| @@ -569,6 +562,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 +613,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 +849,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(); | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -110,14 +110,31 @@ export default { | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.isEditing = this.openmct.editor.isEditing(); | ||||
|         this.timestamp = Date.now(); | ||||
|         this.timestamp = this.openmct.time.clock().currentValue(); | ||||
|  | ||||
|         this.openmct.time.on('clock', (newClock) => { | ||||
|             //newclock can be undefined | ||||
|             if (newClock === undefined) { | ||||
|                 // Show everything | ||||
|                 // Use logic in this.setEditState which does the same thing | ||||
|             } else { | ||||
|                 // Use logic in this.setEditState which does the same thing | ||||
|             } | ||||
|         }); | ||||
|         this.openmct.time.on('bounds', (bounds, isTick) => { | ||||
|             if (isTick === true) { | ||||
|                 this.timestamp = this.openmct.time.clock().currentValue(); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         this.getPlanDataAndSetConfig(this.domainObject); | ||||
|  | ||||
|         this.unlisten = this.openmct.objects.observe(this.domainObject, 'selectFile', this.planFileUpdated); | ||||
|         this.unlistenConfig = this.openmct.objects.observe(this.domainObject, 'configuration', this.setViewFromConfig); | ||||
|         this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.setStatus); | ||||
|         this.status = this.openmct.status.get(this.domainObject.identifier); | ||||
|         this.unlistenTicker = ticker.listen(this.clearPreviousActivities); | ||||
|         //Remove ticker service, we don't need it any more. | ||||
|         //this.unlistenTicker = ticker.listen(this.clearPreviousActivities); | ||||
|         this.openmct.editor.on('isEditing', this.setEditState); | ||||
|  | ||||
|         this.deferAutoScroll = _.debounce(this.deferAutoScroll, 500); | ||||
| @@ -188,7 +205,8 @@ export default { | ||||
|             if (domainObject.type === 'plan') { | ||||
|                 this.getPlanDataAndSetConfig({ | ||||
|                     ...this.domainObject, | ||||
|                     selectFile: domainObject.selectFile | ||||
|                     selectFile: domainObject.selectFile, | ||||
|                     sourceMap: domainObject.sourceMap | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|   | ||||
| @@ -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; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -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,6 +39,8 @@ describe("GrandSearch", () => { | ||||
|     let mockAnnotationObject; | ||||
|     let mockDisplayLayout; | ||||
|     let mockFolderObject; | ||||
|     let mockAnotherFolderObject; | ||||
|     let mockTopObject; | ||||
|     let originalRouterPath; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
| @@ -70,11 +72,29 @@ describe("GrandSearch", () => { | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|         mockTopObject = { | ||||
|             type: 'root', | ||||
|             name: 'Top Folder', | ||||
|             identifier: { | ||||
|                 key: 'topObject', | ||||
|                 namespace: 'fooNameSpace' | ||||
|             } | ||||
|         }; | ||||
|         mockAnotherFolderObject = { | ||||
|             type: 'folder', | ||||
|             name: 'Another Test Folder', | ||||
|             location: 'fooNameSpace:topObject', | ||||
|             identifier: { | ||||
|                 key: 'someParent', | ||||
|                 namespace: 'fooNameSpace' | ||||
|             } | ||||
|         }; | ||||
|         mockFolderObject = { | ||||
|             type: 'folder', | ||||
|             name: 'Test Folder', | ||||
|             location: 'fooNameSpace:someParent', | ||||
|             identifier: { | ||||
|                 key: 'some-folder', | ||||
|                 key: 'someFolder', | ||||
|                 namespace: 'fooNameSpace' | ||||
|             } | ||||
|         }; | ||||
| @@ -122,6 +142,10 @@ 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 { | ||||
|                 return null; | ||||
|             } | ||||
|   | ||||
| @@ -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; | ||||
|             } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user