Compare commits

...

12 Commits

Author SHA1 Message Date
4825fffb44 [Condition Set] Add check for empty string being passed to the makeKeyString util by TelemetryCriterion (#5636)
* Check telemetry is defined before using makeKeyString util

* Add optional chaining in the check

* Add e2e test

* Add check for undefined
2022-08-09 11:35:01 -07:00
41f8cb404d Check for circular references in originalPath - 5615 (#5619)
* check for circular references

* add test

* fix test

* address PR comments by making comments better

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

* Moved firstIdentifier to top of function

* Lint fix

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

* now filtering annotations that are orphaned

* filter object results without valid paths

* add progress bar

* added e2e tests

* removed extraneous click

* fix typos

* fix unit tests

* lint

* address pr comments

* fix tests

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

* remove debug statement

* lint

* fix documentation

* lint

* fix doc

* made some optimizations after talking with akhenry

* fix test

* update docs

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

* Update appActions

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

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

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

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

- Update some docs, still need to clarify some

* Update appActions e2e tests

- Refactor for organization

- Test our new appActions in one go

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

* fix accidental renamed export

* Fix jsdoc return types

* refactor telemetryTable test to use appActions

* Improve selectors

* Refactor test

* improve selector

* add clock mode appActions

* lint

* Fix jsdoc

* Code review comments

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

* Differentiate bg color between real-time and fixed

* scrollIntoView inline: center

* Added watcher for bounds change to trigger thumbnail scroll

* Resolve merge conflict with requestHistory change to telemetry collection

* Split thumbnail into sub component

* Monitor isFixed value to unpause playback status

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

View File

@ -30,18 +30,37 @@
*/ */
/** /**
* This common function creates a `domainObject` with default options. It is the preferred way of creating objects * Defines parameters to be used in the creation of a domain object.
* in the e2e suite when uninterested in properties of the objects themselves. * @typedef {Object} CreateObjectOptions
* @param {import('@playwright/test').Page} page * @property {string} type the type of domain object to create (e.g.: "Sine Wave Generator").
* @param {string} type * @property {string} [name] the desired name of the created domain object.
* @param {string | undefined} name * @property {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the Identifier or uuid of the parent object.
*/ */
async function createDomainObjectWithDefaults(page, type, name) {
// Navigate to focus the 'My Items' folder, and hide the object tree /**
// This is necessary so that subsequent objects can be created without a parent * Contains information about the newly created domain object.
// TODO: Ideally this would navigate to a common `e2e` folder * @typedef {Object} CreatedObjectInfo
await page.goto('./#/browse/mine?hideTree=true'); * @property {string} name the name of the created object
* @property {string} uuid the uuid of the created object
* @property {string} url the relative url to the object (for use with `page.goto()`)
*/
/**
* This common function creates a domain object with the default options. It is the preferred way of creating objects
* in the e2e suite when uninterested in properties of the objects themselves.
*
* @param {import('@playwright/test').Page} page
* @param {CreateObjectOptions} options
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
*/
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) {
const parentUrl = await getHashUrlToDomainObject(page, parent);
// Navigate to the parent object. This is necessary to create the object
// in the correct location, such as a folder, layout, or plot.
await page.goto(`${parentUrl}?hideTree=true`);
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
//Click the Create button //Click the Create button
await page.click('button:has-text("Create")'); await page.click('button:has-text("Create")');
@ -50,7 +69,7 @@ async function createDomainObjectWithDefaults(page, type, name) {
// Modify the name input field of the domain object to accept 'name' // Modify the name input field of the domain object to accept 'name'
if (name) { if (name) {
const nameInput = page.locator('input[type="text"]').nth(2); const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
await nameInput.fill(""); await nameInput.fill("");
await nameInput.fill(name); await nameInput.fill(name);
} }
@ -63,12 +82,28 @@ async function createDomainObjectWithDefaults(page, type, name) {
page.waitForSelector('.c-message-banner__message') page.waitForSelector('.c-message-banner__message')
]); ]);
return name || `Unnamed ${type}`; // Wait until the URL is updated
await page.waitForURL(`**/${parent}/*`);
const uuid = await getFocusedObjectUuid(page);
const objectUrl = await getHashUrlToDomainObject(page, uuid);
if (await _isInEditMode(page, uuid)) {
// Save (exit edit mode)
await page.locator('button[title="Save"]').click();
await page.locator('li[title="Save and Finish Editing"]').click();
}
return {
name: name || `Unnamed ${type}`,
uuid: uuid,
url: objectUrl
};
} }
/** /**
* Open the given `domainObject`'s context menu from the object tree. * Open the given `domainObject`'s context menu from the object tree.
* Expands the 'My Items' folder if it is not already expanded. * Expands the 'My Items' folder if it is not already expanded.
*
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {string} myItemsFolderName the name of the "My Items" folder * @param {string} myItemsFolderName the name of the "My Items" folder
* @param {string} domainObjectName the display name of the `domainObject` * @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 // eslint-disable-next-line no-undef
module.exports = { module.exports = {
createDomainObjectWithDefaults, createDomainObjectWithDefaults,
openObjectTreeContextMenu openObjectTreeContextMenu,
getHashUrlToDomainObject,
getFocusedObjectUuid,
setFixedTimeMode,
setRealTimeMode,
setStartOffset,
setEndOffset
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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;
}

View File

@ -41,7 +41,7 @@ test.describe('Example Imagery Object', () => {
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
// Create a default 'Example Imagery' object // Create a default 'Example Imagery' object
createDomainObjectWithDefaults(page, 'Example Imagery'); createDomainObjectWithDefaults(page, { type: 'Example Imagery' });
await Promise.all([ await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),

View File

@ -0,0 +1,120 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions');
test.describe('Testing LAD table @unstable', () => {
let sineWaveObject;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await setRealTimeMode(page);
// Create Sine Wave Generator
sineWaveObject = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: "Test Sine Wave Generator"
});
});
test('telemetry value exactly matches latest telemetry value received in real time', async ({ page }) => {
// Create LAD table
await createDomainObjectWithDefaults(page, {
type: 'LAD Table',
name: "Test LAD Table"
});
// Edit LAD table
await page.locator('[title="Edit"]').click();
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// Add the Sine Wave Generator to the LAD table and save changes
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-lad-table-wrapper');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
// Subscribe to the Sine Wave Generator data
// On getting data, check if the value found in the LAD table is the most recent value
// from the Sine Wave Generator
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
const subscribeTelemValue = await getTelemValuePromise;
const ladTableValuePromise = await page.waitForSelector(`text="${subscribeTelemValue}"`);
const ladTableValue = await ladTableValuePromise.textContent();
expect(ladTableValue).toBe(subscribeTelemValue);
});
test('telemetry value exactly matches latest telemetry value received in fixed time', async ({ page }) => {
// Create LAD table
await createDomainObjectWithDefaults(page, {
type: 'LAD Table',
name: "Test LAD Table"
});
// Edit LAD table
await page.locator('[title="Edit"]').click();
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// Add the Sine Wave Generator to the LAD table and save changes
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-lad-table-wrapper');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
// Subscribe to the Sine Wave Generator data
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
// Set an offset of 1 minute and then change the time mode to fixed to set a 1 minute historical window
await setStartOffset(page, { mins: '1' });
await setFixedTimeMode(page);
// On getting data, check if the value found in the LAD table is the most recent value
// from the Sine Wave Generator
const subscribeTelemValue = await getTelemValuePromise;
const ladTableValuePromise = await page.waitForSelector(`text="${subscribeTelemValue}"`);
const ladTableValue = await ladTableValuePromise.textContent();
expect(ladTableValue).toBe(subscribeTelemValue);
});
});
/**
* Util for subscribing to a telemetry object by object identifier
* Limitations: Currently only works to return telemetry once to the node scope
* To Do: See if there's a way to await this multiple times to allow for multiple
* values to be returned over time
* @param {import('@playwright/test').Page} page
* @param {string} objectIdentifier identifier for object
* @returns {Promise<string>} the formatted sin telemetry value
*/
async function subscribeToTelemetry(page, objectIdentifier) {
const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getTelemValue', resolve));
await page.evaluate(async (telemetryIdentifier) => {
const telemetryObject = await window.openmct.objects.get(telemetryIdentifier);
const metadata = window.openmct.telemetry.getMetadata(telemetryObject);
const formats = await window.openmct.telemetry.getFormatMap(metadata);
window.openmct.telemetry.subscribe(telemetryObject, (obj) => {
const sinVal = obj.sin;
const formattedSinVal = formats.sin.format(sinVal);
window.getTelemValue(formattedSinVal);
});
}, objectIdentifier);
return getTelemValuePromise;
}

