2022-07-27 21:31:18 +00:00
|
|
|
/*****************************************************************************
|
2024-01-09 21:31:51 +00:00
|
|
|
* Open MCT, Copyright (c) 2014-2024, United States Government
|
2022-07-27 21:31:18 +00:00
|
|
|
* 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.
|
|
|
|
*****************************************************************************/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The fixtures in this file are to be used to consolidate common actions performed by the
|
|
|
|
* various test suites. The goal is only to avoid duplication of code across test suites and not to abstract
|
|
|
|
* away the underlying functionality of the application. For more about the App Action pattern, see /e2e/README.md)
|
|
|
|
*
|
|
|
|
* For example, if two functions are nearly identical in
|
|
|
|
* timer.e2e.spec.js and notebook.e2e.spec.js, that function should be generalized and moved into this file.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
2022-08-22 18:41:38 +00:00
|
|
|
* Defines parameters to be used in the creation of a domain object.
|
|
|
|
* @typedef {Object} CreateObjectOptions
|
|
|
|
* @property {string} type the type of domain object to create (e.g.: "Sine Wave Generator").
|
|
|
|
* @property {string} [name] the desired name of the created domain object.
|
|
|
|
* @property {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the Identifier or uuid of the parent object.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Contains information about the newly created domain object.
|
|
|
|
* @typedef {Object} CreatedObjectInfo
|
|
|
|
* @property {string} name the name of the created object
|
|
|
|
* @property {string} uuid the uuid of the created object
|
|
|
|
* @property {string} url the relative url to the object (for use with `page.goto()`)
|
|
|
|
*/
|
|
|
|
|
2023-01-14 02:12:08 +00:00
|
|
|
/**
|
|
|
|
* Defines parameters to be used in the creation of a notification.
|
|
|
|
* @typedef {Object} CreateNotificationOptions
|
|
|
|
* @property {string} message the message
|
|
|
|
* @property {'info' | 'alert' | 'error'} severity the severity
|
|
|
|
* @property {import('../src/api/notifications/NotificationAPI').NotificationOptions} [notificationOptions] additional options
|
|
|
|
*/
|
|
|
|
|
2024-01-02 15:24:22 +00:00
|
|
|
import { expect } from '@playwright/test';
|
|
|
|
import { Buffer } from 'buffer';
|
|
|
|
import { v4 as genUuid } from 'uuid';
|
2022-09-10 00:31:03 +00:00
|
|
|
|
2022-08-22 18:41:38 +00:00
|
|
|
/**
|
|
|
|
* This common function creates a domain object with the default options. It is the preferred way of creating objects
|
2022-07-27 21:31:18 +00:00
|
|
|
* in the e2e suite when uninterested in properties of the objects themselves.
|
2022-08-22 18:41:38 +00:00
|
|
|
*
|
2024-08-07 21:36:14 +00:00
|
|
|
* @param {import('@playwright/test').Page} page - The Playwright page object.
|
|
|
|
* @param {Object} options - Options for creating the domain object.
|
|
|
|
* @param {string} options.type - The type of domain object to create (e.g., "Sine Wave Generator").
|
|
|
|
* @param {string} [options.name] - The desired name of the created domain object.
|
|
|
|
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} [options.parent='mine'] - The Identifier or uuid of the parent object. Defaults to 'mine' folder
|
2022-08-22 18:41:38 +00:00
|
|
|
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
|
2022-07-27 21:31:18 +00:00
|
|
|
*/
|
2024-08-07 21:36:14 +00:00
|
|
|
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) {
|
2022-11-07 23:50:33 +00:00
|
|
|
if (!name) {
|
|
|
|
name = `${type}:${genUuid()}`;
|
|
|
|
}
|
2023-05-18 21:54:46 +00:00
|
|
|
|
2022-08-22 18:41:38 +00:00
|
|
|
const parentUrl = await getHashUrlToDomainObject(page, parent);
|
2023-05-18 21:54:46 +00:00
|
|
|
|
2022-08-22 18:41:38 +00:00
|
|
|
// Navigate to the parent object. This is necessary to create the object
|
|
|
|
// in the correct location, such as a folder, layout, or plot.
|
2024-07-24 03:41:07 +00:00
|
|
|
await page.goto(parentUrl);
|
2023-05-18 21:54:46 +00:00
|
|
|
|
2024-07-24 03:41:07 +00:00
|
|
|
// Click the Create button
|
|
|
|
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
2023-05-18 21:54:46 +00:00
|
|
|
|
2024-07-24 03:41:07 +00:00
|
|
|
// Click the object specified by 'type'-- case insensitive
|
|
|
|
await page.getByRole('menuitem', { name: new RegExp(`^${type}$`, 'i') }).click();
|
2023-05-18 21:54:46 +00:00
|
|
|
|
2024-08-07 21:36:14 +00:00
|
|
|
// Fill in the name of the object
|
|
|
|
await page.getByLabel('Title', { exact: true }).fill('');
|
|
|
|
await page.getByLabel('Title', { exact: true }).fill(name);
|
2023-05-18 21:54:46 +00:00
|
|
|
|
2022-11-30 01:51:43 +00:00
|
|
|
if (page.testNotes) {
|
|
|
|
// Fill the "Notes" section with information about the
|
|
|
|
// currently running test and its project.
|
2024-08-07 21:36:14 +00:00
|
|
|
// eslint-disable-next-line playwright/no-raw-locators
|
|
|
|
await page.locator('#notes-textarea').fill(page.testNotes);
|
2023-08-03 16:40:52 +00:00
|
|
|
}
|
|
|
|
|
2024-08-07 21:36:14 +00:00
|
|
|
await page.getByRole('button', { name: 'Save' }).click();
|
2023-05-18 21:54:46 +00:00
|
|
|
|
2022-08-22 18:41:38 +00:00
|
|
|
// Wait until the URL is updated
|
|
|
|
await page.waitForURL(`**/${parent}/*`);
|
|
|
|
const uuid = await getFocusedObjectUuid(page);
|
|
|
|
const objectUrl = await getHashUrlToDomainObject(page, uuid);
|
2023-05-18 21:54:46 +00:00
|
|
|
|
2022-08-22 18:41:38 +00:00
|
|
|
if (await _isInEditMode(page, uuid)) {
|
|
|
|
// Save (exit edit mode)
|
2023-11-16 18:31:35 +00:00
|
|
|
await page.getByRole('button', { name: 'Save' }).click();
|
|
|
|
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
2022-08-22 18:41:38 +00:00
|
|
|
}
|
2023-05-18 21:54:46 +00:00
|
|
|
|
2022-08-22 18:41:38 +00:00
|
|
|
return {
|
2022-11-07 23:50:33 +00:00
|
|
|
name,
|
|
|
|
uuid,
|
2022-08-22 18:41:38 +00:00
|
|
|
url: objectUrl
|
|
|
|
};
|
2022-07-27 21:31:18 +00:00
|
|
|
}
|
|
|
|
|
2023-01-14 02:12:08 +00:00
|
|
|
/**
|
|
|
|
* Generate a notification with the given options.
|
|
|
|
* @param {import('@playwright/test').Page} page
|
|
|
|
* @param {CreateNotificationOptions} createNotificationOptions
|
|
|
|
*/
|
|
|
|
async function createNotification(page, createNotificationOptions) {
|
|
|
|
await page.evaluate((_createNotificationOptions) => {
|
|
|
|
const { message, severity, options } = _createNotificationOptions;
|
|
|
|
const notificationApi = window.openmct.notifications;
|
|
|
|
if (severity === 'info') {
|
|
|
|
notificationApi.info(message, options);
|
|
|
|
} else if (severity === 'alert') {
|
|
|
|
notificationApi.alert(message, options);
|
|
|
|
} else {
|
|
|
|
notificationApi.error(message, options);
|
|
|
|
}
|
|
|
|
}, createNotificationOptions);
|
|
|
|
}
|
|
|
|
|
2022-09-20 22:43:48 +00:00
|
|
|
/**
|
2024-08-07 21:36:14 +00:00
|
|
|
* Create a Plan object from JSON with the provided options. Must be used with a json based plan.
|
|
|
|
* Please check appActions.e2e.spec.js for an example of how to use this function.
|
|
|
|
*
|
2022-09-20 22:43:48 +00:00
|
|
|
* @param {import('@playwright/test').Page} page
|
|
|
|
* @param {string} name
|
2024-08-07 21:36:14 +00:00
|
|
|
* @param {Object} json
|
|
|
|
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the uuid or identifier of the parent object. Defaults to 'mine'
|
2022-09-10 00:31:03 +00:00
|
|
|
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
|
|
|
|
*/
|
|
|
|
async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
|
|
|
|
const parentUrl = await getHashUrlToDomainObject(page, parent);
|
2023-05-18 21:54:46 +00:00
|
|
|
|
2022-09-10 00:31:03 +00:00
|
|
|
// Navigate to the parent object. This is necessary to create the object
|
|
|
|
// in the correct location, such as a folder, layout, or plot.
|
2023-09-05 08:53:03 +00:00
|
|
|
await page.goto(`${parentUrl}`);
|
2023-05-18 21:54:46 +00:00
|
|
|
|
2023-11-16 18:31:35 +00:00
|
|
|
await page.getByRole('button', { name: 'Create' }).click();
|
2023-05-18 21:54:46 +00:00
|
|
|
|
2024-08-07 21:36:14 +00:00
|
|
|
await page.getByRole('menuitem', { name: 'Plan' }).click();
|
2023-05-18 21:54:46 +00:00
|
|
|
|
2024-08-07 21:36:14 +00:00
|
|
|
// Fill in the name of the object or generate a random one
|
|
|
|
if (!name) {
|
|
|
|
name = `Plan:${genUuid()}`;
|
|
|
|
}
|
|
|
|
await page.getByLabel('Title', { exact: true }).fill('');
|
|
|
|
await page.getByLabel('Title', { exact: true }).fill(name);
|
2023-05-18 21:54:46 +00:00
|
|
|
|
2022-09-10 00:31:03 +00:00
|
|
|
// Upload buffer from memory
|
2024-08-07 21:36:14 +00:00
|
|
|
await page.getByLabel('Select File...').setInputFiles({
|
2022-09-10 00:31:03 +00:00
|
|
|
name: 'plan.txt',
|
|
|
|
mimeType: 'text/plain',
|
|
|
|
buffer: Buffer.from(JSON.stringify(json))
|
|
|
|
});
|
2023-05-18 21:54:46 +00:00
|
|
|
|
2024-08-07 21:36:14 +00:00
|
|
|
await page.getByLabel('Save').click();
|
2023-05-18 21:54:46 +00:00
|
|
|
|
2022-09-10 00:31:03 +00:00
|
|
|
// Wait until the URL is updated
|
2023-03-16 17:34:31 +00:00
|
|
|
await page.waitForURL(`**/${parent}/*`);
|
2022-09-10 00:31:03 +00:00
|
|
|
const uuid = await getFocusedObjectUuid(page);
|
|
|
|
const objectUrl = await getHashUrlToDomainObject(page, uuid);
|
2023-05-18 21:54:46 +00:00
|
|
|
|
2022-09-10 00:31:03 +00:00
|
|
|
return {
|
|
|
|
uuid,
|
|
|
|
name,
|
|
|
|
url: objectUrl
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-08-12 00:18:08 +00:00
|
|
|
/**
|
|
|
|
* Create a standardized Telemetry Object (Sine Wave Generator) for use in visual tests
|
|
|
|
* and tests against plotting telemetry (e.g. logPlot tests).
|
|
|
|
* @param {import('@playwright/test').Page} page
|
|
|
|
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the uuid or identifier of the parent object. Defaults to 'mine'
|
|
|
|
* @returns {Promise<CreatedObjectInfo>} An object containing information about the telemetry object.
|
|
|
|
*/
|
|
|
|
async function createExampleTelemetryObject(page, parent = 'mine') {
|
|
|
|
const parentUrl = await getHashUrlToDomainObject(page, parent);
|
|
|
|
|
2023-09-05 08:53:03 +00:00
|
|
|
await page.goto(`${parentUrl}`);
|
2023-08-12 00:18:08 +00:00
|
|
|
|
2023-11-16 18:31:35 +00:00
|
|
|
await page.getByRole('button', { name: 'Create' }).click();
|
2023-08-12 00:18:08 +00:00
|
|
|
|
2024-08-07 21:36:14 +00:00
|
|
|
await page.getByRole('menuitem', { name: 'Sine Wave Generator' }).click();
|
2023-08-12 00:18:08 +00:00
|
|
|
|
2023-09-11 23:33:46 +00:00
|
|
|
const name = 'VIPER Rover Heading';
|
2024-08-07 21:36:14 +00:00
|
|
|
await page.getByLabel('Title', { exact: true }).fill(name);
|
2023-08-12 00:18:08 +00:00
|
|
|
|
|
|
|
// Fill out the fields with default values
|
|
|
|
await page.getByRole('spinbutton', { name: 'Period' }).fill('10');
|
|
|
|
await page.getByRole('spinbutton', { name: 'Amplitude' }).fill('1');
|
|
|
|
await page.getByRole('spinbutton', { name: 'Offset' }).fill('0');
|
|
|
|
await page.getByRole('spinbutton', { name: 'Data Rate (hz)' }).fill('1');
|
|
|
|
await page.getByRole('spinbutton', { name: 'Phase (radians)' }).fill('0');
|
|
|
|
await page.getByRole('spinbutton', { name: 'Randomness' }).fill('0');
|
|
|
|
await page.getByRole('spinbutton', { name: 'Loading Delay (ms)' }).fill('0');
|
|
|
|
|
|
|
|
await page.getByRole('button', { name: 'Save' }).click();
|
|
|
|
|
|
|
|
// Wait until the URL is updated
|
|
|
|
await page.waitForURL(`**/${parent}/*`);
|
|
|
|
|
|
|
|
const uuid = await getFocusedObjectUuid(page);
|
|
|
|
const url = await getHashUrlToDomainObject(page, uuid);
|
|
|
|
|
|
|
|
return {
|
|
|
|
name,
|
|
|
|
uuid,
|
|
|
|
url
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-08-07 21:36:14 +00:00
|
|
|
* Navigates directly to a given object url, in fixed time mode, with the given start and end bounds. Note: does not set
|
|
|
|
* default view type.
|
|
|
|
*
|
2023-08-12 00:18:08 +00:00
|
|
|
* @param {import('@playwright/test').Page} page
|
|
|
|
* @param {string} url The url to the domainObject
|
|
|
|
* @param {string | number} start The starting time bound in milliseconds since epoch
|
|
|
|
* @param {string | number} end The ending time bound in milliseconds since epoch
|
|
|
|
*/
|
|
|
|
async function navigateToObjectWithFixedTimeBounds(page, url, start, end) {
|
|
|
|
await page.goto(
|
|
|
|
`${url}?tc.mode=fixed&tc.timeSystem=utc&tc.startBound=${start}&tc.endBound=${end}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-05-09 18:53:11 +00:00
|
|
|
/**
|
2024-08-07 21:36:14 +00:00
|
|
|
* Navigates directly to a given object url, in real-time mode. Note: does not set
|
|
|
|
* default view type.
|
|
|
|
*
|
2024-05-09 18:53:11 +00:00
|
|
|
* @param {import('@playwright/test').Page} page
|
|
|
|
* @param {string} url The url to the domainObject
|
2024-08-07 21:36:14 +00:00
|
|
|
* @param {string | number} start The start offset in milliseconds
|
|
|
|
* @param {string | number} end The end offset in milliseconds
|
2024-05-09 18:53:11 +00:00
|
|
|
*/
|
|
|
|
async function navigateToObjectWithRealTime(page, url, start = '1800000', end = '30000') {
|
|
|
|
await page.goto(
|
|
|
|
`${url}?tc.mode=local&tc.startDelta=${start}&tc.endDelta=${end}&tc.timeSystem=utc`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-07-27 21:31:18 +00:00
|
|
|
/**
|
2024-08-07 21:36:14 +00:00
|
|
|
* Expands the entire object tree (every expandable tree item). Can be used to
|
|
|
|
* ensure that the tree is fully expanded before performing actions on objects.
|
|
|
|
* Can be applied to either the main tree or the create modal tree.
|
2022-08-22 18:41:38 +00:00
|
|
|
*
|
2022-07-27 21:31:18 +00:00
|
|
|
* @param {import('@playwright/test').Page} page
|
2023-01-26 17:25:15 +00:00
|
|
|
* @param {"Main Tree" | "Create Modal Tree"} [treeName="Main Tree"]
|
|
|
|
*/
|
|
|
|
async function expandEntireTree(page, treeName = 'Main Tree') {
|
|
|
|
const treeLocator = page.getByRole('tree', {
|
|
|
|
name: treeName
|
|
|
|
});
|
|
|
|
const collapsedTreeItems = treeLocator
|
|
|
|
.getByRole('treeitem', {
|
|
|
|
expanded: false
|
|
|
|
})
|
2024-08-07 21:36:14 +00:00
|
|
|
.getByLabel(/Expand/);
|
2023-05-18 21:54:46 +00:00
|
|
|
|
2023-01-26 17:25:15 +00:00
|
|
|
while ((await collapsedTreeItems.count()) > 0) {
|
2024-08-07 21:36:14 +00:00
|
|
|
//eslint-disable-next-line playwright/no-nth-methods
|
2023-01-26 17:25:15 +00:00
|
|
|
await collapsedTreeItems.nth(0).click();
|
2023-05-18 21:54:46 +00:00
|
|
|
|
2023-01-30 18:55:24 +00:00
|
|
|
// FIXME: Replace hard wait with something event-driven.
|
|
|
|
// Without the wait, this fails periodically due to a race condition
|
|
|
|
// with Vue rendering (loop exits prematurely).
|
|
|
|
// eslint-disable-next-line playwright/no-wait-for-timeout
|
|
|
|
await page.waitForTimeout(200);
|
2023-01-26 17:25:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-22 18:41:38 +00:00
|
|
|
/**
|
|
|
|
* 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) => {
|
Merge `release/2.0.8` into `master` (#5709)
* Imagery thumbnail regression fixes - 5327 (#5591)
* Add an active class to thumbnail to indicate current focused image
* Differentiate bg color between real-time and fixed
* scrollIntoView inline: center
* Added watcher for bounds change to trigger thumbnail scroll
* Resolve merge conflict with requestHistory change to telemetry collection
* Split thumbnail into sub component
* Monitor isFixed value to unpause playback status
Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
* [e2e] Improve appActions (#5592)
* update selectors to use aria labels
* Update appActions
- Create new function `getHashUrlToDomainObject` to get the browse url to a given object given its uuid
- Create new function `getFocusedObjectUuid`... self explanatory :)
- Update `createDomainObjectWIthDefaults` to make use of the new url generation
- Update `createDomainObject...`'s arguments to be more organized, and accept a parent object
- Update some docs, still need to clarify some
* Update appActions e2e tests
- Refactor for organization
- Test our new appActions in one go
* Update existing usages of `createDomainObject...` to match the new API
* fix accidental renamed export
* Fix jsdoc return types
* refactor telemetryTable test to use appActions
* Improve selectors
* Refactor test
* improve selector
* add clock mode appActions
* lint
* Fix jsdoc
* Code review comments
* mark failing visual tests as fixme temporarily
* Update package.json (#5601)
* Fix menu style in Snow theme (#5557)
* Include the plan source map when generating the time list/plan hybrid object (#5604)
* Search should indicate in progress and no results states, filter orphaned results (#5599)
* no matching result implemented
* now filtering annotations that are orphaned
* filter object results without valid paths
* add progress bar
* added e2e tests
* removed extraneous click
* fix typos
* fix unit tests
* lint
* address pr comments
* fix tests
* fix tests, centralize logic to object api, check for root instead
* remove debug statement
* lint
* fix documentation
* lint
* fix doc
* made some optimizations after talking with akhenry
* fix test
* update docs
* fix docs
* Have in-memory search indexer use composition API (#5578)
* need to remove tags and objects on composition removal
* had to separate out emits from load as it was causing memory indexer to loop upon itself
* Add parsing for areIdsEqual util to consistently remove folders (#5589)
* Add parsing util to identifier for ID comparison
* Moved firstIdentifier to top of function
* Lint fix
Co-authored-by: Andrew Henry <akhenry@gmail.com>
* Revert "Have in-memory search indexer use composition API (#5578)" (#5609)
This reverts commit 7cf11e177c6c48093a6b37902ba3dfb36414ff10.
* [e2e] Tests for Display Layout and LAD Tables and telemetry (#5607)
* Check for circular references in originalPath - 5615 (#5619)
* check for circular references
* add test
* fix test
* address PR comments by making comments better
* fix docs...again
* Update version number
* Prevent cyclic references in link & move actions (#5635)
* do not create circular refs
* add negative validation test
* move to plugin
* add link test too
* fix docs
* refactored per john request
* fix path
* use appAction lib
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
* [Condition Set] Add check for empty string being passed to the makeKeyString util by TelemetryCriterion (#5636) (#5663)
* Check telemetry is defined before using makeKeyString util
* Add optional chaining in the check
* Add e2e test
* Add check for undefined
Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
* [Fault Management] New Example Provider, Unit and e2e tests (#5579)
* added unit tests for fault management plugin
* modified the example fault provider to work out of the box
* updating for new e2e folder structure
* part of the e2e tests
* WIP
* Imagery thumbnail regression fixes - 5327 (#5569)
* Add an active class to thumbnail to indicate current focused image
* Differentiate bg color between real-time and fixed
* scrollIntoView inline: center
* Added watcher for bounds change to trigger thumbnail scroll
* Resolve merge conflict with requestHistory change to telemetry collection
* Split thumbnail into sub component
* Monitor isFixed value to unpause playback status
* updated search to include name, namespace and description added some more e2e tests
* added rest of e2e tests
* fixed my init script, had to disable lint for no-force because it was not working without it, saw online this may be a pw bug
* fix: removing maelstrom theme from application (#5600)
* added some tests for no faults
* visual tests
* added visual tests for fault management
* created utils file for shared functionality between function and visual tests
* updating to 2.0.8
* tryin to remove imagery changes from master
* trying to trigger a refresh
* tryin to refresh
* updated search to include name, namespace and description added some more e2e tests
* added rest of e2e tests
* fix: removing maelstrom theme from application (#5600)
* fixed my init script, had to disable lint for no-force because it was not working without it, saw online this may be a pw bug
* added some tests for no faults
* visual tests
* added visual tests for fault management
* created utils file for shared functionality between function and visual tests
* updating to 2.0.8
* no clue
* still no clue
* removing imports and chaning to requires
* updating utils file to work with require
* fixing paths
* fixing a test I had messed up when adding static exmaple faults
* ONE LAST PATH FIX... hopefully
* typo in files fix
* fix folder typo
* thought I got this one, but apparently not, well I did now! who is laughing now!?
Co-authored-by: Michael Rogers <contact@mhrogers.com>
Co-authored-by: Vitor Henckel <vitor@henckel.com.br>
* Sort tree items locally on rename (#5643)
* fix typo
* Sort the tree items locally on object rename
* Use the navigationPath as a key
- This ensures that objects AND linked objects will be sorted
* add 'tree' and 'treeitem' roles to mct-tree
* WIP tree item reordering test
* Select the first object that matches
* Test that all object links are also reordered
* Get the final uuid before queryParams as notebook sections have uuids
* Make `openObjectTreeContextMenu` more deterministic and update usage
* Add `expandPathToTreeItem` and `expandTreeItemByName` appActions
* add `#tree-pane` id for the tree view
* Add tree visual component test suite and bump percy-cli
* Remove tree appActions
* Better variable name
Co-authored-by: Scott Bell <scott@traclabs.com>
* Mct5549 fix indexer composition error (#5610)
* [Display Layout] Composition and configuration sync (#5669)
LGTM
* [e2e] Stabilize notebook tag tests (#5681)
* Use more deterministic selector
* Hover first to "slow down" e2e actions while in headless mode
* Moves condition set fix into 2.0.8 (#5673)
* Set Focused Image index after a imagery is selected from a timestrip - 5632 (#5664)
* Set focused image when timestamp prop is passed in
* Unused var
* Create timestrip with imagery child
* Add equality check for hovered image and view large image url
* Cleanup
* Time List 5534 for release/2.0.8 (#5678)
* Changes to Time List view. Closes #5534.
- Compacted table row spacing.
- Set all timeframes to display by default when creating a new Time List.
- Removed 'Upload plan' file button from properties.
* Changes to Time List view. Closes #5534.
- Better hint text for editing Timeframe Inspector section.
Co-authored-by: Andrew Henry <akhenry@gmail.com>
* [CI] Enable couchdb e2e testing in open source (#5655)
* Handle couch db not found errors so that interceptors are still invoked. (#5654)
* Fix tests for interceptors
* [e2e] Add test for 'mine' folder initialization
* [e2e] don't fail on expected console errors
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
* [Docs] Update CouchDB local install documentation (#5692)
* Update local CouchDB install docs to include docker workflow
* reformat to source configuration scripts where possible
* correct couchdb case
Co-authored-by: John Hill <john.c.hill@nasa.gov>
* [Time Conductor] History not working correctly (#5687)
* the check for fixed time vs realtime was not updating, have fixed this
* merging in related changes from master pr #4414
* lint fixes
* Update src/plugins/timeConductor/ConductorHistory.vue
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
* setting time mode directly on load
* fixing issue where realtime history was being wiped on reloads while viewing fixed time
* formatting
* stubbed in some tests
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
* Only index if provider does not support search - mct5690 (#5693)
* only index if provider does not support search
* add some tests
* fix tests
* [e2e] Add search couchdb test for duplicates
* [e2e] Modify existing search test instead
* lint
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
* Don't re-request historical data on ticks (#5701)
Don't rerequest telemetry on ticks.
* Fix duplicate declaration from merge
Co-authored-by: Michael Rogers <contact@mhrogers.com>
Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: Alize Nguyen <alizenguyen@gmail.com>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Vitor Henckel <vitor@henckel.com.br>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2022-08-24 18:08:17 +00:00
|
|
|
return window.location.href.split('?')[0].match(regexp).at(-1);
|
2022-08-22 18:41:38 +00:00
|
|
|
}, 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
|
2023-08-12 00:18:08 +00:00
|
|
|
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} identifier the uuid or identifier of the object to get the url for
|
2022-08-22 18:41:38 +00:00
|
|
|
* @returns {Promise<string>} the url of the object
|
|
|
|
*/
|
2023-08-12 00:18:08 +00:00
|
|
|
async function getHashUrlToDomainObject(page, identifier) {
|
2024-07-24 03:41:07 +00:00
|
|
|
await page.waitForLoadState('domcontentloaded');
|
2023-08-12 00:18:08 +00:00
|
|
|
const hashUrl = await page.evaluate(async (objectIdentifier) => {
|
|
|
|
const path = await window.openmct.objects.getOriginalPath(objectIdentifier);
|
2022-08-22 18:41:38 +00:00
|
|
|
let url =
|
|
|
|
'./#/browse/' +
|
|
|
|
[...path]
|
|
|
|
.reverse()
|
|
|
|
.map((object) => window.openmct.objects.makeKeyString(object.identifier))
|
|
|
|
.join('/');
|
2023-05-18 21:54:46 +00:00
|
|
|
|
2022-08-22 18:41:38 +00:00
|
|
|
// Drop the vestigial '/ROOT' if it exists
|
|
|
|
if (url.includes('/ROOT')) {
|
|
|
|
url = url.split('/ROOT').join('');
|
|
|
|
}
|
|
|
|
|
|
|
|
return url;
|
2023-08-12 00:18:08 +00:00
|
|
|
}, identifier);
|
2022-08-22 18:41:38 +00:00
|
|
|
|
|
|
|
return hashUrl;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-10-22 00:05:59 +00:00
|
|
|
* Utilizes the OpenMCT API to detect if the UI is in Edit mode.
|
2022-08-22 18:41:38 +00:00
|
|
|
* @private
|
|
|
|
* @param {import('@playwright/test').Page} page
|
2023-08-12 00:18:08 +00:00
|
|
|
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} identifier
|
2022-10-22 00:05:59 +00:00
|
|
|
* @return {Promise<boolean>} true if the Open MCT is in Edit Mode
|
2022-08-22 18:41:38 +00:00
|
|
|
*/
|
|
|
|
async function _isInEditMode(page, identifier) {
|
|
|
|
// eslint-disable-next-line no-return-await
|
2022-10-22 00:05:59 +00:00
|
|
|
return await page.evaluate(() => window.openmct.editor.isEditing());
|
2022-08-22 18:41:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the time conductor mode to either fixed timespan or realtime mode.
|
2024-08-07 21:36:14 +00:00
|
|
|
* @private
|
2022-08-22 18:41:38 +00:00
|
|
|
* @param {import('@playwright/test').Page} page
|
|
|
|
* @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true
|
|
|
|
*/
|
2024-08-07 21:36:14 +00:00
|
|
|
async function _setTimeConductorMode(page, isFixedTimespan = true) {
|
2022-08-22 18:41:38 +00:00
|
|
|
// Click 'mode' button
|
2023-07-28 02:06:41 +00:00
|
|
|
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
|
|
|
await page.getByRole('button', { name: 'Time Conductor Mode Menu' }).click();
|
2023-09-25 17:15:00 +00:00
|
|
|
// Switch time conductor mode. Note, need to wait here for URL to update as the router is debounced.
|
2022-08-22 18:41:38 +00:00
|
|
|
if (isFixedTimespan) {
|
2023-07-28 02:06:41 +00:00
|
|
|
await page.getByRole('menuitem', { name: /Fixed Timespan/ }).click();
|
2023-09-25 17:15:00 +00:00
|
|
|
await page.waitForURL(/tc\.mode=fixed/);
|
2022-08-22 18:41:38 +00:00
|
|
|
} else {
|
2023-07-28 02:06:41 +00:00
|
|
|
await page.getByRole('menuitem', { name: /Real-Time/ }).click();
|
2023-09-25 17:15:00 +00:00
|
|
|
await page.waitForURL(/tc\.mode=local/);
|
2022-08-22 18:41:38 +00:00
|
|
|
}
|
2024-03-18 23:48:33 +00:00
|
|
|
//dismiss the time conductor popup
|
|
|
|
await page.getByLabel('Discard changes and close time popup').click();
|
2022-08-22 18:41:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the time conductor to fixed timespan mode
|
|
|
|
* @param {import('@playwright/test').Page} page
|
|
|
|
*/
|
|
|
|
async function setFixedTimeMode(page) {
|
2024-08-07 21:36:14 +00:00
|
|
|
await _setTimeConductorMode(page, true);
|
2022-08-22 18:41:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the time conductor to realtime mode
|
|
|
|
* @param {import('@playwright/test').Page} page
|
|
|
|
*/
|
|
|
|
async function setRealTimeMode(page) {
|
2024-08-07 21:36:14 +00:00
|
|
|
await _setTimeConductorMode(page, false);
|
2022-08-22 18:41:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @typedef {Object} OffsetValues
|
2023-07-28 02:06:41 +00:00
|
|
|
* @property {string | undefined} startHours
|
|
|
|
* @property {string | undefined} startMins
|
|
|
|
* @property {string | undefined} startSecs
|
|
|
|
* @property {string | undefined} endHours
|
|
|
|
* @property {string | undefined} endMins
|
|
|
|
* @property {string | undefined} endSecs
|
2022-08-22 18:41:38 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
|
|
|
|
* @param {import('@playwright/test').Page} page
|
2024-07-25 23:55:50 +00:00
|
|
|
* @param {OffsetValues} offset - Object containing offset values
|
|
|
|
* @param {boolean} [offset.submitChanges=true] - If true, submit the offset changes; otherwise, discard them
|
2022-08-22 18:41:38 +00:00
|
|
|
*/
|
2023-07-28 02:06:41 +00:00
|
|
|
async function setTimeConductorOffset(
|
|
|
|
page,
|
2024-07-25 23:55:50 +00:00
|
|
|
{ startHours, startMins, startSecs, endHours, endMins, endSecs, submitChanges = true }
|
2023-07-28 02:06:41 +00:00
|
|
|
) {
|
|
|
|
if (startHours) {
|
2024-07-25 23:55:50 +00:00
|
|
|
await page.getByLabel('Start offset hours').fill(startHours);
|
2023-07-28 02:06:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (startMins) {
|
2024-07-25 23:55:50 +00:00
|
|
|
await page.getByLabel('Start offset minutes').fill(startMins);
|
2023-07-28 02:06:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (startSecs) {
|
2024-07-25 23:55:50 +00:00
|
|
|
await page.getByLabel('Start offset seconds').fill(startSecs);
|
2023-07-28 02:06:41 +00:00
|
|
|
}
|
2022-08-22 18:41:38 +00:00
|
|
|
|
2023-07-28 02:06:41 +00:00
|
|
|
if (endHours) {
|
2024-07-25 23:55:50 +00:00
|
|
|
await page.getByLabel('End offset hours').fill(endHours);
|
2022-08-22 18:41:38 +00:00
|
|
|
}
|
|
|
|
|
2023-07-28 02:06:41 +00:00
|
|
|
if (endMins) {
|
2024-07-25 23:55:50 +00:00
|
|
|
await page.getByLabel('End offset minutes').fill(endMins);
|
2022-08-22 18:41:38 +00:00
|
|
|
}
|
|
|
|
|
2023-07-28 02:06:41 +00:00
|
|
|
if (endSecs) {
|
2024-07-25 23:55:50 +00:00
|
|
|
await page.getByLabel('End offset seconds').fill(endSecs);
|
2022-08-22 18:41:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Click the check button
|
2024-07-25 23:55:50 +00:00
|
|
|
if (submitChanges) {
|
|
|
|
await page.getByLabel('Submit time offsets').click();
|
|
|
|
} else {
|
|
|
|
await page.getByLabel('Discard changes and close time popup').click();
|
|
|
|
}
|
2022-08-22 18:41:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the values (hours, mins, secs) for the start time offset when in realtime mode
|
|
|
|
* @param {import('@playwright/test').Page} page
|
|
|
|
* @param {OffsetValues} offset
|
2024-07-25 23:55:50 +00:00
|
|
|
* @param {boolean} [submit=true] If true, submit the offset changes; otherwise, discard them
|
2022-08-22 18:41:38 +00:00
|
|
|
*/
|
2024-07-25 23:55:50 +00:00
|
|
|
async function setStartOffset(page, { submitChanges = true, ...offset }) {
|
2023-07-19 00:32:05 +00:00
|
|
|
// Click 'mode' button
|
2023-07-28 02:06:41 +00:00
|
|
|
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
2024-07-25 23:55:50 +00:00
|
|
|
await setTimeConductorOffset(page, { submitChanges, ...offset });
|
2022-08-22 18:41:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the values (hours, mins, secs) for the end time offset when in realtime mode
|
|
|
|
* @param {import('@playwright/test').Page} page
|
|
|
|
* @param {OffsetValues} offset
|
2024-07-25 23:55:50 +00:00
|
|
|
* @param {boolean} [submit=true] If true, submit the offset changes; otherwise, discard them
|
2022-08-22 18:41:38 +00:00
|
|
|
*/
|
2024-07-25 23:55:50 +00:00
|
|
|
async function setEndOffset(page, { submitChanges = true, ...offset }) {
|
2023-07-19 00:32:05 +00:00
|
|
|
// Click 'mode' button
|
2023-07-28 02:06:41 +00:00
|
|
|
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
2024-07-25 23:55:50 +00:00
|
|
|
await setTimeConductorOffset(page, { submitChanges, ...offset });
|
2022-08-22 18:41:38 +00:00
|
|
|
}
|
|
|
|
|
2023-08-16 19:02:09 +00:00
|
|
|
/**
|
|
|
|
* Set the time conductor bounds in fixed time mode
|
|
|
|
*
|
|
|
|
* NOTE: Unless explicitly testing the Time Conductor itself, it is advised to instead
|
|
|
|
* navigate directly to the object with the desired time bounds using `navigateToObjectWithFixedTimeBounds()`.
|
|
|
|
* @param {import('@playwright/test').Page} page
|
2024-07-25 23:55:50 +00:00
|
|
|
* @param {Object} bounds - The time conductor bounds
|
|
|
|
* @param {string} [bounds.startDate] - The start date in YYYY-MM-DD format
|
|
|
|
* @param {string} [bounds.startTime] - The start time in HH:mm:ss format
|
|
|
|
* @param {string} [bounds.endDate] - The end date in YYYY-MM-DD format
|
|
|
|
* @param {string} [bounds.endTime] - The end time in HH:mm:ss format
|
|
|
|
* @param {boolean} [bounds.submitChanges=true] - If true, submit the changes; otherwise, discard them.
|
2023-08-16 19:02:09 +00:00
|
|
|
*/
|
2024-07-25 23:55:50 +00:00
|
|
|
async function setTimeConductorBounds(page, { submitChanges = true, ...bounds }) {
|
|
|
|
const { startDate, endDate, startTime, endTime } = bounds;
|
|
|
|
|
|
|
|
// Open the time conductor popup
|
|
|
|
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
|
|
|
|
|
|
|
if (startDate) {
|
|
|
|
await page.getByLabel('Start date').fill(startDate);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (startTime) {
|
|
|
|
await page.getByLabel('Start time').fill(startTime);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (endDate) {
|
|
|
|
await page.getByLabel('End date').fill(endDate);
|
|
|
|
}
|
2023-07-28 02:06:41 +00:00
|
|
|
|
2024-07-25 23:55:50 +00:00
|
|
|
if (endTime) {
|
|
|
|
await page.getByLabel('End time').fill(endTime);
|
|
|
|
}
|
2023-07-28 02:06:41 +00:00
|
|
|
|
2024-07-25 23:55:50 +00:00
|
|
|
if (submitChanges) {
|
|
|
|
await page.getByLabel('Submit time bounds').click();
|
|
|
|
} else {
|
|
|
|
await page.getByLabel('Discard changes and close time popup').click();
|
|
|
|
}
|
2023-07-28 02:06:41 +00:00
|
|
|
}
|
|
|
|
|
2023-08-16 19:02:09 +00:00
|
|
|
/**
|
2024-08-07 21:36:14 +00:00
|
|
|
* Set the bounds of the visible conductor in fixed time mode.
|
|
|
|
* Requires that page already has an independent time conductor in view.
|
2023-08-16 19:02:09 +00:00
|
|
|
* @param {import('@playwright/test').Page} page
|
2024-08-07 21:36:14 +00:00
|
|
|
* @param {string} start - The start date in 'YYYY-MM-DD HH:mm:ss.SSSZ' format
|
|
|
|
* @param {string} end - The end date in 'YYYY-MM-DD HH:mm:ss.SSSZ' format
|
2023-08-16 19:02:09 +00:00
|
|
|
*/
|
2024-08-07 21:36:14 +00:00
|
|
|
async function setFixedIndependentTimeConductorBounds(page, { start, end }) {
|
2024-03-11 23:39:38 +00:00
|
|
|
// Activate Independent Time Conductor
|
|
|
|
await page.getByLabel('Enable Independent Time Conductor').click();
|
2023-07-28 02:06:41 +00:00
|
|
|
|
|
|
|
// Bring up the time conductor popup
|
2024-03-11 23:39:38 +00:00
|
|
|
await page.getByLabel('Independent Time Conductor Settings').click();
|
2024-08-07 21:36:14 +00:00
|
|
|
await expect(page.getByLabel('Time Conductor Options')).toBeInViewport();
|
|
|
|
await _setTimeBounds(page, start, end);
|
2023-07-28 02:06:41 +00:00
|
|
|
|
|
|
|
await page.keyboard.press('Enter');
|
|
|
|
}
|
|
|
|
|
2023-08-16 19:02:09 +00:00
|
|
|
/**
|
|
|
|
* Set the bounds of the visible conductor in fixed time mode
|
2023-09-11 23:33:46 +00:00
|
|
|
* @private
|
2023-08-16 19:02:09 +00:00
|
|
|
* @param {import('@playwright/test').Page} page
|
2024-08-07 21:36:14 +00:00
|
|
|
* @param {string} start - The start date in 'YYYY-MM-DD HH:mm:ss.SSSZ' format
|
|
|
|
* @param {string} end - The end date in 'YYYY-MM-DD HH:mm:ss.SSSZ' format
|
2023-08-16 19:02:09 +00:00
|
|
|
*/
|
2024-08-07 21:36:14 +00:00
|
|
|
async function _setTimeBounds(page, startDate, endDate) {
|
2023-07-28 02:06:41 +00:00
|
|
|
if (startDate) {
|
|
|
|
// Fill start time
|
|
|
|
await page
|
|
|
|
.getByRole('textbox', { name: 'Start date' })
|
|
|
|
.fill(startDate.toString().substring(0, 10));
|
|
|
|
await page
|
|
|
|
.getByRole('textbox', { name: 'Start time' })
|
|
|
|
.fill(startDate.toString().substring(11, 19));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (endDate) {
|
|
|
|
// Fill end time
|
|
|
|
await page.getByRole('textbox', { name: 'End date' }).fill(endDate.toString().substring(0, 10));
|
|
|
|
await page
|
|
|
|
.getByRole('textbox', { name: 'End time' })
|
|
|
|
.fill(endDate.toString().substring(11, 19));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-20 21:36:58 +00:00
|
|
|
/**
|
|
|
|
* Waits and asserts that all plot series data on the page
|
|
|
|
* is loaded and drawn.
|
|
|
|
*
|
|
|
|
* In lieu of a better way to detect when a plot is done rendering,
|
|
|
|
* we [attach a class to the '.gl-plot' element](https://github.com/nasa/openmct/blob/5924d7ea95a0c2d4141c602a3c7d0665cb91095f/src/plugins/plot/MctPlot.vue#L27)
|
|
|
|
* once all pending series data has been loaded. The following appAction retrieves
|
|
|
|
* all plots on the page and waits up to the default timeout for the class to be
|
|
|
|
* attached to each plot.
|
|
|
|
* @param {import('@playwright/test').Page} page
|
2024-08-07 21:36:14 +00:00
|
|
|
* @param {number} [timeout] Provide a custom timeout in milliseconds to override the default timeout
|
2023-04-20 21:36:58 +00:00
|
|
|
*/
|
2024-08-07 21:36:14 +00:00
|
|
|
async function waitForPlotsToRender(page, { timeout } = {}) {
|
|
|
|
//eslint-disable-next-line playwright/no-raw-locators
|
2023-04-20 21:36:58 +00:00
|
|
|
const plotLocator = page.locator('.gl-plot');
|
|
|
|
for (const plot of await plotLocator.all()) {
|
2024-08-07 21:36:14 +00:00
|
|
|
await expect(plot).toHaveClass(/js-series-data-loaded/, { timeout });
|
2023-04-20 21:36:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @typedef {Object} PlotPixel
|
|
|
|
* @property {number} r The value of the red channel (0-255)
|
|
|
|
* @property {number} g The value of the green channel (0-255)
|
|
|
|
* @property {number} b The value of the blue channel (0-255)
|
|
|
|
* @property {number} a The value of the alpha channel (0-255)
|
|
|
|
* @property {string} strValue The rgba string value of the pixel
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Wait for all plots to render and then retrieve and return an array
|
|
|
|
* of canvas plot pixel data (RGBA values).
|
|
|
|
* @param {import('@playwright/test').Page} page
|
|
|
|
* @param {string} canvasSelector The selector for the canvas element
|
|
|
|
* @return {Promise<PlotPixel[]>}
|
|
|
|
*/
|
|
|
|
async function getCanvasPixels(page, canvasSelector) {
|
|
|
|
const canvasHandle = await page.evaluateHandle(
|
|
|
|
(canvas) => document.querySelector(canvas),
|
|
|
|
canvasSelector
|
|
|
|
);
|
|
|
|
const canvasContextHandle = await page.evaluateHandle(
|
|
|
|
(canvas) => canvas.getContext('2d'),
|
|
|
|
canvasHandle
|
2023-05-18 21:54:46 +00:00
|
|
|
);
|
|
|
|
|
2023-04-20 21:36:58 +00:00
|
|
|
await waitForPlotsToRender(page);
|
2024-06-03 15:46:05 +00:00
|
|
|
return page.evaluate(
|
2023-04-20 21:36:58 +00:00
|
|
|
([canvas, ctx]) => {
|
|
|
|
// The document canvas is where the plot points and lines are drawn.
|
|
|
|
// The only way to access the canvas is using document (using page.evaluate)
|
|
|
|
/** @type {ImageData} */
|
|
|
|
const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
|
|
|
|
/** @type {number[]} */
|
|
|
|
const imageDataValues = Object.values(data);
|
|
|
|
/** @type {PlotPixel[]} */
|
|
|
|
const plotPixels = [];
|
|
|
|
// Each pixel consists of four values within the ImageData.data array. The for loop iterates by multiples of four.
|
|
|
|
// The values associated with each pixel are R (red), G (green), B (blue), and A (alpha), in that order.
|
|
|
|
for (let i = 0; i < imageDataValues.length; ) {
|
|
|
|
if (imageDataValues[i] > 0) {
|
|
|
|
plotPixels.push({
|
|
|
|
r: imageDataValues[i],
|
|
|
|
g: imageDataValues[i + 1],
|
|
|
|
b: imageDataValues[i + 2],
|
|
|
|
a: imageDataValues[i + 3],
|
|
|
|
strValue: `rgb(${imageDataValues[i]}, ${imageDataValues[i + 1]}, ${
|
|
|
|
imageDataValues[i + 2]
|
|
|
|
}, ${imageDataValues[i + 3]})`
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
i = i + 4;
|
2023-05-18 21:54:46 +00:00
|
|
|
}
|
|
|
|
|
2024-06-03 15:46:05 +00:00
|
|
|
return plotPixels;
|
2023-04-20 21:36:58 +00:00
|
|
|
},
|
|
|
|
[canvasHandle, canvasContextHandle]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-01-02 15:24:22 +00:00
|
|
|
export {
|
2022-07-27 21:31:18 +00:00
|
|
|
createDomainObjectWithDefaults,
|
2023-08-12 00:18:08 +00:00
|
|
|
createExampleTelemetryObject,
|
2023-01-14 02:12:08 +00:00
|
|
|
createNotification,
|
2022-09-10 00:31:03 +00:00
|
|
|
createPlanFromJSON,
|
2023-04-20 21:36:58 +00:00
|
|
|
expandEntireTree,
|
|
|
|
getCanvasPixels,
|
2023-08-12 00:18:08 +00:00
|
|
|
navigateToObjectWithFixedTimeBounds,
|
2024-05-09 18:53:11 +00:00
|
|
|
navigateToObjectWithRealTime,
|
2024-01-02 15:24:22 +00:00
|
|
|
setEndOffset,
|
2024-08-07 21:36:14 +00:00
|
|
|
setFixedIndependentTimeConductorBounds,
|
2022-08-22 18:41:38 +00:00
|
|
|
setFixedTimeMode,
|
|
|
|
setRealTimeMode,
|
|
|
|
setStartOffset,
|
2023-07-28 02:06:41 +00:00
|
|
|
setTimeConductorBounds,
|
2024-01-02 15:24:22 +00:00
|
|
|
waitForPlotsToRender
|
2022-07-27 21:31:18 +00:00
|
|
|
};
|