mirror of
https://github.com/nasa/openmct.git
synced 2025-06-16 14:18:16 +00:00
Master 2.0.7 (#5672)
This commit is contained in:
@ -30,18 +30,37 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This common function creates a `domainObject` with default options. It is the preferred way of creating objects
|
* Defines parameters to be used in the creation of a domain object.
|
||||||
* in the e2e suite when uninterested in properties of the objects themselves.
|
* @typedef {Object} CreateObjectOptions
|
||||||
* @param {import('@playwright/test').Page} page
|
* @property {string} type the type of domain object to create (e.g.: "Sine Wave Generator").
|
||||||
* @param {string} type
|
* @property {string} [name] the desired name of the created domain object.
|
||||||
* @param {string | undefined} name
|
* @property {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the Identifier or uuid of the parent object.
|
||||||
*/
|
*/
|
||||||
async function createDomainObjectWithDefaults(page, type, name) {
|
|
||||||
// Navigate to focus the 'My Items' folder, and hide the object tree
|
/**
|
||||||
// This is necessary so that subsequent objects can be created without a parent
|
* Contains information about the newly created domain object.
|
||||||
// TODO: Ideally this would navigate to a common `e2e` folder
|
* @typedef {Object} CreatedObjectInfo
|
||||||
await page.goto('./#/browse/mine?hideTree=true');
|
* @property {string} name the name of the created object
|
||||||
|
* @property {string} uuid the uuid of the created object
|
||||||
|
* @property {string} url the relative url to the object (for use with `page.goto()`)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This common function creates a domain object with the default options. It is the preferred way of creating objects
|
||||||
|
* in the e2e suite when uninterested in properties of the objects themselves.
|
||||||
|
*
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {CreateObjectOptions} options
|
||||||
|
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
|
||||||
|
*/
|
||||||
|
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) {
|
||||||
|
const parentUrl = await getHashUrlToDomainObject(page, parent);
|
||||||
|
|
||||||
|
// Navigate to the parent object. This is necessary to create the object
|
||||||
|
// in the correct location, such as a folder, layout, or plot.
|
||||||
|
await page.goto(`${parentUrl}?hideTree=true`);
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
//Click the Create button
|
//Click the Create button
|
||||||
await page.click('button:has-text("Create")');
|
await page.click('button:has-text("Create")');
|
||||||
|
|
||||||
@ -50,7 +69,7 @@ async function createDomainObjectWithDefaults(page, type, name) {
|
|||||||
|
|
||||||
// Modify the name input field of the domain object to accept 'name'
|
// Modify the name input field of the domain object to accept 'name'
|
||||||
if (name) {
|
if (name) {
|
||||||
const nameInput = page.locator('input[type="text"]').nth(2);
|
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
|
||||||
await nameInput.fill("");
|
await nameInput.fill("");
|
||||||
await nameInput.fill(name);
|
await nameInput.fill(name);
|
||||||
}
|
}
|
||||||
@ -63,12 +82,28 @@ async function createDomainObjectWithDefaults(page, type, name) {
|
|||||||
page.waitForSelector('.c-message-banner__message')
|
page.waitForSelector('.c-message-banner__message')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return name || `Unnamed ${type}`;
|
// Wait until the URL is updated
|
||||||
|
await page.waitForURL(`**/${parent}/*`);
|
||||||
|
const uuid = await getFocusedObjectUuid(page);
|
||||||
|
const objectUrl = await getHashUrlToDomainObject(page, uuid);
|
||||||
|
|
||||||
|
if (await _isInEditMode(page, uuid)) {
|
||||||
|
// Save (exit edit mode)
|
||||||
|
await page.locator('button[title="Save"]').click();
|
||||||
|
await page.locator('li[title="Save and Finish Editing"]').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: name || `Unnamed ${type}`,
|
||||||
|
uuid: uuid,
|
||||||
|
url: objectUrl
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the given `domainObject`'s context menu from the object tree.
|
* Open the given `domainObject`'s context menu from the object tree.
|
||||||
* Expands the 'My Items' folder if it is not already expanded.
|
* Expands the 'My Items' folder if it is not already expanded.
|
||||||
|
*
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
* @param {string} myItemsFolderName the name of the "My Items" folder
|
* @param {string} myItemsFolderName the name of the "My Items" folder
|
||||||
* @param {string} domainObjectName the display name of the `domainObject`
|
* @param {string} domainObjectName the display name of the `domainObject`
|
||||||
@ -85,8 +120,154 @@ async function openObjectTreeContextMenu(page, myItemsFolderName, domainObjectNa
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the UUID of the currently focused object by parsing the current URL
|
||||||
|
* and returning the last UUID in the path.
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @returns {Promise<string>} the uuid of the focused object
|
||||||
|
*/
|
||||||
|
async function getFocusedObjectUuid(page) {
|
||||||
|
const UUIDv4Regexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi;
|
||||||
|
const focusedObjectUuid = await page.evaluate((regexp) => {
|
||||||
|
return window.location.href.match(regexp).at(-1);
|
||||||
|
}, UUIDv4Regexp);
|
||||||
|
|
||||||
|
return focusedObjectUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the hashUrl to the domainObject given its uuid.
|
||||||
|
* Useful for directly navigating to the given domainObject.
|
||||||
|
*
|
||||||
|
* URLs returned will be of the form `'./browse/#/mine/<uuid0>/<uuid1>/...'`
|
||||||
|
*
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {string} uuid the uuid of the object to get the url for
|
||||||
|
* @returns {Promise<string>} the url of the object
|
||||||
|
*/
|
||||||
|
async function getHashUrlToDomainObject(page, uuid) {
|
||||||
|
const hashUrl = await page.evaluate(async (objectUuid) => {
|
||||||
|
const path = await window.openmct.objects.getOriginalPath(objectUuid);
|
||||||
|
let url = './#/browse/' + [...path].reverse()
|
||||||
|
.map((object) => window.openmct.objects.makeKeyString(object.identifier))
|
||||||
|
.join('/');
|
||||||
|
|
||||||
|
// Drop the vestigial '/ROOT' if it exists
|
||||||
|
if (url.includes('/ROOT')) {
|
||||||
|
url = url.split('/ROOT').join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}, uuid);
|
||||||
|
|
||||||
|
return hashUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilizes the OpenMCT API to detect if the given object has an active transaction (is in Edit mode).
|
||||||
|
* @private
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} identifier
|
||||||
|
* @return {Promise<boolean>} true if the object has an active transaction, false otherwise
|
||||||
|
*/
|
||||||
|
async function _isInEditMode(page, identifier) {
|
||||||
|
// eslint-disable-next-line no-return-await
|
||||||
|
return await page.evaluate((objectIdentifier) => window.openmct.objects.isTransactionActive(objectIdentifier), identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the time conductor mode to either fixed timespan or realtime mode.
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true
|
||||||
|
*/
|
||||||
|
async function setTimeConductorMode(page, isFixedTimespan = true) {
|
||||||
|
// Click 'mode' button
|
||||||
|
await page.locator('.c-mode-button').click();
|
||||||
|
|
||||||
|
// Switch time conductor mode
|
||||||
|
if (isFixedTimespan) {
|
||||||
|
await page.locator('data-testid=conductor-modeOption-fixed').click();
|
||||||
|
} else {
|
||||||
|
await page.locator('data-testid=conductor-modeOption-realtime').click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the time conductor to fixed timespan mode
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function setFixedTimeMode(page) {
|
||||||
|
await setTimeConductorMode(page, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the time conductor to realtime mode
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function setRealTimeMode(page) {
|
||||||
|
await setTimeConductorMode(page, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} OffsetValues
|
||||||
|
* @property {string | undefined} hours
|
||||||
|
* @property {string | undefined} mins
|
||||||
|
* @property {string | undefined} secs
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {OffsetValues} offset
|
||||||
|
* @param {import('@playwright/test').Locator} offsetButton
|
||||||
|
*/
|
||||||
|
async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) {
|
||||||
|
await offsetButton.click();
|
||||||
|
|
||||||
|
if (hours) {
|
||||||
|
await page.fill('.pr-time-controls__hrs', hours);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mins) {
|
||||||
|
await page.fill('.pr-time-controls__mins', mins);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (secs) {
|
||||||
|
await page.fill('.pr-time-controls__secs', secs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click the check button
|
||||||
|
await page.locator('.pr-time__buttons .icon-check').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the values (hours, mins, secs) for the start time offset when in realtime mode
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {OffsetValues} offset
|
||||||
|
*/
|
||||||
|
async function setStartOffset(page, offset) {
|
||||||
|
const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
|
||||||
|
await setTimeConductorOffset(page, offset, startOffsetButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the values (hours, mins, secs) for the end time offset when in realtime mode
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {OffsetValues} offset
|
||||||
|
*/
|
||||||
|
async function setEndOffset(page, offset) {
|
||||||
|
const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
|
||||||
|
await setTimeConductorOffset(page, offset, endOffsetButton);
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createDomainObjectWithDefaults,
|
createDomainObjectWithDefaults,
|
||||||
openObjectTreeContextMenu
|
openObjectTreeContextMenu,
|
||||||
|
getHashUrlToDomainObject,
|
||||||
|
getFocusedObjectUuid,
|
||||||
|
setFixedTimeMode,
|
||||||
|
setRealTimeMode,
|
||||||
|
setStartOffset,
|
||||||
|
setEndOffset
|
||||||
};
|
};
|
||||||
|
@ -23,19 +23,66 @@
|
|||||||
const { test, expect } = require('../../baseFixtures.js');
|
const { test, expect } = require('../../baseFixtures.js');
|
||||||
const { createDomainObjectWithDefaults } = require('../../appActions.js');
|
const { createDomainObjectWithDefaults } = require('../../appActions.js');
|
||||||
|
|
||||||
test.describe('appActions tests', () => {
|
test.describe('AppActions', () => {
|
||||||
test('createDomainObjectsWithDefaults can create multiple objects in a row', async ({ page }) => {
|
test('createDomainObjectsWithDefaults', async ({ page }) => {
|
||||||
await page.goto('./', { waitUntil: 'networkidle' });
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
await createDomainObjectWithDefaults(page, 'Timer', 'Timer Foo');
|
|
||||||
await createDomainObjectWithDefaults(page, 'Timer', 'Timer Bar');
|
|
||||||
await createDomainObjectWithDefaults(page, 'Timer', 'Timer Baz');
|
|
||||||
|
|
||||||
// Expand the tree
|
const e2eFolder = await createDomainObjectWithDefaults(page, {
|
||||||
await page.click('.c-disclosure-triangle');
|
type: 'Folder',
|
||||||
|
name: 'e2e folder'
|
||||||
|
});
|
||||||
|
|
||||||
// Verify the objects were created
|
await test.step('Create multiple flat objects in a row', async () => {
|
||||||
await expect(page.locator('a :text("Timer Foo")')).toBeVisible();
|
const timer1 = await createDomainObjectWithDefaults(page, {
|
||||||
await expect(page.locator('a :text("Timer Bar")')).toBeVisible();
|
type: 'Timer',
|
||||||
await expect(page.locator('a :text("Timer Baz")')).toBeVisible();
|
name: 'Timer Foo',
|
||||||
|
parent: e2eFolder.uuid
|
||||||
|
});
|
||||||
|
const timer2 = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Timer',
|
||||||
|
name: 'Timer Bar',
|
||||||
|
parent: e2eFolder.uuid
|
||||||
|
});
|
||||||
|
const timer3 = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Timer',
|
||||||
|
name: 'Timer Baz',
|
||||||
|
parent: e2eFolder.uuid
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(timer1.url, { waitUntil: 'networkidle' });
|
||||||
|
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Foo');
|
||||||
|
await page.goto(timer2.url, { waitUntil: 'networkidle' });
|
||||||
|
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Bar');
|
||||||
|
await page.goto(timer3.url, { waitUntil: 'networkidle' });
|
||||||
|
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Baz');
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Create multiple nested objects in a row', async () => {
|
||||||
|
const folder1 = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Folder',
|
||||||
|
name: 'Folder Foo',
|
||||||
|
parent: e2eFolder.uuid
|
||||||
|
});
|
||||||
|
const folder2 = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Folder',
|
||||||
|
name: 'Folder Bar',
|
||||||
|
parent: folder1.uuid
|
||||||
|
});
|
||||||
|
const folder3 = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Folder',
|
||||||
|
name: 'Folder Baz',
|
||||||
|
parent: folder2.uuid
|
||||||
|
});
|
||||||
|
await page.goto(folder1.url, { waitUntil: 'networkidle' });
|
||||||
|
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Foo');
|
||||||
|
await page.goto(folder2.url, { waitUntil: 'networkidle' });
|
||||||
|
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Bar');
|
||||||
|
await page.goto(folder3.url, { waitUntil: 'networkidle' });
|
||||||
|
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Baz');
|
||||||
|
|
||||||
|
expect(folder1.url).toBe(`${e2eFolder.url}/${folder1.uuid}`);
|
||||||
|
expect(folder2.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}`);
|
||||||
|
expect(folder3.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}/${folder3.uuid}`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -58,7 +58,7 @@ test.describe('Renaming Timer Object', () => {
|
|||||||
//Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
//Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||||
await page.goto('./', { waitUntil: 'networkidle' });
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
//We provide some helper functions in appActions like createDomainObjectWithDefaults. This example will create a Timer object
|
//We provide some helper functions in appActions like createDomainObjectWithDefaults. This example will create a Timer object
|
||||||
await createDomainObjectWithDefaults(page, 'Timer');
|
await createDomainObjectWithDefaults(page, { type: 'Timer' });
|
||||||
//Assert the object to be created and check it's name in the title
|
//Assert the object to be created and check it's name in the title
|
||||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer');
|
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer');
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ test.describe('Renaming Timer Object', () => {
|
|||||||
//Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
//Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||||
await page.goto('./', { waitUntil: 'networkidle' });
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
//We provide some helper functions in appActions like createDomainObjectWithDefaults. This example will create a Timer object
|
//We provide some helper functions in appActions like createDomainObjectWithDefaults. This example will create a Timer object
|
||||||
await createDomainObjectWithDefaults(page, 'Timer');
|
await createDomainObjectWithDefaults(page, { type: 'Timer' });
|
||||||
//Expect the object to be created and check it's name in the title
|
//Expect the object to be created and check it's name in the title
|
||||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer');
|
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer');
|
||||||
|
|
||||||
|
@ -31,29 +31,13 @@ TODO: Provide additional validation of object properties as it grows.
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const { createDomainObjectWithDefaults } = require('../../appActions.js');
|
||||||
const { test, expect } = require('../../pluginFixtures.js');
|
const { test, expect } = require('../../pluginFixtures.js');
|
||||||
|
|
||||||
test('Generate Visual Test Data @localStorage', async ({ page, context, openmctConfig }) => {
|
test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
|
||||||
const { myItemsFolderName } = openmctConfig;
|
|
||||||
|
|
||||||
//Go to baseURL
|
//Go to baseURL
|
||||||
await page.goto('./', { waitUntil: 'networkidle' });
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
|
const overlayPlot = await createDomainObjectWithDefaults(page, { type: 'Overlay Plot' });
|
||||||
await page.locator('button:has-text("Create")').click();
|
|
||||||
|
|
||||||
// add overlay plot with defaults
|
|
||||||
await page.locator('li:has-text("Overlay Plot")').click();
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation(),
|
|
||||||
page.locator('text=OK').click(),
|
|
||||||
//Wait for Save Banner to appear1
|
|
||||||
page.waitForSelector('.c-message-banner__message')
|
|
||||||
]);
|
|
||||||
|
|
||||||
// save (exit edit mode)
|
|
||||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
|
||||||
await page.locator('text=Save and Finish Editing').click();
|
|
||||||
|
|
||||||
// click create button
|
// click create button
|
||||||
await page.locator('button:has-text("Create")').click();
|
await page.locator('button:has-text("Create")').click();
|
||||||
@ -67,16 +51,12 @@ test('Generate Visual Test Data @localStorage', async ({ page, context, openmctC
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
page.waitForNavigation(),
|
page.waitForNavigation(),
|
||||||
page.locator('text=OK').click(),
|
page.locator('text=OK').click(),
|
||||||
//Wait for Save Banner to appear1
|
//Wait for Save Banner to appear
|
||||||
page.waitForSelector('.c-message-banner__message')
|
page.waitForSelector('.c-message-banner__message')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// focus the overlay plot
|
// focus the overlay plot
|
||||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
await page.goto(overlayPlot.url);
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation(),
|
|
||||||
page.locator('text=Unnamed Overlay Plot').first().click()
|
|
||||||
]);
|
|
||||||
|
|
||||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
|
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
|
||||||
//Save localStorage for future test execution
|
//Save localStorage for future test execution
|
||||||
|
@ -35,7 +35,10 @@ test.describe('Example Event Generator CRUD Operations', () => {
|
|||||||
//Create a name for the object
|
//Create a name for the object
|
||||||
const newObjectName = 'Test Event Generator';
|
const newObjectName = 'Test Event Generator';
|
||||||
|
|
||||||
await createDomainObjectWithDefaults(page, 'Event Message Generator', newObjectName);
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Event Message Generator',
|
||||||
|
name: newObjectName
|
||||||
|
});
|
||||||
|
|
||||||
//Assertions against newly created object which define standard behavior
|
//Assertions against newly created object which define standard behavior
|
||||||
await expect(page.waitForURL(/.*&view=table/)).toBeTruthy();
|
await expect(page.waitForURL(/.*&view=table/)).toBeTruthy();
|
||||||
|
@ -27,6 +27,7 @@ demonstrate some playwright for test developers. This pattern should not be re-u
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const { test, expect } = require('../../../../pluginFixtures.js');
|
const { test, expect } = require('../../../../pluginFixtures.js');
|
||||||
|
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||||
|
|
||||||
let conditionSetUrl;
|
let conditionSetUrl;
|
||||||
let getConditionSetIdentifierFromUrl;
|
let getConditionSetIdentifierFromUrl;
|
||||||
@ -178,3 +179,24 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('Basic Condition Set Use', () => {
|
||||||
|
test('Can add a condition', async ({ page }) => {
|
||||||
|
//Navigate to baseURL
|
||||||
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
|
// Create a new condition set
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Condition Set',
|
||||||
|
name: "Test Condition Set"
|
||||||
|
});
|
||||||
|
// Change the object to edit mode
|
||||||
|
await page.locator('[title="Edit"]').click();
|
||||||
|
|
||||||
|
// Click Add Condition button
|
||||||
|
await page.locator('#addCondition').click();
|
||||||
|
// Check that the new Unnamed Condition section appears
|
||||||
|
const numOfUnnamedConditions = await page.locator('text=Unnamed Condition').count();
|
||||||
|
expect(numOfUnnamedConditions).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -0,0 +1,122 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
const { test, expect } = require('../../../../pluginFixtures');
|
||||||
|
const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions');
|
||||||
|
|
||||||
|
test.describe('Testing Display Layout @unstable', () => {
|
||||||
|
let sineWaveObject;
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
|
await setRealTimeMode(page);
|
||||||
|
|
||||||
|
// Create Sine Wave Generator
|
||||||
|
sineWaveObject = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Sine Wave Generator',
|
||||||
|
name: "Test Sine Wave Generator"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in real time', async ({ page }) => {
|
||||||
|
// Create a Display Layout
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Display Layout',
|
||||||
|
name: "Test Display Layout"
|
||||||
|
});
|
||||||
|
// Edit Display Layout
|
||||||
|
await page.locator('[title="Edit"]').click();
|
||||||
|
|
||||||
|
// Expand the 'My Items' folder in the left tree
|
||||||
|
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||||
|
// Add the Sine Wave Generator to the Display Layout and save changes
|
||||||
|
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
|
||||||
|
await page.locator('button[title="Save"]').click();
|
||||||
|
await page.locator('text=Save and Finish Editing').click();
|
||||||
|
|
||||||
|
// Subscribe to the Sine Wave Generator data
|
||||||
|
// On getting data, check if the value found in the Display Layout is the most recent value
|
||||||
|
// from the Sine Wave Generator
|
||||||
|
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
|
||||||
|
const formattedTelemetryValue = await getTelemValuePromise;
|
||||||
|
const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`);
|
||||||
|
const displayLayoutValue = await displayLayoutValuePromise.textContent();
|
||||||
|
const trimmedDisplayValue = displayLayoutValue.trim();
|
||||||
|
|
||||||
|
await expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
|
||||||
|
});
|
||||||
|
test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in fixed time', async ({ page }) => {
|
||||||
|
// Create a Display Layout
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Display Layout',
|
||||||
|
name: "Test Display Layout"
|
||||||
|
});
|
||||||
|
// Edit Display Layout
|
||||||
|
await page.locator('[title="Edit"]').click();
|
||||||
|
|
||||||
|
// Expand the 'My Items' folder in the left tree
|
||||||
|
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||||
|
// Add the Sine Wave Generator to the Display Layout and save changes
|
||||||
|
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
|
||||||
|
await page.locator('button[title="Save"]').click();
|
||||||
|
await page.locator('text=Save and Finish Editing').click();
|
||||||
|
|
||||||
|
// Subscribe to the Sine Wave Generator data
|
||||||
|
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
|
||||||
|
// Set an offset of 1 minute and then change the time mode to fixed to set a 1 minute historical window
|
||||||
|
await setStartOffset(page, { mins: '1' });
|
||||||
|
await setFixedTimeMode(page);
|
||||||
|
|
||||||
|
// On getting data, check if the value found in the Display Layout is the most recent value
|
||||||
|
// from the Sine Wave Generator
|
||||||
|
const formattedTelemetryValue = await getTelemValuePromise;
|
||||||
|
const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`);
|
||||||
|
const displayLayoutValue = await displayLayoutValuePromise.textContent();
|
||||||
|
const trimmedDisplayValue = displayLayoutValue.trim();
|
||||||
|
|
||||||
|
await expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Util for subscribing to a telemetry object by object identifier
|
||||||
|
* Limitations: Currently only works to return telemetry once to the node scope
|
||||||
|
* To Do: See if there's a way to await this multiple times to allow for multiple
|
||||||
|
* values to be returned over time
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {string} objectIdentifier identifier for object
|
||||||
|
* @returns {Promise<string>} the formatted sin telemetry value
|
||||||
|
*/
|
||||||
|
async function subscribeToTelemetry(page, objectIdentifier) {
|
||||||
|
const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getTelemValue', resolve));
|
||||||
|
|
||||||
|
await page.evaluate(async (telemetryIdentifier) => {
|
||||||
|
const telemetryObject = await window.openmct.objects.get(telemetryIdentifier);
|
||||||
|
const metadata = window.openmct.telemetry.getMetadata(telemetryObject);
|
||||||
|
const formats = await window.openmct.telemetry.getFormatMap(metadata);
|
||||||
|
window.openmct.telemetry.subscribe(telemetryObject, (obj) => {
|
||||||
|
const sinVal = obj.sin;
|
||||||
|
const formattedSinVal = formats.sin.format(sinVal);
|
||||||
|
window.getTelemValue(formattedSinVal);
|
||||||
|
});
|
||||||
|
}, objectIdentifier);
|
||||||
|
|
||||||
|
return getTelemValuePromise;
|
||||||
|
}
|
@ -41,7 +41,7 @@ test.describe('Example Imagery Object', () => {
|
|||||||
await page.goto('./', { waitUntil: 'networkidle' });
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
// Create a default 'Example Imagery' object
|
// Create a default 'Example Imagery' object
|
||||||
createDomainObjectWithDefaults(page, 'Example Imagery');
|
createDomainObjectWithDefaults(page, { type: 'Example Imagery' });
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
page.waitForNavigation(),
|
page.waitForNavigation(),
|
||||||
|
120
e2e/tests/functional/plugins/lad/lad.e2e.spec.js
Normal file
120
e2e/tests/functional/plugins/lad/lad.e2e.spec.js
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
const { test, expect } = require('../../../../pluginFixtures');
|
||||||
|
const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions');
|
||||||
|
|
||||||
|
test.describe('Testing LAD table @unstable', () => {
|
||||||
|
let sineWaveObject;
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
|
await setRealTimeMode(page);
|
||||||
|
|
||||||
|
// Create Sine Wave Generator
|
||||||
|
sineWaveObject = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Sine Wave Generator',
|
||||||
|
name: "Test Sine Wave Generator"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('telemetry value exactly matches latest telemetry value received in real time', async ({ page }) => {
|
||||||
|
// Create LAD table
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'LAD Table',
|
||||||
|
name: "Test LAD Table"
|
||||||
|
});
|
||||||
|
// Edit LAD table
|
||||||
|
await page.locator('[title="Edit"]').click();
|
||||||
|
|
||||||
|
// Expand the 'My Items' folder in the left tree
|
||||||
|
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||||
|
// Add the Sine Wave Generator to the LAD table and save changes
|
||||||
|
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-lad-table-wrapper');
|
||||||
|
await page.locator('button[title="Save"]').click();
|
||||||
|
await page.locator('text=Save and Finish Editing').click();
|
||||||
|
|
||||||
|
// Subscribe to the Sine Wave Generator data
|
||||||
|
// On getting data, check if the value found in the LAD table is the most recent value
|
||||||
|
// from the Sine Wave Generator
|
||||||
|
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
|
||||||
|
const subscribeTelemValue = await getTelemValuePromise;
|
||||||
|
const ladTableValuePromise = await page.waitForSelector(`text="${subscribeTelemValue}"`);
|
||||||
|
const ladTableValue = await ladTableValuePromise.textContent();
|
||||||
|
|
||||||
|
expect(ladTableValue).toBe(subscribeTelemValue);
|
||||||
|
});
|
||||||
|
test('telemetry value exactly matches latest telemetry value received in fixed time', async ({ page }) => {
|
||||||
|
// Create LAD table
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'LAD Table',
|
||||||
|
name: "Test LAD Table"
|
||||||
|
});
|
||||||
|
// Edit LAD table
|
||||||
|
await page.locator('[title="Edit"]').click();
|
||||||
|
|
||||||
|
// Expand the 'My Items' folder in the left tree
|
||||||
|
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||||
|
// Add the Sine Wave Generator to the LAD table and save changes
|
||||||
|
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-lad-table-wrapper');
|
||||||
|
await page.locator('button[title="Save"]').click();
|
||||||
|
await page.locator('text=Save and Finish Editing').click();
|
||||||
|
|
||||||
|
// Subscribe to the Sine Wave Generator data
|
||||||
|
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
|
||||||
|
// Set an offset of 1 minute and then change the time mode to fixed to set a 1 minute historical window
|
||||||
|
await setStartOffset(page, { mins: '1' });
|
||||||
|
await setFixedTimeMode(page);
|
||||||
|
|
||||||
|
// On getting data, check if the value found in the LAD table is the most recent value
|
||||||
|
// from the Sine Wave Generator
|
||||||
|
const subscribeTelemValue = await getTelemValuePromise;
|
||||||
|
const ladTableValuePromise = await page.waitForSelector(`text="${subscribeTelemValue}"`);
|
||||||
|
const ladTableValue = await ladTableValuePromise.textContent();
|
||||||
|
|
||||||
|
expect(ladTableValue).toBe(subscribeTelemValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Util for subscribing to a telemetry object by object identifier
|
||||||
|
* Limitations: Currently only works to return telemetry once to the node scope
|
||||||
|
* To Do: See if there's a way to await this multiple times to allow for multiple
|
||||||
|
* values to be returned over time
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {string} objectIdentifier identifier for object
|
||||||
|
* @returns {Promise<string>} the formatted sin telemetry value
|
||||||
|
*/
|
||||||
|
async function subscribeToTelemetry(page, objectIdentifier) {
|
||||||
|
const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getTelemValue', resolve));
|
||||||
|
|
||||||
|
await page.evaluate(async (telemetryIdentifier) => {
|
||||||
|
const telemetryObject = await window.openmct.objects.get(telemetryIdentifier);
|
||||||
|
const metadata = window.openmct.telemetry.getMetadata(telemetryObject);
|
||||||
|
const formats = await window.openmct.telemetry.getFormatMap(metadata);
|
||||||
|
window.openmct.telemetry.subscribe(telemetryObject, (obj) => {
|
||||||
|
const sinVal = obj.sin;
|
||||||
|
const formattedSinVal = formats.sin.format(sinVal);
|
||||||
|
window.getTelemValue(formattedSinVal);
|
||||||
|
});
|
||||||
|
}, objectIdentifier);
|
||||||
|
|
||||||
|
return getTelemValuePromise;
|
||||||
|
}
|
@ -36,7 +36,7 @@ async function createNotebookAndEntry(page, iterations = 1) {
|
|||||||
//Go to baseURL
|
//Go to baseURL
|
||||||
await page.goto('./', { waitUntil: 'networkidle' });
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
createDomainObjectWithDefaults(page, 'Notebook');
|
createDomainObjectWithDefaults(page, { type: 'Notebook' });
|
||||||
|
|
||||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||||
// Click text=To start a new entry, click here or drag and drop any object
|
// Click text=To start a new entry, click here or drag and drop any object
|
||||||
@ -139,11 +139,28 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
|||||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
|
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
|
||||||
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
|
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Can delete objects with tags and neither return in search', async ({ page }) => {
|
||||||
|
await createNotebookEntryAndTags(page);
|
||||||
|
// Delete Notebook
|
||||||
|
await page.locator('button[title="More options"]').click();
|
||||||
|
await page.locator('li[title="Remove this object from its containing object."]').click();
|
||||||
|
await page.locator('button:has-text("OK")').click();
|
||||||
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
|
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||||
|
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed');
|
||||||
|
await expect(page.locator('text=No matching results.')).toBeVisible();
|
||||||
|
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sci');
|
||||||
|
await expect(page.locator('text=No matching results.')).toBeVisible();
|
||||||
|
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('dri');
|
||||||
|
await expect(page.locator('text=No matching results.')).toBeVisible();
|
||||||
|
});
|
||||||
test('Tags persist across reload', async ({ page }) => {
|
test('Tags persist across reload', async ({ page }) => {
|
||||||
//Go to baseURL
|
//Go to baseURL
|
||||||
await page.goto('./', { waitUntil: 'networkidle' });
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
await createDomainObjectWithDefaults(page, 'Clock');
|
await createDomainObjectWithDefaults(page, { type: 'Clock' });
|
||||||
|
|
||||||
const ITERATIONS = 4;
|
const ITERATIONS = 4;
|
||||||
await createNotebookEntryAndTags(page, ITERATIONS);
|
await createNotebookEntryAndTags(page, ITERATIONS);
|
||||||
|
@ -20,55 +20,26 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
|
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||||
const { test, expect } = require('../../../../pluginFixtures');
|
const { test, expect } = require('../../../../pluginFixtures');
|
||||||
|
|
||||||
test.describe('Telemetry Table', () => {
|
test.describe('Telemetry Table', () => {
|
||||||
test('unpauses and filters data when paused by button and user changes bounds', async ({ page, openmctConfig }) => {
|
test('unpauses and filters data when paused by button and user changes bounds', async ({ page }) => {
|
||||||
test.info().annotations.push({
|
test.info().annotations.push({
|
||||||
type: 'issue',
|
type: 'issue',
|
||||||
description: 'https://github.com/nasa/openmct/issues/5113'
|
description: 'https://github.com/nasa/openmct/issues/5113'
|
||||||
});
|
});
|
||||||
|
|
||||||
const { myItemsFolderName } = openmctConfig;
|
|
||||||
const bannerMessage = '.c-message-banner__message';
|
|
||||||
const createButton = 'button:has-text("Create")';
|
|
||||||
|
|
||||||
await page.goto('./', { waitUntil: 'networkidle' });
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
// Click create button
|
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
|
||||||
await page.locator(createButton).click();
|
await createDomainObjectWithDefaults(page, {
|
||||||
await page.locator('li:has-text("Telemetry Table")').click();
|
type: 'Sine Wave Generator',
|
||||||
|
parent: table.uuid
|
||||||
await Promise.all([
|
});
|
||||||
page.waitForNavigation(),
|
|
||||||
page.locator('text=OK').click(),
|
|
||||||
// Wait for Save Banner to appear
|
|
||||||
page.waitForSelector(bannerMessage)
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Save (exit edit mode)
|
|
||||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(3).click();
|
|
||||||
await page.locator('text=Save and Finish Editing').click();
|
|
||||||
|
|
||||||
// Click create button
|
|
||||||
await page.locator(createButton).click();
|
|
||||||
|
|
||||||
// add Sine Wave Generator with defaults
|
|
||||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation(),
|
|
||||||
page.locator('text=OK').click(),
|
|
||||||
// Wait for Save Banner to appear
|
|
||||||
page.waitForSelector(bannerMessage)
|
|
||||||
]);
|
|
||||||
|
|
||||||
// focus the Telemetry Table
|
// focus the Telemetry Table
|
||||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
page.goto(table.url);
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation(),
|
|
||||||
page.locator('text=Unnamed Telemetry Table').first().click()
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Click pause button
|
// Click pause button
|
||||||
const pauseButton = page.locator('button.c-button.icon-pause');
|
const pauseButton = page.locator('button.c-button.icon-pause');
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
const { test, expect } = require('../../../../baseFixtures');
|
const { test, expect } = require('../../../../baseFixtures');
|
||||||
|
const { setFixedTimeMode, setRealTimeMode, setStartOffset, setEndOffset } = require('../../../../appActions');
|
||||||
|
|
||||||
test.describe('Time conductor operations', () => {
|
test.describe('Time conductor operations', () => {
|
||||||
test('validate start time does not exceeds end time', async ({ page }) => {
|
test('validate start time does not exceeds end time', async ({ page }) => {
|
||||||
@ -147,88 +148,3 @@ test.describe('Time conductor input fields real-time mode', () => {
|
|||||||
expect(page.url()).toContain(`endDelta=${endDelta}`);
|
expect(page.url()).toContain(`endDelta=${endDelta}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} OffsetValues
|
|
||||||
* @property {string | undefined} hours
|
|
||||||
* @property {string | undefined} mins
|
|
||||||
* @property {string | undefined} secs
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the values (hours, mins, secs) for the start time offset when in realtime mode
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
* @param {OffsetValues} offset
|
|
||||||
*/
|
|
||||||
async function setStartOffset(page, offset) {
|
|
||||||
const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
|
|
||||||
await setTimeConductorOffset(page, offset, startOffsetButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the values (hours, mins, secs) for the end time offset when in realtime mode
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
* @param {OffsetValues} offset
|
|
||||||
*/
|
|
||||||
async function setEndOffset(page, offset) {
|
|
||||||
const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
|
|
||||||
await setTimeConductorOffset(page, offset, endOffsetButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the time conductor to fixed timespan mode
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
async function setFixedTimeMode(page) {
|
|
||||||
await setTimeConductorMode(page, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the time conductor to realtime mode
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
async function setRealTimeMode(page) {
|
|
||||||
await setTimeConductorMode(page, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
* @param {OffsetValues} offset
|
|
||||||
* @param {import('@playwright/test').Locator} offsetButton
|
|
||||||
*/
|
|
||||||
async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) {
|
|
||||||
await offsetButton.click();
|
|
||||||
|
|
||||||
if (hours) {
|
|
||||||
await page.fill('.pr-time-controls__hrs', hours);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mins) {
|
|
||||||
await page.fill('.pr-time-controls__mins', mins);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (secs) {
|
|
||||||
await page.fill('.pr-time-controls__secs', secs);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Click the check button
|
|
||||||
await page.locator('.icon-check').click();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the time conductor mode to either fixed timespan or realtime mode.
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
* @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true
|
|
||||||
*/
|
|
||||||
async function setTimeConductorMode(page, isFixedTimespan = true) {
|
|
||||||
// Click 'mode' button
|
|
||||||
await page.locator('.c-mode-button').click();
|
|
||||||
|
|
||||||
// Switch time conductor mode
|
|
||||||
if (isFixedTimespan) {
|
|
||||||
await page.locator('data-testid=conductor-modeOption-fixed').click();
|
|
||||||
} else {
|
|
||||||
await page.locator('data-testid=conductor-modeOption-realtime').click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -26,7 +26,7 @@ const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('.
|
|||||||
test.describe('Timer', () => {
|
test.describe('Timer', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('./', { waitUntil: 'networkidle' });
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
await createDomainObjectWithDefaults(page, 'timer');
|
await createDomainObjectWithDefaults(page, { type: 'timer' });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Can perform actions on the Timer', async ({ page, openmctConfig }) => {
|
test('Can perform actions on the Timer', async ({ page, openmctConfig }) => {
|
||||||
|
@ -107,6 +107,9 @@ test.describe("Search Tests @unstable", () => {
|
|||||||
|
|
||||||
// Verify that no results are found
|
// Verify that no results are found
|
||||||
expect(await searchResults.count()).toBe(0);
|
expect(await searchResults.count()).toBe(0);
|
||||||
|
|
||||||
|
// Verify proper message appears
|
||||||
|
await expect(page.locator('text=No matching results.')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Validate single object in search result', async ({ page }) => {
|
test('Validate single object in search result', async ({ page }) => {
|
||||||
|
@ -53,7 +53,7 @@ test.describe('Visual - addInit', () => {
|
|||||||
//Go to baseURL
|
//Go to baseURL
|
||||||
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
|
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
await createDomainObjectWithDefaults(page, CUSTOM_NAME);
|
await createDomainObjectWithDefaults(page, { type: CUSTOM_NAME });
|
||||||
|
|
||||||
// Take a snapshot of the newly created CUSTOM_NAME notebook
|
// Take a snapshot of the newly created CUSTOM_NAME notebook
|
||||||
await percySnapshot(page, `Restricted Notebook with CUSTOM_NAME (theme: '${theme}')`);
|
await percySnapshot(page, `Restricted Notebook with CUSTOM_NAME (theme: '${theme}')`);
|
||||||
|
@ -67,9 +67,9 @@ test.describe('Visual - Default', () => {
|
|||||||
await percySnapshot(page, `About (theme: '${theme}')`);
|
await percySnapshot(page, `About (theme: '${theme}')`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Visual - Default Condition Set', async ({ page, theme }) => {
|
test.fixme('Visual - Default Condition Set', async ({ page, theme }) => {
|
||||||
|
|
||||||
await createDomainObjectWithDefaults(page, 'Condition Set');
|
await createDomainObjectWithDefaults(page, { type: 'Condition Set' });
|
||||||
|
|
||||||
// Take a snapshot of the newly created Condition Set object
|
// Take a snapshot of the newly created Condition Set object
|
||||||
await percySnapshot(page, `Default Condition Set (theme: '${theme}')`);
|
await percySnapshot(page, `Default Condition Set (theme: '${theme}')`);
|
||||||
@ -81,7 +81,7 @@ test.describe('Visual - Default', () => {
|
|||||||
description: 'https://github.com/nasa/openmct/issues/5349'
|
description: 'https://github.com/nasa/openmct/issues/5349'
|
||||||
});
|
});
|
||||||
|
|
||||||
await createDomainObjectWithDefaults(page, 'Condition Widget');
|
await createDomainObjectWithDefaults(page, { type: 'Condition Widget' });
|
||||||
|
|
||||||
// Take a snapshot of the newly created Condition Widget object
|
// Take a snapshot of the newly created Condition Widget object
|
||||||
await percySnapshot(page, `Default Condition Widget (theme: '${theme}')`);
|
await percySnapshot(page, `Default Condition Widget (theme: '${theme}')`);
|
||||||
@ -137,8 +137,8 @@ test.describe('Visual - Default', () => {
|
|||||||
await percySnapshot(page, `removed amplitude property value (theme: '${theme}')`);
|
await percySnapshot(page, `removed amplitude property value (theme: '${theme}')`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Visual - Save Successful Banner', async ({ page, theme }) => {
|
test.fixme('Visual - Save Successful Banner', async ({ page, theme }) => {
|
||||||
await createDomainObjectWithDefaults(page, 'Timer');
|
await createDomainObjectWithDefaults(page, { type: 'Timer' });
|
||||||
|
|
||||||
await page.locator('.c-message-banner__message').hover({ trial: true });
|
await page.locator('.c-message-banner__message').hover({ trial: true });
|
||||||
await percySnapshot(page, `Banner message shown (theme: '${theme}')`);
|
await percySnapshot(page, `Banner message shown (theme: '${theme}')`);
|
||||||
@ -159,8 +159,8 @@ test.describe('Visual - Default', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Visual - Default Gauge is correct', async ({ page, theme }) => {
|
test.fixme('Visual - Default Gauge is correct', async ({ page, theme }) => {
|
||||||
await createDomainObjectWithDefaults(page, 'Gauge');
|
await createDomainObjectWithDefaults(page, { type: 'Gauge' });
|
||||||
|
|
||||||
// Take a snapshot of the newly created Gauge object
|
// Take a snapshot of the newly created Gauge object
|
||||||
await percySnapshot(page, `Default Gauge (theme: '${theme}')`);
|
await percySnapshot(page, `Default Gauge (theme: '${theme}')`);
|
||||||
|
@ -46,7 +46,10 @@ test.describe('Grand Search', () => {
|
|||||||
// await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
// await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||||
// await page.locator('text=Save and Finish Editing').click();
|
// await page.locator('text=Save and Finish Editing').click();
|
||||||
const folder1 = 'Folder1';
|
const folder1 = 'Folder1';
|
||||||
await createDomainObjectWithDefaults(page, 'Folder', folder1);
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Folder',
|
||||||
|
name: folder1
|
||||||
|
});
|
||||||
|
|
||||||
// Click [aria-label="OpenMCT Search"] input[type="search"]
|
// Click [aria-label="OpenMCT Search"] input[type="search"]
|
||||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "openmct",
|
"name": "openmct",
|
||||||
"version": "2.1.0-SNAPSHOT",
|
"version": "2.0.8-SNAPSHOT",
|
||||||
"description": "The Open MCT core platform",
|
"description": "The Open MCT core platform",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/eslint-parser": "7.18.9",
|
"@babel/eslint-parser": "7.18.9",
|
||||||
|
@ -40,6 +40,8 @@ const ANNOTATION_TYPES = Object.freeze({
|
|||||||
PLOT_SPATIAL: 'PLOT_SPATIAL'
|
PLOT_SPATIAL: 'PLOT_SPATIAL'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ANNOTATION_TYPE = 'annotation';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} Tag
|
* @typedef {Object} Tag
|
||||||
* @property {String} key a unique identifier for the tag
|
* @property {String} key a unique identifier for the tag
|
||||||
@ -54,7 +56,7 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
|
|
||||||
this.ANNOTATION_TYPES = ANNOTATION_TYPES;
|
this.ANNOTATION_TYPES = ANNOTATION_TYPES;
|
||||||
|
|
||||||
this.openmct.types.addType('annotation', {
|
this.openmct.types.addType(ANNOTATION_TYPE, {
|
||||||
name: 'Annotation',
|
name: 'Annotation',
|
||||||
description: 'A user created note or comment about time ranges, pixel space, and geospatial features.',
|
description: 'A user created note or comment about time ranges, pixel space, and geospatial features.',
|
||||||
creatable: false,
|
creatable: false,
|
||||||
@ -136,6 +138,10 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
this.availableTags[tagKey] = tagsDefinition;
|
this.availableTags[tagKey] = tagsDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isAnnotation(domainObject) {
|
||||||
|
return domainObject && (domainObject.type === ANNOTATION_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
getAvailableTags() {
|
getAvailableTags() {
|
||||||
if (this.availableTags) {
|
if (this.availableTags) {
|
||||||
const rearrangedToArray = Object.keys(this.availableTags).map(tagKey => {
|
const rearrangedToArray = Object.keys(this.availableTags).map(tagKey => {
|
||||||
@ -271,7 +277,10 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
const searchResults = (await Promise.all(this.openmct.objects.search(matchingTagKeys, abortController, this.openmct.objects.SEARCH_TYPES.TAGS))).flat();
|
const searchResults = (await Promise.all(this.openmct.objects.search(matchingTagKeys, abortController, this.openmct.objects.SEARCH_TYPES.TAGS))).flat();
|
||||||
const appliedTagSearchResults = this.#addTagMetaInformationToResults(searchResults, matchingTagKeys);
|
const appliedTagSearchResults = this.#addTagMetaInformationToResults(searchResults, matchingTagKeys);
|
||||||
const appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults);
|
const appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults);
|
||||||
|
const resultsWithValidPath = appliedTargetsModels.filter(result => {
|
||||||
|
return this.openmct.objects.isReachable(result.targetModels?.[0]?.originalPath);
|
||||||
|
});
|
||||||
|
|
||||||
return appliedTargetsModels;
|
return resultsWithValidPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,15 +27,26 @@ describe("The Annotation API", () => {
|
|||||||
let openmct;
|
let openmct;
|
||||||
let mockObjectProvider;
|
let mockObjectProvider;
|
||||||
let mockDomainObject;
|
let mockDomainObject;
|
||||||
|
let mockFolderObject;
|
||||||
let mockAnnotationObject;
|
let mockAnnotationObject;
|
||||||
|
|
||||||
beforeEach((done) => {
|
beforeEach((done) => {
|
||||||
openmct = createOpenMct();
|
openmct = createOpenMct();
|
||||||
openmct.install(new ExampleTagsPlugin());
|
openmct.install(new ExampleTagsPlugin());
|
||||||
const availableTags = openmct.annotation.getAvailableTags();
|
const availableTags = openmct.annotation.getAvailableTags();
|
||||||
|
mockFolderObject = {
|
||||||
|
type: 'root',
|
||||||
|
name: 'folderFoo',
|
||||||
|
location: '',
|
||||||
|
identifier: {
|
||||||
|
key: 'someParent',
|
||||||
|
namespace: 'fooNameSpace'
|
||||||
|
}
|
||||||
|
};
|
||||||
mockDomainObject = {
|
mockDomainObject = {
|
||||||
type: 'notebook',
|
type: 'notebook',
|
||||||
name: 'fooRabbitNotebook',
|
name: 'fooRabbitNotebook',
|
||||||
|
location: 'fooNameSpace:someParent',
|
||||||
identifier: {
|
identifier: {
|
||||||
key: 'some-object',
|
key: 'some-object',
|
||||||
namespace: 'fooNameSpace'
|
namespace: 'fooNameSpace'
|
||||||
@ -68,6 +79,8 @@ describe("The Annotation API", () => {
|
|||||||
return mockDomainObject;
|
return mockDomainObject;
|
||||||
} else if (identifier.key === mockAnnotationObject.identifier.key) {
|
} else if (identifier.key === mockAnnotationObject.identifier.key) {
|
||||||
return mockAnnotationObject;
|
return mockAnnotationObject;
|
||||||
|
} else if (identifier.key === mockFolderObject.identifier.key) {
|
||||||
|
return mockFolderObject;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -150,6 +163,7 @@ describe("The Annotation API", () => {
|
|||||||
// use local worker
|
// use local worker
|
||||||
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
|
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
|
||||||
openmct.objects.inMemorySearchProvider.worker = null;
|
openmct.objects.inMemorySearchProvider.worker = null;
|
||||||
|
await openmct.objects.inMemorySearchProvider.index(mockFolderObject);
|
||||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject);
|
await openmct.objects.inMemorySearchProvider.index(mockDomainObject);
|
||||||
await openmct.objects.inMemorySearchProvider.index(mockAnnotationObject);
|
await openmct.objects.inMemorySearchProvider.index(mockAnnotationObject);
|
||||||
});
|
});
|
||||||
|
@ -34,11 +34,11 @@ import InMemorySearchProvider from './InMemorySearchProvider';
|
|||||||
* Uniquely identifies a domain object.
|
* Uniquely identifies a domain object.
|
||||||
*
|
*
|
||||||
* @typedef Identifier
|
* @typedef Identifier
|
||||||
* @memberof module:openmct.ObjectAPI~
|
|
||||||
* @property {string} namespace the namespace to/from which this domain
|
* @property {string} namespace the namespace to/from which this domain
|
||||||
* object should be loaded/stored.
|
* object should be loaded/stored.
|
||||||
* @property {string} key a unique identifier for the domain object
|
* @property {string} key a unique identifier for the domain object
|
||||||
* within that namespace
|
* within that namespace
|
||||||
|
* @memberof module:openmct.ObjectAPI~
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -615,27 +615,60 @@ export default class ObjectAPI {
|
|||||||
* @param {module:openmct.ObjectAPI~Identifier[]} identifiers
|
* @param {module:openmct.ObjectAPI~Identifier[]} identifiers
|
||||||
*/
|
*/
|
||||||
areIdsEqual(...identifiers) {
|
areIdsEqual(...identifiers) {
|
||||||
|
const firstIdentifier = utils.parseKeyString(identifiers[0]);
|
||||||
|
|
||||||
return identifiers.map(utils.parseKeyString)
|
return identifiers.map(utils.parseKeyString)
|
||||||
.every(identifier => {
|
.every(identifier => {
|
||||||
return identifier === identifiers[0]
|
return identifier === firstIdentifier
|
||||||
|| (identifier.namespace === identifiers[0].namespace
|
|| (identifier.namespace === firstIdentifier.namespace
|
||||||
&& identifier.key === identifiers[0].key);
|
&& identifier.key === firstIdentifier.key);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getOriginalPath(identifier, path = []) {
|
/**
|
||||||
return this.get(identifier).then((domainObject) => {
|
* Given an original path check if the path is reachable via root
|
||||||
path.push(domainObject);
|
* @param {Array<Object>} originalPath an array of path objects to check
|
||||||
let location = domainObject.location;
|
* @returns {boolean} whether the domain object is reachable
|
||||||
|
*/
|
||||||
|
isReachable(originalPath) {
|
||||||
|
if (originalPath && originalPath.length) {
|
||||||
|
return (originalPath[originalPath.length - 1].type === 'root');
|
||||||
|
}
|
||||||
|
|
||||||
if (location) {
|
return false;
|
||||||
return this.getOriginalPath(utils.parseKeyString(location), path);
|
}
|
||||||
} else {
|
|
||||||
return path;
|
#pathContainsDomainObject(keyStringToCheck, path) {
|
||||||
}
|
if (!keyStringToCheck) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.some(pathElement => {
|
||||||
|
const identifierToCheck = utils.parseKeyString(keyStringToCheck);
|
||||||
|
|
||||||
|
return this.areIdsEqual(identifierToCheck, pathElement.identifier);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an identifier, constructs the original path by walking up its parents
|
||||||
|
* @param {module:openmct.ObjectAPI~Identifier} identifier
|
||||||
|
* @param {Array<module:openmct.DomainObject>} path an array of path objects
|
||||||
|
* @returns {Promise<Array<module:openmct.DomainObject>>} a promise containing an array of domain objects
|
||||||
|
*/
|
||||||
|
async getOriginalPath(identifier, path = []) {
|
||||||
|
const domainObject = await this.get(identifier);
|
||||||
|
path.push(domainObject);
|
||||||
|
const { location } = domainObject;
|
||||||
|
if (location && (!this.#pathContainsDomainObject(location, path))) {
|
||||||
|
// if we have a location, and we don't already have this in our constructed path,
|
||||||
|
// then keep walking up the path
|
||||||
|
return this.getOriginalPath(utils.parseKeyString(location), path);
|
||||||
|
} else {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
isObjectPathToALink(domainObject, objectPath) {
|
isObjectPathToALink(domainObject, objectPath) {
|
||||||
return objectPath !== undefined
|
return objectPath !== undefined
|
||||||
&& objectPath.length > 1
|
&& objectPath.length > 1
|
||||||
|
@ -377,6 +377,73 @@ describe("The Object API", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getOriginalPath", () => {
|
||||||
|
let mockGrandParentObject;
|
||||||
|
let mockParentObject;
|
||||||
|
let mockChildObject;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const mockObjectProvider = jasmine.createSpyObj("mock object provider", [
|
||||||
|
"create",
|
||||||
|
"update",
|
||||||
|
"get"
|
||||||
|
]);
|
||||||
|
|
||||||
|
mockGrandParentObject = {
|
||||||
|
type: 'folder',
|
||||||
|
name: 'Grand Parent Folder',
|
||||||
|
location: 'fooNameSpace:child',
|
||||||
|
identifier: {
|
||||||
|
key: 'grandParent',
|
||||||
|
namespace: 'fooNameSpace'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mockParentObject = {
|
||||||
|
type: 'folder',
|
||||||
|
name: 'Parent Folder',
|
||||||
|
location: 'fooNameSpace:grandParent',
|
||||||
|
identifier: {
|
||||||
|
key: 'parent',
|
||||||
|
namespace: 'fooNameSpace'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mockChildObject = {
|
||||||
|
type: 'folder',
|
||||||
|
name: 'Child Folder',
|
||||||
|
location: 'fooNameSpace:parent',
|
||||||
|
identifier: {
|
||||||
|
key: 'child',
|
||||||
|
namespace: 'fooNameSpace'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line require-await
|
||||||
|
mockObjectProvider.get = async (identifier) => {
|
||||||
|
if (identifier.key === mockGrandParentObject.identifier.key) {
|
||||||
|
return mockGrandParentObject;
|
||||||
|
} else if (identifier.key === mockParentObject.identifier.key) {
|
||||||
|
return mockParentObject;
|
||||||
|
} else if (identifier.key === mockChildObject.identifier.key) {
|
||||||
|
return mockChildObject;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
openmct.objects.addProvider('fooNameSpace', mockObjectProvider);
|
||||||
|
|
||||||
|
mockObjectProvider.create.and.returnValue(Promise.resolve(true));
|
||||||
|
mockObjectProvider.update.and.returnValue(Promise.resolve(true));
|
||||||
|
|
||||||
|
openmct.objects.addProvider('fooNameSpace', mockObjectProvider);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can construct paths even with cycles', async () => {
|
||||||
|
const objectPath = await objectAPI.getOriginalPath(mockChildObject.identifier);
|
||||||
|
expect(objectPath.length).toEqual(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("transactions", () => {
|
describe("transactions", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(openmct.editor, 'isEditing').and.returnValue(true);
|
spyOn(openmct.editor, 'isEditing').and.returnValue(true);
|
||||||
|
@ -91,6 +91,10 @@ define([
|
|||||||
* @returns keyString
|
* @returns keyString
|
||||||
*/
|
*/
|
||||||
function makeKeyString(identifier) {
|
function makeKeyString(identifier) {
|
||||||
|
if (!identifier) {
|
||||||
|
throw new Error("Cannot make key string from null identifier");
|
||||||
|
}
|
||||||
|
|
||||||
if (isKeyString(identifier)) {
|
if (isKeyString(identifier)) {
|
||||||
return identifier;
|
return identifier;
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,11 @@ export default class TelemetryCriterion extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initialize() {
|
initialize() {
|
||||||
this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry);
|
this.telemetryObjectIdAsString = "";
|
||||||
|
if (![undefined, null, ""].includes(this.telemetryDomainObjectDefinition?.telemetry)) {
|
||||||
|
this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry);
|
||||||
|
}
|
||||||
|
|
||||||
this.updateTelemetryObjects(this.telemetryDomainObjectDefinition.telemetryObjects);
|
this.updateTelemetryObjects(this.telemetryDomainObjectDefinition.telemetryObjects);
|
||||||
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
|
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
|
||||||
this.subscribeForStaleData();
|
this.subscribeForStaleData();
|
||||||
|
@ -188,7 +188,8 @@ export default {
|
|||||||
if (domainObject.type === 'plan') {
|
if (domainObject.type === 'plan') {
|
||||||
this.getPlanDataAndSetConfig({
|
this.getPlanDataAndSetConfig({
|
||||||
...this.domainObject,
|
...this.domainObject,
|
||||||
selectFile: domainObject.selectFile
|
selectFile: domainObject.selectFile,
|
||||||
|
sourceMap: domainObject.sourceMap
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -25,13 +25,14 @@
|
|||||||
/******************************************************** CONTROL-SPECIFIC MIXINS */
|
/******************************************************** CONTROL-SPECIFIC MIXINS */
|
||||||
@mixin menuOuter() {
|
@mixin menuOuter() {
|
||||||
border-radius: $basicCr;
|
border-radius: $basicCr;
|
||||||
box-shadow: $shdwMenuInner, $shdwMenu;
|
box-shadow: $shdwMenu;
|
||||||
|
@if $shdwMenuInner != none {
|
||||||
|
box-shadow: $shdwMenuInner, $shdwMenu;
|
||||||
|
}
|
||||||
background: $colorMenuBg;
|
background: $colorMenuBg;
|
||||||
color: $colorMenuFg;
|
color: $colorMenuFg;
|
||||||
//filter: $filterMenu; // 2022: causing all kinds of weird visual bugs in Chrome
|
|
||||||
text-shadow: $shdwMenuText;
|
text-shadow: $shdwMenuText;
|
||||||
padding: $interiorMarginSm;
|
padding: $interiorMarginSm;
|
||||||
//box-shadow: $shdwMenu;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -60,14 +61,13 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: nth($menuItemPad, 1) nth($menuItemPad, 2);
|
padding: nth($menuItemPad, 1) nth($menuItemPad, 2);
|
||||||
transition: $transIn;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
@include hover {
|
@include hover {
|
||||||
background: $colorMenuHovBg;
|
background: $colorMenuHovBg;
|
||||||
color: $colorMenuHovFg;
|
color: $colorMenuHovFg;
|
||||||
&:before {
|
&:before {
|
||||||
color: $colorMenuHovIc;
|
color: $colorMenuHovIc !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,7 +77,6 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.searchValue = value;
|
this.searchValue = value;
|
||||||
this.searchLoading = true;
|
|
||||||
// clear any previous search results
|
// clear any previous search results
|
||||||
this.annotationSearchResults = [];
|
this.annotationSearchResults = [];
|
||||||
this.objectSearchResults = [];
|
this.objectSearchResults = [];
|
||||||
@ -85,8 +84,13 @@ export default {
|
|||||||
if (this.searchValue) {
|
if (this.searchValue) {
|
||||||
await this.getSearchResults();
|
await this.getSearchResults();
|
||||||
} else {
|
} else {
|
||||||
this.searchLoading = false;
|
const dropdownOptions = {
|
||||||
this.$refs.searchResultsDropDown.showResults(this.annotationSearchResults, this.objectSearchResults);
|
searchLoading: this.searchLoading,
|
||||||
|
searchValue: this.searchValue,
|
||||||
|
annotationSearchResults: this.annotationSearchResults,
|
||||||
|
objectSearchResults: this.objectSearchResults
|
||||||
|
};
|
||||||
|
this.$refs.searchResultsDropDown.showResults(dropdownOptions);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getPathsForObjects(objectsNeedingPaths) {
|
getPathsForObjects(objectsNeedingPaths) {
|
||||||
@ -103,6 +107,8 @@ export default {
|
|||||||
async getSearchResults() {
|
async getSearchResults() {
|
||||||
// an abort controller will be passed in that will be used
|
// an abort controller will be passed in that will be used
|
||||||
// to cancel an active searches if necessary
|
// to cancel an active searches if necessary
|
||||||
|
this.searchLoading = true;
|
||||||
|
this.$refs.searchResultsDropDown.showSearchStarted();
|
||||||
this.abortSearchController = new AbortController();
|
this.abortSearchController = new AbortController();
|
||||||
const abortSignal = this.abortSearchController.signal;
|
const abortSignal = this.abortSearchController.signal;
|
||||||
try {
|
try {
|
||||||
@ -110,10 +116,15 @@ export default {
|
|||||||
const fullObjectSearchResults = await Promise.all(this.openmct.objects.search(this.searchValue, abortSignal));
|
const fullObjectSearchResults = await Promise.all(this.openmct.objects.search(this.searchValue, abortSignal));
|
||||||
const aggregatedObjectSearchResults = fullObjectSearchResults.flat();
|
const aggregatedObjectSearchResults = fullObjectSearchResults.flat();
|
||||||
const aggregatedObjectSearchResultsWithPaths = await this.getPathsForObjects(aggregatedObjectSearchResults);
|
const aggregatedObjectSearchResultsWithPaths = await this.getPathsForObjects(aggregatedObjectSearchResults);
|
||||||
const filterAnnotations = aggregatedObjectSearchResultsWithPaths.filter(result => {
|
const filterAnnotationsAndValidPaths = aggregatedObjectSearchResultsWithPaths.filter(result => {
|
||||||
return result.type !== 'annotation';
|
if (this.openmct.annotation.isAnnotation(result)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.openmct.objects.isReachable(result?.originalPath);
|
||||||
});
|
});
|
||||||
this.objectSearchResults = filterAnnotations;
|
this.objectSearchResults = filterAnnotationsAndValidPaths;
|
||||||
|
this.searchLoading = false;
|
||||||
this.showSearchResults();
|
this.showSearchResults();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`😞 Error searching`, error);
|
console.error(`😞 Error searching`, error);
|
||||||
@ -125,7 +136,13 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
showSearchResults() {
|
showSearchResults() {
|
||||||
this.$refs.searchResultsDropDown.showResults(this.annotationSearchResults, this.objectSearchResults);
|
const dropdownOptions = {
|
||||||
|
searchLoading: this.searchLoading,
|
||||||
|
searchValue: this.searchValue,
|
||||||
|
annotationSearchResults: this.annotationSearchResults,
|
||||||
|
objectSearchResults: this.objectSearchResults
|
||||||
|
};
|
||||||
|
this.$refs.searchResultsDropDown.showResults(dropdownOptions);
|
||||||
document.body.addEventListener('click', this.handleOutsideClick);
|
document.body.addEventListener('click', this.handleOutsideClick);
|
||||||
},
|
},
|
||||||
handleOutsideClick(event) {
|
handleOutsideClick(event) {
|
||||||
|
@ -39,6 +39,8 @@ describe("GrandSearch", () => {
|
|||||||
let mockAnnotationObject;
|
let mockAnnotationObject;
|
||||||
let mockDisplayLayout;
|
let mockDisplayLayout;
|
||||||
let mockFolderObject;
|
let mockFolderObject;
|
||||||
|
let mockAnotherFolderObject;
|
||||||
|
let mockTopObject;
|
||||||
let originalRouterPath;
|
let originalRouterPath;
|
||||||
|
|
||||||
beforeEach((done) => {
|
beforeEach((done) => {
|
||||||
@ -70,11 +72,29 @@ describe("GrandSearch", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
mockTopObject = {
|
||||||
|
type: 'root',
|
||||||
|
name: 'Top Folder',
|
||||||
|
identifier: {
|
||||||
|
key: 'topObject',
|
||||||
|
namespace: 'fooNameSpace'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mockAnotherFolderObject = {
|
||||||
|
type: 'folder',
|
||||||
|
name: 'Another Test Folder',
|
||||||
|
location: 'fooNameSpace:topObject',
|
||||||
|
identifier: {
|
||||||
|
key: 'someParent',
|
||||||
|
namespace: 'fooNameSpace'
|
||||||
|
}
|
||||||
|
};
|
||||||
mockFolderObject = {
|
mockFolderObject = {
|
||||||
type: 'folder',
|
type: 'folder',
|
||||||
name: 'Test Folder',
|
name: 'Test Folder',
|
||||||
|
location: 'fooNameSpace:someParent',
|
||||||
identifier: {
|
identifier: {
|
||||||
key: 'some-folder',
|
key: 'someFolder',
|
||||||
namespace: 'fooNameSpace'
|
namespace: 'fooNameSpace'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -122,6 +142,10 @@ describe("GrandSearch", () => {
|
|||||||
return mockDisplayLayout;
|
return mockDisplayLayout;
|
||||||
} else if (identifier.key === mockFolderObject.identifier.key) {
|
} else if (identifier.key === mockFolderObject.identifier.key) {
|
||||||
return mockFolderObject;
|
return mockFolderObject;
|
||||||
|
} else if (identifier.key === mockAnotherFolderObject.identifier.key) {
|
||||||
|
return mockAnotherFolderObject;
|
||||||
|
} else if (identifier.key === mockTopObject.identifier.key) {
|
||||||
|
return mockTopObject;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -22,8 +22,6 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="(annotationResults && annotationResults.length) ||
|
|
||||||
(objectResults && objectResults.length)"
|
|
||||||
class="c-gsearch__dropdown"
|
class="c-gsearch__dropdown"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -58,25 +56,40 @@
|
|||||||
@click.native="selectedResult"
|
@click.native="selectedResult"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="searchLoading"
|
||||||
|
> <progress-bar
|
||||||
|
:model="{progressText: 'Searching...',
|
||||||
|
progressPerc: undefined
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!searchLoading && (!annotationResults || !annotationResults.length) &&
|
||||||
|
(!objectResults || !objectResults.length)"
|
||||||
|
>No matching results.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div></template>
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import AnnotationSearchResult from './AnnotationSearchResult.vue';
|
import AnnotationSearchResult from './AnnotationSearchResult.vue';
|
||||||
import ObjectSearchResult from './ObjectSearchResult.vue';
|
import ObjectSearchResult from './ObjectSearchResult.vue';
|
||||||
|
import ProgressBar from '@/ui/components/ProgressBar.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SearchResultsDropDown',
|
name: 'SearchResultsDropDown',
|
||||||
components: {
|
components: {
|
||||||
AnnotationSearchResult,
|
AnnotationSearchResult,
|
||||||
ObjectSearchResult
|
ObjectSearchResult,
|
||||||
|
ProgressBar
|
||||||
},
|
},
|
||||||
inject: ['openmct'],
|
inject: ['openmct'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
resultsShown: false,
|
resultsShown: false,
|
||||||
|
searchLoading: false,
|
||||||
annotationResults: [],
|
annotationResults: [],
|
||||||
objectResults: [],
|
objectResults: [],
|
||||||
previewVisible: false
|
previewVisible: false
|
||||||
@ -91,12 +104,18 @@ export default {
|
|||||||
previewChanged(changedPreviewState) {
|
previewChanged(changedPreviewState) {
|
||||||
this.previewVisible = changedPreviewState;
|
this.previewVisible = changedPreviewState;
|
||||||
},
|
},
|
||||||
showResults(passedAnnotationResults, passedObjectResults) {
|
showSearchStarted() {
|
||||||
if ((passedAnnotationResults && passedAnnotationResults.length)
|
this.searchLoading = true;
|
||||||
|| (passedObjectResults && passedObjectResults.length)) {
|
this.resultsShown = true;
|
||||||
|
this.annotationResults = [];
|
||||||
|
this.objectResults = [];
|
||||||
|
},
|
||||||
|
showResults({searchLoading, searchValue, annotationSearchResults, objectSearchResults}) {
|
||||||
|
this.searchLoading = searchLoading;
|
||||||
|
this.annotationResults = annotationSearchResults;
|
||||||
|
this.objectResults = objectSearchResults;
|
||||||
|
if (searchValue?.length) {
|
||||||
this.resultsShown = true;
|
this.resultsShown = true;
|
||||||
this.annotationResults = passedAnnotationResults;
|
|
||||||
this.objectResults = passedObjectResults;
|
|
||||||
} else {
|
} else {
|
||||||
this.resultsShown = false;
|
this.resultsShown = false;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user