View File

@ -36,7 +36,7 @@ async function createNotebookAndEntry(page, iterations = 1) {
//Go to baseURL //Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
createDomainObjectWithDefaults(page, 'Notebook'); createDomainObjectWithDefaults(page, { type: 'Notebook' });
for (let iteration = 0; iteration < iterations; iteration++) { for (let iteration = 0; iteration < iterations; iteration++) {
// Click text=To start a new entry, click here or drag and drop any object // Click text=To start a new entry, click here or drag and drop any object
@ -139,11 +139,28 @@ test.describe('Tagging in Notebooks @addInit', () => {
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving"); await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
}); });
test('Can delete objects with tags and neither return in search', async ({ page }) => {
await createNotebookEntryAndTags(page);
// Delete Notebook
await page.locator('button[title="More options"]').click();
await page.locator('li[title="Remove this object from its containing object."]').click();
await page.locator('button:has-text("OK")').click();
await page.goto('./', { waitUntil: 'networkidle' });
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed');
await expect(page.locator('text=No matching results.')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sci');
await expect(page.locator('text=No matching results.')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('dri');
await expect(page.locator('text=No matching results.')).toBeVisible();
});
test('Tags persist across reload', async ({ page }) => { test('Tags persist across reload', async ({ page }) => {
//Go to baseURL //Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
await createDomainObjectWithDefaults(page, 'Clock'); await createDomainObjectWithDefaults(page, { type: 'Clock' });
const ITERATIONS = 4; const ITERATIONS = 4;
await createNotebookEntryAndTags(page, ITERATIONS); await createNotebookEntryAndTags(page, ITERATIONS);

View File

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

View File

@ -21,6 +21,7 @@
*****************************************************************************/ *****************************************************************************/
const { test, expect } = require('../../../../baseFixtures'); const { test, expect } = require('../../../../baseFixtures');
const { setFixedTimeMode, setRealTimeMode, setStartOffset, setEndOffset } = require('../../../../appActions');
test.describe('Time conductor operations', () => { test.describe('Time conductor operations', () => {
test('validate start time does not exceeds end time', async ({ page }) => { test('validate start time does not exceeds end time', async ({ page }) => {
@ -147,88 +148,3 @@ test.describe('Time conductor input fields real-time mode', () => {
expect(page.url()).toContain(`endDelta=${endDelta}`); 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();
}
}

View File

@ -26,7 +26,7 @@ const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('.
test.describe('Timer', () => { test.describe('Timer', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
await createDomainObjectWithDefaults(page, 'timer'); await createDomainObjectWithDefaults(page, { type: 'timer' });
}); });
test('Can perform actions on the Timer', async ({ page, openmctConfig }) => { test('Can perform actions on the Timer', async ({ page, openmctConfig }) => {

View File

@ -107,6 +107,9 @@ test.describe("Search Tests @unstable", () => {
// Verify that no results are found // Verify that no results are found
expect(await searchResults.count()).toBe(0); expect(await searchResults.count()).toBe(0);
// Verify proper message appears
await expect(page.locator('text=No matching results.')).toBeVisible();
}); });
test('Validate single object in search result', async ({ page }) => { test('Validate single object in search result', async ({ page }) => {

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "openmct", "name": "openmct",
"version": "2.1.0-SNAPSHOT", "version": "2.0.7",
"description": "The Open MCT core platform", "description": "The Open MCT core platform",
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "7.18.9", "@babel/eslint-parser": "7.18.9",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,68 @@
<!--
Open MCT, Copyright (c) 2014-2022, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div
class="c-imagery__thumb c-thumb"
:class="{
'active': active,
'selected': selected,
'real-time': realTime
}"
:title="image.formattedTime"
>
<a
href=""
:download="image.imageDownloadName"
@click.prevent
>
<img
class="c-thumb__image"
:src="image.url"
>
</a>
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
</div>
</template>
<script>
export default {
props: {
image: {
type: Object,
required: true
},
active: {
type: Boolean,
required: true
},
selected: {
type: Boolean,
required: true
},
realTime: {
type: Boolean,
required: true
}
}
};
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -39,6 +39,8 @@ describe("GrandSearch", () => {
let mockAnnotationObject; let mockAnnotationObject;
let mockDisplayLayout; let mockDisplayLayout;
let mockFolderObject; let mockFolderObject;
let mockAnotherFolderObject;
let mockTopObject;
let originalRouterPath; let originalRouterPath;
beforeEach((done) => { 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 = { mockFolderObject = {
type: 'folder', type: 'folder',
name: 'Test Folder', name: 'Test Folder',
location: 'fooNameSpace:someParent',
identifier: { identifier: {
key: 'some-folder', key: 'someFolder',
namespace: 'fooNameSpace' namespace: 'fooNameSpace'
} }
}; };
@ -122,6 +142,10 @@ describe("GrandSearch", () => {
return mockDisplayLayout; return mockDisplayLayout;
} else if (identifier.key === mockFolderObject.identifier.key) { } else if (identifier.key === mockFolderObject.identifier.key) {
return mockFolderObject; return mockFolderObject;
} else if (identifier.key === mockAnotherFolderObject.identifier.key) {
return mockAnotherFolderObject;
} else if (identifier.key === mockTopObject.identifier.key) {
return mockTopObject;
} else { } else {
return null; return null;
} }

View File

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