Master 2.0.7 (#5672)

This commit is contained in:
Shefali Joshi 2022-08-22 11:41:38 -07:00 committed by GitHub
parent efadf9036f
commit 291e62687e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 805 additions and 228 deletions

View File

@ -30,18 +30,37 @@
*/
/**
* This common function creates a `domainObject` with default options. It is the preferred way of creating objects
* in the e2e suite when uninterested in properties of the objects themselves.
* @param {import('@playwright/test').Page} page
* @param {string} type
* @param {string | undefined} name
* Defines parameters to be used in the creation of a domain object.
* @typedef {Object} CreateObjectOptions
* @property {string} type the type of domain object to create (e.g.: "Sine Wave Generator").
* @property {string} [name] the desired name of the created domain object.
* @property {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the Identifier or uuid of the parent object.
*/
async function createDomainObjectWithDefaults(page, type, name) {
// Navigate to focus the 'My Items' folder, and hide the object tree
// This is necessary so that subsequent objects can be created without a parent
// TODO: Ideally this would navigate to a common `e2e` folder
await page.goto('./#/browse/mine?hideTree=true');
/**
* Contains information about the newly created domain object.
* @typedef {Object} CreatedObjectInfo
* @property {string} name the name of the created object
* @property {string} uuid the uuid of the created object
* @property {string} url the relative url to the object (for use with `page.goto()`)
*/
/**
* This common function creates a domain object with the default options. It is the preferred way of creating objects
* in the e2e suite when uninterested in properties of the objects themselves.
*
* @param {import('@playwright/test').Page} page
* @param {CreateObjectOptions} options
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
*/
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) {
const parentUrl = await getHashUrlToDomainObject(page, parent);
// Navigate to the parent object. This is necessary to create the object
// in the correct location, such as a folder, layout, or plot.
await page.goto(`${parentUrl}?hideTree=true`);
await page.waitForLoadState('networkidle');
//Click the Create button
await page.click('button:has-text("Create")');
@ -50,7 +69,7 @@ async function createDomainObjectWithDefaults(page, type, name) {
// Modify the name input field of the domain object to accept 'name'
if (name) {
const nameInput = page.locator('input[type="text"]').nth(2);
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
await nameInput.fill("");
await nameInput.fill(name);
}
@ -63,12 +82,28 @@ async function createDomainObjectWithDefaults(page, type, name) {
page.waitForSelector('.c-message-banner__message')
]);
return name || `Unnamed ${type}`;
// Wait until the URL is updated
await page.waitForURL(`**/${parent}/*`);
const uuid = await getFocusedObjectUuid(page);
const objectUrl = await getHashUrlToDomainObject(page, uuid);
if (await _isInEditMode(page, uuid)) {
// Save (exit edit mode)
await page.locator('button[title="Save"]').click();
await page.locator('li[title="Save and Finish Editing"]').click();
}
return {
name: name || `Unnamed ${type}`,
uuid: uuid,
url: objectUrl
};
}
/**
* Open the given `domainObject`'s context menu from the object tree.
* Expands the 'My Items' folder if it is not already expanded.
*
* @param {import('@playwright/test').Page} page
* @param {string} myItemsFolderName the name of the "My Items" folder
* @param {string} domainObjectName the display name of the `domainObject`
@ -85,8 +120,154 @@ async function openObjectTreeContextMenu(page, myItemsFolderName, domainObjectNa
});
}
/**
* Gets the UUID of the currently focused object by parsing the current URL
* and returning the last UUID in the path.
* @param {import('@playwright/test').Page} page
* @returns {Promise<string>} the uuid of the focused object
*/
async function getFocusedObjectUuid(page) {
const UUIDv4Regexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi;
const focusedObjectUuid = await page.evaluate((regexp) => {
return window.location.href.match(regexp).at(-1);
}, UUIDv4Regexp);
return focusedObjectUuid;
}
/**
* Returns the hashUrl to the domainObject given its uuid.
* Useful for directly navigating to the given domainObject.
*
* URLs returned will be of the form `'./browse/#/mine/<uuid0>/<uuid1>/...'`
*
* @param {import('@playwright/test').Page} page
* @param {string} uuid the uuid of the object to get the url for
* @returns {Promise<string>} the url of the object
*/
async function getHashUrlToDomainObject(page, uuid) {
const hashUrl = await page.evaluate(async (objectUuid) => {
const path = await window.openmct.objects.getOriginalPath(objectUuid);
let url = './#/browse/' + [...path].reverse()
.map((object) => window.openmct.objects.makeKeyString(object.identifier))
.join('/');
// Drop the vestigial '/ROOT' if it exists
if (url.includes('/ROOT')) {
url = url.split('/ROOT').join('');
}
return url;
}, uuid);
return hashUrl;
}
/**
* Utilizes the OpenMCT API to detect if the given object has an active transaction (is in Edit mode).
* @private
* @param {import('@playwright/test').Page} page
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} identifier
* @return {Promise<boolean>} true if the object has an active transaction, false otherwise
*/
async function _isInEditMode(page, identifier) {
// eslint-disable-next-line no-return-await
return await page.evaluate((objectIdentifier) => window.openmct.objects.isTransactionActive(objectIdentifier), identifier);
}
/**
* Set the time conductor mode to either fixed timespan or realtime mode.
* @param {import('@playwright/test').Page} page
* @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true
*/
async function setTimeConductorMode(page, isFixedTimespan = true) {
// Click 'mode' button
await page.locator('.c-mode-button').click();
// Switch time conductor mode
if (isFixedTimespan) {
await page.locator('data-testid=conductor-modeOption-fixed').click();
} else {
await page.locator('data-testid=conductor-modeOption-realtime').click();
}
}
/**
* Set the time conductor to fixed timespan mode
* @param {import('@playwright/test').Page} page
*/
async function setFixedTimeMode(page) {
await setTimeConductorMode(page, true);
}
/**
* Set the time conductor to realtime mode
* @param {import('@playwright/test').Page} page
*/
async function setRealTimeMode(page) {
await setTimeConductorMode(page, false);
}
/**
* @typedef {Object} OffsetValues
* @property {string | undefined} hours
* @property {string | undefined} mins
* @property {string | undefined} secs
*/
/**
* Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
* @param {import('@playwright/test').Locator} offsetButton
*/
async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) {
await offsetButton.click();
if (hours) {
await page.fill('.pr-time-controls__hrs', hours);
}
if (mins) {
await page.fill('.pr-time-controls__mins', mins);
}
if (secs) {
await page.fill('.pr-time-controls__secs', secs);
}
// Click the check button
await page.locator('.pr-time__buttons .icon-check').click();
}
/**
* Set the values (hours, mins, secs) for the start time offset when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
*/
async function setStartOffset(page, offset) {
const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
await setTimeConductorOffset(page, offset, startOffsetButton);
}
/**
* Set the values (hours, mins, secs) for the end time offset when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
*/
async function setEndOffset(page, offset) {
const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
await setTimeConductorOffset(page, offset, endOffsetButton);
}
// eslint-disable-next-line no-undef
module.exports = {
createDomainObjectWithDefaults,
openObjectTreeContextMenu
openObjectTreeContextMenu,
getHashUrlToDomainObject,
getFocusedObjectUuid,
setFixedTimeMode,
setRealTimeMode,
setStartOffset,
setEndOffset
};

