test(e2e): Major refactor and stabilization of e2e tests (#7581)

* fix: update broken locator

* update eslint package

* first pass of lint fixes

* update package

* change ruleset

* update component tests to match linting rules

* driveby

* start to factor out bad locators

* update gauge component

* update notebook snapshot drop area

* Update plot aria

* add draggable true to tree items

* update package

* driveby to remove dead code

* unneeded

* unneeded

* tells a screenreader that this is a row and a cell

* adds an id for dragondrops

* this should be a button

* first pass at fixing tooltip selectors

* review comments

* Updating more tests

* update to remove expect expect given our use of check functions

* add expand component

* move role around

* update more locators

* force

* new local storage

* remove choochoo steps

* test: do `lint:fix` and also add back accidentally removed code

* test: add back more removed code

* test: remove `unstable` annotation from tests which are not unstable

* test: remove invalid test-- the "new" time conductor doesn't allow for millisecond changes in fixed time

* test: fix unstable gauge test

* test: remove useless asserts-- this was secretly non-functional. now that we've fixed it, it makes no sense and just fails

* test: add back accidentally removed changes

* test: revert changes that break test

* test: more fixes

* Remove all notion of the unstable/stable e2e tests

* test: eviscerate the flake with FACTS and LOGIC

* test: fix anotha one

* lint fixes

* test: no need to wait for save dialog

* test: fix more tests

* lint: fix more warnings

* test: fix anotha one

* test: use `toHaveLength` instead of `.length).toBe()`

* test: stabilize tabs view example imagery test

* fix: more tests be fixed

* test: more `toHaveCount()`s please

* test: revert more accidentally removed fixes

* test: fix selector

* test: fix anotha one

* update lint rules to clean up bad locators in shared fixtures

* update and remove bad appActions

* test: fix some restricted notebook tests

* test: mass find/replace to enforce `toHaveCount()` instead of `.count()).toBe()`

* Remove some bad appActions and update text

* test: fix da tree tests

* test: await not await await

* test: fix upload plan appAction and add a11y

* Updating externalFixtures with best practice locators and add missing appAction framework tests

* test: fix test

* test: fix appAction test for plans

* test: yum yum fix'em up and get rid of some dragon drops

* fix: alas, a `.only()` got my hopes up that i was done fixing tests

* test: add `setTimeConductorMode` test "suite" which covers most TC related appActions

* test: fix arg

* test(couchdb): fix some network tests via expect polling

* Stabalize visual test

* getCanasPixels

* test: stabilize tooltip telemetry table test, better a11y for tooltips

* chore: update to use `docker compose` instead of `docker-compose`

* New rules, new tests, new me

* fix sort order

* test: add `waitForPlotsToRender` framework test, passthru timeout override

* test: remove `clockOptions` test as we have `page.clock` now

* test: refactor out `overrideClock`

* test: use `clock.install` instead

* test: use `clock.install` instead

* time clock fix

* test: fix timer tests

* remove ever reference to old base fixture

* test: stabilize restricted notebook test

* lint fixes

* test: use clock.install

* update timelist

* test: update visual tests to use `page.clock()`, update snapshots

* test: stabilize tree renaming/reordering test

* a11y: add aria-label and role=region to object view

* refactor: use `dragTo`

* refactor: use `dragTo`, other small fixes

* test: use `page.clock()` to stabilize tooltip telemetry table test

* test: use web-first assertion to stabilize staleness test

* test: knock out a few more `page.click`s

* test: destroy all `page.click()`s

* refactor: consistently use `'Ok'` instead of `'OK'` and `'Ok'` mixed

* test: remove gauge aria label

* test: more test fixes

* test: more fixes and refactors

* docs: add comment

* test: refactor all instances of `dragAndDrop`

* test: remove redundant test (covered in previous test steps)

* test: stabilize imagery operations tests for display layout

* chore: remove bad unicorn rule

* chore(lint): remove unused disable directives

---------

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
This commit is contained in:
John Hill 2024-08-07 14:36:14 -07:00 committed by GitHub
parent 4ee68cccd6
commit 0413e77d8a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
120 changed files with 1968 additions and 1824 deletions

View File

@ -93,7 +93,7 @@ jobs:
- generate_and_store_version_and_filesystem_artifacts
e2e-test:
parameters:
suite: #stable or full
suite: #ci or full
type: string
executor: pw-focal-development
parallelism: 7
@ -162,7 +162,7 @@ jobs:
- run: npx playwright@1.45.2 install #Necessary for bare ubuntu machine
- run: |
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
docker compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
sleep 3
bash src/plugins/persistence/couch/setup-couchdb.sh
- run: sh src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh #Replace LocalStorage Plugin with CouchDB
@ -253,8 +253,8 @@ workflows:
name: node18-chrome
node-version: lts/hydrogen
- e2e-test:
name: e2e-stable
suite: stable
name: e2e-ci
suite: ci
- e2e-mobile
- visual-a11y:
name: visual-a11y-ci

View File

@ -482,19 +482,10 @@
"composables",
"countup",
"darkmatter",
"Undeletes"
],
"dictionaries": [
"npm",
"softwareTerms",
"node",
"html",
"css",
"bash",
"en_US",
"en-gb",
"misc"
"Undeletes",
"SSSZ"
],
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US", "en-gb", "misc"],
"ignorePaths": [
"package.json",
"dist/**",
@ -505,4 +496,4 @@
"html-test-results",
"test-results"
]
}
}

View File

