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

github.com/nasa/openmct.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShefali Joshi <simplyrender@gmail.com>2022-08-22 21:41:38 +0300
committerGitHub <noreply@github.com>2022-08-22 21:41:38 +0300
commit291e62687ea44021c0b394fa8ed3e91081b3238e (patch)
tree2f41870f4077212065d9ee02abb7a0755d9de349
parentefadf9036fd96558430668740d080920a2db40fa (diff)
Master 2.0.7 (#5672)
-rw-r--r--e2e/appActions.js203
-rw-r--r--e2e/tests/framework/appActions.e2e.spec.js69
-rw-r--r--e2e/tests/framework/exampleTemplate.e2e.spec.js4
-rw-r--r--e2e/tests/framework/generateVisualTestData.e2e.spec.js30
-rw-r--r--e2e/tests/functional/example/eventGenerator.e2e.spec.js5
-rw-r--r--e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js22
-rw-r--r--e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js122
-rw-r--r--e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js2
-rw-r--r--e2e/tests/functional/plugins/lad/lad.e2e.spec.js120
-rw-r--r--e2e/tests/functional/plugins/notebook/tags.e2e.spec.js21
-rw-r--r--e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js45
-rw-r--r--e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js86
-rw-r--r--e2e/tests/functional/plugins/timer/timer.e2e.spec.js2
-rw-r--r--e2e/tests/functional/search.e2e.spec.js3
-rw-r--r--e2e/tests/visual/addInit.visual.spec.js2
-rw-r--r--e2e/tests/visual/default.visual.spec.js14
-rw-r--r--e2e/tests/visual/search.visual.spec.js5
-rw-r--r--package.json2
-rw-r--r--src/api/annotation/AnnotationAPI.js13
-rw-r--r--src/api/annotation/AnnotationAPISpec.js14
-rw-r--r--src/api/objects/ObjectAPI.js59
-rw-r--r--src/api/objects/ObjectAPISpec.js67
-rw-r--r--src/api/objects/object-utils.js4
-rw-r--r--src/plugins/condition/criterion/TelemetryCriterion.js6
-rw-r--r--src/plugins/timelist/Timelist.vue3
-rw-r--r--src/styles/_controls.scss10
-rw-r--r--src/ui/layout/search/GrandSearch.vue31
-rw-r--r--src/ui/layout/search/GrandSearchSpec.js26
-rw-r--r--src/ui/layout/search/SearchResultsDropDown.vue39
29 files changed, 803 insertions, 226 deletions
diff --git a/e2e/appActions.js b/e2e/appActions.js
index c119d0c03..e4fbf7589 100644
--- a/e2e/appActions.js
+++ b/e2e/appActions.js
@@ -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.
+ * @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.
+ */
+
+/**
+ * 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 {string} type
- * @param {string | undefined} name
+ * @param {CreateObjectOptions} options
+ * @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain 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');
+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
};
diff --git a/e2e/tests/framework/appActions.e2e.spec.js b/e2e/tests/framework/appActions.e2e.spec.js
index fdb228708..3a95000ba 100644
--- a/e2e/tests/framework/appActions.e2e.spec.js
+++ b/e2e/tests/framework/appActions.e2e.spec.js
@@ -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}`);
+ });
});
});
diff --git a/e2e/tests/framework/exampleTemplate.e2e.spec.js b/e2e/tests/framework/exampleTemplate.e2e.spec.js
index b5d20062e..038be15ac 100644
--- a/e2e/tests/framework/exampleTemplate.e2e.spec.js
+++ b/e2e/tests/framework/exampleTemplate.e2e.spec.js
@@ -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');
diff --git a/e2e/tests/framework/generateVisualTestData.e2e.spec.js b/e2e/tests/framework/generateVisualTestData.e2e.spec.js
index df6350d25..7cb37719f 100644
--- a/e2e/tests/framework/generateVisualTestData.e2e.spec.js
+++ b/e2e/tests/framework/generateVisualTestData.e2e.spec.js
@@ -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
diff --git a/e2e/tests/functional/example/eventGenerator.e2e.spec.js b/e2e/tests/functional/example/eventGenerator.e2e.spec.js
index bba8940b7..0db74c480 100644
--- a/e2e/tests/functional/example/eventGenerator.e2e.spec.js
+++ b/e2e/tests/functional/example/eventGenerator.e2e.spec.js
@@ -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();
diff --git a/e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js b/e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js
index 847f9b28a..f3f9826ae 100644
--- a/e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js
+++ b/e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js
@@ -27,6 +27,7 @@ demonstrate some playwright for test developers. This pattern should not be re-u
*/
const { test, expect } = require('../../../../pluginFixtures.js');
+const { createDomainObjectWithDefaults } = require('../../../../appActions');
let conditionSetUrl;
let getConditionSetIdentifierFromUrl;
@@ -178,3 +179,24 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
});
});
+
+test.describe('Basic Condition Set Use', () => {
+ test('Can add a condition', async ({ page }) => {
+ //Navigate to baseURL
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ // Create a new condition set
+ await createDomainObjectWithDefaults(page, {
+ type: 'Condition Set',
+ name: "Test Condition Set"
+ });
+ // Change the object to edit mode
+ await page.locator('[title="Edit"]').click();
+
+ // Click Add Condition button
+ await page.locator('#addCondition').click();
+ // Check that the new Unnamed Condition section appears
+ const numOfUnnamedConditions = await page.locator('text=Unnamed Condition').count();
+ expect(numOfUnnamedConditions).toEqual(1);
+ });
+});
diff --git a/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js b/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js
new file mode 100644
index 000000000..83090fc0e
--- /dev/null
+++ b/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js
@@ -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;
+}
diff --git a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js
index 54256f996..42e532b44 100644
--- a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js
+++ b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js
@@ -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(),
diff --git a/e2e/tests/functional/plugins/lad/lad.e2e.spec.js b/e2e/tests/functional/plugins/lad/lad.e2e.spec.js
new file mode 100644
index 000000000..4ec084c1b
--- /dev/null
+++ b/e2e/tests/functional/plugins/lad/lad.e2e.spec.js
@@ -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;
+}
diff --git a/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js b/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js
index 2d4860672..4e7c00450 100644
--- a/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js
+++ b/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js
@@ -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);
diff --git a/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js b/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js
index dbbf5c1a9..15656603e 100644
--- a/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js
+++ b/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js
@@ -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');
diff --git a/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js b/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js
index 8d764526b..59d317170 100644
--- a/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js
+++ b/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js
@@ -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();
- }
-}
diff --git a/e2e/tests/functional/plugins/timer/timer.e2e.spec.js b/e2e/tests/functional/plugins/timer/timer.e2e.spec.js
index bf8dca37c..0235a1d2c 100644
--- a/e2e/tests/functional/plugins/timer/timer.e2e.spec.js
+++ b/e2e/tests/functional/plugins/timer/timer.e2e.spec.js
@@ -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 }) => {
diff --git a/e2e/tests/functional/search.e2e.spec.js b/e2e/tests/functional/search.e2e.spec.js
index 4aeb19c0d..0daa1b3f9 100644
--- a/e2e/tests/functional/search.e2e.spec.js
+++ b/e2e/tests/functional/search.e2e.spec.js
@@ -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 }) => {
diff --git a/e2e/tests/visual/addInit.visual.spec.js b/e2e/tests/visual/addInit.visual.spec.js
index 5a04c253e..5c73a82ab 100644
--- a/e2e/tests/visual/addInit.visual.spec.js
+++ b/e2e/tests/visual/addInit.visual.spec.js
@@ -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}')`);
diff --git a/e2e/tests/visual/default.visual.spec.js b/e2e/tests/visual/default.visual.spec.js
index 8b67c9326..0ce27e7ec 100644
--- a/e2e/tests/visual/default.visual.spec.js
+++ b/e2e/tests/visual/default.visual.spec.js
@@ -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}')`);
diff --git a/e2e/tests/visual/search.visual.spec.js b/e2e/tests/visual/search.visual.spec.js
index 1af0e65b5..1cbf29250 100644
--- a/e2e/tests/visual/search.visual.spec.js
+++ b/e2e/tests/visual/search.visual.spec.js
@@ -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();
diff --git a/package.json b/package.json
index c526c1e3d..a3b11c41b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "openmct",
- "version": "2.1.0-SNAPSHOT",
+ "version": "2.0.8-SNAPSHOT",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.18.9",
diff --git a/src/api/annotation/AnnotationAPI.js b/src/api/annotation/AnnotationAPI.js
index a19762038..6b9e910be 100644
--- a/src/api/annotation/AnnotationAPI.js
+++ b/src/api/annotation/AnnotationAPI.js
@@ -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;
}
}
diff --git a/src/api/annotation/AnnotationAPISpec.js b/src/api/annotation/AnnotationAPISpec.js
index 731aead3c..8aa45864d 100644
--- a/src/api/annotation/AnnotationAPISpec.js
+++ b/src/api/annotation/AnnotationAPISpec.js
@@ -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);
});
diff --git a/src/api/objects/ObjectAPI.js b/src/api/objects/ObjectAPI.js
index ed5b53e6c..b82bc6159 100644
--- a/src/api/objects/ObjectAPI.js
+++ b/src/api/objects/ObjectAPI.js
@@ -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');
+ }
+
+ return false;
+ }
- if (location) {
- 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) {
return objectPath !== undefined
&& objectPath.length > 1
diff --git a/src/api/objects/ObjectAPISpec.js b/src/api/objects/ObjectAPISpec.js
index 4f6574598..e473fc572 100644
--- a/src/api/objects/ObjectAPISpec.js
+++ b/src/api/objects/ObjectAPISpec.js
@@ -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);
diff --git a/src/api/objects/object-utils.js b/src/api/objects/object-utils.js
index 6b4a78a77..abcb59c41 100644
--- a/src/api/objects/object-utils.js
+++ b/src/api/objects/object-utils.js
@@ -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;
}
diff --git a/src/plugins/condition/criterion/TelemetryCriterion.js b/src/plugins/condition/criterion/TelemetryCriterion.js
index e343b9d59..75e91f7dd 100644
--- a/src/plugins/condition/criterion/TelemetryCriterion.js
+++ b/src/plugins/condition/criterion/TelemetryCriterion.js
@@ -51,7 +51,11 @@ export default class TelemetryCriterion extends EventEmitter {
}
initialize() {
- this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry);
+ this.telemetryObjectIdAsString = "";
+ if (![undefined, null, ""].includes(this.telemetryDomainObjectDefinition?.telemetry)) {
+ this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry);
+ }
+
this.updateTelemetryObjects(this.telemetryDomainObjectDefinition.telemetryObjects);
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
this.subscribeForStaleData();
diff --git a/src/plugins/timelist/Timelist.vue b/src/plugins/timelist/Timelist.vue
index 58af224c3..0e6559941 100644
--- a/src/plugins/timelist/Timelist.vue
+++ b/src/plugins/timelist/Timelist.vue
@@ -188,7 +188,8 @@ export default {
if (domainObject.type === 'plan') {
this.getPlanDataAndSetConfig({
...this.domainObject,
- selectFile: domainObject.selectFile
+ selectFile: domainObject.selectFile,
+ sourceMap: domainObject.sourceMap
});
}
},
diff --git a/src/styles/_controls.scss b/src/styles/_controls.scss
index 3e356036d..c98692578 100644
--- a/src/styles/_controls.scss
+++ b/src/styles/_controls.scss
@@ -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;
}
}
diff --git a/src/ui/layout/search/GrandSearch.vue b/src/ui/layout/search/GrandSearch.vue
index 49a48baaf..88dfe198e 100644
--- a/src/ui/layout/search/GrandSearch.vue
+++ b/src/ui/layout/search/GrandSearch.vue
@@ -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) {
diff --git a/src/ui/layout/search/GrandSearchSpec.js b/src/ui/layout/search/GrandSearchSpec.js
index bb8d6dbc0..32d719b11 100644
--- a/src/ui/layout/search/GrandSearchSpec.js
+++ b/src/ui/layout/search/GrandSearchSpec.js
@@ -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;
}
diff --git a/src/ui/layout/search/SearchResultsDropDown.vue b/src/ui/layout/search/SearchResultsDropDown.vue
index 214038958..54e098a31 100644
--- a/src/ui/layout/search/SearchResultsDropDown.vue
+++ b/src/ui/layout/search/SearchResultsDropDown.vue
@@ -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;
}