View File

@ -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}`);
});
});
});

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
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');

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');
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

View File

@ -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();

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 { 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);
});
});

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' });
// Create a default 'Example Imagery' object
createDomainObjectWithDefaults(page, 'Example Imagery');
createDomainObjectWithDefaults(page, { type: 'Example Imagery' });
await Promise.all([
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
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);

View File

@ -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');

View File

@ -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();
}
}

View File

@ -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 }) => {

View File

@ -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 }) => {

View File

@ -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}')`);

View File

@ -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}')`);

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=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();

View File

@ -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",

View File

@ -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;
}
}

View File

@ -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);
});

View File

@ -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,25 +615,58 @@ export default class ObjectAPI {
* @param {module:openmct.ObjectAPI~Identifier[]} identifiers
*/
areIdsEqual(...identifiers) {
const firstIdentifier = utils.parseKeyString(identifiers[0]);
return identifiers.map(utils.parseKeyString)
.every(identifier => {
return identifier === identifiers[0]
|| (identifier.namespace === identifiers[0].namespace
&& identifier.key === identifiers[0].key);
return identifier === firstIdentifier
|| (identifier.namespace === firstIdentifier.namespace
&& identifier.key === firstIdentifier.key);
});
}
getOriginalPath(identifier, path = []) {
return this.get(identifier).then((domainObject) => {
path.push(domainObject);
let location = domainObject.location;
/**
* Given an original path check if the path is reachable via root
* @param {Array<Object>} originalPath an array of path objects to check
* @returns {boolean} whether the domain object is reachable
*/
isReachable(originalPath) {
if (originalPath && originalPath.length) {
return (originalPath[originalPath.length - 1].type === 'root');
}
if (location) {
return false;
}
#pathContainsDomainObject(keyStringToCheck, path) {
if (!keyStringToCheck) {
return false;
}
return path.some(pathElement => {
const identifierToCheck = utils.parseKeyString(keyStringToCheck);
return this.areIdsEqual(identifierToCheck, pathElement.identifier);
});
}
/**
* Given an identifier, constructs the original path by walking up its parents
* @param {module:openmct.ObjectAPI~Identifier} identifier
* @param {Array<module:openmct.DomainObject>} path an array of path objects
* @returns {Promise<Array<module:openmct.DomainObject>>} a promise containing an array of domain objects
*/
async getOriginalPath(identifier, path = []) {
const domainObject = await this.get(identifier);
path.push(domainObject);
const { location } = domainObject;
if (location && (!this.#pathContainsDomainObject(location, path))) {
// if we have a location, and we don't already have this in our constructed path,
// then keep walking up the path
return this.getOriginalPath(utils.parseKeyString(location), path);
} else {
return path;
}
});
}
isObjectPathToALink(domainObject, objectPath) {

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", () => {
beforeEach(() => {
spyOn(openmct.editor, 'isEditing').and.returnValue(true);

View File

@ -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;
}

View File

@ -51,7 +51,11 @@ export default class TelemetryCriterion extends EventEmitter {
}
initialize() {
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();

View File

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

View File

@ -25,13 +25,14 @@
/******************************************************** CONTROL-SPECIFIC MIXINS */
@mixin menuOuter() {
border-radius: $basicCr;
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;
}
}

View File

@ -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) {

View File

@ -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;
}

View File

@ -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>
</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;
}