@ -5,9 +5,8 @@ const config = {
browser: true,
es2024: true,
jasmine: true,
node: true,
worker: true,
serviceworker: true
amd: true,
node: true
},
globals: {
_: 'readonly',

View File

@ -42,7 +42,7 @@ jobs:
- name: Start CouchDB Docker Container and Init with Setup Scripts
run: |
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
docker compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
sleep 3
bash src/plugins/persistence/couch/setup-couchdb.sh
bash src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh

View File

@ -34,7 +34,7 @@ jobs:
- run: npm ci --no-audit --progress=false
- name: Run E2E Tests (Repeated 10 Times)
run: npm run test:e2e:stable -- --retries=0 --repeat-each=10 --max-failures=50
run: npm run test:e2e:ci -- --retries=0 --repeat-each=10 --max-failures=50
- name: Archive test results
if: success() || failure()

View File

@ -109,7 +109,7 @@ Our e2e (end-to-end), Visual, and Performance tests leverage the Playwright fram
- **e2e Tests**: These tests are run on every commit. To run the tests locally, use:
```sh
npm run test:e2e:stable
npm run test:e2e:ci
```
- **Visual Tests**: For running the visual test suite, use:

View File

@ -66,8 +66,8 @@ The e2e line coverage is a bit more complex than the karma implementation. This
1. Each e2e suite will start webpack with the ```npm run start:coverage``` command with config `webpack.coverage.mjs` and the `babel-plugin-istanbul` plugin to generate code coverage during e2e test execution using our custom [baseFixture](./baseFixtures.js).
1. During testcase execution, each e2e shard will generate its piece of the larger coverage suite. **This coverage file is not merged**. The raw coverage file is stored in a `.nyc_report` directory.
1. [nyc](https://github.com/istanbuljs/nyc) converts this directory into a `lcov` file with the following command `npm run cov:e2e:report`
1. Most of the tests are run in the '@stable' configuration and focus on chrome/ubuntu at a single resolution. This coverage is published to codecov with `npm run cov:e2e:stable:publish`.
1. The rest of our coverage only appears when run against `@unstable` tests, persistent datastore (couchdb), non-ubuntu machines, and non-chrome browsers with the `npm run cov:e2e:full:publish` flag. Since this happens about once a day, we have leveraged codecov.io's carryforward flag to report on lines covered outside of each commit on an individual PR.
1. Most of the tests focus on chrome/ubuntu at a single resolution. This coverage is published to codecov with `npm run cov:e2e:ci:publish`.
1. The rest of our coverage only appears when run against persistent datastore (couchdb), non-ubuntu machines, and non-chrome browsers with the `npm run cov:e2e:full:publish` flag. Since this happens about once a day, we have leveraged codecov.io's carryforward flag to report on lines covered outside of each commit on an individual PR.
### Limitations in our code coverage reporting

View File

@ -11,18 +11,18 @@ coverage:
informational: true
precision: 2
round: down
range: '66...100'
range: "66...100"
flags:
unit:
carryforward: false
e2e-stable:
e2e-ci:
carryforward: false
e2e-full:
carryforward: true
comment:
layout: 'diff,flags,files,footer'
layout: "diff,flags,files,footer"
behavior: default
require_changes: false
show_carryforward_flags: true

View File

@ -1,14 +1,24 @@
/* eslint-disable no-undef */
module.exports = {
extends: ['plugin:playwright/playwright-test'],
extends: ['plugin:playwright/recommended'],
rules: {
'playwright/max-nested-describe': ['error', { max: 1 }]
'playwright/max-nested-describe': ['error', { max: 1 }],
'playwright/expect-expect': 'off'
},
overrides: [
{
files: ['tests/visual/*.spec.js'],
//Apply Best Practices to externalFixtures and exampleTemplate.e2e.spec.js
files: [
'appActions.js',
'baseFixtures.js',
'pluginFixtures.js',
'**/exampleTemplate.e2e.spec.js'
],
rules: {
'playwright/no-wait-for-timeout': 'off'
'playwright/no-raw-locators': 'error',
'playwright/no-nth-methods': 'error',
'playwright/no-get-by-title': 'error',
'playwright/prefer-comparison-matcher': 'error'
}
}
]

View File

@ -225,14 +225,13 @@ Current list of test tags:
|:-:|-|
|`@mobile` | Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no create button).|
|`@a11y` | Test case or test suite to execute playwright-axe accessibility checks and generate a11y reports.|
|`@gds` | Denotes a GDS Test Case used in the VIPER Mission.|
|`@addInit` | Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of `npm start`.|
|`@localStorage` | Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB). See [note](#utilizing-localstorage)|
|`@snapshot` | Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of the playwright container.|
|`@unstable` | A new test or test which is known to be flaky.|
|`@2p` | Indicates that multiple users are involved, or multiple tabs/pages are used. Useful for testing multi-user interactivity.|
|`@generatedata` | Indicates that a test is used to generate testdata or test the generated test data. Usually to be associated with localstorage, but this may grow over time.|
|`@clock` | A test which modifies the clock. These have expanded out of the visual tests and into the functional tests.
|`@framework` | A test for open mct e2e capabilities. This is primarily to ensure we don't break projects which depend on sourcing this project's fixtures like appActions.js.
### Continuous Integration
@ -248,7 +247,7 @@ Our CI environment consists of 3 main modes of operation:
CircleCI
- Stable e2e tests against ubuntu and chrome
- e2e tests against ubuntu and chrome
- Performance tests against ubuntu and chrome
- e2e tests are linted
- Visual and a11y tests are run in a single resolution on the default `espresso` theme
@ -287,18 +286,6 @@ So for every commit, Playwright is effectively running 4 x 2 concurrent browserc
At the same time, we don't want to waste CI resources on parallel runs, so we've configured each shard to fail after 5 test failures. Test failure logs are recorded and stored to allow fast triage.
#### Test Promotion
In order to maintain fast and reliable feedback, tests go through a promotion process. All new test cases or test suites must be labeled with the `@unstable` annotation. The Open MCT dev team runs these unstable tests in our private repos to ensure they work downstream and are reliable.
- To run the stable tests, use the `npm run test:e2e:stable` command.
- To run the new and flaky tests, use the `npm run test:e2e:unstable` command.
A testcase and testsuite are to be unmarked as @unstable when:
1. They run as part of "full" run 5 times without failure.
2. They've been by a Open MCT Developer 5 times in the closed source repo without failure.
### Cross-browser and Cross-operating system
#### **What's supported:**
@ -380,8 +367,7 @@ By adhering to this principle, we can create tests that are both robust and refl
1. Avoid creating locator aliases. This likely means that you're compensating for a bad locator. Improve the application instead.
1. Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });` instead of `{ waitUntil: 'networkidle' }`. Tests run against deployments with websockets often have issues with the networkidle detection.
#### How to make tests faster and more resilient
#### How to make tests faster and more resilient to application changes
1. Avoid app interaction when possible. The best way of doing this is to navigate directly by URL:
```js
@ -396,6 +382,16 @@ By adhering to this principle, we can create tests that are both robust and refl
- Initial navigation should _almost_ always use the `{ waitUntil: 'domcontentloaded' }` option.
1. Avoid repeated setup to test a single assertion. Write longer tests with multiple soft assertions.
This ensures that your changes will be picked up with large refactors.
1. Use [user-facing locators](https://playwright.dev/docs/best-practices#use-locators) (Now a eslint rule!)
```js
page.getByRole('button', { name: 'Create' } )
```
Instead of
```js
page.locator('.c-create-button')
```
Note: `page.locator()` can be used in performance tests as xk6-browser does not yet support the new `page.getBy` pattern and css lookups can be [1.5x faster](https://serpapi.com/blog/css-selectors-faster-than-getbyrole-playwright/)
##### Utilizing LocalStorage
@ -448,6 +444,7 @@ By adhering to this principle, we can create tests that are both robust and refl
- Use Open MCT's fixed-time mode unless explicitly testing realtime clock
- Employ the `createExampleTelemetryObject` appAction to source telemetry and specify a `name` to avoid autogenerated names.
- Avoid creating objects with a time component like timers and clocks.
- Utilize the playwright clock() API. See @clock Annotations for examples.
5. **Hide the Tree and Inspector**: Generally, your test will not require comparisons involving the tree and inspector. These aspects are covered in component-specific tests (explained below). To exclude them from the comparison by default, navigate to the root of the main view with the tree and inspector hidden:
- `await page.goto('./#/browse/mine?hideTree=true&hideInspector=true')`
@ -493,29 +490,25 @@ For best practices with regards to mocking network responses, see our [couchdb.e
The following contains a list of tips and tricks which don't exactly fit into a FAQ or Best Practices doc.
- (Advanced) Overriding the Browser's Clock
It is possible to override the browser's clock in order to control time-based elements. Since this can cause unwanted behavior (i.e. Tree not rendering), only use this sparingly. To do this, use the `overrideClock` fixture as such:
It is possible to override the browser's clock in order to control time-based elements. Since this can cause unwanted behavior -- i.e. Tree not rendering -- only use this sparingly. Use the `page.clock()` API as such:
```js
import { test, expect } from '../../pluginFixtures.js';
test.describe('foo test suite', () => {
// All subsequent tests in this suite will override the clock
test.use({
clockOptions: {
now: 1732413600000, // A timestamp given as milliseconds since the epoch
shouldAdvanceTime: true // Should the clock tick?
}
test.describe('foo test suite @clock', () => {
test.beforeEach(async ({ page }) => {
//Set clock time
await page.clock.install({ time: MISSION_TIME });
await page.clock.resume();
//Navigate to page with new clock
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('bar test', async ({ page }) => {
// ...
test('bar here', async ({ page }) => {
/// ...
});
});
```
More info and options for `overrideClock` can be found in [baseFixtures.js](baseFixtures.js)
- Working with multiple pages
There are instances where multiple browser pages will needed to verify multi-page or multi-tab application behavior. Make sure to use the `@2p` annotation as well as name each page appropriately: i.e. `page1` and `page2` or `tab1` and `tab2` depending on the intended use case. Generally pages should be used unless testing `sharedWorker` code, specifically.

View File

@ -35,7 +35,6 @@
* @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.
* @property {Record<string, string>} [customParameters] any additional parameters to be passed to the domain object's form. E.g. '[aria-label="Data Rate (hz)"]': {'0.1'}
*/
/**
@ -62,14 +61,14 @@ import { v4 as genUuid } from 'uuid';
* 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
* @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
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
*/
async function createDomainObjectWithDefaults(
page,
{ type, name, parent = 'mine', customParameters = {} }
) {
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) {
if (!name) {
name = `${type}:${genUuid()}`;
}
@ -86,32 +85,18 @@ async function createDomainObjectWithDefaults(
// Click the object specified by 'type'-- case insensitive
await page.getByRole('menuitem', { name: new RegExp(`^${type}$`, 'i') }).click();
// Modify the name input field of the domain object to accept 'name'
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
await nameInput.fill('');
await nameInput.fill(name);
// Fill in the name of the object
await page.getByLabel('Title', { exact: true }).fill('');
await page.getByLabel('Title', { exact: true }).fill(name);
if (page.testNotes) {
// Fill the "Notes" section with information about the
// currently running test and its project.
const notesInput = page.locator('form[name="mctForm"] #notes-textarea');
await notesInput.fill(page.testNotes);
// eslint-disable-next-line playwright/no-raw-locators
await page.locator('#notes-textarea').fill(page.testNotes);
}
// If there are any further parameters, fill them in
for (const [key, value] of Object.entries(customParameters)) {
const input = page.locator(`form[name="mctForm"] ${key}`);
await input.fill('');
await input.fill(value);
}
// Click OK button and wait for Navigate event
await Promise.all([
page.waitForLoadState(),
await page.getByRole('button', { name: 'Save' }).click(),
// Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
await page.getByRole('button', { name: 'Save' }).click();
// Wait until the URL is updated
await page.waitForURL(`**/${parent}/*`);
@ -151,61 +136,41 @@ async function createNotification(page, createNotificationOptions) {
}
/**
* Expand an item in the tree by a given object name.
* 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.
*
* @param {import('@playwright/test').Page} page
* @param {string} name
*/
async function expandTreePaneItemByName(page, name) {
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
const expandTriangle = treeItem.locator('.c-disclosure-triangle');
await expandTriangle.click();
}
/**
* Create a Plan object from JSON with the provided options.
* @param {import('@playwright/test').Page} page
* @param {*} options
* @param {Object} json
* @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 newly created domain object.
*/
async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
if (!name) {
name = `Plan:${genUuid()}`;
}
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}`);
// Click the Create button
await page.getByRole('button', { name: 'Create' }).click();
// Click 'Plan' menu option
await page.click(`li:text("Plan")`);
await page.getByRole('menuitem', { name: 'Plan' }).click();
// Modify the name input field of the domain object to accept 'name'
const nameInput = page.getByLabel('Title', { exact: true });
await nameInput.fill('');
await nameInput.fill(name);
// 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);
// Upload buffer from memory
await page.locator('input#fileElem').setInputFiles({
await page.getByLabel('Select File...').setInputFiles({
name: 'plan.txt',
mimeType: 'text/plain',
buffer: Buffer.from(JSON.stringify(json))
});
// Click OK button and wait for Navigate event
await Promise.all([
page.waitForLoadState(),
page.click('[aria-label="Save"]'),
// Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
await page.getByLabel('Save').click();
// Wait until the URL is updated
await page.waitForURL(`**/${parent}/*`);
@ -233,10 +198,10 @@ async function createExampleTelemetryObject(page, parent = 'mine') {
await page.getByRole('button', { name: 'Create' }).click();
await page.locator('li:has-text("Sine Wave Generator")').click();
await page.getByRole('menuitem', { name: 'Sine Wave Generator' }).click();
const name = 'VIPER Rover Heading';
await page.getByRole('dialog').locator('input[type="text"]').fill(name);
await page.getByLabel('Title', { exact: true }).fill(name);
// Fill out the fields with default values
await page.getByRole('spinbutton', { name: 'Period' }).fill('10');
@ -263,7 +228,9 @@ async function createExampleTelemetryObject(page, parent = 'mine') {
}
/**
* Navigates directly to a given object url, in fixed time mode, with the given start and end bounds.
* 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.
*
* @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
@ -276,9 +243,13 @@ async function navigateToObjectWithFixedTimeBounds(page, url, start, end) {
}
/**
* Navigates directly to a given object url, in real-time mode.
* Navigates directly to a given object url, in real-time mode. Note: does not set
* default view type.
*
* @param {import('@playwright/test').Page} page
* @param {string} url The url to the domainObject
* @param {string | number} start The start offset in milliseconds
* @param {string | number} end The end offset in milliseconds
*/
async function navigateToObjectWithRealTime(page, url, start = '1800000', end = '30000') {
await page.goto(
@ -287,23 +258,11 @@ async function navigateToObjectWithRealTime(page, url, start = '1800000', end =
}
/**
* Open the given `domainObject`'s context menu from the object tree.
* Expands the path to the object and scrolls to it if necessary.
* 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.
*
* @param {import('@playwright/test').Page} page
* @param {string} url the url to the object
*/
async function openObjectTreeContextMenu(page, url) {
await page.goto(url);
await page.getByLabel('Show selected item in tree').click();
await page.locator('.is-navigated-object').click({
button: 'right'
});
}
/**
* Expands the entire object tree (every expandable tree item).
* @param {import('@playwright/test').Page} page
* @param {"Main Tree" | "Create Modal Tree"} [treeName="Main Tree"]
*/
async function expandEntireTree(page, treeName = 'Main Tree') {
@ -314,9 +273,10 @@ async function expandEntireTree(page, treeName = 'Main Tree') {
.getByRole('treeitem', {
expanded: false
})
.locator('span.c-disclosure-triangle.is-enabled');
.getByLabel(/Expand/);
while ((await collapsedTreeItems.count()) > 0) {
//eslint-disable-next-line playwright/no-nth-methods
await collapsedTreeItems.nth(0).click();
// FIXME: Replace hard wait with something event-driven.
@ -388,10 +348,11 @@ async function _isInEditMode(page, identifier) {
/**
* Set the time conductor mode to either fixed timespan or realtime mode.
* @private
* @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) {
async function _setTimeConductorMode(page, isFixedTimespan = true) {
// Click 'mode' button
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
await page.getByRole('button', { name: 'Time Conductor Mode Menu' }).click();
@ -412,7 +373,7 @@ async function setTimeConductorMode(page, isFixedTimespan = true) {
* @param {import('@playwright/test').Page} page
*/
async function setFixedTimeMode(page) {
await setTimeConductorMode(page, true);
await _setTimeConductorMode(page, true);
}
/**
@ -420,7 +381,7 @@ async function setFixedTimeMode(page) {
* @param {import('@playwright/test').Page} page
*/
async function setRealTimeMode(page) {
await setTimeConductorMode(page, false);
await _setTimeConductorMode(page, false);
}
/**
@ -542,19 +503,20 @@ async function setTimeConductorBounds(page, { submitChanges = true, ...bounds })
}
/**
* Set the independent time conductor bounds in fixed time mode
* Set the bounds of the visible conductor in fixed time mode.
* Requires that page already has an independent time conductor in view.
* @param {import('@playwright/test').Page} page
* @param {string} startDate
* @param {string} endDate
* @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
*/
async function setIndependentTimeConductorBounds(page, { start, end }) {
async function setFixedIndependentTimeConductorBounds(page, { start, end }) {
// Activate Independent Time Conductor
await page.getByLabel('Enable Independent Time Conductor').click();
// Bring up the time conductor popup
await page.getByLabel('Independent Time Conductor Settings').click();
await expect(page.locator('.itc-popout')).toBeInViewport();
await setTimeBounds(page, start, end);
await expect(page.getByLabel('Time Conductor Options')).toBeInViewport();
await _setTimeBounds(page, start, end);
await page.keyboard.press('Enter');
}
@ -563,10 +525,10 @@ async function setIndependentTimeConductorBounds(page, { start, end }) {
* Set the bounds of the visible conductor in fixed time mode
* @private
* @param {import('@playwright/test').Page} page
* @param {string} startDate
* @param {string} endDate
* @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
*/
async function setTimeBounds(page, startDate, endDate) {
async function _setTimeBounds(page, startDate, endDate) {
if (startDate) {
// Fill start time
await page
@ -596,11 +558,13 @@ async function setTimeBounds(page, startDate, endDate) {
* 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
* @param {number} [timeout] Provide a custom timeout in milliseconds to override the default timeout
*/
async function waitForPlotsToRender(page) {
async function waitForPlotsToRender(page, { timeout } = {}) {
//eslint-disable-next-line playwright/no-raw-locators
const plotLocator = page.locator('.gl-plot');
for (const plot of await plotLocator.all()) {
await expect(plot).toHaveClass(/js-series-data-loaded/);
await expect(plot).toHaveClass(/js-series-data-loaded/, { timeout });
}
}
@ -665,41 +629,20 @@ async function getCanvasPixels(page, canvasSelector) {
);
}
/**
* @param {import('@playwright/test').Page} page
* @param {string} myItemsFolderName
* @param {string} url
* @param {string} newName
*/
async function renameObjectFromContextMenu(page, url, newName) {
await openObjectTreeContextMenu(page, url);
await page.click('li:text("Edit Properties")');
const nameInput = page.getByLabel('Title', { exact: true });
await nameInput.fill('');
await nameInput.fill(newName);
await page.click('[aria-label="Save"]');
}
export {
createDomainObjectWithDefaults,
createExampleTelemetryObject,
createNotification,
createPlanFromJSON,
expandEntireTree,
expandTreePaneItemByName,
getCanvasPixels,
getFocusedObjectUuid,
getHashUrlToDomainObject,
navigateToObjectWithFixedTimeBounds,
navigateToObjectWithRealTime,
openObjectTreeContextMenu,
renameObjectFromContextMenu,
setEndOffset,
setFixedIndependentTimeConductorBounds,
setFixedTimeMode,
setIndependentTimeConductorBounds,
setRealTimeMode,
setStartOffset,
setTimeConductorBounds,
setTimeConductorMode,
waitForPlotsToRender
};

View File

@ -30,7 +30,6 @@
import { expect, request, test } from '@playwright/test';
import fs from 'fs';
import path from 'path';
import sinon from 'sinon';
import { fileURLToPath } from 'url';
import { v4 as uuid } from 'uuid';
@ -70,82 +69,6 @@ const extendedTest = test.extend({
*/
coveragePath: [istanbulCLIOutput, { option: true }],
/**
* This allows the test to manipulate the browser clock. This is useful for Visual and Snapshot tests which need
* the Time Indicator Clock to be in a specific state.
*
* Warning: Has many limitations and secondary side effects in Open MCT.
* 1. The tree component does not render.
* 2. page.WaitForNavigation does not trigger.
*
* Usage:
* ```js
* test.use({
* clockOptions: {
* now: MISSION_TIME,
* shouldAdvanceTime: true
* ```
* If clockOptions are provided, will override the default clock with fake timers provided by SinonJS.
*
* Default: `undefined`
*
* @see {@link https://github.com/microsoft/playwright/issues/6347 Github RFE}
* @see {@link https://github.com/sinonjs/fake-timers/#var-clock--faketimersinstallconfig SinonJS FakeTimers Config}
* @type {import('@types/sinonjs__fake-timers').FakeTimerInstallOpts}
*/
clockOptions: [undefined, { option: true }],
overrideClock: [
async ({ context, clockOptions }, use) => {
if (clockOptions !== undefined) {
await context.addInitScript({
path: fileURLToPath(new URL('../node_modules/sinon/pkg/sinon.js', import.meta.url))
});
await context.addInitScript((options) => {
window.__clock = sinon.useFakeTimers(options);
}, clockOptions);
}
await use(context);
},
{
auto: true,
scope: 'test'
}
],
/**
* Exposes a function to manually tick the clock. This is useful when overriding the clock to not
* tick (`shouldAdvanceTime: false`) for visual tests, as events such as re-renders and router params
* updates are clock-driven and must be manually ticked.
*
* Usage:
* ```js
* test.describe('Manual Clock Tick', () => {
* test.use({
* clockOptions: {
* now: MISSION_TIME, // Set to the desired time
* shouldAdvanceTime: false // Clock overridden to no longer tick
* }
* });
* test('Visual - Manual Clock Tick', async ({ page, tick }) => {
* // Tick the clock 2 seconds in the future
* await tick(2000);
* });
* });
* ```
*
* @param {Object} param0
* @param {import('@playwright/test').Page} param0.page
* @param {import('@playwright/test').Use} param0.use
*/
tick: async ({ page }, use) => {
// eslint-disable-next-line func-style
const tick = async (milliseconds) => {
await page.evaluate((_milliseconds) => {
window.__clock.tick(_milliseconds);
}, milliseconds);
};
await use(tick);
},
/**
* Extends the base context class to add codecoverage shim.
* @see {@link https://github.com/mxschmitt/playwright-test-coverage Github Project}
@ -184,20 +107,7 @@ const extendedTest = test.extend({
* Extends the base page class to enable console log error detection.
* @see {@link https://github.com/microsoft/playwright/discussions/11690 Github Discussion}
*/
page: async ({ page, failOnConsoleError, clockOptions }, use) => {
// If overriding the clock, we must also override the Date.now()
// function in the generatorWorker context. This is necessary
// to ensure that example telemetry data is generated for the new clock time.
if (clockOptions?.now !== undefined) {
page.on('worker', (worker) => {
if (worker.url().includes('generatorWorker')) {
worker.evaluate((time) => {
self.Date.now = () => time;
}, clockOptions.now);
}
});
}
page: async ({ page, failOnConsoleError }, use) => {
// Capture any console errors during test execution
const messages = [];
page.on('console', (msg) => messages.push(msg));
@ -207,28 +117,12 @@ const extendedTest = test.extend({
// Assert against console errors during teardown
if (failOnConsoleError) {
messages.forEach((msg) =>
// eslint-disable-next-line playwright/no-standalone-expect
expect
.soft(msg.type(), `Console error detected: ${_consoleMessageToString(msg)}`)
.not.toEqual('error')
);
}
},
/**
* Extends the base browser class to enable CDP connection definition in playwright.config.js. Once
* that RFE is implemented, this function can be removed.
* @see {@link https://github.com/microsoft/playwright/issues/8379 Github RFE}
*/
browser: async ({ playwright, browser }, use, workerInfo) => {
// Use browserless if configured
if (workerInfo.project.name.match(/browserless/)) {
const vBrowser = await playwright.chromium.connectOverCDP({
endpointURL: 'ws://localhost:3003'
});
await use(vBrowser);
} else {
// Use Local Browser for testing.
await use(browser);
}
}
});

View File

@ -59,8 +59,9 @@ export async function navigateToFaultManagementWithoutExample(page) {
/**
* @param {import('@playwright/test').Page} page
*/
export async function navigateToFaultItemInTree(page) {
await page.goto('./', { waitUntil: 'networkidle' });
async function navigateToFaultItemInTree(page) {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.waitForURL('**/#/browse/mine?**');
const faultManagementTreeItem = page
.getByRole('tree', {

View File

@ -63,7 +63,7 @@ async function dragAndDropEmbed(page, notebookObject) {
// Expand the tree to reveal the notebook
await page.getByLabel('Show selected item in tree').click();
// Drag and drop the SWG into the notebook
await page.dragAndDrop(`text=${swg.name}`, NOTEBOOK_DROP_AREA);
await page.getByLabel(`Navigate to ${swg.name}`).dragTo(page.locator(NOTEBOOK_DROP_AREA));
await commitEntry(page);
}
@ -84,6 +84,7 @@ async function startAndAddRestrictedNotebookObject(page) {
path: fileURLToPath(new URL('./addInitRestrictedNotebook.js', import.meta.url))
});
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.waitForURL('**/browse/mine?**');
return createDomainObjectWithDefaults(page, {
type: CUSTOM_NAME,
@ -95,10 +96,9 @@ async function startAndAddRestrictedNotebookObject(page) {
* @param {import('@playwright/test').Page} page
*/
async function lockPage(page) {
const commitButton = page.locator('button:has-text("Commit Entries")');
await commitButton.click();
//Wait until Lock Banner is visible
// Click the Commit Entries button
await page.getByLabel('Commit Entries').click();
// Wait until Lock Banner is visible
await page.locator('text=Lock Page').click();
}

View File

@ -30,9 +30,9 @@ import { expect } from '../pluginFixtures.js';
* start time as the start bound and the current activity's end time as the end bound.
* @param {import('@playwright/test').Page} page the page
* @param {Object} plan The raw plan json to assert against
* @param {string} objectUrl The URL of the object to assert against (plan or gantt chart)
* @param {string} planObjectUrl The URL of the object to assert against (plan or gantt chart)
*/
export async function assertPlanActivities(page, plan, objectUrl) {
export async function assertPlanActivities(page, plan, planObjectUrl) {
const groups = Object.keys(plan);
for (const group of groups) {
for (let i = 0; i < plan[group].length; i++) {
@ -48,13 +48,12 @@ export async function assertPlanActivities(page, plan, objectUrl) {
// Switch to fixed time mode with all plan events within the bounds
await page.goto(
`${objectUrl}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=plan.view`
`${planObjectUrl}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=plan.view`
);
// Assert that the number of activities in the plan view matches the number of
// activities in the plan data within the specified time bounds
const eventCount = await page.locator('.activity-bounds').count();
expect(eventCount).toEqual(
await expect(page.locator('.activity-bounds')).toHaveCount(
Object.values(plan)
.flat()
.filter((event) =>
@ -101,8 +100,8 @@ export async function assertPlanOrderedSwimLanes(page, plan, objectUrl) {
for (let i = 0; i < groups.length; i++) {
// Assert that the order of groups in the plan view matches the order of
// groups in the plan data
const groupName = await planGroups[i].innerText();
expect(groupName).toEqual(groups[i].name);
const groupName = planGroups[i];
await expect(groupName).toHaveText(groups[i].name);
}
}

View File

@ -14,16 +14,14 @@
"test:visual": "percy exec"
},
"devDependencies": {
"@types/sinonjs__fake-timers": "8.1.5",
"@percy/cli": "1.27.4",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.45.2",
"@axe-core/playwright": "4.8.5",
"sinon": "17.0.0"
"@axe-core/playwright": "4.8.5"
},
"author": {
"name": "National Aeronautics and Space Administration",
"url": "https://www.nasa.gov"
},
"license": "Apache-2.0"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -6,15 +6,15 @@
"localStorage": [
{
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"e78ca721-fb5e-409b-bf6d-597c87cb716f\",\"namespace\":\"\"},{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602460,\"created\":1732413600960,\"persisted\":1732413602460},\"e78ca721-fb5e-409b-bf6d-597c87cb716f\":{\"identifier\":{\"key\":\"e78ca721-fb5e-409b-bf6d-597c87cb716f\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with Telemetry Object\\nchrome\",\"modified\":1732413603880,\"location\":\"mine\",\"created\":1732413601740,\"persisted\":1732413603880},\"c6100044-56be-44b3-acca-6b9fddfb3849\":{\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"identifier\":{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"exceedFloat32\":false,\"staleness\":false},\"modified\":1732413602460,\"location\":\"mine\",\"created\":1732413602460,\"persisted\":1732413602460}}"
},
{
"name": "mct-recent-objects",
"value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"},\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"exceedFloat32\":false,\"staleness\":false},\"modified\":1732413602460,\"location\":\"mine\",\"created\":1732413602460,\"persisted\":1732413602460},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"e78ca721-fb5e-409b-bf6d-597c87cb716f\",\"namespace\":\"\"},{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602460,\"created\":1732413600960,\"persisted\":1732413602460},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/c6100044-56be-44b3-acca-6b9fddfb3849\",\"domainObject\":{\"identifier\":{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"},\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"exceedFloat32\":false,\"staleness\":false},\"modified\":1732413602460,\"location\":\"mine\",\"created\":1732413602460,\"persisted\":1732413602460}},{\"objectPath\":[{\"identifier\":{\"key\":\"e78ca721-fb5e-409b-bf6d-597c87cb716f\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with Telemetry Object\\nchrome\",\"modified\":1732413603880,\"location\":\"mine\",\"created\":1732413601740,\"persisted\":1732413603880},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"e78ca721-fb5e-409b-bf6d-597c87cb716f\",\"namespace\":\"\"},{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602460,\"created\":1732413600960,\"persisted\":1732413602460},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/e78ca721-fb5e-409b-bf6d-597c87cb716f\",\"domainObject\":{\"identifier\":{\"key\":\"e78ca721-fb5e-409b-bf6d-597c87cb716f\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with Telemetry Object\\nchrome\",\"modified\":1732413603880,\"location\":\"mine\",\"created\":1732413601740,\"persisted\":1732413603880}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"e78ca721-fb5e-409b-bf6d-597c87cb716f\",\"namespace\":\"\"},{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602460,\"created\":1732413600960,\"persisted\":1732413602460},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine\",\"domainObject\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"e78ca721-fb5e-409b-bf6d-597c87cb716f\",\"namespace\":\"\"},{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602460,\"created\":1732413600960,\"persisted\":1732413602460}}]"
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"d3014736-1182-4b70-8122-6d0c6ef540e1\",\"namespace\":\"\"},{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413605677,\"created\":1732413603298,\"persisted\":1732413605677},\"d3014736-1182-4b70-8122-6d0c6ef540e1\":{\"identifier\":{\"key\":\"d3014736-1182-4b70-8122-6d0c6ef540e1\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate Overlay Plot with Telemetry Object\\nchrome\",\"modified\":1732413607031,\"location\":\"mine\",\"created\":1732413605018,\"persisted\":1732413607031},\"8c53d61f-b514-4535-be87-0fb20eb56576\":{\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"identifier\":{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"exceedFloat32\":false,\"staleness\":false},\"modified\":1732413605677,\"location\":\"mine\",\"created\":1732413605677,\"persisted\":1732413605677}}"
},
{
"name": "mct-tree-expanded",
"value": "[]"
},
{
"name": "mct-recent-objects",
"value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"},\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"exceedFloat32\":false,\"staleness\":false},\"modified\":1732413605677,\"location\":\"mine\",\"created\":1732413605677,\"persisted\":1732413605677},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"d3014736-1182-4b70-8122-6d0c6ef540e1\",\"namespace\":\"\"},{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413605677,\"created\":1732413603298,\"persisted\":1732413605677},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/8c53d61f-b514-4535-be87-0fb20eb56576\",\"domainObject\":{\"identifier\":{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"},\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"exceedFloat32\":false,\"staleness\":false},\"modified\":1732413605677,\"location\":\"mine\",\"created\":1732413605677,\"persisted\":1732413605677}},{\"objectPath\":[{\"identifier\":{\"key\":\"d3014736-1182-4b70-8122-6d0c6ef540e1\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate Overlay Plot with Telemetry Object\\nchrome\",\"modified\":1732413607031,\"location\":\"mine\",\"created\":1732413605018,\"persisted\":1732413607031},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"d3014736-1182-4b70-8122-6d0c6ef540e1\",\"namespace\":\"\"},{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413605677,\"created\":1732413603298,\"persisted\":1732413605677},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/d3014736-1182-4b70-8122-6d0c6ef540e1\",\"domainObject\":{\"identifier\":{\"key\":\"d3014736-1182-4b70-8122-6d0c6ef540e1\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate Overlay Plot with Telemetry Object\\nchrome\",\"modified\":1732413607031,\"location\":\"mine\",\"created\":1732413605018,\"persisted\":1732413607031}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"d3014736-1182-4b70-8122-6d0c6ef540e1\",\"namespace\":\"\"},{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413605677,\"created\":1732413603298,\"persisted\":1732413605677},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine\",\"domainObject\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"d3014736-1182-4b70-8122-6d0c6ef540e1\",\"namespace\":\"\"},{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413605677,\"created\":1732413603298,\"persisted\":1732413605677}}]"
}
]
}

View File

@ -6,7 +6,7 @@
"localStorage": [
{
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"67ca2e0a-b37e-4eda-86a4-ccdbb228bbc0\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413601720,\"created\":1732413600920,\"persisted\":1732413601720},\"67ca2e0a-b37e-4eda-86a4-ccdbb228bbc0\":{\"identifier\":{\"key\":\"67ca2e0a-b37e-4eda-86a4-ccdbb228bbc0\",\"namespace\":\"\"},\"name\":\"Overlay Plot with 5s Delay\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"8f524b49-ad06-47f9-98e0-087b31a2f3e0\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"8f524b49-ad06-47f9-98e0-087b31a2f3e0\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with 5s Delay\\nchrome\",\"modified\":1732413603020,\"location\":\"mine\",\"created\":1732413601720,\"persisted\":1732413603020},\"8f524b49-ad06-47f9-98e0-087b31a2f3e0\":{\"identifier\":{\"key\":\"8f524b49-ad06-47f9-98e0-087b31a2f3e0\",\"namespace\":\"\"},\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":5000,\"infinityValues\":false,\"exceedFloat32\":false,\"staleness\":false},\"modified\":1732413602920,\"location\":\"67ca2e0a-b37e-4eda-86a4-ccdbb228bbc0\",\"created\":1732413602420,\"persisted\":1732413602920}}"
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f34f457e-d7f4-4fc4-ba71-52e19e925646\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413605044,\"created\":1732413603140,\"persisted\":1732413605044},\"f34f457e-d7f4-4fc4-ba71-52e19e925646\":{\"identifier\":{\"key\":\"f34f457e-d7f4-4fc4-ba71-52e19e925646\",\"namespace\":\"\"},\"name\":\"Overlay Plot with 5s Delay\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"c568fa66-62e0-4eee-97eb-cdbc7421e556\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"c568fa66-62e0-4eee-97eb-cdbc7421e556\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate Overlay Plot with 5s Delay\\nchrome\",\"modified\":1732413606208.9,\"location\":\"mine\",\"created\":1732413605044,\"persisted\":1732413606208.9},\"c568fa66-62e0-4eee-97eb-cdbc7421e556\":{\"identifier\":{\"key\":\"c568fa66-62e0-4eee-97eb-cdbc7421e556\",\"namespace\":\"\"},\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":5000,\"infinityValues\":false,\"exceedFloat32\":false,\"staleness\":false},\"modified\":1732413606049,\"location\":\"f34f457e-d7f4-4fc4-ba71-52e19e925646\",\"created\":1732413605554,\"persisted\":1732413606049}}"
},
{
"name": "mct-tree-expanded",

File diff suppressed because one or more lines are too long

View File

@ -19,19 +19,29 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import fs from 'fs';
import {
createDomainObjectWithDefaults,
createExampleTelemetryObject,
createNotification,
createPlanFromJSON,
expandEntireTree,
openObjectTreeContextMenu,
getCanvasPixels,
navigateToObjectWithFixedTimeBounds,
navigateToObjectWithRealTime,
setEndOffset,
setFixedIndependentTimeConductorBounds,
setFixedTimeMode,
setRealTimeMode,
setTimeConductorBounds
setStartOffset,
setTimeConductorBounds,
waitForPlotsToRender
} from '../../appActions.js';
import { assertPlanActivities, setBoundsToSpanAllActivities } from '../../helper/planningUtils.js';
import { expect, test } from '../../pluginFixtures.js';
test.describe('AppActions', () => {
test.describe('AppActions @framework', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
@ -94,6 +104,38 @@ test.describe('AppActions', () => {
expect(folder3.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}/${folder3.uuid}`);
});
});
test('createExampleTelemetryObject', async ({ page }) => {
const gauge = await createDomainObjectWithDefaults(page, {
type: 'Gauge',
name: 'Gauge with no data'
});
const swgWithParent = await createExampleTelemetryObject(page, gauge.uuid);
await page.goto(swgWithParent.url);
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(swgWithParent.name);
await page.getByLabel('More actions').click();
await page.getByLabel('Edit Properties...').click();
// Check Default values of created object
await expect(page.getByLabel('Title', { exact: true })).toHaveValue('VIPER Rover Heading');
await expect(page.getByRole('spinbutton', { name: 'Period' })).toHaveValue('10');
await expect(page.getByRole('spinbutton', { name: 'Amplitude' })).toHaveValue('1');
await expect(page.getByRole('spinbutton', { name: 'Offset' })).toHaveValue('0');
await expect(page.getByRole('spinbutton', { name: 'Data Rate (hz)' })).toHaveValue('1');
await expect(page.getByRole('spinbutton', { name: 'Phase (radians)' })).toHaveValue('0');
await expect(page.getByRole('spinbutton', { name: 'Randomness' })).toHaveValue('0');
await expect(page.getByRole('spinbutton', { name: 'Loading Delay (ms)' })).toHaveValue('0');
await page.getByLabel('Cancel').click();
const swgWithoutParent = await createExampleTelemetryObject(page);
await page.getByLabel('Show selected item in tree').click();
expect(swgWithParent.url).toBe(`${gauge.url}/${swgWithParent.uuid}`);
expect(swgWithoutParent.url).toBe(`./#/browse/mine/${swgWithoutParent.uuid}`);
});
test('createNotification', async ({ page }) => {
await createNotification(page, {
message: 'Test info notification',
@ -117,6 +159,19 @@ test.describe('AppActions', () => {
await expect(page.locator('.c-message-banner')).toHaveClass(/error/);
await page.locator('[aria-label="Dismiss"]').click();
});
test('createPlanFromJSON', async ({ page }) => {
const examplePlanSmall1 = JSON.parse(
fs.readFileSync(
new URL('../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url)
)
);
const plan = await createPlanFromJSON(page, {
name: 'Test Plan',
json: examplePlanSmall1
});
await setBoundsToSpanAllActivities(page, examplePlanSmall1, plan.url);
await assertPlanActivities(page, examplePlanSmall1, plan.url);
});
test('expandEntireTree', async ({ page }) => {
const rootFolder = await createDomainObjectWithDefaults(page, {
type: 'Folder'
@ -153,46 +208,135 @@ test.describe('AppActions', () => {
name: 'Main Tree'
});
const treePaneCollapsedItems = treePane.getByRole('treeitem', { expanded: false });
expect(await treePaneCollapsedItems.count()).toBe(0);
await expect(treePaneCollapsedItems).toHaveCount(0);
await page.goto('./#/browse/mine');
//Click the Create button
await page.getByRole('button', { name: 'Create' }).click();
// Click the object specified by 'type'
await page.click(`li[role='menuitem']:text("Clock")`);
await page.getByRole('menuitem', { name: 'Clock' }).click();
await expandEntireTree(page, 'Create Modal Tree');
const locatorTree = page.getByRole('tree', {
name: 'Create Modal Tree'
});
const locatorTreeCollapsedItems = locatorTree.locator('role=treeitem[expanded=false]');
expect(await locatorTreeCollapsedItems.count()).toBe(0);
await expect(locatorTreeCollapsedItems).toHaveCount(0);
});
test('openObjectTreeContextMenu', async ({ page }) => {
const folder = await createDomainObjectWithDefaults(page, {
type: 'Folder'
test('getCanvasPixels', async ({ page }) => {
let overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
await openObjectTreeContextMenu(page, folder.url);
await expect(page.getByLabel(`${folder.name} Context Menu`)).toBeVisible();
await createExampleTelemetryObject(page, overlayPlot.uuid);
await page.goto(overlayPlot.url);
//Get pixel data from Canvas
const plotPixels = await getCanvasPixels(page, 'canvas');
const plotPixelSize = plotPixels.length;
expect(plotPixelSize).toBeGreaterThan(0);
});
test('setTimeConductorMode', async ({ page }) => {
await setFixedTimeMode(page);
test('navigateToObjectWithFixedTimeBounds', async ({ page }) => {
const exampleTelemetry = await createExampleTelemetryObject(page);
//Navigate without explicit bounds
await navigateToObjectWithFixedTimeBounds(page, exampleTelemetry.url);
await expect(page.getByLabel('Start bounds:')).toBeVisible();
await expect(page.getByLabel('End bounds:')).toBeVisible();
await setRealTimeMode(page);
await expect(page.getByLabel('Start offset')).toBeVisible();
await expect(page.getByLabel('End offset')).toBeVisible();
//Navigate with explicit bounds
await navigateToObjectWithFixedTimeBounds(
page,
exampleTelemetry.url,
1693592063607,
1693593893607
);
await expect(page.getByLabel('Start bounds: 2023-09-01 18:')).toBeVisible();
await expect(page.getByLabel('End bounds: 2023-09-01 18:44:')).toBeVisible();
});
test('setTimeConductorBounds', async ({ page }) => {
// Assume in real-time mode by default
await setFixedTimeMode(page);
await setTimeConductorBounds(page, {
startDate: '2024-01-01',
endDate: '2024-01-02',
startTime: '00:00:00',
endTime: '23:59:59'
test('navigateToObjectWithRealTime', async ({ page }) => {
const exampleTelemetry = await createExampleTelemetryObject(page);
//Navigate without explicit bounds
await navigateToObjectWithRealTime(page, exampleTelemetry.url);
await expect(page.getByLabel('Start offset:')).toBeVisible();
await expect(page.getByLabel('End offset: 00:00:')).toBeVisible();
//Navigate with explicit bounds
await navigateToObjectWithRealTime(page, exampleTelemetry.url, 1693592063607, 1693593893607);
await expect(page.getByLabel('Start offset: 18:14:')).toBeVisible();
await expect(page.getByLabel('End offset: 18:44:')).toBeVisible();
});
test('setTimeConductorMode', async ({ page }) => {
await test.step('setFixedTimeMode', async () => {
await setFixedTimeMode(page);
await expect(page.getByLabel('Start bounds:')).toBeVisible();
await expect(page.getByLabel('End bounds:')).toBeVisible();
});
await expect(page.getByLabel('Start bounds: 2024-01-01 00:00:00')).toBeVisible();
await expect(page.getByLabel('End bounds: 2024-01-02 23:59:59')).toBeVisible();
await test.step('setTimeConductorBounds', async () => {
await setTimeConductorBounds(page, {
startDate: '2024-01-01',
endDate: '2024-01-02',
startTime: '00:00:00',
endTime: '23:59:59'
});
await expect(page.getByLabel('Start bounds: 2024-01-01 00:00:00')).toBeVisible();
await expect(page.getByLabel('End bounds: 2024-01-02 23:59:59')).toBeVisible();
});
await test.step('setRealTimeMode', async () => {
await setRealTimeMode(page);
await expect(page.getByLabel('Start offset')).toBeVisible();
await expect(page.getByLabel('End offset')).toBeVisible();
});
await test.step('setStartOffset', async () => {
await setStartOffset(page, {
startHours: '04',
startMins: '20',
startSecs: '22'
});
await expect(page.getByLabel('Start offset: 04:20:22')).toBeVisible();
});
await test.step('setEndOffset', async () => {
await setEndOffset(page, {
endHours: '04',
endMins: '20',
endSecs: '22'
});
await expect(page.getByLabel('End offset: 04:20:22')).toBeVisible();
});
});
test('setFixedIndependentTimeConductorBounds', async ({ page }) => {
// Create a Display Layout
const displayLayout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout'
});
await createDomainObjectWithDefaults(page, {
type: 'Example Imagery',
parent: displayLayout.uuid
});