From 0413e77d8a4a1514c052b766367b696fe20e2a58 Mon Sep 17 00:00:00 2001 From: John Hill Date: Wed, 7 Aug 2024 14:36:14 -0700 Subject: [PATCH] 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 --- .circleci/config.yml | 8 +- .cspell.json | 17 +- .eslintrc.cjs | 5 +- .github/workflows/e2e-couchdb.yml | 2 +- .github/workflows/e2e-flakefinder.yml | 2 +- README.md | 2 +- TESTING.md | 4 +- codecov.yml | 6 +- e2e/.eslintrc.cjs | 18 +- e2e/README.md | 55 +-- e2e/appActions.js | 177 +++---- e2e/baseFixtures.js | 110 +---- e2e/helper/faultUtils.js | 5 +- e2e/helper/notebookUtils.js | 10 +- e2e/helper/planningUtils.js | 13 +- e2e/package.json | 6 +- .../display_layout_with_child_layouts.json | 10 +- ...isplay_layout_with_child_overlay_plot.json | 4 +- .../flexible_layout_with_child_layouts.json | 10 +- e2e/test-data/overlay_plot_storage.json | 10 +- .../overlay_plot_with_delay_storage.json | 2 +- e2e/test-data/recycled_local_storage.json | 4 +- e2e/tests/framework/appActions.e2e.spec.js | 196 ++++++-- e2e/tests/framework/baseFixtures.e2e.spec.js | 41 +- .../framework/exampleTemplate.e2e.spec.js | 41 +- .../generateLocalStorageData.e2e.spec.js | 36 +- .../framework/pluginFixtures.e2e.spec.js | 2 +- .../functional/clearDataAction.e2e.spec.js | 2 +- e2e/tests/functional/couchdb.e2e.spec.js | 6 +- .../sineWaveLimitProvider.e2e.spec.js | 10 +- e2e/tests/functional/forms.e2e.spec.js | 58 ++- e2e/tests/functional/notification.e2e.spec.js | 10 +- .../functional/planning/plan.e2e.spec.js | 6 +- .../functional/planning/timelist.e2e.spec.js | 22 +- .../timelistControlledClock.e2e.spec.js | 56 +-- .../functional/planning/timestrip.e2e.spec.js | 29 +- .../conditionSet/conditionSet.e2e.spec.js | 72 ++- .../conditionSetOperations.e2e.spec.js | 368 +++++++++++++++ .../displayLayout/displayLayout.e2e.spec.js | 143 +++--- .../faultManagement.e2e.spec.js | 16 +- .../flexibleLayout/flexibleLayout.e2e.spec.js | 8 +- .../plugins/gauge/gauge.e2e.spec.js | 71 ++- .../imagery/exampleImagery.e2e.spec.js | 31 +- .../exportAsJson.e2e.spec.js | 23 +- .../numericData.e2e.spec.js | 6 +- .../functional/plugins/lad/lad.e2e.spec.js | 241 +++++----- .../plugins/lad/ladTable.e2e.spec.js | 2 +- .../plugins/notebook/notebook.e2e.spec.js | 38 +- .../notebookSnapshotImage.e2e.spec.js | 4 +- .../notebook/notebookWithCouchDB.e2e.spec.js | 24 +- .../notebook/restrictedNotebook.e2e.spec.js | 54 ++- .../plugins/plot/autoscale.e2e.spec.js | 15 +- .../plugins/plot/logPlot.e2e.spec.js | 145 +++--- .../plugins/plot/overlayPlot.e2e.spec.js | 58 ++- .../plugins/plot/plotControls.e2e.spec.js | 11 +- .../plugins/plot/plotRendering.e2e.spec.js | 8 +- .../plugins/plot/scatterPlot.e2e.spec.js | 36 +- .../plugins/plot/stackedPlot.e2e.spec.js | 19 +- .../plugins/plot/tagging.e2e.spec.js | 2 +- .../reloadAction/reloadAction.e2e.spec.js | 40 +- .../styling/flexLayoutStyling.e2e.spec.js | 2 +- .../functional/plugins/tabs/tabs.e2e.spec.js | 4 +- .../telemetryTable/telemetryTable.e2e.spec.js | 21 +- .../timeConductor/datepicker.e2e.spec.js | 13 +- .../timeConductor/timeConductor.e2e.spec.js | 26 -- .../plugins/timer/timer.e2e.spec.js | 70 +-- .../functional/recentObjects.e2e.spec.js | 42 +- e2e/tests/functional/renaming.e2e.spec.js | 34 +- e2e/tests/functional/search.e2e.spec.js | 8 +- e2e/tests/functional/staleness.e2e.spec.js | 6 +- e2e/tests/functional/tooltips.e2e.spec.js | 431 +++++++++--------- e2e/tests/functional/tree.e2e.spec.js | 167 ++++--- e2e/tests/mobile/smoke.e2e.spec.js | 12 +- .../contract/imagery.contract.perf.spec.js | 14 +- .../contract/notebook.contract.perf.spec.js | 19 +- .../memory/navigation.memory.perf.spec.js | 4 +- e2e/tests/performance/tabs.perf.spec.js | 2 +- e2e/tests/performance/tagging.perf.spec.js | 2 +- .../components/header.visual.spec.js | 1 + .../components/inspector.visual.spec.js | 15 +- .../components/timeConductor.visual.spec.js | 12 +- ...me-conductor-axis-resized-chrome-linux.png | Bin 33614 -> 31101 bytes .../time-conductor-realtime-chrome-linux.png | Bin 29757 -> 27354 bytes .../components/tree.visual.spec.js | 29 +- .../controlledClock.visual.spec.js | 8 +- .../visual-a11y/displayLayout.visual.spec.js | 8 +- e2e/tests/visual-a11y/notebook.visual.spec.js | 12 +- .../planning-timelist.visual.spec.js | 13 +- .../planning-timestrip.visual.spec.js | 6 +- .../visual-a11y/planning-view.visual.spec.js | 13 +- package-lock.json | 184 ++------ package.json | 11 +- src/api/forms/components/FormProperties.vue | 2 +- .../forms/components/controls/FileInput.vue | 1 + .../tooltips/components/TooltipComponent.vue | 13 +- src/exporters/ImageExporter.js | 2 +- src/plugins/LADTable/components/LadRow.vue | 1 + src/plugins/LADTable/components/LadTable.vue | 20 +- .../displayLayout/DisplayLayoutToolbar.js | 2 +- .../components/DisplayLayout.vue | 3 +- .../components/DisplayLayoutGrid.vue | 2 +- src/plugins/flexibleLayout/toolbarProvider.js | 4 +- src/plugins/formActions/pluginSpec.js | 4 +- .../gauge/components/GaugeComponent.vue | 9 +- .../components/Compass/CompassRose.vue | 5 +- .../styles/SavedStyleSelector.vue | 2 +- .../inspectorViews/styles/SavedStylesView.vue | 4 +- .../notebook/components/NotebookComponent.vue | 7 +- .../notebook/components/NotebookEntry.vue | 1 + src/plugins/persistence/couch/README.md | 33 +- src/plugins/plot/MctPlot.vue | 21 +- .../plot/inspector/PlotOptionsItem.vue | 3 +- .../plot/inspector/forms/SeriesForm.vue | 3 +- src/plugins/plot/legend/PlotLegend.vue | 11 +- src/plugins/remove/RemoveAction.js | 2 +- src/plugins/tabs/components/TabsComponent.vue | 2 +- src/ui/components/ObjectView.vue | 11 +- src/ui/layout/BrowseBar.vue | 2 +- src/ui/layout/MctTree.vue | 1 + src/ui/layout/RecentObjectsList.vue | 2 +- 120 files changed, 1968 insertions(+), 1824 deletions(-) create mode 100644 e2e/tests/functional/plugins/conditionSet/conditionSetOperations.e2e.spec.js diff --git a/.circleci/config.yml b/.circleci/config.yml index 3f7988be22..5a44b836c6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 diff --git a/.cspell.json b/.cspell.json index cc3394b05e..3ba2caaac6 100644 --- a/.cspell.json +++ b/.cspell.json @@ -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" ] -} \ No newline at end of file +} diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 8511e79a31..6607a1c553 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -5,9 +5,8 @@ const config = { browser: true, es2024: true, jasmine: true, - node: true, - worker: true, - serviceworker: true + amd: true, + node: true }, globals: { _: 'readonly', diff --git a/.github/workflows/e2e-couchdb.yml b/.github/workflows/e2e-couchdb.yml index 61ccdf6274..48ba6f352a 100644 --- a/.github/workflows/e2e-couchdb.yml +++ b/.github/workflows/e2e-couchdb.yml @@ -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 diff --git a/.github/workflows/e2e-flakefinder.yml b/.github/workflows/e2e-flakefinder.yml index 4688e5d18a..c7eccf222d 100644 --- a/.github/workflows/e2e-flakefinder.yml +++ b/.github/workflows/e2e-flakefinder.yml @@ -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() diff --git a/README.md b/README.md index 054c73722f..af09dbf132 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/TESTING.md b/TESTING.md index ba8f9cc8a3..24358c6ae3 100644 --- a/TESTING.md +++ b/TESTING.md @@ -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 diff --git a/codecov.yml b/codecov.yml index 07da9fa4c3..aab53fc30c 100644 --- a/codecov.yml +++ b/codecov.yml @@ -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 diff --git a/e2e/.eslintrc.cjs b/e2e/.eslintrc.cjs index 9d378f77c0..a141c4bf2f 100644 --- a/e2e/.eslintrc.cjs +++ b/e2e/.eslintrc.cjs @@ -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' } } ] diff --git a/e2e/README.md b/e2e/README.md index f52a5d4131..b10956ba44 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -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. diff --git a/e2e/appActions.js b/e2e/appActions.js index 35b238843e..f1602bc3cd 100644 --- a/e2e/appActions.js +++ b/e2e/appActions.js @@ -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} [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} 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} 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 }; diff --git a/e2e/baseFixtures.js b/e2e/baseFixtures.js index 5cfb2678e4..6d0662a875 100644 --- a/e2e/baseFixtures.js +++ b/e2e/baseFixtures.js @@ -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); - } } }); diff --git a/e2e/helper/faultUtils.js b/e2e/helper/faultUtils.js index 54c30e23c5..05c7df98db 100644 --- a/e2e/helper/faultUtils.js +++ b/e2e/helper/faultUtils.js @@ -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', { diff --git a/e2e/helper/notebookUtils.js b/e2e/helper/notebookUtils.js index acf13b6ad2..07682c4ae5 100644 --- a/e2e/helper/notebookUtils.js +++ b/e2e/helper/notebookUtils.js @@ -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(); } diff --git a/e2e/helper/planningUtils.js b/e2e/helper/planningUtils.js index 9150dd7565..43988532f3 100644 --- a/e2e/helper/planningUtils.js +++ b/e2e/helper/planningUtils.js @@ -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); } } diff --git a/e2e/package.json b/e2e/package.json index 3b1343d0e5..af4cbafafc 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -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" -} \ No newline at end of file +} diff --git a/e2e/test-data/display_layout_with_child_layouts.json b/e2e/test-data/display_layout_with_child_layouts.json index 899a0e31b3..f19c38d484 100644 --- a/e2e/test-data/display_layout_with_child_layouts.json +++ b/e2e/test-data/display_layout_with_child_layouts.json @@ -6,15 +6,15 @@ "localStorage": [ { "name": "mct", - "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"80d08d10-2f2b-4ebb-847c-2385016718e7\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413601880,\"created\":1732413600900,\"persisted\":1732413601880},\"80d08d10-2f2b-4ebb-847c-2385016718e7\":{\"identifier\":{\"key\":\"80d08d10-2f2b-4ebb-847c-2385016718e7\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"cdff8398-81e5-4a37-8c2d-8ec0a0ed7d16\",\"namespace\":\"\"},{\"key\":\"62ac54e8-e424-41b3-889c-83f36c13676d\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":32,\"height\":18,\"x\":1,\"y\":30,\"identifier\":{\"key\":\"cdff8398-81e5-4a37-8c2d-8ec0a0ed7d16\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"a71236dd-9178-4b96-88d9-b93ceee12f32\"},{\"width\":32,\"height\":18,\"x\":30,\"y\":1,\"identifier\":{\"key\":\"62ac54e8-e424-41b3-889c-83f36c13676d\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"b512cc87-9291-4b55-b9a5-4e5641bc8b83\"}],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413605020,\"location\":\"mine\",\"created\":1732413601880,\"persisted\":1732413605020},\"cdff8398-81e5-4a37-8c2d-8ec0a0ed7d16\":{\"name\":\"Child Layout 1\",\"type\":\"layout\",\"identifier\":{\"key\":\"cdff8398-81e5-4a37-8c2d-8ec0a0ed7d16\",\"namespace\":\"\"},\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413602980,\"location\":\"80d08d10-2f2b-4ebb-847c-2385016718e7\",\"created\":1732413602980,\"persisted\":1732413602980},\"62ac54e8-e424-41b3-889c-83f36c13676d\":{\"name\":\"Child Layout 2\",\"type\":\"layout\",\"identifier\":{\"key\":\"62ac54e8-e424-41b3-889c-83f36c13676d\",\"namespace\":\"\"},\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413604120,\"location\":\"80d08d10-2f2b-4ebb-847c-2385016718e7\",\"created\":1732413604120,\"persisted\":1732413604120}}" - }, - { - "name": "mct-recent-objects", - "value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"80d08d10-2f2b-4ebb-847c-2385016718e7\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"cdff8398-81e5-4a37-8c2d-8ec0a0ed7d16\",\"namespace\":\"\"},{\"key\":\"62ac54e8-e424-41b3-889c-83f36c13676d\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":32,\"height\":18,\"x\":1,\"y\":1,\"identifier\":{\"key\":\"cdff8398-81e5-4a37-8c2d-8ec0a0ed7d16\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"a71236dd-9178-4b96-88d9-b93ceee12f32\"}],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413604120,\"location\":\"mine\",\"created\":1732413601880,\"persisted\":1732413604120},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"80d08d10-2f2b-4ebb-847c-2385016718e7\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413601880,\"created\":1732413600900,\"persisted\":1732413601880},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/80d08d10-2f2b-4ebb-847c-2385016718e7\",\"domainObject\":{\"identifier\":{\"key\":\"80d08d10-2f2b-4ebb-847c-2385016718e7\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"cdff8398-81e5-4a37-8c2d-8ec0a0ed7d16\",\"namespace\":\"\"},{\"key\":\"62ac54e8-e424-41b3-889c-83f36c13676d\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":32,\"height\":18,\"x\":1,\"y\":1,\"identifier\":{\"key\":\"cdff8398-81e5-4a37-8c2d-8ec0a0ed7d16\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"a71236dd-9178-4b96-88d9-b93ceee12f32\"}],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413604120,\"location\":\"mine\",\"created\":1732413601880,\"persisted\":1732413604120}},{\"objectPath\":[{\"identifier\":{\"key\":\"62ac54e8-e424-41b3-889c-83f36c13676d\",\"namespace\":\"\"},\"name\":\"Child Layout 2\",\"type\":\"layout\",\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413604120,\"location\":\"80d08d10-2f2b-4ebb-847c-2385016718e7\",\"created\":1732413604120,\"persisted\":1732413604120},{\"identifier\":{\"key\":\"80d08d10-2f2b-4ebb-847c-2385016718e7\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"cdff8398-81e5-4a37-8c2d-8ec0a0ed7d16\",\"namespace\":\"\"},{\"key\":\"62ac54e8-e424-41b3-889c-83f36c13676d\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":32,\"height\":18,\"x\":1,\"y\":1,\"identifier\":{\"key\":\"cdff8398-81e5-4a37-8c2d-8ec0a0ed7d16\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"a71236dd-9178-4b96-88d9-b93ceee12f32\"}],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413604120,\"location\":\"mine\",\"created\":1732413601880,\"persisted\":1732413604120},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"80d08d10-2f2b-4ebb-847c-2385016718e7\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413601880,\"created\":1732413600900,\"persisted\":1732413601880},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/80d08d10-2f2b-4ebb-847c-2385016718e7/62ac54e8-e424-41b3-889c-83f36c13676d\",\"domainObject\":{\"identifier\":{\"key\":\"62ac54e8-e424-41b3-889c-83f36c13676d\",\"namespace\":\"\"},\"name\":\"Child Layout 2\",\"type\":\"layout\",\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413604120,\"location\":\"80d08d10-2f2b-4ebb-847c-2385016718e7\",\"created\":1732413604120,\"persisted\":1732413604120}},{\"objectPath\":[{\"identifier\":{\"key\":\"cdff8398-81e5-4a37-8c2d-8ec0a0ed7d16\",\"namespace\":\"\"},\"name\":\"Child Layout 1\",\"type\":\"layout\",\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413602980,\"location\":\"80d08d10-2f2b-4ebb-847c-2385016718e7\",\"created\":1732413602980,\"persisted\":1732413602980},{\"identifier\":{\"key\":\"80d08d10-2f2b-4ebb-847c-2385016718e7\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"cdff8398-81e5-4a37-8c2d-8ec0a0ed7d16\",\"namespace\":\"\"},{\"key\":\"62ac54e8-e424-41b3-889c-83f36c13676d\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":32,\"height\":18,\"x\":1,\"y\":1,\"identifier\":{\"key\":\"cdff8398-81e5-4a37-8c2d-8ec0a0ed7d16\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"a71236dd-9178-4b96-88d9-b93ceee12f32\"}],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413604120,\"location\":\"mine\",\"created\":1732413601880,\"persisted\":1732413604120},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"80d08d10-2f2b-4ebb-847c-2385016718e7\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413601880,\"created\":1732413600900,\"persisted\":1732413601880},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/80d08d10-2f2b-4ebb-847c-2385016718e7/cdff8398-81e5-4a37-8c2d-8ec0a0ed7d16\",\"domainObject\":{\"identifier\":{\"key\":\"cdff8398-81e5-4a37-8c2d-8ec0a0ed7d16\",\"namespace\":\"\"},\"name\":\"Child Layout 1\",\"type\":\"layout\",\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413602980,\"location\":\"80d08d10-2f2b-4ebb-847c-2385016718e7\",\"created\":1732413602980,\"persisted\":1732413602980}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"80d08d10-2f2b-4ebb-847c-2385016718e7\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413601880,\"created\":1732413600900,\"persisted\":1732413601880},{\"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\":\"80d08d10-2f2b-4ebb-847c-2385016718e7\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413601880,\"created\":1732413600900,\"persisted\":1732413601880}}]" + "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f11f93ab-8918-4732-b20c-617b7b2e16ad\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413604666,\"created\":1732413603039,\"persisted\":1732413604666},\"f11f93ab-8918-4732-b20c-617b7b2e16ad\":{\"identifier\":{\"key\":\"f11f93ab-8918-4732-b20c-617b7b2e16ad\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"034e9aab-8b24-493b-876f-80ed474b61fb\",\"namespace\":\"\"},{\"key\":\"ea2f53e0-3376-4ba7-8df7-349339e41d64\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":32,\"height\":18,\"x\":1,\"y\":30,\"identifier\":{\"key\":\"034e9aab-8b24-493b-876f-80ed474b61fb\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"94b55aa2-8f61-431a-9453-4bbfda9119fb\"},{\"width\":32,\"height\":18,\"x\":30,\"y\":1,\"identifier\":{\"key\":\"ea2f53e0-3376-4ba7-8df7-349339e41d64\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"97d028f7-ffa6-494b-96d6-d8526e399766\"}],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413607743,\"location\":\"mine\",\"created\":1732413604666,\"persisted\":1732413607743},\"034e9aab-8b24-493b-876f-80ed474b61fb\":{\"name\":\"Child Layout 1\",\"type\":\"layout\",\"identifier\":{\"key\":\"034e9aab-8b24-493b-876f-80ed474b61fb\",\"namespace\":\"\"},\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413605564,\"location\":\"f11f93ab-8918-4732-b20c-617b7b2e16ad\",\"created\":1732413605564,\"persisted\":1732413605564},\"ea2f53e0-3376-4ba7-8df7-349339e41d64\":{\"name\":\"Child Layout 2\",\"type\":\"layout\",\"identifier\":{\"key\":\"ea2f53e0-3376-4ba7-8df7-349339e41d64\",\"namespace\":\"\"},\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413606944,\"location\":\"f11f93ab-8918-4732-b20c-617b7b2e16ad\",\"created\":1732413606944,\"persisted\":1732413606944}}" }, { "name": "mct-tree-expanded", "value": "[]" + }, + { + "name": "mct-recent-objects", + "value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"f11f93ab-8918-4732-b20c-617b7b2e16ad\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"034e9aab-8b24-493b-876f-80ed474b61fb\",\"namespace\":\"\"},{\"key\":\"ea2f53e0-3376-4ba7-8df7-349339e41d64\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":32,\"height\":18,\"x\":1,\"y\":1,\"identifier\":{\"key\":\"034e9aab-8b24-493b-876f-80ed474b61fb\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"94b55aa2-8f61-431a-9453-4bbfda9119fb\"}],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413606944,\"location\":\"mine\",\"created\":1732413604666,\"persisted\":1732413606944},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f11f93ab-8918-4732-b20c-617b7b2e16ad\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413604666,\"created\":1732413603039,\"persisted\":1732413604666},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/f11f93ab-8918-4732-b20c-617b7b2e16ad\",\"domainObject\":{\"identifier\":{\"key\":\"f11f93ab-8918-4732-b20c-617b7b2e16ad\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"034e9aab-8b24-493b-876f-80ed474b61fb\",\"namespace\":\"\"},{\"key\":\"ea2f53e0-3376-4ba7-8df7-349339e41d64\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":32,\"height\":18,\"x\":1,\"y\":1,\"identifier\":{\"key\":\"034e9aab-8b24-493b-876f-80ed474b61fb\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"94b55aa2-8f61-431a-9453-4bbfda9119fb\"}],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413606944,\"location\":\"mine\",\"created\":1732413604666,\"persisted\":1732413606944}},{\"objectPath\":[{\"identifier\":{\"key\":\"ea2f53e0-3376-4ba7-8df7-349339e41d64\",\"namespace\":\"\"},\"name\":\"Child Layout 2\",\"type\":\"layout\",\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413606944,\"location\":\"f11f93ab-8918-4732-b20c-617b7b2e16ad\",\"created\":1732413606944,\"persisted\":1732413606944},{\"identifier\":{\"key\":\"f11f93ab-8918-4732-b20c-617b7b2e16ad\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"034e9aab-8b24-493b-876f-80ed474b61fb\",\"namespace\":\"\"},{\"key\":\"ea2f53e0-3376-4ba7-8df7-349339e41d64\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":32,\"height\":18,\"x\":1,\"y\":1,\"identifier\":{\"key\":\"034e9aab-8b24-493b-876f-80ed474b61fb\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"94b55aa2-8f61-431a-9453-4bbfda9119fb\"}],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413606944,\"location\":\"mine\",\"created\":1732413604666,\"persisted\":1732413606944},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f11f93ab-8918-4732-b20c-617b7b2e16ad\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413604666,\"created\":1732413603039,\"persisted\":1732413604666},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/f11f93ab-8918-4732-b20c-617b7b2e16ad/ea2f53e0-3376-4ba7-8df7-349339e41d64\",\"domainObject\":{\"identifier\":{\"key\":\"ea2f53e0-3376-4ba7-8df7-349339e41d64\",\"namespace\":\"\"},\"name\":\"Child Layout 2\",\"type\":\"layout\",\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413606944,\"location\":\"f11f93ab-8918-4732-b20c-617b7b2e16ad\",\"created\":1732413606944,\"persisted\":1732413606944}},{\"objectPath\":[{\"identifier\":{\"key\":\"034e9aab-8b24-493b-876f-80ed474b61fb\",\"namespace\":\"\"},\"name\":\"Child Layout 1\",\"type\":\"layout\",\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413605564,\"location\":\"f11f93ab-8918-4732-b20c-617b7b2e16ad\",\"created\":1732413605564,\"persisted\":1732413605564},{\"identifier\":{\"key\":\"f11f93ab-8918-4732-b20c-617b7b2e16ad\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"034e9aab-8b24-493b-876f-80ed474b61fb\",\"namespace\":\"\"},{\"key\":\"ea2f53e0-3376-4ba7-8df7-349339e41d64\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":32,\"height\":18,\"x\":1,\"y\":1,\"identifier\":{\"key\":\"034e9aab-8b24-493b-876f-80ed474b61fb\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"94b55aa2-8f61-431a-9453-4bbfda9119fb\"}],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413606944,\"location\":\"mine\",\"created\":1732413604666,\"persisted\":1732413606944},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f11f93ab-8918-4732-b20c-617b7b2e16ad\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413604666,\"created\":1732413603039,\"persisted\":1732413604666},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/f11f93ab-8918-4732-b20c-617b7b2e16ad/034e9aab-8b24-493b-876f-80ed474b61fb\",\"domainObject\":{\"identifier\":{\"key\":\"034e9aab-8b24-493b-876f-80ed474b61fb\",\"namespace\":\"\"},\"name\":\"Child Layout 1\",\"type\":\"layout\",\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413605564,\"location\":\"f11f93ab-8918-4732-b20c-617b7b2e16ad\",\"created\":1732413605564,\"persisted\":1732413605564}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f11f93ab-8918-4732-b20c-617b7b2e16ad\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413604666,\"created\":1732413603039,\"persisted\":1732413604666},{\"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\":\"f11f93ab-8918-4732-b20c-617b7b2e16ad\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413604666,\"created\":1732413603039,\"persisted\":1732413604666}}]" } ] } diff --git a/e2e/test-data/display_layout_with_child_overlay_plot.json b/e2e/test-data/display_layout_with_child_overlay_plot.json index b6854feba2..5bebeeeb84 100644 --- a/e2e/test-data/display_layout_with_child_overlay_plot.json +++ b/e2e/test-data/display_layout_with_child_overlay_plot.json @@ -6,7 +6,7 @@ "localStorage": [ { "name": "mct", - "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"29836e66-111a-45f8-81ed-f662661be9f9\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602140,\"created\":1732413600860,\"persisted\":1732413602140},\"29836e66-111a-45f8-81ed-f662661be9f9\":{\"identifier\":{\"key\":\"29836e66-111a-45f8-81ed-f662661be9f9\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"55cd0300-6e57-4992-b670-0c2880c0e6b2\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":32,\"height\":18,\"x\":1,\"y\":1,\"identifier\":{\"key\":\"55cd0300-6e57-4992-b670-0c2880c0e6b2\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"022720f7-a6b5-40c3-b051-75f5d10a9042\"}],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 1 child overlay plot\\nchrome\",\"modified\":1732413604780,\"location\":\"mine\",\"created\":1732413602140,\"persisted\":1732413604780},\"55cd0300-6e57-4992-b670-0c2880c0e6b2\":{\"identifier\":{\"key\":\"55cd0300-6e57-4992-b670-0c2880c0e6b2\",\"namespace\":\"\"},\"name\":\"Child Overlay Plot 1\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"ec13f652-4636-4763-8e88-898144cbc6f2\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"ec13f652-4636-4763-8e88-898144cbc6f2\",\"namespace\":\"\"}}],\"useIndependentTime\":true,\"timeOptions\":{\"clockOffsets\":{\"start\":-1800000,\"end\":30000},\"fixedOffsets\":{\"start\":1731438671000,\"end\":1731442271000},\"clock\":\"local\",\"mode\":\"fixed\"}},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 1 child overlay plot\\nchrome\",\"modified\":1732413605500,\"location\":\"29836e66-111a-45f8-81ed-f662661be9f9\",\"created\":1732413603280,\"persisted\":1732413605500},\"ec13f652-4636-4763-8e88-898144cbc6f2\":{\"name\":\"Child SWG 1\",\"type\":\"generator\",\"identifier\":{\"key\":\"ec13f652-4636-4763-8e88-898144cbc6f2\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"exceedFloat32\":false,\"staleness\":false},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 1 child overlay plot\\nchrome\",\"modified\":1732413604440,\"location\":\"55cd0300-6e57-4992-b670-0c2880c0e6b2\",\"created\":1732413604440,\"persisted\":1732413604440}}" + "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"712d07f1-3585-465a-a6db-3c40a9edcde7\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413604993,\"created\":1732413603983,\"persisted\":1732413604993},\"712d07f1-3585-465a-a6db-3c40a9edcde7\":{\"identifier\":{\"key\":\"712d07f1-3585-465a-a6db-3c40a9edcde7\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":32,\"height\":18,\"x\":1,\"y\":1,\"identifier\":{\"key\":\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"7aea18ca-1537-4f4f-98e4-a57b4cd8f9a7\"}],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate display layout with 1 child overlay plot\\nchrome\",\"modified\":1732413608088.7,\"location\":\"mine\",\"created\":1732413604993,\"persisted\":1732413608088.7},\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\":{\"identifier\":{\"key\":\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\",\"namespace\":\"\"},\"name\":\"Child Overlay Plot 1\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"fa050882-fcec-49c0-aa78-236a595accaf\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"fa050882-fcec-49c0-aa78-236a595accaf\",\"namespace\":\"\"}}],\"useIndependentTime\":true,\"timeOptions\":{\"clockOffsets\":{\"start\":-1800000,\"end\":30000},\"fixedOffsets\":{\"start\":1731438671000,\"end\":1731442271000},\"clock\":\"local\",\"mode\":\"fixed\"}},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate display layout with 1 child overlay plot\\nchrome\",\"modified\":1732413608703.7,\"location\":\"712d07f1-3585-465a-a6db-3c40a9edcde7\",\"created\":1732413606392.7,\"persisted\":1732413608703.7},\"fa050882-fcec-49c0-aa78-236a595accaf\":{\"name\":\"Child SWG 1\",\"type\":\"generator\",\"identifier\":{\"key\":\"fa050882-fcec-49c0-aa78-236a595accaf\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"exceedFloat32\":false,\"staleness\":false},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate display layout with 1 child overlay plot\\nchrome\",\"modified\":1732413607788,\"location\":\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\",\"created\":1732413607788,\"persisted\":1732413607788}}" }, { "name": "mct-tree-expanded", @@ -18,7 +18,7 @@ }, { "name": "mct-recent-objects", - "value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"29836e66-111a-45f8-81ed-f662661be9f9\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"55cd0300-6e57-4992-b670-0c2880c0e6b2\",\"namespace\":\"\"}],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 1 child overlay plot\\nchrome\",\"modified\":1732413603280,\"location\":\"mine\",\"created\":1732413602140,\"persisted\":1732413603280},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"29836e66-111a-45f8-81ed-f662661be9f9\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602140,\"created\":1732413600860,\"persisted\":1732413602140},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/29836e66-111a-45f8-81ed-f662661be9f9\",\"domainObject\":{\"identifier\":{\"key\":\"29836e66-111a-45f8-81ed-f662661be9f9\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"55cd0300-6e57-4992-b670-0c2880c0e6b2\",\"namespace\":\"\"}],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 1 child overlay plot\\nchrome\",\"modified\":1732413603280,\"location\":\"mine\",\"created\":1732413602140,\"persisted\":1732413603280}},{\"objectPath\":[{\"identifier\":{\"key\":\"ec13f652-4636-4763-8e88-898144cbc6f2\",\"namespace\":\"\"},\"name\":\"Child SWG 1\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"exceedFloat32\":false,\"staleness\":false},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 1 child overlay plot\\nchrome\",\"modified\":1732413604440,\"location\":\"55cd0300-6e57-4992-b670-0c2880c0e6b2\",\"created\":1732413604440,\"persisted\":1732413604440},{\"identifier\":{\"key\":\"55cd0300-6e57-4992-b670-0c2880c0e6b2\",\"namespace\":\"\"},\"name\":\"Child Overlay Plot 1\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"ec13f652-4636-4763-8e88-898144cbc6f2\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 1 child overlay plot\\nchrome\",\"modified\":1732413604440,\"location\":\"29836e66-111a-45f8-81ed-f662661be9f9\",\"created\":1732413603280,\"persisted\":1732413604440},{\"identifier\":{\"key\":\"29836e66-111a-45f8-81ed-f662661be9f9\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"55cd0300-6e57-4992-b670-0c2880c0e6b2\",\"namespace\":\"\"}],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 1 child overlay plot\\nchrome\",\"modified\":1732413603280,\"location\":\"mine\",\"created\":1732413602140,\"persisted\":1732413603280},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"29836e66-111a-45f8-81ed-f662661be9f9\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602140,\"created\":1732413600860,\"persisted\":1732413602140},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/29836e66-111a-45f8-81ed-f662661be9f9/55cd0300-6e57-4992-b670-0c2880c0e6b2/ec13f652-4636-4763-8e88-898144cbc6f2\",\"domainObject\":{\"identifier\":{\"key\":\"ec13f652-4636-4763-8e88-898144cbc6f2\",\"namespace\":\"\"},\"name\":\"Child SWG 1\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"exceedFloat32\":false,\"staleness\":false},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 1 child overlay plot\\nchrome\",\"modified\":1732413604440,\"location\":\"55cd0300-6e57-4992-b670-0c2880c0e6b2\",\"created\":1732413604440,\"persisted\":1732413604440}},{\"objectPath\":[{\"identifier\":{\"key\":\"55cd0300-6e57-4992-b670-0c2880c0e6b2\",\"namespace\":\"\"},\"name\":\"Child Overlay Plot 1\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"ec13f652-4636-4763-8e88-898144cbc6f2\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 1 child overlay plot\\nchrome\",\"modified\":1732413604440,\"location\":\"29836e66-111a-45f8-81ed-f662661be9f9\",\"created\":1732413603280,\"persisted\":1732413604440},{\"identifier\":{\"key\":\"29836e66-111a-45f8-81ed-f662661be9f9\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"55cd0300-6e57-4992-b670-0c2880c0e6b2\",\"namespace\":\"\"}],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 1 child overlay plot\\nchrome\",\"modified\":1732413603280,\"location\":\"mine\",\"created\":1732413602140,\"persisted\":1732413603280},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"29836e66-111a-45f8-81ed-f662661be9f9\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602140,\"created\":1732413600860,\"persisted\":1732413602140},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/29836e66-111a-45f8-81ed-f662661be9f9/55cd0300-6e57-4992-b670-0c2880c0e6b2\",\"domainObject\":{\"identifier\":{\"key\":\"55cd0300-6e57-4992-b670-0c2880c0e6b2\",\"namespace\":\"\"},\"name\":\"Child Overlay Plot 1\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"ec13f652-4636-4763-8e88-898144cbc6f2\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 1 child overlay plot\\nchrome\",\"modified\":1732413604440,\"location\":\"29836e66-111a-45f8-81ed-f662661be9f9\",\"created\":1732413603280,\"persisted\":1732413604440}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"29836e66-111a-45f8-81ed-f662661be9f9\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602140,\"created\":1732413600860,\"persisted\":1732413602140},{\"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\":\"29836e66-111a-45f8-81ed-f662661be9f9\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602140,\"created\":1732413600860,\"persisted\":1732413602140}}]" + "value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"712d07f1-3585-465a-a6db-3c40a9edcde7\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\",\"namespace\":\"\"}],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate display layout with 1 child overlay plot\\nchrome\",\"modified\":1732413606392.7,\"location\":\"mine\",\"created\":1732413604993,\"persisted\":1732413606392.7},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"712d07f1-3585-465a-a6db-3c40a9edcde7\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413604993,\"created\":1732413603983,\"persisted\":1732413604993},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/712d07f1-3585-465a-a6db-3c40a9edcde7\",\"domainObject\":{\"identifier\":{\"key\":\"712d07f1-3585-465a-a6db-3c40a9edcde7\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\",\"namespace\":\"\"}],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate display layout with 1 child overlay plot\\nchrome\",\"modified\":1732413606392.7,\"location\":\"mine\",\"created\":1732413604993,\"persisted\":1732413606392.7}},{\"objectPath\":[{\"identifier\":{\"key\":\"fa050882-fcec-49c0-aa78-236a595accaf\",\"namespace\":\"\"},\"name\":\"Child SWG 1\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"exceedFloat32\":false,\"staleness\":false},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate display layout with 1 child overlay plot\\nchrome\",\"modified\":1732413607788,\"location\":\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\",\"created\":1732413607788,\"persisted\":1732413607788},{\"identifier\":{\"key\":\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\",\"namespace\":\"\"},\"name\":\"Child Overlay Plot 1\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"fa050882-fcec-49c0-aa78-236a595accaf\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate display layout with 1 child overlay plot\\nchrome\",\"modified\":1732413607788,\"location\":\"712d07f1-3585-465a-a6db-3c40a9edcde7\",\"created\":1732413606392.7,\"persisted\":1732413607788},{\"identifier\":{\"key\":\"712d07f1-3585-465a-a6db-3c40a9edcde7\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\",\"namespace\":\"\"}],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate display layout with 1 child overlay plot\\nchrome\",\"modified\":1732413606392.7,\"location\":\"mine\",\"created\":1732413604993,\"persisted\":1732413606392.7},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"712d07f1-3585-465a-a6db-3c40a9edcde7\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413604993,\"created\":1732413603983,\"persisted\":1732413604993},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/712d07f1-3585-465a-a6db-3c40a9edcde7/6731f693-b09e-46de-a0cb-9331f1fb2e6d/fa050882-fcec-49c0-aa78-236a595accaf\",\"domainObject\":{\"identifier\":{\"key\":\"fa050882-fcec-49c0-aa78-236a595accaf\",\"namespace\":\"\"},\"name\":\"Child SWG 1\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"exceedFloat32\":false,\"staleness\":false},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate display layout with 1 child overlay plot\\nchrome\",\"modified\":1732413607788,\"location\":\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\",\"created\":1732413607788,\"persisted\":1732413607788}},{\"objectPath\":[{\"identifier\":{\"key\":\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\",\"namespace\":\"\"},\"name\":\"Child Overlay Plot 1\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"fa050882-fcec-49c0-aa78-236a595accaf\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate display layout with 1 child overlay plot\\nchrome\",\"modified\":1732413607788,\"location\":\"712d07f1-3585-465a-a6db-3c40a9edcde7\",\"created\":1732413606392.7,\"persisted\":1732413607788},{\"identifier\":{\"key\":\"712d07f1-3585-465a-a6db-3c40a9edcde7\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\",\"namespace\":\"\"}],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate display layout with 1 child overlay plot\\nchrome\",\"modified\":1732413606392.7,\"location\":\"mine\",\"created\":1732413604993,\"persisted\":1732413606392.7},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"712d07f1-3585-465a-a6db-3c40a9edcde7\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413604993,\"created\":1732413603983,\"persisted\":1732413604993},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/712d07f1-3585-465a-a6db-3c40a9edcde7/6731f693-b09e-46de-a0cb-9331f1fb2e6d\",\"domainObject\":{\"identifier\":{\"key\":\"6731f693-b09e-46de-a0cb-9331f1fb2e6d\",\"namespace\":\"\"},\"name\":\"Child Overlay Plot 1\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"fa050882-fcec-49c0-aa78-236a595accaf\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate display layout with 1 child overlay plot\\nchrome\",\"modified\":1732413607788,\"location\":\"712d07f1-3585-465a-a6db-3c40a9edcde7\",\"created\":1732413606392.7,\"persisted\":1732413607788}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"712d07f1-3585-465a-a6db-3c40a9edcde7\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413604993,\"created\":1732413603983,\"persisted\":1732413604993},{\"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\":\"712d07f1-3585-465a-a6db-3c40a9edcde7\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413604993,\"created\":1732413603983,\"persisted\":1732413604993}}]" } ] } diff --git a/e2e/test-data/flexible_layout_with_child_layouts.json b/e2e/test-data/flexible_layout_with_child_layouts.json index 696f2f7f37..0f3a18a8b8 100644 --- a/e2e/test-data/flexible_layout_with_child_layouts.json +++ b/e2e/test-data/flexible_layout_with_child_layouts.json @@ -6,15 +6,15 @@ "localStorage": [ { "name": "mct", - "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"a9ea2ec4-c456-4b73-8ed5-c4da07d40732\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413601660,\"created\":1732413600900,\"persisted\":1732413601660},\"a9ea2ec4-c456-4b73-8ed5-c4da07d40732\":{\"identifier\":{\"key\":\"a9ea2ec4-c456-4b73-8ed5-c4da07d40732\",\"namespace\":\"\"},\"name\":\"Parent Flexible Layout\",\"type\":\"flexible-layout\",\"configuration\":{\"containers\":[{\"id\":\"f7154ceb-c37b-40ee-af98-6b9cb8a3ec62\",\"frames\":[{\"id\":\"885b1828-fbf1-4f59-8c57-c5db7ad625b1\",\"domainObjectIdentifier\":{\"key\":\"bd6c95a1-31b2-4eaf-b962-f1b3da3de21a\",\"namespace\":\"\"},\"size\":100,\"noFrame\":false}],\"size\":50},{\"id\":\"f73ad0b3-c719-45e8-8056-a93eb7fea00f\",\"frames\":[],\"size\":50}],\"rowsLayout\":false},\"composition\":[{\"key\":\"bd6c95a1-31b2-4eaf-b962-f1b3da3de21a\",\"namespace\":\"\"},{\"key\":\"a0294124-ac2f-442b-9e6e-2086c6b0c2bd\",\"namespace\":\"\"}],\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate flexible layout with 2 child display layouts\\nchrome\",\"modified\":1732413603920,\"location\":\"mine\",\"created\":1732413601660,\"persisted\":1732413603920},\"bd6c95a1-31b2-4eaf-b962-f1b3da3de21a\":{\"name\":\"Child Layout 1\",\"type\":\"layout\",\"identifier\":{\"key\":\"bd6c95a1-31b2-4eaf-b962-f1b3da3de21a\",\"namespace\":\"\"},\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate flexible layout with 2 child display layouts\\nchrome\",\"modified\":1732413602800,\"location\":\"a9ea2ec4-c456-4b73-8ed5-c4da07d40732\",\"created\":1732413602800,\"persisted\":1732413602800},\"a0294124-ac2f-442b-9e6e-2086c6b0c2bd\":{\"name\":\"Child Layout 2\",\"type\":\"layout\",\"identifier\":{\"key\":\"a0294124-ac2f-442b-9e6e-2086c6b0c2bd\",\"namespace\":\"\"},\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate flexible layout with 2 child display layouts\\nchrome\",\"modified\":1732413603920,\"location\":\"a9ea2ec4-c456-4b73-8ed5-c4da07d40732\",\"created\":1732413603920,\"persisted\":1732413603920}}" - }, - { - "name": "mct-recent-objects", - "value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"a9ea2ec4-c456-4b73-8ed5-c4da07d40732\",\"namespace\":\"\"},\"name\":\"Parent Flexible Layout\",\"type\":\"flexible-layout\",\"configuration\":{\"containers\":[{\"id\":\"f7154ceb-c37b-40ee-af98-6b9cb8a3ec62\",\"frames\":[{\"id\":\"885b1828-fbf1-4f59-8c57-c5db7ad625b1\",\"domainObjectIdentifier\":{\"key\":\"bd6c95a1-31b2-4eaf-b962-f1b3da3de21a\",\"namespace\":\"\"},\"size\":100,\"noFrame\":false}],\"size\":50},{\"id\":\"f73ad0b3-c719-45e8-8056-a93eb7fea00f\",\"frames\":[],\"size\":50}],\"rowsLayout\":false},\"composition\":[{\"key\":\"bd6c95a1-31b2-4eaf-b962-f1b3da3de21a\",\"namespace\":\"\"},{\"key\":\"a0294124-ac2f-442b-9e6e-2086c6b0c2bd\",\"namespace\":\"\"}],\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate flexible layout with 2 child display layouts\\nchrome\",\"modified\":1732413603920,\"location\":\"mine\",\"created\":1732413601660,\"persisted\":1732413603920},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"a9ea2ec4-c456-4b73-8ed5-c4da07d40732\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413601660,\"created\":1732413600900,\"persisted\":1732413601660},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/a9ea2ec4-c456-4b73-8ed5-c4da07d40732\",\"domainObject\":{\"identifier\":{\"key\":\"a9ea2ec4-c456-4b73-8ed5-c4da07d40732\",\"namespace\":\"\"},\"name\":\"Parent Flexible Layout\",\"type\":\"flexible-layout\",\"configuration\":{\"containers\":[{\"id\":\"f7154ceb-c37b-40ee-af98-6b9cb8a3ec62\",\"frames\":[{\"id\":\"885b1828-fbf1-4f59-8c57-c5db7ad625b1\",\"domainObjectIdentifier\":{\"key\":\"bd6c95a1-31b2-4eaf-b962-f1b3da3de21a\",\"namespace\":\"\"},\"size\":100,\"noFrame\":false}],\"size\":50},{\"id\":\"f73ad0b3-c719-45e8-8056-a93eb7fea00f\",\"frames\":[],\"size\":50}],\"rowsLayout\":false},\"composition\":[{\"key\":\"bd6c95a1-31b2-4eaf-b962-f1b3da3de21a\",\"namespace\":\"\"},{\"key\":\"a0294124-ac2f-442b-9e6e-2086c6b0c2bd\",\"namespace\":\"\"}],\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate flexible layout with 2 child display layouts\\nchrome\",\"modified\":1732413603920,\"location\":\"mine\",\"created\":1732413601660,\"persisted\":1732413603920}},{\"objectPath\":[{\"identifier\":{\"key\":\"a0294124-ac2f-442b-9e6e-2086c6b0c2bd\",\"namespace\":\"\"},\"name\":\"Child Layout 2\",\"type\":\"layout\",\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate flexible layout with 2 child display layouts\\nchrome\",\"modified\":1732413603920,\"location\":\"a9ea2ec4-c456-4b73-8ed5-c4da07d40732\",\"created\":1732413603920,\"persisted\":1732413603920},{\"identifier\":{\"key\":\"a9ea2ec4-c456-4b73-8ed5-c4da07d40732\",\"namespace\":\"\"},\"name\":\"Parent Flexible Layout\",\"type\":\"flexible-layout\",\"configuration\":{\"containers\":[{\"id\":\"f7154ceb-c37b-40ee-af98-6b9cb8a3ec62\",\"frames\":[{\"id\":\"885b1828-fbf1-4f59-8c57-c5db7ad625b1\",\"domainObjectIdentifier\":{\"key\":\"bd6c95a1-31b2-4eaf-b962-f1b3da3de21a\",\"namespace\":\"\"},\"size\":100,\"noFrame\":false}],\"size\":50},{\"id\":\"f73ad0b3-c719-45e8-8056-a93eb7fea00f\",\"frames\":[],\"size\":50}],\"rowsLayout\":false},\"composition\":[{\"key\":\"bd6c95a1-31b2-4eaf-b962-f1b3da3de21a\",\"namespace\":\"\"},{\"key\":\"a0294124-ac2f-442b-9e6e-2086c6b0c2bd\",\"namespace\":\"\"}],\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate flexible layout with 2 child display layouts\\nchrome\",\"modified\":1732413603920,\"location\":\"mine\",\"created\":1732413601660,\"persisted\":1732413603920},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"a9ea2ec4-c456-4b73-8ed5-c4da07d40732\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413601660,\"created\":1732413600900,\"persisted\":1732413601660},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/a9ea2ec4-c456-4b73-8ed5-c4da07d40732/a0294124-ac2f-442b-9e6e-2086c6b0c2bd\",\"domainObject\":{\"identifier\":{\"key\":\"a0294124-ac2f-442b-9e6e-2086c6b0c2bd\",\"namespace\":\"\"},\"name\":\"Child Layout 2\",\"type\":\"layout\",\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate flexible layout with 2 child display layouts\\nchrome\",\"modified\":1732413603920,\"location\":\"a9ea2ec4-c456-4b73-8ed5-c4da07d40732\",\"created\":1732413603920,\"persisted\":1732413603920}},{\"objectPath\":[{\"identifier\":{\"key\":\"bd6c95a1-31b2-4eaf-b962-f1b3da3de21a\",\"namespace\":\"\"},\"name\":\"Child Layout 1\",\"type\":\"layout\",\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate flexible layout with 2 child display layouts\\nchrome\",\"modified\":1732413602800,\"location\":\"a9ea2ec4-c456-4b73-8ed5-c4da07d40732\",\"created\":1732413602800,\"persisted\":1732413602800},{\"identifier\":{\"key\":\"a9ea2ec4-c456-4b73-8ed5-c4da07d40732\",\"namespace\":\"\"},\"name\":\"Parent Flexible Layout\",\"type\":\"flexible-layout\",\"configuration\":{\"containers\":[{\"id\":\"f7154ceb-c37b-40ee-af98-6b9cb8a3ec62\",\"frames\":[{\"id\":\"885b1828-fbf1-4f59-8c57-c5db7ad625b1\",\"domainObjectIdentifier\":{\"key\":\"bd6c95a1-31b2-4eaf-b962-f1b3da3de21a\",\"namespace\":\"\"},\"size\":100,\"noFrame\":false}],\"size\":50},{\"id\":\"f73ad0b3-c719-45e8-8056-a93eb7fea00f\",\"frames\":[],\"size\":50}],\"rowsLayout\":false},\"composition\":[{\"key\":\"bd6c95a1-31b2-4eaf-b962-f1b3da3de21a\",\"namespace\":\"\"},{\"key\":\"a0294124-ac2f-442b-9e6e-2086c6b0c2bd\",\"namespace\":\"\"}],\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate flexible layout with 2 child display layouts\\nchrome\",\"modified\":1732413603920,\"location\":\"mine\",\"created\":1732413601660,\"persisted\":1732413603920},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"a9ea2ec4-c456-4b73-8ed5-c4da07d40732\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413601660,\"created\":1732413600900,\"persisted\":1732413601660},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/a9ea2ec4-c456-4b73-8ed5-c4da07d40732/bd6c95a1-31b2-4eaf-b962-f1b3da3de21a\",\"domainObject\":{\"identifier\":{\"key\":\"bd6c95a1-31b2-4eaf-b962-f1b3da3de21a\",\"namespace\":\"\"},\"name\":\"Child Layout 1\",\"type\":\"layout\",\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate flexible layout with 2 child display layouts\\nchrome\",\"modified\":1732413602800,\"location\":\"a9ea2ec4-c456-4b73-8ed5-c4da07d40732\",\"created\":1732413602800,\"persisted\":1732413602800}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"a9ea2ec4-c456-4b73-8ed5-c4da07d40732\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413601660,\"created\":1732413600900,\"persisted\":1732413601660},{\"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\":\"a9ea2ec4-c456-4b73-8ed5-c4da07d40732\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413601660,\"created\":1732413600900,\"persisted\":1732413601660}}]" + "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"3b2f155b-ec69-48d5-ac58-76af10187a35\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413605725.7,\"created\":1732413604014,\"persisted\":1732413605725.7},\"3b2f155b-ec69-48d5-ac58-76af10187a35\":{\"identifier\":{\"key\":\"3b2f155b-ec69-48d5-ac58-76af10187a35\",\"namespace\":\"\"},\"name\":\"Parent Flexible Layout\",\"type\":\"flexible-layout\",\"configuration\":{\"containers\":[{\"id\":\"344ecaa0-d7f6-45de-a5bc-74bc3575e529\",\"frames\":[{\"id\":\"928f4a48-d080-4983-b3b0-8089b925c702\",\"domainObjectIdentifier\":{\"key\":\"a6141275-b3a0-4768-8e4a-197f1cb809dd\",\"namespace\":\"\"},\"size\":100,\"noFrame\":false}],\"size\":50},{\"id\":\"de887070-1474-4ce6-8863-8002926d745e\",\"frames\":[],\"size\":50}],\"rowsLayout\":false},\"composition\":[{\"key\":\"a6141275-b3a0-4768-8e4a-197f1cb809dd\",\"namespace\":\"\"},{\"key\":\"20cad719-b961-457c-abcf-262944bdd1ce\",\"namespace\":\"\"}],\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate flexible layout with 2 child display layouts\\nchrome\",\"modified\":1732413607918,\"location\":\"mine\",\"created\":1732413605725.7,\"persisted\":1732413607918},\"a6141275-b3a0-4768-8e4a-197f1cb809dd\":{\"name\":\"Child Layout 1\",\"type\":\"layout\",\"identifier\":{\"key\":\"a6141275-b3a0-4768-8e4a-197f1cb809dd\",\"namespace\":\"\"},\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate flexible layout with 2 child display layouts\\nchrome\",\"modified\":1732413606530,\"location\":\"3b2f155b-ec69-48d5-ac58-76af10187a35\",\"created\":1732413606530,\"persisted\":1732413606530},\"20cad719-b961-457c-abcf-262944bdd1ce\":{\"name\":\"Child Layout 2\",\"type\":\"layout\",\"identifier\":{\"key\":\"20cad719-b961-457c-abcf-262944bdd1ce\",\"namespace\":\"\"},\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate flexible layout with 2 child display layouts\\nchrome\",\"modified\":1732413607918,\"location\":\"3b2f155b-ec69-48d5-ac58-76af10187a35\",\"created\":1732413607918,\"persisted\":1732413607918}}" }, { "name": "mct-tree-expanded", "value": "[]" + }, + { + "name": "mct-recent-objects", + "value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"3b2f155b-ec69-48d5-ac58-76af10187a35\",\"namespace\":\"\"},\"name\":\"Parent Flexible Layout\",\"type\":\"flexible-layout\",\"configuration\":{\"containers\":[{\"id\":\"344ecaa0-d7f6-45de-a5bc-74bc3575e529\",\"frames\":[{\"id\":\"928f4a48-d080-4983-b3b0-8089b925c702\",\"domainObjectIdentifier\":{\"key\":\"a6141275-b3a0-4768-8e4a-197f1cb809dd\",\"namespace\":\"\"},\"size\":100,\"noFrame\":false}],\"size\":50},{\"id\":\"de887070-1474-4ce6-8863-8002926d745e\",\"frames\":[],\"size\":50}],\"rowsLayout\":false},\"composition\":[{\"key\":\"a6141275-b3a0-4768-8e4a-197f1cb809dd\",\"namespace\":\"\"},{\"key\":\"20cad719-b961-457c-abcf-262944bdd1ce\",\"namespace\":\"\"}],\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate flexible layout with 2 child display layouts\\nchrome\",\"modified\":1732413607918,\"location\":\"mine\",\"created\":1732413605725.7,\"persisted\":1732413607918},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"3b2f155b-ec69-48d5-ac58-76af10187a35\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413605725.7,\"created\":1732413604014,\"persisted\":1732413605725.7},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/3b2f155b-ec69-48d5-ac58-76af10187a35\",\"domainObject\":{\"identifier\":{\"key\":\"3b2f155b-ec69-48d5-ac58-76af10187a35\",\"namespace\":\"\"},\"name\":\"Parent Flexible Layout\",\"type\":\"flexible-layout\",\"configuration\":{\"containers\":[{\"id\":\"344ecaa0-d7f6-45de-a5bc-74bc3575e529\",\"frames\":[{\"id\":\"928f4a48-d080-4983-b3b0-8089b925c702\",\"domainObjectIdentifier\":{\"key\":\"a6141275-b3a0-4768-8e4a-197f1cb809dd\",\"namespace\":\"\"},\"size\":100,\"noFrame\":false}],\"size\":50},{\"id\":\"de887070-1474-4ce6-8863-8002926d745e\",\"frames\":[],\"size\":50}],\"rowsLayout\":false},\"composition\":[{\"key\":\"a6141275-b3a0-4768-8e4a-197f1cb809dd\",\"namespace\":\"\"},{\"key\":\"20cad719-b961-457c-abcf-262944bdd1ce\",\"namespace\":\"\"}],\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate flexible layout with 2 child display layouts\\nchrome\",\"modified\":1732413607918,\"location\":\"mine\",\"created\":1732413605725.7,\"persisted\":1732413607918}},{\"objectPath\":[{\"identifier\":{\"key\":\"20cad719-b961-457c-abcf-262944bdd1ce\",\"namespace\":\"\"},\"name\":\"Child Layout 2\",\"type\":\"layout\",\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate flexible layout with 2 child display layouts\\nchrome\",\"modified\":1732413607918,\"location\":\"3b2f155b-ec69-48d5-ac58-76af10187a35\",\"created\":1732413607918,\"persisted\":1732413607918},{\"identifier\":{\"key\":\"3b2f155b-ec69-48d5-ac58-76af10187a35\",\"namespace\":\"\"},\"name\":\"Parent Flexible Layout\",\"type\":\"flexible-layout\",\"configuration\":{\"containers\":[{\"id\":\"344ecaa0-d7f6-45de-a5bc-74bc3575e529\",\"frames\":[{\"id\":\"928f4a48-d080-4983-b3b0-8089b925c702\",\"domainObjectIdentifier\":{\"key\":\"a6141275-b3a0-4768-8e4a-197f1cb809dd\",\"namespace\":\"\"},\"size\":100,\"noFrame\":false}],\"size\":50},{\"id\":\"de887070-1474-4ce6-8863-8002926d745e\",\"frames\":[],\"size\":50}],\"rowsLayout\":false},\"composition\":[{\"key\":\"a6141275-b3a0-4768-8e4a-197f1cb809dd\",\"namespace\":\"\"},{\"key\":\"20cad719-b961-457c-abcf-262944bdd1ce\",\"namespace\":\"\"}],\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate flexible layout with 2 child display layouts\\nchrome\",\"modified\":1732413607918,\"location\":\"mine\",\"created\":1732413605725.7,\"persisted\":1732413607918},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"3b2f155b-ec69-48d5-ac58-76af10187a35\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413605725.7,\"created\":1732413604014,\"persisted\":1732413605725.7},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/3b2f155b-ec69-48d5-ac58-76af10187a35/20cad719-b961-457c-abcf-262944bdd1ce\",\"domainObject\":{\"identifier\":{\"key\":\"20cad719-b961-457c-abcf-262944bdd1ce\",\"namespace\":\"\"},\"name\":\"Child Layout 2\",\"type\":\"layout\",\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate flexible layout with 2 child display layouts\\nchrome\",\"modified\":1732413607918,\"location\":\"3b2f155b-ec69-48d5-ac58-76af10187a35\",\"created\":1732413607918,\"persisted\":1732413607918}},{\"objectPath\":[{\"identifier\":{\"key\":\"a6141275-b3a0-4768-8e4a-197f1cb809dd\",\"namespace\":\"\"},\"name\":\"Child Layout 1\",\"type\":\"layout\",\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate flexible layout with 2 child display layouts\\nchrome\",\"modified\":1732413606530,\"location\":\"3b2f155b-ec69-48d5-ac58-76af10187a35\",\"created\":1732413606530,\"persisted\":1732413606530},{\"identifier\":{\"key\":\"3b2f155b-ec69-48d5-ac58-76af10187a35\",\"namespace\":\"\"},\"name\":\"Parent Flexible Layout\",\"type\":\"flexible-layout\",\"configuration\":{\"containers\":[{\"id\":\"344ecaa0-d7f6-45de-a5bc-74bc3575e529\",\"frames\":[{\"id\":\"928f4a48-d080-4983-b3b0-8089b925c702\",\"domainObjectIdentifier\":{\"key\":\"a6141275-b3a0-4768-8e4a-197f1cb809dd\",\"namespace\":\"\"},\"size\":100,\"noFrame\":false}],\"size\":50},{\"id\":\"de887070-1474-4ce6-8863-8002926d745e\",\"frames\":[],\"size\":50}],\"rowsLayout\":false},\"composition\":[{\"key\":\"a6141275-b3a0-4768-8e4a-197f1cb809dd\",\"namespace\":\"\"},{\"key\":\"20cad719-b961-457c-abcf-262944bdd1ce\",\"namespace\":\"\"}],\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate flexible layout with 2 child display layouts\\nchrome\",\"modified\":1732413607918,\"location\":\"mine\",\"created\":1732413605725.7,\"persisted\":1732413607918},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"3b2f155b-ec69-48d5-ac58-76af10187a35\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413605725.7,\"created\":1732413604014,\"persisted\":1732413605725.7},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/3b2f155b-ec69-48d5-ac58-76af10187a35/a6141275-b3a0-4768-8e4a-197f1cb809dd\",\"domainObject\":{\"identifier\":{\"key\":\"a6141275-b3a0-4768-8e4a-197f1cb809dd\",\"namespace\":\"\"},\"name\":\"Child Layout 1\",\"type\":\"layout\",\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate flexible layout with 2 child display layouts\\nchrome\",\"modified\":1732413606530,\"location\":\"3b2f155b-ec69-48d5-ac58-76af10187a35\",\"created\":1732413606530,\"persisted\":1732413606530}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"3b2f155b-ec69-48d5-ac58-76af10187a35\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413605725.7,\"created\":1732413604014,\"persisted\":1732413605725.7},{\"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\":\"3b2f155b-ec69-48d5-ac58-76af10187a35\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413605725.7,\"created\":1732413604014,\"persisted\":1732413605725.7}}]" } ] } diff --git a/e2e/test-data/overlay_plot_storage.json b/e2e/test-data/overlay_plot_storage.json index 310795dbc0..947c4119be 100644 --- a/e2e/test-data/overlay_plot_storage.json +++ b/e2e/test-data/overlay_plot_storage.json @@ -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}}]" } ] } diff --git a/e2e/test-data/overlay_plot_with_delay_storage.json b/e2e/test-data/overlay_plot_with_delay_storage.json index ac3aeb65f0..4f13a19cad 100644 --- a/e2e/test-data/overlay_plot_with_delay_storage.json +++ b/e2e/test-data/overlay_plot_with_delay_storage.json @@ -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", diff --git a/e2e/test-data/recycled_local_storage.json b/e2e/test-data/recycled_local_storage.json index 7da5a89a39..2f9ee4db7e 100644 --- a/e2e/test-data/recycled_local_storage.json +++ b/e2e/test-data/recycled_local_storage.json @@ -6,7 +6,7 @@ "localStorage": [ { "name": "mct", - "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},{\"key\":\"ffb49de1-af27-4318-a22f-59899988f4e9\",\"namespace\":\"\"},{\"key\":\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\",\"namespace\":\"\"},{\"key\":\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1704955298732,\"modified\":1704955298732},\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"73f2d9ae-d1f3-4561-b7fc-ecd5df557249\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1652303755999,\"location\":\"mine\",\"persisted\":1652303756002},\"2d02a680-eb7e-4645-bba2-dd298f76efb8\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4291d80c-303c-4d8d-85e1-10f012b864fb\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1654538965702,\"location\":\"mine\",\"persisted\":1654538965702},\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"2b6bf89f-877b-42b8-acc1-a9a575efdbe1\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658610682787,\"location\":\"mine\",\"persisted\":1658610682787},\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"b9a9c413-4b94-401d-b0c7-5e404f182616\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658618261112,\"location\":\"mine\",\"persisted\":1658618261112},\"3e294eae-6124-409b-a870-554d1bdcdd6f\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"108043b1-9c88-4e1d-8deb-fbf2cdb528f9\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658618890910,\"location\":\"mine\",\"persisted\":1658618890910},\"ec24d05d-5df5-4c96-9241-b73636cd19a9\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4062bd9b-b788-43dd-ab0a-8fa10a78d4b3\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658619295363,\"location\":\"mine\",\"persisted\":1658619295363},\"0ec517e8-6c11-4d98-89b5-c300fe61b304\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"2f1585da-6f7e-4ccd-8a20-590fdf177b5d\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1689710689550,\"location\":\"mine\",\"created\":1689710689550,\"persisted\":1689710689550},\"ffb49de1-af27-4318-a22f-59899988f4e9\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"ffb49de1-af27-4318-a22f-59899988f4e9\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"0a04f110-e5c4-4503-9276-6e8f783d5bd5\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1694110416938,\"location\":\"mine\",\"created\":1694110416938,\"persisted\":1694110416938},\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"6c8bdbec-41f5-4138-a88f-ae6ebdbc9a90\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1694112912985,\"location\":\"mine\",\"created\":1694112912985,\"persisted\":1694112912985},\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"6776a2ec-fa49-4b06-80ed-a6eaf4f86f56\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1704955298729,\"location\":\"mine\",\"created\":1704955298729,\"persisted\":1704955298729}}" + "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},{\"key\":\"ffb49de1-af27-4318-a22f-59899988f4e9\",\"namespace\":\"\"},{\"key\":\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\",\"namespace\":\"\"},{\"key\":\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\",\"namespace\":\"\"},{\"key\":\"c1717964-ffed-47aa-9ed9-647ba5a3db67\",\"namespace\":\"\"},{\"key\":\"10581641-5de3-4606-95aa-04cd811f2f53\",\"namespace\":\"\"},{\"key\":\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1721850933441,\"modified\":1721850933441},\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"73f2d9ae-d1f3-4561-b7fc-ecd5df557249\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1652303755999,\"location\":\"mine\",\"persisted\":1652303756002},\"2d02a680-eb7e-4645-bba2-dd298f76efb8\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4291d80c-303c-4d8d-85e1-10f012b864fb\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1654538965702,\"location\":\"mine\",\"persisted\":1654538965702},\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"2b6bf89f-877b-42b8-acc1-a9a575efdbe1\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658610682787,\"location\":\"mine\",\"persisted\":1658610682787},\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"b9a9c413-4b94-401d-b0c7-5e404f182616\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658618261112,\"location\":\"mine\",\"persisted\":1658618261112},\"3e294eae-6124-409b-a870-554d1bdcdd6f\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"108043b1-9c88-4e1d-8deb-fbf2cdb528f9\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658618890910,\"location\":\"mine\",\"persisted\":1658618890910},\"ec24d05d-5df5-4c96-9241-b73636cd19a9\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4062bd9b-b788-43dd-ab0a-8fa10a78d4b3\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658619295363,\"location\":\"mine\",\"persisted\":1658619295363},\"0ec517e8-6c11-4d98-89b5-c300fe61b304\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"2f1585da-6f7e-4ccd-8a20-590fdf177b5d\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1689710689550,\"location\":\"mine\",\"created\":1689710689550,\"persisted\":1689710689550},\"ffb49de1-af27-4318-a22f-59899988f4e9\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"ffb49de1-af27-4318-a22f-59899988f4e9\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"0a04f110-e5c4-4503-9276-6e8f783d5bd5\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1694110416938,\"location\":\"mine\",\"created\":1694110416938,\"persisted\":1694110416938},\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"6c8bdbec-41f5-4138-a88f-ae6ebdbc9a90\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1694112912985,\"location\":\"mine\",\"created\":1694112912985,\"persisted\":1694112912985},\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"6776a2ec-fa49-4b06-80ed-a6eaf4f86f56\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1704955298729,\"location\":\"mine\",\"created\":1704955298729,\"persisted\":1704955298729},\"c1717964-ffed-47aa-9ed9-647ba5a3db67\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"c1717964-ffed-47aa-9ed9-647ba5a3db67\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"9a72ce62-012c-4a81-8f2c-51db5410de76\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1721850626897,\"location\":\"mine\",\"created\":1721850626897,\"persisted\":1721850626897},\"10581641-5de3-4606-95aa-04cd811f2f53\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"10581641-5de3-4606-95aa-04cd811f2f53\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"71e8050e-e063-4a35-b891-f38ddf63fa6d\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1721850725791,\"location\":\"mine\",\"created\":1721850725791,\"persisted\":1721850725791},\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"91ebe1a1-dcac-49b0-9985-d3e32cef5260\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1721850933438,\"location\":\"mine\",\"created\":1721850933438,\"persisted\":1721850933439}}" }, { "name": "mct-tree-expanded", @@ -18,7 +18,7 @@ }, { "name": "mct-recent-objects", - "value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"6776a2ec-fa49-4b06-80ed-a6eaf4f86f56\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1704955298729,\"location\":\"mine\",\"created\":1704955298729,\"persisted\":1704955298729},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},{\"key\":\"ffb49de1-af27-4318-a22f-59899988f4e9\",\"namespace\":\"\"},{\"key\":\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\",\"namespace\":\"\"},{\"key\":\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1704955298732,\"modified\":1704955298732},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\",\"domainObject\":{\"identifier\":{\"key\":\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"6776a2ec-fa49-4b06-80ed-a6eaf4f86f56\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1704955298729,\"location\":\"mine\",\"created\":1704955298729,\"persisted\":1704955298729}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},{\"key\":\"ffb49de1-af27-4318-a22f-59899988f4e9\",\"namespace\":\"\"},{\"key\":\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\",\"namespace\":\"\"},{\"key\":\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1704955298732,\"modified\":1704955298732},{\"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\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},{\"key\":\"ffb49de1-af27-4318-a22f-59899988f4e9\",\"namespace\":\"\"},{\"key\":\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\",\"namespace\":\"\"},{\"key\":\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1704955298732,\"modified\":1704955298732}},{\"objectPath\":[{\"identifier\":{\"key\":\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"6c8bdbec-41f5-4138-a88f-ae6ebdbc9a90\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1694112912985,\"location\":\"mine\",\"created\":1694112912985,\"persisted\":1694112912985},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},{\"key\":\"ffb49de1-af27-4318-a22f-59899988f4e9\",\"namespace\":\"\"},{\"key\":\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\",\"namespace\":\"\"},{\"key\":\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1704955298732,\"modified\":1704955298732},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\",\"domainObject\":{\"identifier\":{\"key\":\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"6c8bdbec-41f5-4138-a88f-ae6ebdbc9a90\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1694112912985,\"location\":\"mine\",\"created\":1694112912985,\"persisted\":1694112912985}},{\"objectPath\":[{\"identifier\":{\"key\":\"ffb49de1-af27-4318-a22f-59899988f4e9\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"0a04f110-e5c4-4503-9276-6e8f783d5bd5\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1694110416938,\"location\":\"mine\",\"created\":1694110416938,\"persisted\":1694110416938},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},{\"key\":\"ffb49de1-af27-4318-a22f-59899988f4e9\",\"namespace\":\"\"},{\"key\":\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\",\"namespace\":\"\"},{\"key\":\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1704955298732,\"modified\":1704955298732},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/ffb49de1-af27-4318-a22f-59899988f4e9\",\"domainObject\":{\"identifier\":{\"key\":\"ffb49de1-af27-4318-a22f-59899988f4e9\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"0a04f110-e5c4-4503-9276-6e8f783d5bd5\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1694110416938,\"location\":\"mine\",\"created\":1694110416938,\"persisted\":1694110416938}},{\"objectPath\":[{\"identifier\":{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"2f1585da-6f7e-4ccd-8a20-590fdf177b5d\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1689710689550,\"location\":\"mine\",\"created\":1689710689550,\"persisted\":1689710689550},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},{\"key\":\"ffb49de1-af27-4318-a22f-59899988f4e9\",\"namespace\":\"\"},{\"key\":\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\",\"namespace\":\"\"},{\"key\":\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1704955298732,\"modified\":1704955298732},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"domainObject\":{\"identifier\":{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"2f1585da-6f7e-4ccd-8a20-590fdf177b5d\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1689710689550,\"location\":\"mine\",\"created\":1689710689550,\"persisted\":1689710689550}}]" + "value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"91ebe1a1-dcac-49b0-9985-d3e32cef5260\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1721850933438,\"location\":\"mine\",\"created\":1721850933438,\"persisted\":1721850933439},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},{\"key\":\"ffb49de1-af27-4318-a22f-59899988f4e9\",\"namespace\":\"\"},{\"key\":\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\",\"namespace\":\"\"},{\"key\":\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\",\"namespace\":\"\"},{\"key\":\"c1717964-ffed-47aa-9ed9-647ba5a3db67\",\"namespace\":\"\"},{\"key\":\"10581641-5de3-4606-95aa-04cd811f2f53\",\"namespace\":\"\"},{\"key\":\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1721850933441,\"modified\":1721850933441},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/d9d79500-916d-4ff2-a7ea-6cf300c85ce3\",\"domainObject\":{\"identifier\":{\"key\":\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"91ebe1a1-dcac-49b0-9985-d3e32cef5260\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1721850933438,\"location\":\"mine\",\"created\":1721850933438,\"persisted\":1721850933439}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},{\"key\":\"ffb49de1-af27-4318-a22f-59899988f4e9\",\"namespace\":\"\"},{\"key\":\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\",\"namespace\":\"\"},{\"key\":\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\",\"namespace\":\"\"},{\"key\":\"c1717964-ffed-47aa-9ed9-647ba5a3db67\",\"namespace\":\"\"},{\"key\":\"10581641-5de3-4606-95aa-04cd811f2f53\",\"namespace\":\"\"},{\"key\":\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1721850933441,\"modified\":1721850933441},{\"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\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},{\"key\":\"ffb49de1-af27-4318-a22f-59899988f4e9\",\"namespace\":\"\"},{\"key\":\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\",\"namespace\":\"\"},{\"key\":\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\",\"namespace\":\"\"},{\"key\":\"c1717964-ffed-47aa-9ed9-647ba5a3db67\",\"namespace\":\"\"},{\"key\":\"10581641-5de3-4606-95aa-04cd811f2f53\",\"namespace\":\"\"},{\"key\":\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1721850933441,\"modified\":1721850933441}},{\"objectPath\":[{\"identifier\":{\"key\":\"10581641-5de3-4606-95aa-04cd811f2f53\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"71e8050e-e063-4a35-b891-f38ddf63fa6d\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1721850725791,\"location\":\"mine\",\"created\":1721850725791,\"persisted\":1721850725791},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},{\"key\":\"ffb49de1-af27-4318-a22f-59899988f4e9\",\"namespace\":\"\"},{\"key\":\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\",\"namespace\":\"\"},{\"key\":\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\",\"namespace\":\"\"},{\"key\":\"c1717964-ffed-47aa-9ed9-647ba5a3db67\",\"namespace\":\"\"},{\"key\":\"10581641-5de3-4606-95aa-04cd811f2f53\",\"namespace\":\"\"},{\"key\":\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1721850933441,\"modified\":1721850933441},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/10581641-5de3-4606-95aa-04cd811f2f53\",\"domainObject\":{\"identifier\":{\"key\":\"10581641-5de3-4606-95aa-04cd811f2f53\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"71e8050e-e063-4a35-b891-f38ddf63fa6d\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1721850725791,\"location\":\"mine\",\"created\":1721850725791,\"persisted\":1721850725791}},{\"objectPath\":[{\"identifier\":{\"key\":\"c1717964-ffed-47aa-9ed9-647ba5a3db67\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"9a72ce62-012c-4a81-8f2c-51db5410de76\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1721850626897,\"location\":\"mine\",\"created\":1721850626897,\"persisted\":1721850626897},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},{\"key\":\"ffb49de1-af27-4318-a22f-59899988f4e9\",\"namespace\":\"\"},{\"key\":\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\",\"namespace\":\"\"},{\"key\":\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\",\"namespace\":\"\"},{\"key\":\"c1717964-ffed-47aa-9ed9-647ba5a3db67\",\"namespace\":\"\"},{\"key\":\"10581641-5de3-4606-95aa-04cd811f2f53\",\"namespace\":\"\"},{\"key\":\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1721850933441,\"modified\":1721850933441},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/c1717964-ffed-47aa-9ed9-647ba5a3db67\",\"domainObject\":{\"identifier\":{\"key\":\"c1717964-ffed-47aa-9ed9-647ba5a3db67\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"9a72ce62-012c-4a81-8f2c-51db5410de76\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1721850626897,\"location\":\"mine\",\"created\":1721850626897,\"persisted\":1721850626897}},{\"objectPath\":[{\"identifier\":{\"key\":\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"6776a2ec-fa49-4b06-80ed-a6eaf4f86f56\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1704955298729,\"location\":\"mine\",\"created\":1704955298729,\"persisted\":1704955298729},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},{\"key\":\"ffb49de1-af27-4318-a22f-59899988f4e9\",\"namespace\":\"\"},{\"key\":\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\",\"namespace\":\"\"},{\"key\":\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\",\"namespace\":\"\"},{\"key\":\"c1717964-ffed-47aa-9ed9-647ba5a3db67\",\"namespace\":\"\"},{\"key\":\"10581641-5de3-4606-95aa-04cd811f2f53\",\"namespace\":\"\"},{\"key\":\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1721850933441,\"modified\":1721850933441},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\",\"domainObject\":{\"identifier\":{\"key\":\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"6776a2ec-fa49-4b06-80ed-a6eaf4f86f56\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1704955298729,\"location\":\"mine\",\"created\":1704955298729,\"persisted\":1704955298729}},{\"objectPath\":[{\"identifier\":{\"key\":\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"6c8bdbec-41f5-4138-a88f-ae6ebdbc9a90\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1694112912985,\"location\":\"mine\",\"created\":1694112912985,\"persisted\":1694112912985},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},{\"key\":\"ffb49de1-af27-4318-a22f-59899988f4e9\",\"namespace\":\"\"},{\"key\":\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\",\"namespace\":\"\"},{\"key\":\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\",\"namespace\":\"\"},{\"key\":\"c1717964-ffed-47aa-9ed9-647ba5a3db67\",\"namespace\":\"\"},{\"key\":\"10581641-5de3-4606-95aa-04cd811f2f53\",\"namespace\":\"\"},{\"key\":\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1721850933441,\"modified\":1721850933441},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\",\"domainObject\":{\"identifier\":{\"key\":\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"6c8bdbec-41f5-4138-a88f-ae6ebdbc9a90\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1694112912985,\"location\":\"mine\",\"created\":1694112912985,\"persisted\":1694112912985}},{\"objectPath\":[{\"identifier\":{\"key\":\"ffb49de1-af27-4318-a22f-59899988f4e9\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"0a04f110-e5c4-4503-9276-6e8f783d5bd5\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1694110416938,\"location\":\"mine\",\"created\":1694110416938,\"persisted\":1694110416938},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},{\"key\":\"ffb49de1-af27-4318-a22f-59899988f4e9\",\"namespace\":\"\"},{\"key\":\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\",\"namespace\":\"\"},{\"key\":\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\",\"namespace\":\"\"},{\"key\":\"c1717964-ffed-47aa-9ed9-647ba5a3db67\",\"namespace\":\"\"},{\"key\":\"10581641-5de3-4606-95aa-04cd811f2f53\",\"namespace\":\"\"},{\"key\":\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1721850933441,\"modified\":1721850933441},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/ffb49de1-af27-4318-a22f-59899988f4e9\",\"domainObject\":{\"identifier\":{\"key\":\"ffb49de1-af27-4318-a22f-59899988f4e9\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"0a04f110-e5c4-4503-9276-6e8f783d5bd5\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1694110416938,\"location\":\"mine\",\"created\":1694110416938,\"persisted\":1694110416938}},{\"objectPath\":[{\"identifier\":{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"2f1585da-6f7e-4ccd-8a20-590fdf177b5d\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1689710689550,\"location\":\"mine\",\"created\":1689710689550,\"persisted\":1689710689550},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},{\"key\":\"ffb49de1-af27-4318-a22f-59899988f4e9\",\"namespace\":\"\"},{\"key\":\"c6c65ad5-5c09-43c2-8a12-fdeb64d3e1a4\",\"namespace\":\"\"},{\"key\":\"62c4ade7-85ce-45bd-8cdb-25f0c58c8a28\",\"namespace\":\"\"},{\"key\":\"c1717964-ffed-47aa-9ed9-647ba5a3db67\",\"namespace\":\"\"},{\"key\":\"10581641-5de3-4606-95aa-04cd811f2f53\",\"namespace\":\"\"},{\"key\":\"d9d79500-916d-4ff2-a7ea-6cf300c85ce3\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1721850933441,\"modified\":1721850933441},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"domainObject\":{\"identifier\":{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"2f1585da-6f7e-4ccd-8a20-590fdf177b5d\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1689710689550,\"location\":\"mine\",\"created\":1689710689550,\"persisted\":1689710689550}}]" } ] } diff --git a/e2e/tests/framework/appActions.e2e.spec.js b/e2e/tests/framework/appActions.e2e.spec.js index d58d73968c..280ef33f17 100644 --- a/e2e/tests/framework/appActions.e2e.spec.js +++ b/e2e/tests/framework/appActions.e2e.spec.js @@ -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 + }); + + const startDate = '2021-12-30 01:01:00.000Z'; + const endDate = '2021-12-30 01:11:00.000Z'; + await setFixedIndependentTimeConductorBounds(page, { start: startDate, end: endDate }); + + // check image date + await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible(); + + // flip it off + await page.getByRole('switch').click(); + // timestamp shouldn't be in the past anymore + await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden(); + }); + test.fail('waitForPlotsToRender', async ({ page }) => { + // Create a SWG + await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator' + }); + // Edit the SWG + await page.getByLabel('More actions').click(); + await page.getByLabel('Edit Properties...').click(); + // Set loading delay to 10 seconds + await page.getByLabel('Loading Delay (ms)', { exact: true }).fill('10000'); + await page.getByLabel('Save').click(); + // Reload the page + await page.reload(); + // Expect this step to fail + await waitForPlotsToRender(page, { timeout: 1000 }); }); }); diff --git a/e2e/tests/framework/baseFixtures.e2e.spec.js b/e2e/tests/framework/baseFixtures.e2e.spec.js index 133f5f5604..337a650ef5 100644 --- a/e2e/tests/framework/baseFixtures.e2e.spec.js +++ b/e2e/tests/framework/baseFixtures.e2e.spec.js @@ -26,7 +26,7 @@ relates to how we've extended it (i.e. ./e2e/baseFixtures.js) and assumptions ma (`npm start` and ./e2e/webpack-dev-middleware.js) */ -import { expect, test } from '../../baseFixtures.js'; +import { test } from '../../baseFixtures.js'; import { MISSION_TIME } from '../../constants.js'; test.describe('baseFixtures tests', () => { @@ -42,6 +42,21 @@ test.describe('baseFixtures tests', () => { page.waitForEvent('console') // always wait for the event to happen while triggering it! ]); }); + test('Verify that tests fail if console.error is thrown with clock override @clock', async ({ + page + }) => { + test.fail(); + //Set clock time + await page.clock.install({ time: MISSION_TIME }); + await page.clock.resume(); + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + //Verify that ../fixtures.js detects console log errors + await Promise.all([ + page.evaluate(() => console.error('This should result in a failure')), + page.waitForEvent('console') // always wait for the event to happen while triggering it! + ]); + }); test('Verify that tests pass if console.warn is thrown', async ({ page }) => { //Go to baseURL await page.goto('./', { waitUntil: 'domcontentloaded' }); @@ -53,27 +68,3 @@ test.describe('baseFixtures tests', () => { ]); }); }); - -test.describe('baseFixtures tests @clock', () => { - test.use({ - clockOptions: { - now: MISSION_TIME, - shouldAdvanceTime: false - } - }); - - test.beforeEach(async ({ page }) => { - await page.goto('./', { waitUntil: 'domcontentloaded' }); - }); - - test('Can use clockOptions and tick fixtures to control the clock', async ({ page, tick }) => { - let time = await page.evaluate(() => new Date().getTime()); - expect(time).toBe(MISSION_TIME); - await tick(1000); - time = await page.evaluate(() => new Date().getTime()); - expect(time).toBe(MISSION_TIME + 1000 * 1); - await tick(1000); - time = await page.evaluate(() => new Date().getTime()); - expect(time).toBe(MISSION_TIME + 1000 * 2); - }); -}); diff --git a/e2e/tests/framework/exampleTemplate.e2e.spec.js b/e2e/tests/framework/exampleTemplate.e2e.spec.js index fa696b365f..631be082b1 100644 --- a/e2e/tests/framework/exampleTemplate.e2e.spec.js +++ b/e2e/tests/framework/exampleTemplate.e2e.spec.js @@ -30,7 +30,6 @@ * Demonstrated: * - Using appActions to leverage existing functions * - Structure - * - @unstable annotation * - await, expect, test, describe syntax * - Writing a custom function for a test suite * - Test stub for unfinished test coverage (test.fixme) @@ -45,7 +44,7 @@ */ // Structure: Some standard Imports. Please update the required pathing. -import { createDomainObjectWithDefaults } from '../../appActions.js'; +import { createDomainObjectWithDefaults, createExampleTelemetryObject } from '../../appActions.js'; import { expect, test } from '../../pluginFixtures.js'; /** @@ -53,11 +52,8 @@ import { expect, test } from '../../pluginFixtures.js'; * Try to keep a single describe block per logical groups of tests. * If your test runtime exceeds 5 minutes or 500 lines, it's likely that it will need to be split. * - * Annotations: - * Please use the @unstable tag at the end of the test title so that our automation can pick it up - * as a part of our test promotion pipeline. */ -test.describe('Renaming Timer Object', () => { +test.describe('Example - Renaming Timer Object', () => { // Top-level declaration of the Timer object created in beforeEach(). // We can then use this throughout the entire test suite. let timer; @@ -70,7 +66,7 @@ test.describe('Renaming Timer Object', () => { timer = await createDomainObjectWithDefaults(page, { type: 'Timer' }); // Assert the object to be created and check its name in the title - await expect(page.locator('.l-browse-bar__object-name')).toContainText(timer.name); + await expect(page.getByRole('main')).toContainText(timer.name); }); /** @@ -85,7 +81,7 @@ test.describe('Renaming Timer Object', () => { await renameTimerFrom3DotMenu(page, timer.url, newObjectName); // Assert that the name has changed in the browser bar to the value we assigned above - await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName); + await expect(page.getByRole('main')).toContainText(newObjectName); }); test('An existing Timer object can be renamed twice', async ({ page }) => { @@ -95,13 +91,13 @@ test.describe('Renaming Timer Object', () => { await renameTimerFrom3DotMenu(page, timer.url, newObjectName); // Assert that the name has changed in the browser bar to the value we assigned above - await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName); + await expect(page.getByRole('main')).toContainText(newObjectName); // Rename the Timer object again await renameTimerFrom3DotMenu(page, timer.url, newObjectName2); // Assert that the name has changed in the browser bar to the second value - await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName2); + await expect(page.getByRole('main')).toContainText(newObjectName2); }); /** @@ -121,7 +117,7 @@ test.describe('Renaming Timer Object', () => { * The next most important concept in our testing is working with telemetry objects. Telemetry is at the core of Open MCT * and we have developed a great pattern for working with it. */ -test.describe('Advanced: Working with telemetry objects', () => { +test.describe('Advanced Example - Working with telemetry objects', () => { let displayLayout; test.beforeEach(async ({ page }) => { await page.goto('./', { waitUntil: 'domcontentloaded' }); @@ -132,17 +128,14 @@ test.describe('Advanced: Working with telemetry objects', () => { name: 'Display Layout with Embedded SWG' }); // Create Telemetry object within the parent object created above - await createDomainObjectWithDefaults(page, { - type: 'Sine Wave Generator', - name: 'Telemetry', - parent: displayLayout.uuid //reference the display layout in the creation process - }); + //reference the display layout in the creation process + await createExampleTelemetryObject(page, displayLayout.uuid); }); test('Can directly navigate to a Display Layout with embedded telemetry', async ({ page }) => { //Now you can directly navigate to the displayLayout created in the beforeEach with the embedded telemetry await page.goto(displayLayout.url); //Expect the created Telemetry Object to be visible when directly navigating to the displayLayout - await expect(page.getByTitle('Sine')).toBeVisible(); + await expect(page.getByLabel('Alpha-numeric telemetry name')).toBeVisible(); }); }); @@ -160,18 +153,14 @@ test.describe('Advanced: Working with telemetry objects', () => { * @param {string} newNameForTimer New name for object */ async function renameTimerFrom3DotMenu(page, timerUrl, newNameForTimer) { - // Navigate to the timer object + // Navigate to the timer object directly await page.goto(timerUrl); - // Click on 3 Dot Menu - await page.locator('button[title="More actions"]').click(); - - // Click text=Edit Properties... - await page.locator('text=Edit Properties...').click(); + await page.getByLabel('More actions').click(); + await page.getByLabel('Edit Properties...').click(); // Rename the timer object - await page.locator('text=Properties Title Notes >> input[type="text"]').fill(newNameForTimer); + await page.getByLabel('Title', { exact: true }).fill(newNameForTimer); - // Click Ok button to Save - await page.locator('button:has-text("OK")').click(); + await page.getByLabel('Save').click(); } diff --git a/e2e/tests/framework/generateLocalStorageData.e2e.spec.js b/e2e/tests/framework/generateLocalStorageData.e2e.spec.js index ee9ff5d73b..544a04e25f 100644 --- a/e2e/tests/framework/generateLocalStorageData.e2e.spec.js +++ b/e2e/tests/framework/generateLocalStorageData.e2e.spec.js @@ -36,7 +36,7 @@ import { fileURLToPath } from 'url'; import { createDomainObjectWithDefaults, createExampleTelemetryObject, - setIndependentTimeConductorBounds, + setFixedIndependentTimeConductorBounds, setTimeConductorBounds } from '../../appActions.js'; import { MISSION_TIME } from '../../constants.js'; @@ -45,14 +45,10 @@ import { expect, test } from '../../pluginFixtures.js'; const overlayPlotName = 'Overlay Plot with Telemetry Object'; test.describe('Generate Visual Test Data @localStorage @generatedata @clock', () => { - test.use({ - clockOptions: { - now: MISSION_TIME, - shouldAdvanceTime: true - } - }); - test.beforeEach(async ({ page }) => { + // Override the clock + await page.clock.install({ time: MISSION_TIME }); + await page.clock.resume(); // Go to baseURL await page.goto('./', { waitUntil: 'domcontentloaded' }); }); @@ -112,7 +108,7 @@ test.describe('Generate Visual Test Data @localStorage @generatedata @clock', () await page.goto(parent.url, { waitUntil: 'domcontentloaded' }); - await setIndependentTimeConductorBounds(page, { + await setFixedIndependentTimeConductorBounds(page, { start: '2024-11-12 19:11:11.000Z', end: '2024-11-12 20:11:11.000Z' }); @@ -211,11 +207,7 @@ test.describe('Generate Visual Test Data @localStorage @generatedata @clock', () // TODO: Flesh Out Assertions against created Objects await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlotName); await page.getByRole('tab', { name: 'Config' }).click(); - await page - .getByRole('list', { name: 'Plot Series Properties' }) - .locator('span') - .first() - .click(); + await page.getByLabel('Plot Series Items').getByLabel('Expand').click(); // TODO: Modify the Overlay Plot to use fixed Scaling // TODO: Verify Autoscaling. @@ -274,7 +266,7 @@ test.describe('Generate Visual Test Data @localStorage @generatedata @clock', () page.waitForNavigation(), page.locator('text=OK').click(), //Wait for Save Banner to appear - page.waitForSelector('.c-message-banner__message') + page.locator('.c-message-banner__message').hover({ trial: true }) ]); // focus the overlay plot @@ -284,7 +276,7 @@ test.describe('Generate Visual Test Data @localStorage @generatedata @clock', () // Clear Recently Viewed await page.getByRole('button', { name: 'Clear Recently Viewed' }).click(); - await page.getByRole('button', { name: 'OK', exact: true }).click(); + await page.getByRole('button', { name: 'Ok', exact: true }).click(); //Save localStorage for future test execution await context.storageState({ path: fileURLToPath( @@ -307,11 +299,7 @@ test.describe('Validate Overlay Plot with Telemetry Object @localStorage @genera // TODO: Flesh Out Assertions against created Objects await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlotName); await page.getByRole('tab', { name: 'Config' }).click(); - await page - .getByRole('list', { name: 'Plot Series Properties' }) - .locator('span') - .first() - .click(); + await page.getByLabel('Plot Series Items').getByLabel('Expand').click(); // TODO: Modify the Overlay Plot to use fixed Scaling // TODO: Verify Autoscaling. @@ -352,11 +340,7 @@ test.describe('Validate Overlay Plot with 5s Delay Telemetry Object @localStorag // TODO: Flesh Out Assertions against created Objects await expect(page.locator('.l-browse-bar__object-name')).toContainText(plotName); await page.getByRole('tab', { name: 'Config' }).click(); - await page - .getByRole('list', { name: 'Plot Series Properties' }) - .locator('span') - .first() - .click(); + await page.getByLabel('Plot Series Items').getByLabel('Expand').click(); // TODO: Modify the Overlay Plot to use fixed Scaling // TODO: Verify Autoscaling. diff --git a/e2e/tests/framework/pluginFixtures.e2e.spec.js b/e2e/tests/framework/pluginFixtures.e2e.spec.js index f9b0cb5a7b..e1eaa637d0 100644 --- a/e2e/tests/framework/pluginFixtures.e2e.spec.js +++ b/e2e/tests/framework/pluginFixtures.e2e.spec.js @@ -31,7 +31,7 @@ import { test } from '../../pluginFixtures.js'; test.describe.skip('pluginFixtures tests', () => { // test.use({ domainObjectName: 'Timer' }); // let timerUUID; - // test('Creates a timer object @framework @unstable', ({ domainObject }) => { + // test('Creates a timer object @framework', ({ domainObject }) => { // const { uuid } = domainObject; // const uuidRegexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/; // expect(uuid).toMatch(uuidRegexp); diff --git a/e2e/tests/functional/clearDataAction.e2e.spec.js b/e2e/tests/functional/clearDataAction.e2e.spec.js index 56e6b2f274..0e3165276b 100644 --- a/e2e/tests/functional/clearDataAction.e2e.spec.js +++ b/e2e/tests/functional/clearDataAction.e2e.spec.js @@ -59,6 +59,6 @@ test.describe('Clear Data Action', () => { // Verify that the background image is no longer visible await expect(page.locator(backgroundImageSelector)).toBeHidden(); - expect(await page.locator('.c-thumb__image').count()).toBe(0); + await expect(page.locator('.c-thumb__image')).toHaveCount(0); }); }); diff --git a/e2e/tests/functional/couchdb.e2e.spec.js b/e2e/tests/functional/couchdb.e2e.spec.js index b4042e8eaf..3afa9033e2 100644 --- a/e2e/tests/functional/couchdb.e2e.spec.js +++ b/e2e/tests/functional/couchdb.e2e.spec.js @@ -41,7 +41,7 @@ test.describe('CouchDB Status Indicator with mocked responses @couchdb', () => { //Go to baseURL await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', { - waitUntil: 'networkidle' + waitUntil: 'domcontentloaded' }); await expect(page.locator('div:has-text("CouchDB is connected")').nth(3)).toBeVisible(); }); @@ -56,7 +56,7 @@ test.describe('CouchDB Status Indicator with mocked responses @couchdb', () => { //Go to baseURL await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', { - waitUntil: 'networkidle' + waitUntil: 'domcontentloaded' }); await expect(page.locator('div:has-text("CouchDB is offline")').nth(3)).toBeVisible(); }); @@ -71,7 +71,7 @@ test.describe('CouchDB Status Indicator with mocked responses @couchdb', () => { //Go to baseURL await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', { - waitUntil: 'networkidle' + waitUntil: 'domcontentloaded' }); await expect(page.locator('div:has-text("CouchDB connectivity unknown")').nth(3)).toBeVisible(); }); diff --git a/e2e/tests/functional/example/generator/sineWaveLimitProvider.e2e.spec.js b/e2e/tests/functional/example/generator/sineWaveLimitProvider.e2e.spec.js index a0c50055b4..ca50314164 100644 --- a/e2e/tests/functional/example/generator/sineWaveLimitProvider.e2e.spec.js +++ b/e2e/tests/functional/example/generator/sineWaveLimitProvider.e2e.spec.js @@ -41,7 +41,7 @@ test.describe('Sine Wave Generator', () => { await page.getByRole('button', { name: 'Create' }).click(); // Click Sine Wave Generator - await page.click('text=Sine Wave Generator'); + await page.getByRole('menuitem', { name: 'Sine Wave Generator' }).click(); // Verify that the each required field has required indicator // Title @@ -107,11 +107,11 @@ test.describe('Sine Wave Generator', () => { await page.locator('.field.control.l-input-sm input').first().press('ArrowUp'); await page.locator('.field.control.l-input-sm input').first().press('ArrowUp'); - const value = await page.locator('.field.control.l-input-sm input').first().inputValue(); - await expect(value).toBe('6'); + const value = page.locator('.field.control.l-input-sm input').first(); + await expect(value).toHaveValue('6'); - //Click text=OK - await Promise.all([page.waitForNavigation(), page.click('button:has-text("OK")')]); + //Click save button + await page.getByLabel('Save').click(); // Verify that the Sine Wave Generator is displayed and correct // Verify object properties diff --git a/e2e/tests/functional/forms.e2e.spec.js b/e2e/tests/functional/forms.e2e.spec.js index 7e3ba59221..761f20ea63 100644 --- a/e2e/tests/functional/forms.e2e.spec.js +++ b/e2e/tests/functional/forms.e2e.spec.js @@ -45,25 +45,23 @@ test.describe('Form Validation Behavior', () => { await page.getByRole('menuitem', { name: 'Folder' }).click(); // Fill in empty string into title and trigger validation with 'Tab' - await page.click('text=Properties Title Notes >> input[type="text"]'); - await page.fill('text=Properties Title Notes >> input[type="text"]', ''); - await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab'); + await page.getByLabel('Title', { exact: true }).fill(''); + await page.getByLabel('Title', { exact: true }).press('Tab'); //Required Field Form Validation - await expect(page.locator('button:has-text("OK")')).toBeDisabled(); + await expect(page.getByLabel('Save')).toBeDisabled(); await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/); //Correct Form Validation for missing title and trigger validation with 'Tab' - await page.click('text=Properties Title Notes >> input[type="text"]'); - await page.fill('text=Properties Title Notes >> input[type="text"]', TEST_FOLDER); - await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab'); + await page.getByLabel('Title', { exact: true }).fill(TEST_FOLDER); + await page.getByLabel('Title', { exact: true }).press('Tab'); //Required Field Form Validation is corrected - await expect(page.locator('button:has-text("OK")')).toBeEnabled(); + await expect(page.getByLabel('Save')).toBeEnabled(); await expect(page.locator('.c-form-row__state-indicator').first()).not.toHaveClass(/invalid/); //Finish Creating Domain Object - await Promise.all([page.waitForNavigation(), page.click('button:has-text("OK")')]); + await page.getByLabel('Save').click(); //Verify that the Domain Object has been created with the corrected title property await expect(page.locator('.l-browse-bar__object-name')).toContainText(TEST_FOLDER); @@ -87,8 +85,8 @@ test.describe('Form File Input Behavior', () => { await page.getByRole('button', { name: 'Save' }).click(); - const type = await page.locator('#file-input-type').textContent(); - await expect(type).toBe(`"string"`); + const type = page.locator('#file-input-type'); + await expect(type).toHaveText(`"string"`); }); test('Can select an image file type', async ({ page }) => { @@ -101,8 +99,8 @@ test.describe('Form File Input Behavior', () => { await page.getByRole('button', { name: 'Save' }).click(); - const type = await page.locator('#file-input-type').textContent(); - await expect(type).toBe(`"object"`); + const type = page.locator('#file-input-type'); + await expect(type).toHaveText(`"object"`); }); }); @@ -123,11 +121,11 @@ test.describe('Persistence operations @addInit', () => { await page.getByRole('button', { name: 'Create' }).click(); - await page.click('text=Condition Set'); + await page.getByRole('menuitem', { name: 'Condition Set' }).click(); await page.locator('form[name="mctForm"] >> text=Persistence Testing').click(); - const okButton = page.locator('button:has-text("OK")'); + const okButton = page.getByLabel('Save'); await expect(okButton).toBeDisabled(); }); }); @@ -158,14 +156,12 @@ test.describe('Persistence operations @couchdb', () => { }); // Open the edit form for the clock object - await page.click('button[title="More actions"]'); - await page.click('li[title="Edit properties of this object."]'); + await page.getByLabel('More actions').click(); + await page.getByLabel('Edit Properties...').click(); // Modify the display format from default 12hr -> 24hr and click 'Save' - await page - .locator('select[aria-label="12 or 24 hour clock"]') - .selectOption({ value: 'clock24' }); - await page.click('button[aria-label="Save"]'); + await page.getByLabel('12 or 24 hour clock').selectOption({ value: 'clock24' }); + await page.getByLabel('Save').click(); await expect .poll(() => putRequestCount, { @@ -188,8 +184,8 @@ test.describe('Persistence operations @couchdb', () => { // Both pages: Go to baseURL await Promise.all([ - page.goto('./', { waitUntil: 'networkidle' }), - page2.goto('./', { waitUntil: 'networkidle' }) + page.goto('./', { waitUntil: 'domcontentloaded' }), + page2.goto('./', { waitUntil: 'domcontentloaded' }) ]); //Slow down the test a bit @@ -202,14 +198,14 @@ test.describe('Persistence operations @couchdb', () => { // Both pages: Click the Create button await Promise.all([ - page.click('button:has-text("Create")'), - page2.click('button:has-text("Create")') + page.getByRole('button', { name: 'Create' }).click(), + page2.getByRole('button', { name: 'Create' }).click() ]); // Both pages: Click "Clock" in the Create menu await Promise.all([ - page.click(`li[role='menuitem']:text("Clock")`), - page2.click(`li[role='menuitem']:text("Clock")`) + page.getByRole('menuitem', { name: 'Clock' }).click(), + page2.getByRole('menuitem', { name: 'Clock' }).click() ]); // Generate unique names for both objects @@ -236,9 +232,9 @@ test.describe('Persistence operations @couchdb', () => { // conditions for a conflict error from the first page. await Promise.all([ page2.waitForLoadState(), - page2.click('[aria-label="Save"]'), + page2.getByLabel('Save').click(), // Wait for Save Banner to appear - page2.waitForSelector('.c-message-banner__message') + page2.locator('.c-message-banner__message').hover({ trial: true }) ]); // Close Page 2, we're done with it. @@ -249,9 +245,9 @@ test.describe('Persistence operations @couchdb', () => { // the composition of the parent folder. await Promise.all([ page.waitForLoadState(), - page.click('[aria-label="Save"]'), + page.getByLabel('Save').click(), // Wait for Save Banner to appear - page.waitForSelector('.c-message-banner__message') + page.locator('.c-message-banner__message').hover({ trial: true }) ]); // Page 1: Verify that the conflict has occurred and an error notification is displayed. diff --git a/e2e/tests/functional/notification.e2e.spec.js b/e2e/tests/functional/notification.e2e.spec.js index 02984225dc..daee85aaf5 100644 --- a/e2e/tests/functional/notification.e2e.spec.js +++ b/e2e/tests/functional/notification.e2e.spec.js @@ -50,13 +50,13 @@ test.describe('Notifications List', () => { }); // Verify that there is a button with aria-label "Review 2 Notifications" - expect(await page.locator('button[aria-label="Review 2 Notifications"]').count()).toBe(1); + await expect(page.locator('button[aria-label="Review 2 Notifications"]')).toHaveCount(1); // Click on button with aria-label "Review 2 Notifications" - await page.click('button[aria-label="Review 2 Notifications"]'); + await page.getByLabel('Review 2 Notifications').click(); // Click on button with aria-label="Dismiss notification of Error message" - await page.click('button[aria-label="Dismiss notification of Error message"]'); + await page.getByLabel('Dismiss notification of Error message').click(); // Verify there is no a notification (listitem) with the text "Error message" since it was dismissed expect(await page.locator('div[role="dialog"] div[role="listitem"]').innerText()).not.toContain( @@ -69,10 +69,10 @@ test.describe('Notifications List', () => { ); // Click on button with aria-label="Dismiss notification of Alert message" - await page.click('button[aria-label="Dismiss notification of Alert message"]'); + await page.getByLabel('Dismiss notification of Alert message').click(); // Verify that there is no dialog since the notification overlay was closed automatically after all notifications were dismissed - expect(await page.locator('div[role="dialog"]').count()).toBe(0); + await expect(page.locator('div[role="dialog"]')).toHaveCount(0); }); }); diff --git a/e2e/tests/functional/planning/plan.e2e.spec.js b/e2e/tests/functional/planning/plan.e2e.spec.js index 61784bfa9a..a34fe23732 100644 --- a/e2e/tests/functional/planning/plan.e2e.spec.js +++ b/e2e/tests/functional/planning/plan.e2e.spec.js @@ -21,7 +21,7 @@ *****************************************************************************/ import fs from 'fs'; -import { createPlanFromJSON } from '../../../appActions.js'; +import { createPlanFromJSON, navigateToObjectWithFixedTimeBounds } from '../../../appActions.js'; import { addPlanGetInterceptor, assertPlanActivities, @@ -81,9 +81,7 @@ test.describe('Plan', () => { } // Switch to fixed time mode with all plan events within the bounds - await page.goto( - `${plan.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=plan.view` - ); + await navigateToObjectWithFixedTimeBounds(page, plan.url, startBound, endBound); // select the first activity in the list await page.getByText('Past event 1').click(); diff --git a/e2e/tests/functional/planning/timelist.e2e.spec.js b/e2e/tests/functional/planning/timelist.e2e.spec.js index a0e600d63f..abfef4a584 100644 --- a/e2e/tests/functional/planning/timelist.e2e.spec.js +++ b/e2e/tests/functional/planning/timelist.e2e.spec.js @@ -21,7 +21,11 @@ *****************************************************************************/ import fs from 'fs'; -import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../../appActions.js'; +import { + createDomainObjectWithDefaults, + createPlanFromJSON, + navigateToObjectWithFixedTimeBounds +} from '../../../appActions.js'; import { expect, test } from '../../../pluginFixtures.js'; const examplePlanSmall1 = JSON.parse( @@ -59,14 +63,12 @@ test.describe('Time List', () => { const endBound = lastActivity.end; // Switch to fixed time mode with all plan events within the bounds - await page.goto( - `${timelist.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=timelist.view` - ); + await navigateToObjectWithFixedTimeBounds(page, timelist.url, startBound, endBound); // Verify all events are displayed const eventCount = await page.getByRole('row').count(); // subtracting one for the header - await expect(eventCount - 1).toEqual(firstGroupItems.length); + expect(eventCount - 1).toEqual(firstGroupItems.length); }); await test.step('Does not show milliseconds in times', async () => { @@ -124,9 +126,7 @@ test("View a timelist in expanded view, verify all the activities are displayed const endBound = lastActivity.end; // Switch to fixed time mode with all plan events within the bounds - await page.goto( - `${timelist.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=timelist.view` - ); + await navigateToObjectWithFixedTimeBounds(page, timelist.url, startBound, endBound); // Change the object to edit mode await page.getByRole('button', { name: 'Edit Object' }).click(); @@ -142,7 +142,7 @@ test("View a timelist in expanded view, verify all the activities are displayed // Verify all events are displayed const eventCount = await page.getByRole('row').count(); - await expect(eventCount).toEqual(firstGroupItems.length); + expect(eventCount).toEqual(firstGroupItems.length); }); await test.step('Shows activity properties when a row is selected in the expanded view', async () => { @@ -158,7 +158,7 @@ test("View a timelist in expanded view, verify all the activities are displayed await test.step("Verify absence of progress indication for an activity that's not in progress", async () => { // When an activity is not in progress, the progress pie is not visible - const hidden = await page.getByRole('row').locator('path').nth(1).isHidden(); - await expect(hidden).toBe(true); + const hidden = page.getByRole('row').locator('path').nth(1); + await expect(hidden).toBeHidden(); }); }); diff --git a/e2e/tests/functional/planning/timelistControlledClock.e2e.spec.js b/e2e/tests/functional/planning/timelistControlledClock.e2e.spec.js index 4baec66718..156d48aebe 100644 --- a/e2e/tests/functional/planning/timelistControlledClock.e2e.spec.js +++ b/e2e/tests/functional/planning/timelistControlledClock.e2e.spec.js @@ -22,12 +22,16 @@ /* Collection of Time List tests set to run with browser clock manipulate made possible with the -clockOptions plugin fixture. +page.clock() API. */ import fs from 'fs'; -import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../../appActions.js'; +import { + createDomainObjectWithDefaults, + createPlanFromJSON, + navigateToObjectWithRealTime +} from '../../../appActions.js'; import { createTimelistWithPlanAndSetActivityInProgress, getEarliestStartTime, @@ -93,14 +97,12 @@ const COUNTDOWN = Object.freeze({ SECONDS: 5 }); +const FIRST_ACTIVITY_SMALL_1 = getFirstActivity(examplePlanSmall1); + test.describe('Time List with controlled clock @clock', () => { - test.use({ - clockOptions: { - now: getEarliestStartTime(examplePlanSmall3), - shouldAdvanceTime: true - } - }); test.beforeEach(async ({ page }) => { + await page.clock.install({ time: getEarliestStartTime(examplePlanSmall3) }); + await page.clock.resume(); await page.goto('./', { waitUntil: 'domcontentloaded' }); // Create Time List const timelist = await createDomainObjectWithDefaults(page, { @@ -115,9 +117,7 @@ test.describe('Time List with controlled clock @clock', () => { }); // Navigate to the Time List in real-time mode - await page.goto( - `${timelist.url}?tc.mode=local&tc.startDelta=900000&tc.endDelta=1800000&tc.timeSystem=utc&view=grid` - ); + await navigateToObjectWithRealTime(page, timelist.url, 900000, 1800000); //Expand the viewport to show the entire time list await page.getByLabel('Collapse Inspect Pane').click(); @@ -172,16 +172,9 @@ test.describe('Time List with controlled clock @clock', () => { }); test.describe('Activity progress when activity is in the future @clock', () => { - const firstActivity = getFirstActivity(examplePlanSmall1); - - test.use({ - clockOptions: { - now: firstActivity.start - 1, - shouldAdvanceTime: true - } - }); - test.beforeEach(async ({ page }) => { + await page.clock.install({ time: FIRST_ACTIVITY_SMALL_1.start - 1 }); + await page.clock.resume(); await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1); }); @@ -195,18 +188,13 @@ test.describe('Activity progress when activity is in the future @clock', () => { }); test.describe('Activity progress when now is between start and end of the activity @clock', () => { - const firstActivity = getFirstActivity(examplePlanSmall1); test.beforeEach(async ({ page }) => { + await page.clock.install({ time: FIRST_ACTIVITY_SMALL_1.start + 50000 }); + await page.clock.resume(); + await page.goto('./', { waitUntil: 'domcontentloaded' }); await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1); }); - test.use({ - clockOptions: { - now: firstActivity.start + 50000, - shouldAdvanceTime: true - } - }); - test('progress pie is partially filled', async ({ page }) => { const anActivity = page.getByRole('row').nth(0); const pathElement = anActivity.getByLabel('Activity in progress').locator('path'); @@ -216,16 +204,10 @@ test.describe('Activity progress when now is between start and end of the activi }); test.describe('Activity progress when now is after end of the activity @clock', () => { - const firstActivity = getFirstActivity(examplePlanSmall1); - - test.use({ - clockOptions: { - now: firstActivity.end + 10000, - shouldAdvanceTime: true - } - }); - test.beforeEach(async ({ page }) => { + await page.clock.install({ time: FIRST_ACTIVITY_SMALL_1.end + 10000 }); + await page.clock.resume(); + await page.goto('./', { waitUntil: 'domcontentloaded' }); await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1); }); diff --git a/e2e/tests/functional/planning/timestrip.e2e.spec.js b/e2e/tests/functional/planning/timestrip.e2e.spec.js index e6b36181c7..1d82071cd1 100644 --- a/e2e/tests/functional/planning/timestrip.e2e.spec.js +++ b/e2e/tests/functional/planning/timestrip.e2e.spec.js @@ -23,7 +23,8 @@ import { createDomainObjectWithDefaults, createPlanFromJSON, - setIndependentTimeConductorBounds + navigateToObjectWithFixedTimeBounds, + setFixedIndependentTimeConductorBounds } from '../../../appActions.js'; import { expect, test } from '../../../pluginFixtures.js'; @@ -73,7 +74,7 @@ const testPlan = { }; test.describe('Time Strip', () => { - test('Create two Time Strips, add a single Plan to both, and verify they can have separate Independent Time Contexts @unstable', async ({ + test('Create two Time Strips, add a single Plan to both, and verify they can have separate Independent Time Contexts', async ({ page }) => { test.info().annotations.push({ @@ -103,17 +104,17 @@ test.describe('Time Strip', () => { await page.goto(timestrip.url); // Expand the tree to show the plan - await page.click("button[title='Show selected item in tree']"); - await page.dragAndDrop(`role=treeitem[name=/${createdPlan.name}/]`, '.c-object-view'); - await page.click("button[title='Save']"); - await page.click("li[title='Save and Finish Editing']"); + await page.getByLabel('Show selected item in tree').click(); + await page + .getByLabel(`Navigate to ${createdPlan.name}`) + .dragTo(page.getByLabel('Object View')); + await page.getByLabel('Save').click(); + await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); const startBound = testPlan.TEST_GROUP[0].start; const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end; // Switch to fixed time mode with all plan events within the bounds - await page.goto( - `${timestrip.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=time-strip.view` - ); + await navigateToObjectWithFixedTimeBounds(page, timestrip.url, startBound, endBound); // Verify all events are displayed const eventCount = await page.locator('.activity-bounds').count(); @@ -131,7 +132,7 @@ test.describe('Time Strip', () => { const startBoundString = new Date(startBound).toISOString().replace('T', ' '); const endBoundString = new Date(endBound).toISOString().replace('T', ' '); - await setIndependentTimeConductorBounds(page, { + await setFixedIndependentTimeConductorBounds(page, { start: startBoundString, end: endBoundString }); @@ -149,9 +150,9 @@ test.describe('Time Strip', () => { expect(objectName).toBe(createdTimeStrip.name); // Drag the existing Plan onto the newly created Time Strip, and save. - await page.dragAndDrop(`role=treeitem[name=/${plan.name}/]`, '.c-object-view'); - await page.click("button[title='Save']"); - await page.click("li[title='Save and Finish Editing']"); + await page.getByLabel(`Navigate to ${plan.name}`).dragTo(page.getByLabel('Object View')); + await page.getByLabel('Save').click(); + await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); // All events should be displayed at this point because the // initial independent context bounds will match the global bounds @@ -163,7 +164,7 @@ test.describe('Time Strip', () => { const startBoundString = new Date(startBound).toISOString().replace('T', ' '); const endBoundString = new Date(endBound).toISOString().replace('T', ' '); - await setIndependentTimeConductorBounds(page, { + await setFixedIndependentTimeConductorBounds(page, { start: startBoundString, end: endBoundString }); diff --git a/e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js b/e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js index bfa1e6e015..6b76088b39 100644 --- a/e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js +++ b/e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js @@ -41,11 +41,10 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage @2p', () => const context = await browser.newContext(); const page = await context.newPage(); await page.goto('./', { waitUntil: 'domcontentloaded' }); - await page.getByRole('button', { name: 'Create' }).click(); - - await page.locator('li[role="menuitem"]:has-text("Condition Set")').click(); - - await Promise.all([page.waitForNavigation(), page.click('button:has-text("OK")')]); + const conditionSet = await createDomainObjectWithDefaults(page, { + type: 'Condition Set', + name: 'Unnamed Condition Set' + }); //Save localStorage for future test execution await context.storageState({ @@ -55,7 +54,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage @2p', () => }); //Set object identifier from url - conditionSetUrl = page.url(); + conditionSetUrl = conditionSet.url; await page.close(); }); @@ -68,44 +67,39 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage @2p', () => }); //Begin suite of tests again localStorage - test.fixme( - 'Condition set object properties persist in main view and inspector @localStorage', - async ({ page }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/7421' - }); - //Navigate to baseURL with injected localStorage - await page.goto(conditionSetUrl, { waitUntil: 'networkidle' }); + test('Condition set object properties persist in main view and inspector after reload @localStorage', async ({ + page + }) => { + //Navigate to baseURL with injected localStorage + await page.goto(conditionSetUrl, { waitUntil: 'domcontentloaded' }); - //Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto() - await expect - .soft(page.locator('.l-browse-bar__object-name')) - .toContainText('Unnamed Condition Set'); + //Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto() + await expect.soft(page.getByRole('main')).toContainText('Unnamed Condition Set'); - //Assertions on loaded Condition Set in Inspector - expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy(); + //Assertions on loaded Condition Set in Inspector + await expect( + page.getByLabel('Title inspector properties').getByLabel('inspector property value') + ).toContainText('Unnamed Condition Set'); - //Reload Page - await Promise.all([page.reload(), page.waitForLoadState('networkidle')]); + //Reload Page + await page.reload({ waitUntil: 'domcontentloaded' }); + + //Re-verify after reload + await expect(page.getByRole('main')).toContainText('Unnamed Condition Set'); + + //Assertions on loaded Condition Set in Inspector + await expect( + page.getByLabel('Title inspector properties').getByLabel('inspector property value') + ).toContainText('Unnamed Condition Set'); + }); - //Re-verify after reload - await expect - .soft(page.locator('.l-browse-bar__object-name')) - .toContainText('Unnamed Condition Set'); - //Assertions on loaded Condition Set in Inspector - expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy(); - } - ); test('condition set object can be modified on @localStorage', async ({ page, openmctConfig }) => { const { myItemsFolderName } = openmctConfig; - await page.goto(conditionSetUrl, { waitUntil: 'networkidle' }); + await page.goto(conditionSetUrl, { waitUntil: 'domcontentloaded' }); //Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto() - await expect - .soft(page.locator('.l-browse-bar__object-name')) - .toContainText('Unnamed Condition Set'); + await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); //Update the Condition Set properties // Click Edit Button @@ -151,7 +145,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage @2p', () => expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); //Reload Page - await Promise.all([page.reload(), page.waitForLoadState('networkidle')]); + await Promise.all([page.reload(), page.waitForLoadState('domcontentloaded')]); //Verify Main section reflects updated Name Property await expect @@ -213,7 +207,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage @2p', () => //Feature? //Domain Object is still available by direct URL after delete - await page.goto(conditionSetUrl, { waitUntil: 'networkidle' }); + await page.goto(conditionSetUrl, { waitUntil: 'domcontentloaded' }); await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); }); }); @@ -267,7 +261,7 @@ test.describe('Basic Condition Set Use', () => { await page.getByLabel('Edit Object').click(); // Expand the 'My Items' folder in the left tree - page.click('button[title="Show selected item in tree"]'); + await page.getByLabel('Show selected item in tree').click(); // Add the Alpha & Beta Sine Wave Generator to the Condition Set and save changes const treePane = page.getByRole('tree', { name: 'Main Tree' @@ -538,7 +532,7 @@ test.describe('Condition Set Composition', () => { .getByLabel(`${exampleTelemetry.name} Context Menu`) .getByRole('menuitem', { name: 'Remove' }) .click(); - await page.getByRole('button', { name: 'OK', exact: true }).click(); + await page.getByRole('button', { name: 'Ok', exact: true }).click(); await page .getByLabel(`Navigate to ${conditionSet.name} conditionSet Object`, { exact: true }) diff --git a/e2e/tests/functional/plugins/conditionSet/conditionSetOperations.e2e.spec.js b/e2e/tests/functional/plugins/conditionSet/conditionSetOperations.e2e.spec.js new file mode 100644 index 0000000000..5cd5d86d25 --- /dev/null +++ b/e2e/tests/functional/plugins/conditionSet/conditionSetOperations.e2e.spec.js @@ -0,0 +1,368 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2024, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/* +This test suite is dedicated to tests which verify the basic operations surrounding conditionSets. Note: this +suite is sharing state between tests which is considered an anti-pattern. Implementing in this way to +demonstrate some playwright for test developers. This pattern should not be re-used in other CRUD suites. +*/ + +import { + createDomainObjectWithDefaults, + createExampleTelemetryObject +} from '../../../../appActions.js'; +import { expect, test } from '../../../../pluginFixtures.js'; + +test.describe('Basic Condition Set Use', () => { + let conditionSet; + + test.beforeEach(async ({ page }) => { + // Open a browser, navigate to the main page, and wait until all network events to resolve + await page.goto('./', { waitUntil: 'domcontentloaded' }); + // Create a new condition set + conditionSet = await createDomainObjectWithDefaults(page, { + type: 'Condition Set', + name: 'Test Condition Set' + }); + }); + test('Creating a condition defaults the condition name to "Unnamed Condition"', async ({ + page + }) => { + await page.goto(conditionSet.url); + + // Change the object to edit mode + await page.getByLabel('Edit Object').click(); + + // Click Add Condition button + await page.locator('#addCondition').click(); + // Check that the new Unnamed Condition section appears + const numOfUnnamedConditions = await page + .locator('.c-condition__name', { hasText: 'Unnamed Condition' }) + .count(); + expect(numOfUnnamedConditions).toEqual(1); + }); + test('ConditionSet should display appropriate view options', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/5924' + }); + + await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + name: 'Alpha Sine Wave Generator' + }); + await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + name: 'Beta Sine Wave Generator' + }); + + await page.goto(conditionSet.url); + + // Change the object to edit mode + await page.getByLabel('Edit Object').click(); + + // Expand the 'My Items' folder in the left tree + await page.getByLabel('Show selected item in tree').click(); + // Add the Alpha & Beta Sine Wave Generator to the Condition Set and save changes + const treePane = page.getByRole('tree', { + name: 'Main Tree' + }); + const alphaGeneratorTreeItem = treePane.getByRole('treeitem', { + name: 'Alpha Sine Wave Generator' + }); + const betaGeneratorTreeItem = treePane.getByRole('treeitem', { + name: 'Beta Sine Wave Generator' + }); + const conditionCollection = page.locator('#conditionCollection'); + + await alphaGeneratorTreeItem.dragTo(conditionCollection); + await betaGeneratorTreeItem.dragTo(conditionCollection); + + await page.locator('button[title="Save"]').click(); + await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); + + await page.getByLabel('Open the View Switcher Menu').click(); + + await expect(page.getByRole('menuitem', { name: /Lad Table/ })).toBeHidden(); + await expect(page.getByRole('menuitem', { name: /Conditions View/ })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: /Plot/ })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: /Telemetry Table/ })).toBeVisible(); + await page.getByLabel('Plot').click(); + await expect( + page.getByLabel('Plot Legend Collapsed').getByText('Test Condition Set') + ).toBeVisible(); + await page.getByLabel('Open the View Switcher Menu').click(); + await page.getByLabel('Telemetry Table').click(); + await expect(page.getByRole('searchbox', { name: 'output filter input' })).toBeVisible(); + await page.getByLabel('Open the View Switcher Menu').click(); + await page.getByLabel('Conditions View').click(); + await expect(page.getByText('Current Output')).toBeVisible(); + }); + test('ConditionSet has correct outputs when telemetry is and is not available', async ({ + page + }) => { + const exampleTelemetry = await createExampleTelemetryObject(page); + + await page.getByLabel('Show selected item in tree').click(); + await page.goto(conditionSet.url); + // Change the object to edit mode + await page.getByLabel('Edit Object').click(); + + // Create two conditions + await page.locator('#addCondition').click(); + await page.locator('#addCondition').click(); + await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition'); + await page.locator('#conditionCollection').getByRole('textbox').nth(1).fill('Second Condition'); + + // Add Telemetry to ConditionSet + const sineWaveGeneratorTreeItem = page + .getByRole('tree', { + name: 'Main Tree' + }) + .getByRole('treeitem', { + name: exampleTelemetry.name + }); + const conditionCollection = page.locator('#conditionCollection'); + await sineWaveGeneratorTreeItem.dragTo(conditionCollection); + + // Modify First Criterion + const firstCriterionTelemetry = page.locator( + '[aria-label="Criterion Telemetry Selection"] >> nth=0' + ); + firstCriterionTelemetry.selectOption({ label: exampleTelemetry.name }); + const firstCriterionMetadata = page.locator( + '[aria-label="Criterion Metadata Selection"] >> nth=0' + ); + firstCriterionMetadata.selectOption({ label: 'Sine' }); + const firstCriterionComparison = page.locator( + '[aria-label="Criterion Comparison Selection"] >> nth=0' + ); + firstCriterionComparison.selectOption({ label: 'is greater than or equal to' }); + const firstCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=0'); + await firstCriterionInput.fill('0'); + + // Modify First Criterion + const secondCriterionTelemetry = page.locator( + '[aria-label="Criterion Telemetry Selection"] >> nth=1' + ); + secondCriterionTelemetry.selectOption({ label: exampleTelemetry.name }); + + const secondCriterionMetadata = page.locator( + '[aria-label="Criterion Metadata Selection"] >> nth=1' + ); + secondCriterionMetadata.selectOption({ label: 'Sine' }); + + const secondCriterionComparison = page.locator( + '[aria-label="Criterion Comparison Selection"] >> nth=1' + ); + secondCriterionComparison.selectOption({ label: 'is less than' }); + + const secondCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=1'); + await secondCriterionInput.fill('0'); + + // Save ConditionSet + await page.locator('button[title="Save"]').click(); + await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); + + // Validate that the condition set is evaluating and outputting + // the correct value when the underlying telemetry subscription is active. + let outputValue = page.getByLabel('Current Output Value'); + await expect(outputValue).toHaveText('false'); + + await page.goto(exampleTelemetry.url); + + // Edit SWG to add 8 second loading delay to simulate the case + // where telemetry is not available. + await page.getByTitle('More actions').click(); + await page.getByRole('menuitem', { name: 'Edit Properties...' }).click(); + await page.getByRole('spinbutton', { name: 'Loading Delay (ms)' }).fill('8000'); + await page.getByLabel('Save').click(); + + // Expect that the output value is blank or '---' if the + // underlying telemetry subscription is not active. + await page.goto(conditionSet.url); + await expect(outputValue).toHaveText('---'); + }); + + test('ConditionSet has correct outputs when test data is enabled', async ({ page }) => { + const exampleTelemetry = await createExampleTelemetryObject(page); + + await page.getByLabel('Show selected item in tree').click(); + await page.goto(conditionSet.url); + // Change the object to edit mode + await page.getByLabel('Edit Object').click(); + + // Create two conditions + await page.locator('#addCondition').click(); + await page.locator('#addCondition').click(); + await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition'); + await page.locator('#conditionCollection').getByRole('textbox').nth(1).fill('Second Condition'); + + // Add Telemetry to ConditionSet + const sineWaveGeneratorTreeItem = page + .getByRole('tree', { + name: 'Main Tree' + }) + .getByRole('treeitem', { + name: exampleTelemetry.name + }); + const conditionCollection = page.locator('#conditionCollection'); + await sineWaveGeneratorTreeItem.dragTo(conditionCollection); + + // Modify First Criterion + const firstCriterionTelemetry = page.locator( + '[aria-label="Criterion Telemetry Selection"] >> nth=0' + ); + firstCriterionTelemetry.selectOption({ label: exampleTelemetry.name }); + const firstCriterionMetadata = page.locator( + '[aria-label="Criterion Metadata Selection"] >> nth=0' + ); + firstCriterionMetadata.selectOption({ label: 'Sine' }); + const firstCriterionComparison = page.locator( + '[aria-label="Criterion Comparison Selection"] >> nth=0' + ); + firstCriterionComparison.selectOption({ label: 'is greater than or equal to' }); + const firstCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=0'); + await firstCriterionInput.fill('0'); + + // Modify Second Criterion + const secondCriterionTelemetry = page.locator( + '[aria-label="Criterion Telemetry Selection"] >> nth=1' + ); + await secondCriterionTelemetry.selectOption({ label: exampleTelemetry.name }); + + const secondCriterionMetadata = page.locator( + '[aria-label="Criterion Metadata Selection"] >> nth=1' + ); + await secondCriterionMetadata.selectOption({ label: 'Sine' }); + + const secondCriterionComparison = page.locator( + '[aria-label="Criterion Comparison Selection"] >> nth=1' + ); + await secondCriterionComparison.selectOption({ label: 'is less than' }); + + const secondCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=1'); + await secondCriterionInput.fill('0'); + + // Enable test data + await page.getByLabel('Apply Test Data').nth(1).click(); + const testDataTelemetry = page.locator('[aria-label="Test Data Telemetry Selection"] >> nth=0'); + await testDataTelemetry.selectOption({ label: exampleTelemetry.name }); + + const testDataMetadata = page.locator('[aria-label="Test Data Metadata Selection"] >> nth=0'); + await testDataMetadata.selectOption({ label: 'Sine' }); + + const testInput = page.locator('[aria-label="Test Data Input"] >> nth=0'); + await testInput.fill('0'); + + // Validate that the condition set is evaluating and outputting + // the correct value when the underlying telemetry subscription is active. + let outputValue = page.getByLabel('Current Output Value'); + await expect(outputValue).toHaveText('false'); + + await page.goto(exampleTelemetry.url); + }); + + test.fixme('Ensure condition sets work with telemetry like operator status', ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/7484' + }); + }); +}); + +test.describe('Condition Set Composition', () => { + let conditionSet; + let exampleTelemetry; + + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + // Create Condition Set + conditionSet = await createDomainObjectWithDefaults(page, { + type: 'Condition Set' + }); + + // Create Telemetry Object as child to Condition Set + exampleTelemetry = await createExampleTelemetryObject(page, conditionSet.uuid); + + // Edit Condition Set + await page.goto(conditionSet.url); + await page.getByRole('button', { name: 'Edit Object' }).click(); + + // Add Condition to Condition Set + await page.getByRole('button', { name: 'Add Condition' }).click(); + + // Enter Condition Output + await page.getByLabel('Condition Name Input').first().fill('Negative'); + await page.getByLabel('Condition Output Type').first().selectOption({ value: 'string' }); + await page.getByLabel('Condition Output String').first().fill('Negative'); + + // Condition Trigger default is okay so no change needed to form + + // Enter Condition Criterion + await page.getByLabel('Criterion Telemetry Selection').first().selectOption({ value: 'all' }); + await page.getByLabel('Criterion Metadata Selection').first().selectOption({ value: 'sin' }); + await page + .locator('select[aria-label="Criterion Comparison Selection"]') + .first() + .selectOption({ value: 'lessThan' }); + await page.getByLabel('Criterion Input').first().fill('0'); + + // Save the Condition Set + await page.getByRole('button', { name: 'Save' }).click(); + await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); + }); + + test('You can remove telemetry from a condition set with existing conditions', async ({ + page + }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/7710' + }); + + await page.getByLabel('Expand My Items folder').click(); + await page.getByLabel(`Expand ${conditionSet.name} conditionSet`).click(); + + await page + .getByLabel(`Navigate to ${exampleTelemetry.name}`, { exact: false }) + .click({ button: 'right' }); + + await page + .getByLabel(`${exampleTelemetry.name} Context Menu`) + .getByRole('menuitem', { name: 'Remove' }) + .click(); + await page.getByRole('button', { name: 'Ok', exact: true }).click(); + + await page + .getByLabel(`Navigate to ${conditionSet.name} conditionSet Object`, { exact: true }) + .click(); + await page.getByRole('button', { name: 'Edit Object' }).click(); + await page.getByRole('tab', { name: 'Elements' }).click(); + expect( + await page + .getByRole('tabpanel', { name: 'Inspector Views' }) + .getByRole('listitem', { name: exampleTelemetry.name }) + .count() + ).toEqual(0); + }); +}); diff --git a/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js b/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js index a187c88522..5b69dc6b90 100644 --- a/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js +++ b/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js @@ -24,8 +24,8 @@ import { fileURLToPath } from 'url'; import { createDomainObjectWithDefaults, navigateToObjectWithFixedTimeBounds, + setFixedIndependentTimeConductorBounds, setFixedTimeMode, - setIndependentTimeConductorBounds, setRealTimeMode, setStartOffset } from '../../../../appActions.js'; @@ -75,18 +75,12 @@ test.describe('Display Layout Sub-object Actions @localStorage', () => { const TEST_FIXED_END_TIME = TEST_FIXED_START_TIME + 3600000; // 2024-11-11 20:11:11.000Z // Verify the ITC has the expected initial bounds - expect( - await page - .getByLabel('Child Overlay Plot 1 Frame Controls') - .getByLabel('Start bounds') - .textContent() - ).toEqual(INIT_ITC_START_BOUNDS); - expect( - await page - .getByLabel('Child Overlay Plot 1 Frame Controls') - .getByLabel('End bounds') - .textContent() - ).toEqual(INIT_ITC_END_BOUNDS); + await expect( + page.getByLabel('Child Overlay Plot 1 Frame Controls').getByLabel('Start bounds') + ).toHaveText(INIT_ITC_START_BOUNDS); + await expect( + page.getByLabel('Child Overlay Plot 1 Frame Controls').getByLabel('End bounds') + ).toHaveText(INIT_ITC_END_BOUNDS); // Update the global fixed bounds to 2024-11-11 19:11:11.000Z / 2024-11-11 20:11:11.000Z const url = page.url().split('?')[0]; @@ -98,18 +92,12 @@ test.describe('Display Layout Sub-object Actions @localStorage', () => { ); // ITC bounds should still match the initial ITC bounds - expect( - await page - .getByLabel('Child Overlay Plot 1 Frame Controls') - .getByLabel('Start bounds') - .textContent() - ).toEqual(INIT_ITC_START_BOUNDS); - expect( - await page - .getByLabel('Child Overlay Plot 1 Frame Controls') - .getByLabel('End bounds') - .textContent() - ).toEqual(INIT_ITC_END_BOUNDS); + await expect( + page.getByLabel('Child Overlay Plot 1 Frame Controls').getByLabel('Start bounds') + ).toHaveText(INIT_ITC_START_BOUNDS); + await expect( + page.getByLabel('Child Overlay Plot 1 Frame Controls').getByLabel('End bounds') + ).toHaveText(INIT_ITC_END_BOUNDS); // Open the Child Overlay Plot 1 in a new tab await page.getByLabel('View menu items').click(); @@ -120,28 +108,22 @@ test.describe('Display Layout Sub-object Actions @localStorage', () => { await newPage.waitForLoadState('domcontentloaded'); // Verify that the global time conductor bounds in the new page match the updated global bounds - expect( - await newPage.getByLabel('Global Time Conductor').getByLabel('Start bounds').textContent() - ).toEqual(NEW_GLOBAL_START_BOUNDS); - expect( - await newPage.getByLabel('Global Time Conductor').getByLabel('End bounds').textContent() - ).toEqual(NEW_GLOBAL_END_BOUNDS); + await expect(newPage.getByLabel('Global Time Conductor').getByLabel('Start bounds')).toHaveText( + NEW_GLOBAL_START_BOUNDS + ); + await expect(newPage.getByLabel('Global Time Conductor').getByLabel('End bounds')).toHaveText( + NEW_GLOBAL_END_BOUNDS + ); // Verify that the ITC is enabled in the new page await expect(newPage.getByLabel('Disable Independent Time Conductor')).toBeVisible(); // Verify that the ITC bounds in the new page match the original ITC bounds - expect( - await newPage - .getByLabel('Independent Time Conductor Panel') - .getByLabel('Start bounds') - .textContent() - ).toEqual(INIT_ITC_START_BOUNDS); - expect( - await newPage - .getByLabel('Independent Time Conductor Panel') - .getByLabel('End bounds') - .textContent() - ).toEqual(INIT_ITC_END_BOUNDS); + await expect( + newPage.getByLabel('Independent Time Conductor Panel').getByLabel('Start bounds') + ).toHaveText(INIT_ITC_START_BOUNDS); + await expect( + newPage.getByLabel('Independent Time Conductor Panel').getByLabel('End bounds') + ).toHaveText(INIT_ITC_END_BOUNDS); }); }); @@ -174,17 +156,17 @@ test.describe('Display Layout Toolbar Actions @localStorage', () => { test('can add/remove Image to a single layout', async ({ page }) => { const layoutObject = 'Image'; await test.step("Add and remove image element from the parent's layout", async () => { - expect(await page.getByLabel(`Move ${layoutObject} Frame`).count()).toBe(0); + await expect(page.getByLabel(`Move ${layoutObject} Frame`)).toHaveCount(0); await addLayoutObject(page, PARENT_DISPLAY_LAYOUT_NAME, layoutObject); - expect(await page.getByLabel(`Move ${layoutObject} Frame`).count()).toBe(1); + await expect(page.getByLabel(`Move ${layoutObject} Frame`)).toHaveCount(1); await removeLayoutObject(page, layoutObject); - expect(await page.getByLabel(`Move ${layoutObject} Frame`).count()).toBe(0); + await expect(page.getByLabel(`Move ${layoutObject} Frame`)).toHaveCount(0); }); await test.step("Add and remove image from the child's layout", async () => { await addLayoutObject(page, CHILD_DISPLAY_LAYOUT_NAME1, layoutObject); - expect(await page.getByLabel(`Move ${layoutObject} Frame`).count()).toBe(1); + await expect(page.getByLabel(`Move ${layoutObject} Frame`)).toHaveCount(1); await removeLayoutObject(page, layoutObject); - expect(await page.getByLabel(`Move ${layoutObject} Frame`).count()).toBe(0); + await expect(page.getByLabel(`Move ${layoutObject} Frame`)).toHaveCount(0); }); }); test(`can add/remove Box to a single layout`, async ({ page }) => { @@ -253,20 +235,17 @@ test.describe('Display Layout', () => { const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { name: new RegExp(sineWaveObject.name) }); - const layoutGridHolder = page.locator('.l-layout__grid-holder'); - await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder); + await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid')); await page.locator('button[title="Save"]').click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); // Subscribe to the Sine Wave Generator data // On getting data, check if the value found in the Display Layout is the most recent value // from the Sine Wave Generator - const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid); - const formattedTelemetryValue = getTelemValuePromise; - const displayLayoutValuePromise = await page.waitForSelector( - `text="${formattedTelemetryValue}"` - ); - const displayLayoutValue = await displayLayoutValuePromise.textContent(); + const getTelemValuePromise = subscribeToTelemetry(page, sineWaveObject.uuid); + const formattedTelemetryValue = await getTelemValuePromise; + await expect(page.getByText(formattedTelemetryValue)).toBeVisible(); + const displayLayoutValue = await page.getByText(formattedTelemetryValue).textContent(); const trimmedDisplayValue = displayLayoutValue.trim(); expect(trimmedDisplayValue).toBe(formattedTelemetryValue); @@ -298,24 +277,21 @@ test.describe('Display Layout', () => { const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { name: new RegExp(sineWaveObject.name) }); - const layoutGridHolder = page.locator('.l-layout__grid-holder'); - await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder); + await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid')); await page.locator('button[title="Save"]').click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); // Subscribe to the Sine Wave Generator data - const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid); + const getTelemValuePromise = subscribeToTelemetry(page, sineWaveObject.uuid); // Set an offset of 1 minute and then change the time mode to fixed to set a 1 minute historical window - await setStartOffset(page, { mins: '1' }); + await setStartOffset(page, { startMins: '1' }); await setFixedTimeMode(page); // On getting data, check if the value found in the Display Layout is the most recent value // from the Sine Wave Generator - const formattedTelemetryValue = getTelemValuePromise; - const displayLayoutValuePromise = await page.waitForSelector( - `text="${formattedTelemetryValue}"` - ); - const displayLayoutValue = await displayLayoutValuePromise.textContent(); + const formattedTelemetryValue = await getTelemValuePromise; + await expect(page.getByText(formattedTelemetryValue)).toBeVisible(); + const displayLayoutValue = await page.getByText(formattedTelemetryValue).textContent(); const trimmedDisplayValue = displayLayoutValue.trim(); expect(trimmedDisplayValue).toBe(formattedTelemetryValue); @@ -340,8 +316,7 @@ test.describe('Display Layout', () => { const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { name: new RegExp(sineWaveObject.name) }); - const layoutGridHolder = page.locator('.l-layout__grid-holder'); - await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder); + await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid')); await page.locator('button[title="Save"]').click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); @@ -382,8 +357,7 @@ test.describe('Display Layout', () => { const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { name: new RegExp(sineWaveObject.name) }); - const layoutGridHolder = page.locator('.l-layout__grid-holder'); - await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder); + await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid')); await page.locator('button[title="Save"]').click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); @@ -428,8 +402,7 @@ test.describe('Display Layout', () => { const exampleImageryTreeItem = treePane.getByRole('treeitem', { name: new RegExp(exampleImageryObject.name) }); - let layoutGridHolder = page.locator('.l-layout__grid-holder'); - await exampleImageryTreeItem.dragTo(layoutGridHolder); + await exampleImageryTreeItem.dragTo(page.getByLabel('Layout Grid')); //adjust so that we can see the independent time conductor toggle // Adjust object height @@ -445,7 +418,7 @@ test.describe('Display Layout', () => { const startDate = '2021-12-30 01:01:00.000Z'; const endDate = '2021-12-30 01:11:00.000Z'; - await setIndependentTimeConductorBounds(page, { start: startDate, end: endDate }); + await setFixedIndependentTimeConductorBounds(page, { start: startDate, end: endDate }); // check image date await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible(); @@ -485,9 +458,8 @@ test.describe('Display Layout', () => { name: new RegExp(sineWaveObject.name) }); - let layoutGridHolder = page.locator('.l-layout__grid-holder'); // eslint-disable-next-line playwright/no-force-option - await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder, { force: true }); + await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'), { force: true }); await page.getByText('View type').click(); await page.getByText('Overlay Plot').click(); @@ -495,14 +467,13 @@ test.describe('Display Layout', () => { const anotherSineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { name: new RegExp(anotherSineWaveObject.name) }); - layoutGridHolder = page.locator('.l-layout__grid-holder'); // eslint-disable-next-line playwright/no-force-option - await anotherSineWaveGeneratorTreeItem.dragTo(layoutGridHolder, { force: true }); + await anotherSineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'), { force: true }); await page.getByText('View type').click(); await page.getByText('Overlay Plot').click(); - await page.locator('button[title="Save"]').click(); + await page.getByLabel('Save').click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); // Time to inspect some network traffic @@ -519,10 +490,10 @@ test.describe('Display Layout', () => { await page.reload(); // wait for annotations requests to be batched and requested - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Network requests for the composite telemetry with multiple items should be: // 1. a single batched request for annotations - expect(networkRequests.length).toBe(1); + await expect.poll(() => networkRequests, { timeout: 10000 }).toHaveLength(1); await setRealTimeMode(page); @@ -531,15 +502,15 @@ test.describe('Display Layout', () => { await page.reload(); // wait for annotations to not load (if we have any, we've got a problem) - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // In real time mode, we don't fetch annotations at all - expect(networkRequests.length).toBe(0); + await expect.poll(() => networkRequests, { timeout: 10000 }).toHaveLength(0); }); }); async function addAndRemoveDrawingObjectAndAssert(page, layoutObject, DISPLAY_LAYOUT_NAME) { - expect(await page.getByLabel(layoutObject, { exact: true }).count()).toBe(0); + await expect(page.getByLabel(layoutObject, { exact: true })).toHaveCount(0); await addLayoutObject(page, DISPLAY_LAYOUT_NAME, layoutObject); expect( await page @@ -549,7 +520,7 @@ async function addAndRemoveDrawingObjectAndAssert(page, layoutObject, DISPLAY_LA .count() ).toBe(1); await removeLayoutObject(page, layoutObject); - expect(await page.getByLabel(layoutObject, { exact: true }).count()).toBe(0); + await expect(page.getByLabel(layoutObject, { exact: true })).toHaveCount(0); } /** @@ -565,7 +536,7 @@ async function removeLayoutObject(page, layoutObject) { // eslint-disable-next-line playwright/no-force-option .click({ force: true }); await page.getByTitle('Delete the selected object').click(); - await page.getByRole('button', { name: 'OK', exact: true }).click(); + await page.getByRole('button', { name: 'Ok', exact: true }).click(); } /** @@ -584,10 +555,10 @@ async function addLayoutObject(page, layoutName, layoutObject) { .click(); if (layoutObject === 'Text') { await page.getByRole('textbox', { name: 'Text' }).fill('Hello, Universe!'); - await page.getByText('OK').click(); + await page.getByText('Ok').click(); } else if (layoutObject === 'Image') { await page.getByLabel('Image URL').fill(TINY_IMAGE_BASE64); - await page.getByText('OK').click(); + await page.getByText('Ok').click(); } } diff --git a/e2e/tests/functional/plugins/faultManagement/faultManagement.e2e.spec.js b/e2e/tests/functional/plugins/faultManagement/faultManagement.e2e.spec.js index 7b3898a34c..792c2abacc 100644 --- a/e2e/tests/functional/plugins/faultManagement/faultManagement.e2e.spec.js +++ b/e2e/tests/functional/plugins/faultManagement/faultManagement.e2e.spec.js @@ -62,17 +62,15 @@ test.describe('The Fault Management Plugin using example faults', () => { await selectFaultItem(page, 1); await page.getByRole('tab', { name: 'Config' }).click(); - const selectedFaultName = await page - .locator('.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname') - .textContent(); - const inspectorFaultNameCount = await page - .locator(`.c-inspector__properties >> :text("${selectedFaultName}")`) - .count(); + + const inspectorFaultName = page + .getByLabel('Source inspector properties') + .getByLabel('inspector property value'); await expect( page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').first() ).toHaveClass(/is-selected/); - expect(inspectorFaultNameCount).toEqual(1); + await expect(inspectorFaultName).toHaveCount(1); }); test('When selecting multiple faults, no specific fault information is shown in the inspector', async ({ @@ -110,13 +108,13 @@ test.describe('The Fault Management Plugin using example faults', () => { // check it is removed from standard view const afterShelvedFault = getFaultByName(page, shelvedFaultName); - expect(await afterShelvedFault.count()).toBe(0); + await expect(afterShelvedFault).toHaveCount(0); await changeViewTo(page, 'shelved'); const shelvedViewFault = getFaultByName(page, shelvedFaultName); - expect(await shelvedViewFault.count()).toBe(1); + await expect(shelvedViewFault).toHaveCount(1); }); test('Allows you to acknowledge a fault', async ({ page }) => { diff --git a/e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js b/e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js index d82e4a87ee..41540834ae 100644 --- a/e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js +++ b/e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js @@ -24,7 +24,7 @@ import { fileURLToPath } from 'url'; import { createDomainObjectWithDefaults, - setIndependentTimeConductorBounds + setFixedIndependentTimeConductorBounds } from '../../../../appActions.js'; import { expect, test } from '../../../../pluginFixtures.js'; @@ -248,7 +248,7 @@ test.describe('Flexible Layout', () => { await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); // flip on independent time conductor - await setIndependentTimeConductorBounds(page, { + await setFixedIndependentTimeConductorBounds(page, { start: '2021-12-30 01:01:00.000Z', end: '2021-12-30 01:11:00.000Z' }); @@ -292,7 +292,7 @@ test.describe('Flexible Layout Toolbar Actions @localStorage', () => { await expect(page.getByRole('dialog', { name: 'Overlay' })).toContainText( 'This action will permanently delete this container from this Flexible Layout. Do you want to continue?' ); - await page.getByRole('button', { name: 'OK', exact: true }).click(); + await page.getByRole('button', { name: 'Ok', exact: true }).click(); expect(await containerHandles.count()).toEqual(2); }); test('Remove Frame', async ({ page }) => { @@ -302,7 +302,7 @@ test.describe('Flexible Layout Toolbar Actions @localStorage', () => { await expect(page.getByRole('dialog', { name: 'Overlay' })).toContainText( 'This action will remove this frame from this Flexible Layout. Do you want to continue?' ); - await page.getByRole('button', { name: 'OK', exact: true }).click(); + await page.getByRole('button', { name: 'Ok', exact: true }).click(); expect(await page.getByRole('group', { name: 'Frame' }).count()).toEqual(1); }); test('Columns/Rows Layout Toggle', async ({ page }) => { diff --git a/e2e/tests/functional/plugins/gauge/gauge.e2e.spec.js b/e2e/tests/functional/plugins/gauge/gauge.e2e.spec.js index d2dd067f30..d85c89883a 100644 --- a/e2e/tests/functional/plugins/gauge/gauge.e2e.spec.js +++ b/e2e/tests/functional/plugins/gauge/gauge.e2e.spec.js @@ -38,7 +38,7 @@ test.describe('Gauge', () => { await page.goto('./', { waitUntil: 'domcontentloaded' }); }); - test('Can add and remove telemetry sources @unstable', async ({ page }) => { + test('Can add and remove telemetry sources', async ({ page }) => { // Create the gauge with defaults const gauge = await createDomainObjectWithDefaults(page, { type: 'Gauge' }); @@ -53,6 +53,7 @@ test.describe('Gauge', () => { // the SWG appears in the elements pool await page.goto(gauge.url); await page.getByLabel('Edit Object').click(); + await page.getByRole('tab', { name: 'Elements' }).click(); await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible(); await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); @@ -65,38 +66,35 @@ test.describe('Gauge', () => { }); // Verify that the 'Replace telemetry source' modal appears and accept it - await expect - .soft( - page.locator( - 'text=This action will replace the current telemetry source. Do you want to continue?' - ) + await expect( + page.getByText( + 'This action will replace the current telemetry source. Do you want to continue?' ) - .toBeVisible(); - await page.click('text=Ok'); + ).toBeVisible(); + await page.getByRole('button', { name: 'Ok', exact: true }).click(); // Navigate to the gauge and verify that the new SWG // appears in the elements pool and the old one is gone await page.goto(gauge.url); await page.getByLabel('Edit Object').click(); - await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeHidden(); - await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg2.name}`)).toBeVisible(); + await page.getByRole('tab', { name: 'Elements' }).click(); + await expect(page.getByLabel(`Preview ${swg1.name}`)).toBeHidden(); + await expect(page.getByLabel(`Preview ${swg2.name}`)).toBeVisible(); await page.getByRole('button', { name: 'Save' }).click(); // Right click on the new SWG in the elements pool and delete it - await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({ + await page.getByLabel(`Preview ${swg2.name}`).click({ button: 'right' }); - await page.locator('li[title="Remove this object from its containing object."]').click(); + await page.getByLabel('Remove').click(); // Verify that the 'Remove object' confirmation modal appears and accept it - await expect - .soft( - page.locator( - 'text=Warning! This action will remove this object. Are you sure you want to continue?' - ) + await expect( + page.getByText( + 'Warning! This action will remove this object. Are you sure you want to continue?' ) - .toBeVisible(); - await page.click('text=Ok'); + ).toBeVisible(); + await page.getByRole('button', { name: 'Ok', exact: true }).click(); // Verify that the elements pool shows no elements await expect(page.locator('text="No contained elements"')).toBeVisible(); @@ -110,11 +108,11 @@ test.describe('Gauge', () => { await page.getByRole('button', { name: 'Create' }).click(); // Click the object specified by 'type' - await page.click(`li[role='menuitem']:text("Gauge")`); + await page.getByRole('menuitem', { name: 'Gauge' }).click(); // FIXME: We need better selectors for these custom form controls const displayCurrentValueSwitch = page.locator('.c-toggle-switch__slider >> nth=0'); - await displayCurrentValueSwitch.setChecked(false); - await page.click('button[aria-label="Save"]'); + await displayCurrentValueSwitch.uncheck(); + await page.getByLabel('Save').click(); // TODO: Verify changes in the UI }); @@ -126,24 +124,21 @@ test.describe('Gauge', () => { // Create the gauge with defaults await createDomainObjectWithDefaults(page, { type: 'Gauge' }); - await page.click('button[title="More actions"]'); - await page.click('li[role="menuitem"]:has-text("Edit Properties")'); + await page.getByLabel('More actions').click(); + await page.getByRole('menuitem', { name: 'Edit Properties...' }).click(); // FIXME: We need better selectors for these custom form controls const displayCurrentValueSwitch = page.locator('.c-toggle-switch__slider >> nth=0'); - await displayCurrentValueSwitch.setChecked(false); - await page.click('button[aria-label="Save"]'); + await displayCurrentValueSwitch.uncheck(); + await page.getByLabel('Save').click(); // TODO: Verify changes in the UI }); - test.fixme('Gauge does not display NaN when data not available', async ({ page }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/7421' - }); + test('Gauge does not display NaN when data not available', async ({ page }) => { // Create a Gauge const gauge = await createDomainObjectWithDefaults(page, { - type: 'Gauge' + type: 'Gauge', + name: 'Gauge with no data' }); // Create a Sine Wave Generator in the Gauge with a loading delay @@ -154,7 +149,7 @@ test.describe('Gauge', () => { await page.getByRole('menuitem', { name: /Edit Properties.../ }).click(); //Edit Example Telemetry Object to include 5s loading Delay - await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000'); + await page.getByLabel('Loading Delay (ms)', { exact: true }).fill('5000'); await page.getByRole('button', { name: 'Save' }).click(); @@ -162,9 +157,13 @@ test.describe('Gauge', () => { await page.waitForURL(`**/${gauge.uuid}/*`); // Nav to the Gauge - await page.goto(gauge.url); - const gaugeNoDataText = await page.locator('.js-dial-current-value tspan').textContent(); - expect(gaugeNoDataText).toBe('--'); + await page.goto(gauge.url, { waitUntil: 'domcontentloaded' }); + // Check that the value is not displayed + //TODO https://github.com/nasa/openmct/issues/7790 update this locator + await expect(page.getByTitle('Value is currently out of')).toHaveAttribute( + 'aria-valuenow', + '--' + ); }); test('Gauge enforces composition policy', async ({ page }) => { diff --git a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js index 224bc519ba..d75e104e0d 100644 --- a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js +++ b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js @@ -75,6 +75,7 @@ test.describe('Example Imagery Object', () => { const backgroundImage = page.getByLabel('Focused Image Element'); await backgroundImage.click({ button: 'right', + // Need force option here due to annotation overlay which blocks playwright's click // eslint-disable-next-line playwright/no-force-option force: true }); @@ -351,8 +352,8 @@ test.describe('Example Imagery Object', () => { }); test('Uses low fetch priority', async ({ page }) => { - const priority = await page.locator('.js-imageryView-image').getAttribute('fetchpriority'); - expect(priority).toBe('low'); + const priority = page.locator('.js-imageryView-image'); + await expect(priority).toHaveAttribute('fetchpriority', 'low'); }); }); @@ -362,7 +363,7 @@ test.describe('Example Imagery in Display Layout @clock', () => { test.beforeEach(async ({ page }) => { // We mock the clock so that we don't need to wait for time driven events // to verify functionality. - await page.clock.setSystemTime(MISSION_TIME); + await page.clock.install({ time: MISSION_TIME }); await page.clock.resume(); // Go to baseURL @@ -447,6 +448,8 @@ test.describe('Example Imagery in Display Layout @clock', () => { await page.locator('div[title="Resize object width"] > input').click(); await page.locator('div[title="Resize object width"] > input').fill('50'); + await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport(); + await performImageryViewOperationsAndAssert(page, displayLayout); }); @@ -528,7 +531,7 @@ test.describe('Example Imagery in Flexible layout @clock', () => { test.beforeEach(async ({ page }) => { // We mock the clock so that we don't need to wait for time driven events // to verify functionality. - await page.clock.setSystemTime(MISSION_TIME); + await page.clock.install({ time: MISSION_TIME }); await page.clock.resume(); await page.goto('./', { waitUntil: 'domcontentloaded' }); @@ -543,6 +546,8 @@ test.describe('Example Imagery in Flexible layout @clock', () => { // Navigate back to Flexible Layout await page.goto(flexibleLayout.url); + // Wait for image thumbnail auto-scroll to complete + await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport(); }); test('Can double-click on the image to view large image', async ({ page }) => { @@ -573,7 +578,7 @@ test.describe('Example Imagery in Tabs View @clock', () => { test.beforeEach(async ({ page }) => { // We mock the clock so that we don't need to wait for time driven events // to verify functionality. - await page.clock.setSystemTime(MISSION_TIME); + await page.clock.install({ time: MISSION_TIME }); await page.clock.resume(); await page.goto('./', { waitUntil: 'domcontentloaded' }); @@ -586,25 +591,21 @@ test.describe('Example Imagery in Tabs View @clock', () => { await page.getByRole('button', { name: 'Create' }).click(); // Click text=Example Imagery - await page.click('li[role="menuitem"]:has-text("Example Imagery")'); + await page.getByRole('menuitem', { name: 'Example Imagery' }).click(); // Clear and set Image load delay to minimum value await page.locator('input[type="number"]').clear(); await page.locator('input[type="number"]').fill(`${IMAGE_LOAD_DELAY}`); - // Click text=OK - await Promise.all([ - page.waitForNavigation({ waitUntil: 'networkidle' }), - page.click('button:has-text("OK")'), - //Wait for Save Banner to appear - page.waitForSelector('.c-message-banner__message') - ]); + await page.getByLabel('Save').click(); await expect(page.locator('.l-browse-bar__object-name')).toContainText( 'Unnamed Example Imagery' ); await page.goto(tabsView.url); + // Wait for image thumbnail auto-scroll to complete + await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport(); }); test('Imagery View operations @clock', async ({ page }) => { await performImageryViewOperationsAndAssert(page, tabsView); @@ -836,8 +837,7 @@ async function assertBackgroundImageUrlFromBackgroundCss(page) { * @param {import('@playwright/test').Page} page */ async function panZoomAndAssertImageProperties(page) { - const imageryHintsText = await page.locator('.c-imagery__hints').innerText(); - expect(expectedAltText).toEqual(imageryHintsText); + await expect(page.locator('.c-imagery__hints')).toContainText(expectedAltText); const zoomedBoundingBox = await page.getByLabel('Focused Image Element').boundingBox(); const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; @@ -1056,7 +1056,6 @@ async function createImageryViewWithShortDelay(page, { name, parent }) { /** * @param {import('@playwright/test').Page} page */ -// eslint-disable-next-line require-await async function waitForZoomAndPanTransitions(page) { // Wait for image to stabilize await page.getByLabel('Focused Image Element').hover({ trial: true }); diff --git a/e2e/tests/functional/plugins/importAndExportAsJSON/exportAsJson.e2e.spec.js b/e2e/tests/functional/plugins/importAndExportAsJSON/exportAsJson.e2e.spec.js index e8cc3c6344..753ae18d9d 100644 --- a/e2e/tests/functional/plugins/importAndExportAsJSON/exportAsJson.e2e.spec.js +++ b/e2e/tests/functional/plugins/importAndExportAsJSON/exportAsJson.e2e.spec.js @@ -26,10 +26,7 @@ This test suite is dedicated to tests which verify the basic operations surround import fs from 'fs/promises'; -import { - createDomainObjectWithDefaults, - openObjectTreeContextMenu -} from '../../../../appActions.js'; +import { createDomainObjectWithDefaults } from '../../../../appActions.js'; import { expect, test } from '../../../../baseFixtures.js'; import { navigateToFaultManagementWithExample } from '../../../../helper/faultUtils.js'; @@ -51,7 +48,10 @@ test.describe('ExportAsJSON', () => { await page.goto(folder.url); // Open context menu and initiate download - await openObjectTreeContextMenu(page, folder.url); + await page.getByLabel('Show selected item in tree').click(); + await page.getByRole('treeitem', { name: 'Expand e2e folder folder' }).click({ + button: 'right' + }); const [download] = await Promise.all([ page.waitForEvent('download'), // Waits for the download event page.getByLabel('Export as JSON').click() // Triggers the download @@ -105,7 +105,12 @@ test.describe('ExportAsJSON', () => { await page.goto(timer.url); //do this against parent folder.url, NOT timer.url child - await openObjectTreeContextMenu(page, folder.url); + // Open context menu and initiate download + await page.getByLabel('Show selected item in tree').click(); + await page.getByRole('treeitem', { name: 'Collapse e2e folder folder' }).click({ + button: 'right' + }); + // Open context menu and initiate download const [download] = await Promise.all([ page.waitForEvent('download'), // Waits for the download event @@ -141,18 +146,18 @@ test.describe('ExportAsJSON Disabled Actions', () => { }); test('Verify that the ExportAsJSON dropdown does not appear for the item X', async ({ page }) => { await page.getByLabel('More actions').click(); - await expect(await page.getByLabel('Export as JSON')).toHaveCount(0); + await expect(page.getByLabel('Export as JSON')).toHaveCount(0); await page.getByRole('treeitem', { name: 'Fault Management' }).click({ button: 'right' }); - await expect(await page.getByLabel('Export as JSON')).toHaveCount(0); + await expect(page.getByLabel('Export as JSON')).toHaveCount(0); }); }); test.describe('ExportAsJSON ProgressBar @couchdb', () => { let folder; test.beforeEach(async ({ page }) => { - await page.goto('./', { waitUntil: 'networkidle' }); + await page.goto('./', { waitUntil: 'domcontentloaded' }); // Perform actions to create the domain object folder = await createDomainObjectWithDefaults(page, { type: 'Folder' diff --git a/e2e/tests/functional/plugins/inspectorDataVisualization/numericData.e2e.spec.js b/e2e/tests/functional/plugins/inspectorDataVisualization/numericData.e2e.spec.js index d8ff8df76e..80e28bb5cd 100644 --- a/e2e/tests/functional/plugins/inspectorDataVisualization/numericData.e2e.spec.js +++ b/e2e/tests/functional/plugins/inspectorDataVisualization/numericData.e2e.spec.js @@ -36,7 +36,7 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat }); test('Can click on telemetry and see data in inspector @2p', async ({ page, context }) => { - const initStartBounds = await page.getByLabel('Start bounds').textContent(); + const initStartBounds = page.getByLabel('Start bounds'); const initEndBounds = await page.getByLabel('End bounds').textContent(); const exampleDataVisualizationSource = await createDomainObjectWithDefaults(page, { type: 'Example Data Visualization Source' @@ -81,7 +81,9 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat await expect(newPage).toHaveTitle('Second Sine Wave Generator'); // Verify that "Open in New Tab" preserves the time bounds - expect(initStartBounds).toEqual(await newPage.getByLabel('Start bounds').textContent()); + await expect(initStartBounds).toHaveText( + await newPage.getByLabel('Start bounds').textContent() + ); expect(initEndBounds).toEqual(await newPage.getByLabel('End bounds').textContent()); }); }); diff --git a/e2e/tests/functional/plugins/lad/lad.e2e.spec.js b/e2e/tests/functional/plugins/lad/lad.e2e.spec.js index 289b1690e8..fd2f385bd7 100644 --- a/e2e/tests/functional/plugins/lad/lad.e2e.spec.js +++ b/e2e/tests/functional/plugins/lad/lad.e2e.spec.js @@ -22,7 +22,7 @@ import { createDomainObjectWithDefaults, - openObjectTreeContextMenu, + navigateToObjectWithRealTime, setFixedTimeMode, setRealTimeMode, setStartOffset @@ -56,61 +56,61 @@ test.describe('Testing LAD table configuration', () => { await page.getByRole('tab', { name: 'LAD Table Configuration' }).click(); // make sure headers are visible initially - await expect(page.getByRole('cell', { name: 'Timestamp', exact: true })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'Type', exact: true })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'WATCH' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Units' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible(); // hide timestamp column await page.getByLabel('Timestamp', { exact: true }).uncheck(); - await expect(page.getByRole('cell', { name: 'Timestamp', exact: true })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'Type', exact: true })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'WATCH' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeHidden(); + await expect(page.getByRole('columnheader', { name: 'Units' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible(); // hide units & type column await page.getByLabel('Units').uncheck(); await page.getByLabel('Type', { exact: true }).uncheck(); - await expect(page.getByRole('cell', { name: 'Timestamp', exact: true })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'Type', exact: true })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'WATCH' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeHidden(); + await expect(page.getByRole('columnheader', { name: 'Units' })).toBeHidden(); + await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeHidden(); + await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible(); // hide WATCH column await page.getByLabel('WATCH').uncheck(); - await expect(page.getByRole('cell', { name: 'Timestamp', exact: true })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'Type', exact: true })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'WATCH' })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeHidden(); + await expect(page.getByRole('columnheader', { name: 'Units' })).toBeHidden(); + await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeHidden(); + await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeHidden(); + await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible(); // save and reload and verify they columns are still hidden - await page.locator('button[title="Save"]').click(); + await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); await page.reload(); - await expect(page.getByRole('cell', { name: 'Timestamp', exact: true })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'Type', exact: true })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'WATCH' })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeHidden(); + await expect(page.getByRole('columnheader', { name: 'Units' })).toBeHidden(); + await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeHidden(); + await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeHidden(); + await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible(); // Edit LAD table await page.getByLabel('Edit Object').click(); @@ -118,27 +118,27 @@ test.describe('Testing LAD table configuration', () => { // show timestamp column await page.getByLabel('Timestamp', { exact: true }).check(); - await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'Type', exact: true })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'Timestamp', exact: true })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'WATCH' })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Units' })).toBeHidden(); + await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeHidden(); + await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeHidden(); + await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible(); // save and reload and make sure timestamp is still visible - await page.locator('button[title="Save"]').click(); + await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); await page.reload(); - await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'Type', exact: true })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'Timestamp', exact: true })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'WATCH' })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Units' })).toBeHidden(); + await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeHidden(); + await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeHidden(); + await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible(); // Edit LAD table await page.getByLabel('Edit Object').click(); @@ -148,27 +148,27 @@ test.describe('Testing LAD table configuration', () => { await page.getByLabel('Units').check(); await page.getByLabel('Type', { exact: true }).check(); await page.getByLabel('WATCH').check(); - await expect(page.getByRole('cell', { name: 'Timestamp', exact: true })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'Type', exact: true })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'WATCH' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Units' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible(); // save and reload and make sure all columns are still visible await page.locator('button[title="Save"]').click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); await page.reload(); - await expect(page.getByRole('cell', { name: 'Timestamp', exact: true })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'Type', exact: true })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'WATCH' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Units' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible(); }); test('When adding something without Units, do not show Units column', async ({ page }) => { @@ -185,14 +185,14 @@ test.describe('Testing LAD table configuration', () => { await page.getByRole('tab', { name: 'LAD Table Configuration' }).click(); // make sure Sine Wave headers are visible initially too - await expect(page.getByRole('cell', { name: 'Timestamp', exact: true })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'Type', exact: true })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'WATCH' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Units' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible(); // save and reload and verify they columns are still hidden await page.getByLabel('Save').click(); @@ -201,25 +201,25 @@ test.describe('Testing LAD table configuration', () => { // Remove Sine Wave Generator openObjectTreeContextMenu(page, sineWaveObject.url); await page.getByRole('menuitem', { name: /Remove/ }).click(); - await page.getByRole('button', { name: 'OK', exact: true }).click(); + await page.getByRole('button', { name: 'Ok', exact: true }).click(); // Ensure Units & Limit columns are gone // as Event Generator don't have them await page.goto(ladTable.url); - await expect(page.getByRole('cell', { name: 'Timestamp', exact: true })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'Type', exact: true })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'WATCH' })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'WARNING' })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeHidden(); + await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Units' })).toBeHidden(); + await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeHidden(); + await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeHidden(); + await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeHidden(); + await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeHidden(); + await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeHidden(); }); test("LAD Tables don't allow selection of rows but does show context click menus", async ({ page }) => { - const cell = await page.locator('.js-first-data'); + const cell = page.locator('.js-first-data'); const userSelectable = await cell.evaluate((el) => { return window.getComputedStyle(el).getPropertyValue('user-select'); }); @@ -237,19 +237,21 @@ test.describe('Testing LAD table configuration', () => { }); }); -test.describe('Testing LAD table @unstable', () => { +test.describe('Testing LAD table', () => { let sineWaveObject; test.beforeEach(async ({ page }) => { await page.goto('./', { waitUntil: 'domcontentloaded' }); - await setRealTimeMode(page); // Create Sine Wave Generator sineWaveObject = await createDomainObjectWithDefaults(page, { type: 'Sine Wave Generator', name: 'Test Sine Wave Generator' }); + + // Switch to real time mode by navigating directly to the URL + await navigateToObjectWithRealTime(page, sineWaveObject.url); }); - test('telemetry value exactly matches latest telemetry value received in real time', async ({ + test('telemetry value exactly matches latest telemetry value received in realtime mode', async ({ page }) => { // Create LAD table @@ -261,23 +263,23 @@ test.describe('Testing LAD table @unstable', () => { await page.getByLabel('Edit Object').click(); // Expand the 'My Items' folder in the left tree - await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); + await page.getByLabel('Expand My Items').click(); // Add the Sine Wave Generator to the LAD table and save changes - await page.dragAndDrop('text=Test Sine Wave Generator', '.c-lad-table-wrapper'); - await page.locator('button[title="Save"]').click(); + await page.getByLabel('Preview Test Sine Wave').dragTo(page.locator('#lad-table-drop-area')); + await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); // Subscribe to the Sine Wave Generator data // On getting data, check if the value found in the LAD table is the most recent value // from the Sine Wave Generator - const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid); + const getTelemValuePromise = subscribeToTelemetry(page, sineWaveObject.uuid); const subscribeTelemValue = await getTelemValuePromise; - const ladTableValuePromise = await page.waitForSelector(`text="${subscribeTelemValue}"`); - const ladTableValue = await ladTableValuePromise.textContent(); + await expect(page.getByLabel('lad value')).toHaveText(subscribeTelemValue); + const ladTableValue = await page.getByText(subscribeTelemValue).textContent(); - expect(ladTableValue).toBe(subscribeTelemValue); + expect(ladTableValue).toEqual(subscribeTelemValue); }); - test('telemetry value exactly matches latest telemetry value received in fixed time', async ({ + test('telemetry value exactly matches latest telemetry value received in fixed time mode', async ({ page }) => { // Create LAD table @@ -289,25 +291,23 @@ test.describe('Testing LAD table @unstable', () => { await page.getByLabel('Edit Object').click(); // Expand the 'My Items' folder in the left tree - await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); + await page.getByLabel('Expand My Items').click(); // Add the Sine Wave Generator to the LAD table and save changes - await page.dragAndDrop('text=Test Sine Wave Generator', '.c-lad-table-wrapper'); - await page.locator('button[title="Save"]').click(); + await page.getByLabel('Preview Test Sine Wave').dragTo(page.locator('#lad-table-drop-area')); + await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); // Subscribe to the Sine Wave Generator data - const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid); + const getTelemValuePromise = subscribeToTelemetry(page, sineWaveObject.uuid); // Set an offset of 1 minute and then change the time mode to fixed to set a 1 minute historical window - await setStartOffset(page, { mins: '1' }); + await setRealTimeMode(page); + await setStartOffset(page, { startMins: '01' }); await setFixedTimeMode(page); // On getting data, check if the value found in the LAD table is the most recent value // from the Sine Wave Generator const subscribeTelemValue = await getTelemValuePromise; - const ladTableValuePromise = await page.waitForSelector(`text="${subscribeTelemValue}"`); - const ladTableValue = await ladTableValuePromise.textContent(); - - expect(ladTableValue).toBe(subscribeTelemValue); + await expect(page.getByLabel('lad value')).toHaveText(subscribeTelemValue); }); }); @@ -338,3 +338,18 @@ async function subscribeToTelemetry(page, objectIdentifier) { return getTelemValuePromise; } + +/** + * Open the given `domainObject`'s context menu from the object tree. + * Expands the path to the object and scrolls to it if necessary. + * + * @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' + }); +} diff --git a/e2e/tests/functional/plugins/lad/ladTable.e2e.spec.js b/e2e/tests/functional/plugins/lad/ladTable.e2e.spec.js index 42ca563ffe..10bae36f35 100644 --- a/e2e/tests/functional/plugins/lad/ladTable.e2e.spec.js +++ b/e2e/tests/functional/plugins/lad/ladTable.e2e.spec.js @@ -76,7 +76,7 @@ test.describe('LAD Table', () => { // Right-click the SWG treeitem context menu and click 'Remove' and confirm await page.getByRole('treeitem', { name: swg.name }).click({ button: 'right' }); await page.getByRole('menuitem', { name: 'Remove' }).click(); - await page.getByRole('button', { name: 'OK', exact: true }).click(); + await page.getByRole('button', { name: 'Ok', exact: true }).click(); // Assert that the SWG is no longer in the tree and the table is empty await expect(page.getByRole('treeitem', { name: swg.name })).toBeHidden(); diff --git a/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js index 296cd20f4e..070a99058e 100644 --- a/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js @@ -259,7 +259,7 @@ test.describe('Notebook export tests', () => { const exportedText = await streamToString(readStream); expect(exportedText).toContain('Foo bar entry'); }); - test.fixme('can export multiple notebook entries as text ', async ({ page }) => {}); + test.fixme('can export multiple notebook entries as text', async ({ page }) => {}); test.fixme('can export all notebook entry metdata', async ({ page }) => {}); test.fixme('can export all notebook tags', async ({ page }) => {}); test.fixme('can export all notebook snapshots', async ({ page }) => {}); @@ -296,7 +296,7 @@ test.describe('Notebook entry tests', () => { await expect(page.getByLabel('Notebook Entry Input')).toBeVisible(); await expect(page.getByLabel('Notebook Entry', { exact: true })).toHaveClass(/is-selected/); }); - test('When an object is dropped into a notebook, a new entry is created and it should be focused @unstable', async ({ + test('When an object is dropped into a notebook, a new entry is created and it should be focused', async ({ page }) => { // Create Overlay Plot @@ -320,7 +320,7 @@ test.describe('Notebook entry tests', () => { await expect(embed).toHaveClass(/icon-plot-overlay/); expect(embedName).toBe(overlayPlot.name); }); - test('When an object is dropped into a notebooks existing entry, it should be focused @unstable', async ({ + test('When an object is dropped into a notebooks existing entry, it should be focused', async ({ page }) => { // Create Overlay Plot @@ -354,19 +354,19 @@ test.describe('Notebook entry tests', () => { await page.goto(notebookObject.url); await nbUtils.enterTextEntry(page, 'First Entry'); - await page.hover('text="First Entry"'); - await page.click('button[title="Delete this entry"]'); - await page.getByRole('button', { name: 'Ok' }).filter({ hasText: 'Ok' }).click(); - await expect(page.locator('text="First Entry"')).toBeHidden(); + await page.getByLabel('Notebook Entry', { exact: true }).hover(); + await page.getByLabel('Delete this entry').click(); + await page.getByRole('button', { name: 'Ok', exact: true }).click(); + await expect(page.getByText('First Entry')).toBeHidden(); await nbUtils.enterTextEntry(page, 'Another First Entry'); await nbUtils.enterTextEntry(page, 'Second Entry'); await nbUtils.enterTextEntry(page, 'Third Entry'); - await page.hover('[aria-label="Notebook Entry"] >> nth=2'); - await page.click('button[title="Delete this entry"] >> nth=2'); - await page.getByRole('button', { name: 'Ok' }).filter({ hasText: 'Ok' }).click(); - await expect(page.locator('text="Third Entry"')).toBeHidden(); - await expect(page.locator('text="Another First Entry"')).toBeVisible(); - await expect(page.locator('text="Second Entry"')).toBeVisible(); + await page.getByLabel('Notebook Entry', { exact: true }).nth(2).hover(); + await page.getByLabel('Delete this entry').nth(2).click(); + await page.getByRole('button', { name: 'Ok', exact: true }).click(); + await expect(page.getByText('Third Entry')).toBeHidden(); + await expect(page.getByText('Another First Entry')).toBeVisible(); + await expect(page.getByText('Second Entry')).toBeVisible(); }); test('when a valid link is entered into a notebook entry, it becomes clickable when viewing', async ({ page @@ -383,7 +383,7 @@ test.describe('Notebook entry tests', () => { const validLink = page.locator(`a[href="${TEST_LINK}"]`); - expect(await validLink.count()).toBe(1); + await expect(validLink).toHaveCount(1); // Start waiting for popup before clicking. Note no await. const popupPromise = page.waitForEvent('popup'); @@ -410,7 +410,7 @@ test.describe('Notebook entry tests', () => { const invalidLink = page.locator(`a[href="${TEST_LINK}"]`); - expect(await invalidLink.count()).toBe(0); + await expect(invalidLink).toHaveCount(0); }); test('when a link is entered, but it is not in the whitelisted urls, it does not become clickable when viewing', async ({ page @@ -427,7 +427,7 @@ test.describe('Notebook entry tests', () => { const invalidLink = page.locator(`a[href="${TEST_LINK}"]`); - expect(await invalidLink.count()).toBe(0); + await expect(invalidLink).toHaveCount(0); }); test('when a valid link with a subdomain and a valid domain in the whitelisted urls is entered into a notebook entry, it becomes clickable when viewing', async ({ page @@ -444,7 +444,7 @@ test.describe('Notebook entry tests', () => { const validLink = page.locator(`a[href="${INVALID_TEST_LINK}"]`); - expect(await validLink.count()).toBe(1); + await expect(validLink).toHaveCount(1); }); test('when a valid secure link is entered into a notebook entry, it becomes clickable when viewing', async ({ page @@ -461,7 +461,7 @@ test.describe('Notebook entry tests', () => { const validLink = page.locator(`a[href="${TEST_LINK}"]`); - expect(await validLink.count()).toBe(1); + await expect(validLink).toHaveCount(1); // Start waiting for popup before clicking. Note no await. const popupPromise = page.waitForEvent('popup'); @@ -494,7 +494,7 @@ test.describe('Notebook entry tests', () => { const unsanitizedLink = page.locator(`a[href="${TEST_LINK_BAD}"]`); expect.soft(await sanitizedLink.count()).toBe(1); - expect(await unsanitizedLink.count()).toBe(0); + await expect(unsanitizedLink).toHaveCount(0); }); test('Can add markdown to a notebook entry', async ({ page }) => { await page.goto(notebookObject.url); diff --git a/e2e/tests/functional/plugins/notebook/notebookSnapshotImage.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebookSnapshotImage.e2e.spec.js index 15fd97b214..8159238e14 100644 --- a/e2e/tests/functional/plugins/notebook/notebookSnapshotImage.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/notebookSnapshotImage.e2e.spec.js @@ -76,7 +76,7 @@ test.describe('Snapshot image tests', () => { const secondThumbnail = page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).nth(1); await secondThumbnail.waitFor({ state: 'attached' }); // expect two embedded images now - expect(await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).count()).toBe(2); + await expect(page.getByRole('img', { name: 'favicon-96x96.png thumbnail' })).toHaveCount(2); await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More actions').click(); @@ -86,7 +86,7 @@ test.describe('Snapshot image tests', () => { await secondThumbnail.waitFor({ state: 'detached' }); // expect one embedded image now as we deleted the other - expect(await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).count()).toBe(1); + await expect(page.getByRole('img', { name: 'favicon-96x96.png thumbnail' })).toHaveCount(1); }); }); diff --git a/e2e/tests/functional/plugins/notebook/notebookWithCouchDB.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebookWithCouchDB.e2e.spec.js index 985af38447..e16117c331 100644 --- a/e2e/tests/functional/plugins/notebook/notebookWithCouchDB.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/notebookWithCouchDB.e2e.spec.js @@ -37,7 +37,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => { // Create Notebook testNotebook = await createDomainObjectWithDefaults(page, { type: 'Notebook' }); - await page.goto(testNotebook.url, { waitUntil: 'networkidle' }); + await page.goto(testNotebook.url, { waitUntil: 'domcontentloaded' }); }); test('Inspect Notebook Entry Network Requests', async ({ page }) => { @@ -55,15 +55,15 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => { // Waits for the next request with the specified url page.waitForRequest(`**/openmct/${testNotebook.uuid}`), // Triggers the request - page.click('[aria-label="Add Page"]') + page.getByLabel('Add Page').click() ]); // Ensures that there are no other network requests - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Assert that only two requests are made // Network Requests are: // 1) The actual POST to create the page - expect(notebookElementsRequests.length).toBe(1); + expect(notebookElementsRequests).toHaveLength(1); // Assert on request object expect(notebookUrlRequest.postDataJSON().metadata.name).toBe(testNotebook.name); @@ -77,7 +77,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => { // 2) The shared worker event from 👆 POST request notebookElementsRequests = []; await nbUtils.enterTextEntry(page, 'First Entry'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); expect(notebookElementsRequests.length).toBeLessThanOrEqual(2); // Add some tags @@ -120,8 +120,8 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => { expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(12); // Add two more pages - await page.click('[aria-label="Add Page"]'); - await page.click('[aria-label="Add Page"]'); + await page.getByLabel('Add Page').click(); + await page.getByLabel('Add Page').click(); // Add three entries await nbUtils.enterTextEntry(page, 'First Entry'); @@ -141,7 +141,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => { // 4) The shared worker event from 👆 POST request notebookElementsRequests = []; await nbUtils.enterTextEntry(page, 'Fourth Entry'); - page.waitForLoadState('networkidle'); + page.waitForLoadState('domcontentloaded'); expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4); @@ -153,7 +153,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => { // 4) The shared worker event from 👆 POST request notebookElementsRequests = []; await nbUtils.enterTextEntry(page, 'Fifth Entry'); - page.waitForLoadState('networkidle'); + page.waitForLoadState('domcontentloaded'); expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4); @@ -164,7 +164,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => { // 4) The shared worker event from 👆 POST request notebookElementsRequests = []; await nbUtils.enterTextEntry(page, 'Sixth Entry'); - page.waitForLoadState('networkidle'); + page.waitForLoadState('domcontentloaded'); expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4); }); @@ -227,7 +227,7 @@ async function addTagAndAwaitNetwork(page, tagName) { page.locator(`[aria-label="Autocomplete Options"] >> text=${tagName}`).click(), expect(page.locator(`[aria-label="Tag"]:has-text("${tagName}")`)).toBeVisible() ]); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); } /** @@ -246,5 +246,5 @@ async function removeTagAndAwaitNetwork(page, tagName) { ) ]); await expect(page.locator(`[aria-label="Tag"]:has-text("${tagName}")`)).toBeHidden(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); } diff --git a/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js b/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js index 9b66e044f2..d57ebb754d 100644 --- a/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js @@ -20,7 +20,6 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { openObjectTreeContextMenu } from '../../../../appActions.js'; import { dragAndDropEmbed, enterTextEntry, @@ -51,27 +50,22 @@ test.describe('Restricted Notebook', () => { const restrictedNotebookTreeObject = page.locator(`a:has-text("${notebook.name}")`); // notebook tree object exists - expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1); + await expect(restrictedNotebookTreeObject).toHaveCount(1); // Click Remove Text await page.locator('li[role="menuitem"]:has-text("Remove")').click(); - // Click 'OK' on confirmation window and wait for save banner to appear - await Promise.all([ - page.waitForNavigation(), - page.locator('button:has-text("OK")').click(), - page.waitForSelector('.c-message-banner__message') - ]); + // Click 'Ok' on confirmation window + await page.locator('button:has-text("OK")').click(); // has been deleted - expect(await restrictedNotebookTreeObject.count()).toEqual(0); + await expect(restrictedNotebookTreeObject).toHaveCount(0); }); test('Can be locked if at least one page has one entry @addInit', async ({ page }) => { await enterTextEntry(page, TEST_TEXT); - const commitButton = page.locator('button:has-text("Commit Entries")'); - expect(await commitButton.count()).toEqual(1); + await expect(page.getByLabel('Commit Entries')).toHaveCount(1); }); }); @@ -86,20 +80,18 @@ test.describe('Restricted Notebook with at least one entry and with the page loc await page.locator('button.c-notebook__toggle-nav-button').click(); }); - test('Locked page should now be in a locked state @addInit @unstable', async ({ - page - }, testInfo) => { + test('Locked page should now be in a locked state @addInit', async ({ page }, testInfo) => { // eslint-disable-next-line playwright/no-skipped-test test.skip(testInfo.project === 'chrome-beta', 'Test is unreliable on chrome-beta'); // main lock message on page const lockMessage = page.locator( 'text=This page has been committed and cannot be modified or removed' ); - expect.soft(await lockMessage.count()).toEqual(1); + await expect(lockMessage).toHaveCount(1); // lock icon on page in sidebar const pageLockIcon = page.locator('ul.c-notebook__pages li div.icon-lock'); - expect.soft(await pageLockIcon.count()).toEqual(1); + await expect(pageLockIcon).toHaveCount(1); // no way to remove a restricted notebook with a locked page await openObjectTreeContextMenu(page, notebook.url); @@ -119,17 +111,14 @@ test.describe('Restricted Notebook with at least one entry and with the page loc await page.getByText('Unnamed Page').nth(1).fill(TEST_TEXT_NAME); // expect to be able to rename unlocked pages - const newPageElement = page.getByText(TEST_TEXT_NAME); - const newPageCount = await newPageElement.count(); - await newPageElement.press('Enter'); // exit contenteditable state - expect.soft(newPageCount).toEqual(1); + await page.getByText(TEST_TEXT_NAME).press('Enter'); // exit contenteditable state + await expect(page.locator('div').filter({ hasText: /^Test Page$/ })).toHaveCount(1); // enter test text await enterTextEntry(page, TEST_TEXT); // expect new page to be lockable - const commitButton = page.getByRole('button', { name: ' Commit Entries' }); - expect.soft(await commitButton.count()).toEqual(1); + await expect(page.getByLabel('Commit Entries')).toHaveCount(1); // Click the context menu button for the new page await page.getByTitle('Open context menu').click(); @@ -140,7 +129,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc // deleted page, should no longer exist const deletedPageElement = page.getByText(TEST_TEXT_NAME); - expect(await deletedPageElement.count()).toEqual(0); + await expect(deletedPageElement).toHaveCount(0); }); }); @@ -173,7 +162,7 @@ test.describe('can export restricted notebook as text', () => { await startAndAddRestrictedNotebookObject(page); }); - test('basic functionality ', async ({ page }) => { + test('basic functionality', async ({ page }) => { await enterTextEntry(page, `Foo bar entry`); // Click on 3 Dot Menu await page.locator('button[title="More actions"]').click(); @@ -190,8 +179,23 @@ test.describe('can export restricted notebook as text', () => { expect(exportedText).toContain('Foo bar entry'); }); - test.fixme('can export multiple notebook entries as text ', async ({ page }) => {}); + test.fixme('can export multiple notebook entries as text', async ({ page }) => {}); test.fixme('can export all notebook entry metdata', async ({ page }) => {}); test.fixme('can export all notebook tags', async ({ page }) => {}); test.fixme('can export all notebook snapshots', async ({ page }) => {}); }); + +/** + * Open the given `domainObject`'s context menu from the object tree. + * Expands the path to the object and scrolls to it if necessary. + * + * @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' + }); +} diff --git a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js index 1cd53295c3..464d5284e6 100644 --- a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js @@ -24,7 +24,10 @@ Testsuite for plot autoscale. */ -import { createDomainObjectWithDefaults } from '../../../../appActions.js'; +import { + createDomainObjectWithDefaults, + navigateToObjectWithFixedTimeBounds +} from '../../../../appActions.js'; import { expect, test } from '../../../../pluginFixtures.js'; test.use({ viewport: { @@ -51,9 +54,7 @@ test.describe('Autoscale', () => { }); // Switch to fixed time, start: 2022-03-28 22:00:00.000 UTC, end: 2022-03-28 22:00:30.000 UTC - await page.goto( - `${overlayPlot.url}?tc.mode=fixed&tc.startBound=1648591200000&tc.endBound=1648591230000&tc.timeSystem=utc&view=plot-overlay` - ); + await navigateToObjectWithFixedTimeBounds(page, overlayPlot.url, 1648591200000, 1648591230000); await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']); @@ -69,15 +70,15 @@ test.describe('Autoscale', () => { await page.getByLabel('Y Axis 1 Maximum value').fill('2'); // save - await page.click('button[title="Save"]'); + await page.getByLabel('Save').click(); await Promise.all([ page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(), //Wait for Save Banner to appear - page.waitForSelector('.c-message-banner__message') + page.locator('.c-message-banner__message').hover({ trial: true }) ]); //Wait until Save Banner is gone await page.locator('.c-message-banner__close-button').click(); - await page.waitForSelector('.c-message-banner__message', { state: 'detached' }); + await page.locator('.c-message-banner__message').waitFor({ state: 'detached' }); // Make sure that after turning off autoscale, the user entered range values are reflected in the ticks. await testYTicks(page, [ diff --git a/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js b/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js index e327b3d093..3b92bb3933 100644 --- a/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js @@ -29,15 +29,46 @@ import { createDomainObjectWithDefaults, setTimeConductorBounds } from '../../.. import { expect, test } from '../../../../pluginFixtures.js'; test.describe('Log plot tests', () => { - test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({ - page, - openmctConfig - }) => { - const { myItemsFolderName } = openmctConfig; - //Test is slow and should be split in the future - test.slow(); + test.beforeEach(async ({ page }) => { + // fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z + await page.goto('./', { waitUntil: 'domcontentloaded' }); - await makeOverlayPlot(page, myItemsFolderName); + // Set a specific time range for consistency, otherwise it will change + // on every test to a range based on the current time. + const startDate = '2022-03-29'; + const startTime = '22:00:00'; + const endDate = '2022-03-29'; + const endTime = '22:00:30'; + + await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime }); + + const overlayPlot = await createDomainObjectWithDefaults(page, { + type: 'Overlay Plot', + name: 'Unnamed Overlay Plot' + }); + + // create a sinewave generator + await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + name: 'Unnamed Sine Wave Generator', + parent: overlayPlot.uuid + }); + + await page.getByLabel('More actions').click(); + await page.getByLabel('Edit Properties...').click(); + + // set amplitude to 6, offset 4, data rate 2 hz + await page.getByLabel('Amplitude', { exact: true }).fill('6'); + await page.getByLabel('Offset', { exact: true }).fill('4'); + await page.getByLabel('Data Rate (hz)', { exact: true }).fill('2'); + + await page.getByLabel('Save').click(); + + await page.goto(overlayPlot.url); + }); + test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({ + page + }) => { await testRegularTicks(page); await enableEditMode(page); await page.getByRole('tab', { name: 'Config' }).click(); @@ -47,83 +78,35 @@ test.describe('Log plot tests', () => { await testRegularTicks(page); await enableLogMode(page); await testLogTicks(page); - await saveOverlayPlot(page); + await page.getByRole('button', { name: 'Save' }).click(); + await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); await testLogTicks(page); }); // Leaving test as 'TODO' for now. // NOTE: Not eligible for community contributions. - test.fixme( - 'Verify that log mode option is reflected in import/export JSON', - async ({ page, openmctConfig }) => { - const { myItemsFolderName } = openmctConfig; + test.fixme('Verify that log mode option is reflected in import/export JSON', async ({ page }) => { + await enableEditMode(page); + await enableLogMode(page); + await page.getByRole('button', { name: 'Save' }).click(); + await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); - await makeOverlayPlot(page, myItemsFolderName); - await enableEditMode(page); - await enableLogMode(page); - await saveOverlayPlot(page); + // TODO ...export, delete the overlay, then import it... - // TODO ...export, delete the overlay, then import it... + //await testLogTicks(page); - //await testLogTicks(page); - - // TODO, the plot is slightly at different position that in the other test, so this fails. - // ...We can fix it by copying all steps from the first test... - // await testLogPlotPixels(page); - } - ); + // TODO, the plot is slightly at different position that in the other test, so this fails. + // ...We can fix it by copying all steps from the first test... + // await testLogPlotPixels(page); + }); }); -/** - * Makes an overlay plot with a sine wave generator and clicks on the overlay plot in the sidebar so it is the active thing displayed. - * @param {import('@playwright/test').Page} page - * @param {string} myItemsFolderName - */ -async function makeOverlayPlot(page, myItemsFolderName) { - // fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z - await page.goto('./', { waitUntil: 'domcontentloaded' }); - - // Set a specific time range for consistency, otherwise it will change - // on every test to a range based on the current time. - - const startDate = '2022-03-29'; - const startTime = '22:00:00'; - const endDate = '2022-03-29'; - const endTime = '22:00:30'; - - await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime }); - - const overlayPlot = await createDomainObjectWithDefaults(page, { - type: 'Overlay Plot', - name: 'Unnamed Overlay Plot' - }); - - // create a sinewave generator - await createDomainObjectWithDefaults(page, { - type: 'Sine Wave Generator', - name: 'Unnamed Sine Wave Generator', - parent: overlayPlot.uuid - }); - - await page.getByLabel('More actions').click(); - await page.getByLabel('Edit Properties...').click(); - - // set amplitude to 6, offset 4, data rate 2 hz - await page.getByLabel('Amplitude', { exact: true }).fill('6'); - await page.getByLabel('Offset', { exact: true }).fill('4'); - await page.getByLabel('Data Rate (hz)', { exact: true }).fill('2'); - - await page.getByLabel('Save').click(); - - await page.goto(overlayPlot.url); -} - /** * @param {import('@playwright/test').Page} page */ async function testRegularTicks(page) { const yTicks = page.locator('.gl-plot-y-tick-label'); - expect(await yTicks.count()).toBe(7); + await expect(yTicks).toHaveCount(7); await expect(yTicks.nth(0)).toHaveText('-2'); await expect(yTicks.nth(1)).toHaveText('0'); await expect(yTicks.nth(2)).toHaveText('2'); @@ -138,7 +121,7 @@ async function testRegularTicks(page) { */ async function testLogTicks(page) { const yTicks = page.locator('.gl-plot-y-tick-label'); - expect(await yTicks.count()).toBe(9); + await expect(yTicks).toHaveCount(9); await expect(yTicks.nth(0)).toHaveText('-2.98'); await expect(yTicks.nth(1)).toHaveText('-1.51'); await expect(yTicks.nth(2)).toHaveText('-0.58'); @@ -175,26 +158,6 @@ async function disableLogMode(page) { await page.getByRole('checkbox', { name: 'Log mode' }).uncheck(); } -/** - * @param {import('@playwright/test').Page} page - */ -async function saveOverlayPlot(page) { - // save overlay plot - await page - .locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button') - .nth(1) - .click(); - - await Promise.all([ - page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(), - //Wait for Save Banner to appear - page.waitForSelector('.c-message-banner__message') - ]); - //Wait until Save Banner is gone - await page.locator('.c-message-banner__close-button').click(); - await page.waitForSelector('.c-message-banner__message', { state: 'detached' }); -} - /** * @param {import('@playwright/test').Page} page */ diff --git a/e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js b/e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js index c5a9aa6874..7af05a00ea 100644 --- a/e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js @@ -52,8 +52,8 @@ test.describe('Overlay Plot', () => { await page.getByRole('tab', { name: 'Config' }).click(); // navigate to plot series color palette - await page.click('.l-browse-bar__actions__edit'); - await page.locator('li.c-tree__item.menus-to-left .c-disclosure-triangle').click(); + await page.getByLabel('Edit Object').click(); + await page.getByLabel('Expand Sine Wave Generator:').click(); await page.locator('.c-click-swatch--menu').click(); await page.locator('.c-palette__item[style="background: rgb(255, 166, 61);"]').click(); // gets color for swatch located in legend @@ -93,7 +93,7 @@ test.describe('Overlay Plot', () => { await expect(page.getByLabel('Plot Legend Expanded')).toBeHidden(); await expect(page.getByLabel('Expand by Default')).toHaveText(/No/); - expect(await page.getByLabel('Plot Legend Item').count()).toBe(3); + await expect(page.getByLabel('Plot Legend Item')).toHaveCount(3); // Change the legend to expand by default await page.getByLabel('Edit Object').click(); @@ -136,8 +136,8 @@ test.describe('Overlay Plot', () => { await page.goto(overlayPlot.url); // Assert that no limit lines are shown by default - await page.waitForSelector('.js-limit-area', { state: 'attached' }); - expect(await page.locator('.c-plot-limit-line').count()).toBe(0); + await page.locator('.js-limit-area').waitFor({ state: 'attached' }); + await expect(page.locator('.c-plot-limit-line')).toHaveCount(0); // Enter edit mode await page.getByLabel('Edit Object').click(); @@ -200,8 +200,8 @@ test.describe('Overlay Plot', () => { await page.goto(overlayPlot.url); // Assert that no limit lines are shown by default - await page.waitForSelector('.js-limit-area', { state: 'attached' }); - expect(await page.locator('.c-plot-limit-line').count()).toBe(0); + await expect(page.locator('.js-limit-area')).toBeAttached(); + await expect(page.locator('.c-plot-limit-line')).toHaveCount(0); // Enter edit mode await page.getByLabel('Edit Object').click(); @@ -309,32 +309,26 @@ test.describe('Overlay Plot', () => { expect(yAxis3Group.getByRole('listitem').nth(0).getByText(swgB.name)).toBeTruthy(); }); - test.fixme( - 'Clicking on an item in the elements pool brings up the plot preview with data points', - async ({ page }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/7421' - }); + test('Clicking on an item in the elements pool brings up the plot preview with data points', async ({ + page + }) => { + const swgA = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + parent: overlayPlot.uuid + }); - const swgA = await createDomainObjectWithDefaults(page, { - type: 'Sine Wave Generator', - parent: overlayPlot.uuid - }); + await page.goto(overlayPlot.url); + // Wait for plot series data to load and be drawn + await waitForPlotsToRender(page); + await page.getByLabel('Edit Object').click(); - await page.goto(overlayPlot.url); - // Wait for plot series data to load and be drawn - await waitForPlotsToRender(page); - await page.getByLabel('Edit Object').click(); + await page.getByRole('tab', { name: 'Elements' }).click(); - await page.getByRole('tab', { name: 'Elements' }).click(); - - await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click(); - const plotPixels = await getCanvasPixels(page, '.js-overlay canvas'); - const plotPixelSize = plotPixels.length; - expect(plotPixelSize).toBeGreaterThan(0); - } - ); + await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click(); + const plotPixels = await getCanvasPixels(page, '.js-overlay canvas'); + const plotPixelSize = plotPixels.length; + expect(plotPixelSize).toBeGreaterThan(0); + }); test('Can remove an item via the elements pool action menu', async ({ page }) => { const swgA = await createDomainObjectWithDefaults(page, { @@ -357,7 +351,7 @@ test.describe('Overlay Plot', () => { await expect(swgAElementsPoolItem).toBeVisible(); await swgAElementsPoolItem.click({ button: 'right' }); await page.getByRole('menuitem', { name: 'Remove' }).click(); - await page.getByRole('button', { name: 'OK', exact: true }).click(); + await page.getByRole('button', { name: 'Ok', exact: true }).click(); await expect(swgAElementsPoolItem).toBeHidden(); await page.getByRole('button', { name: 'Save' }).click(); @@ -387,7 +381,7 @@ async function assertLimitLinesExistAndAreVisible(page) { // Wait for plot series data to load await waitForPlotsToRender(page); // Wait for limit lines to be created - await page.waitForSelector('.js-limit-area', { state: 'attached' }); + await page.locator('.js-limit-area').waitFor({ state: 'attached' }); // There should be 10 limit lines created by default await expect(page.locator('.c-plot-limit-line')).toHaveCount(10); const limitLineCount = await page.locator('.c-plot-limit-line').count(); diff --git a/e2e/tests/functional/plugins/plot/plotControls.e2e.spec.js b/e2e/tests/functional/plugins/plot/plotControls.e2e.spec.js index ba614cf90c..87f608f528 100644 --- a/e2e/tests/functional/plugins/plot/plotControls.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/plotControls.e2e.spec.js @@ -85,15 +85,8 @@ test.describe('Plot Controls', () => { await page.getByLabel('Y Axis 1 Maximum value').fill('1'); // save - await page.click('button[title="Save"]'); - await Promise.all([ - page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(), - //Wait for Save Banner to appear - page.waitForSelector('.c-message-banner__message') - ]); - //Wait until Save Banner is gone - await page.locator('.c-message-banner__close-button').click(); - await page.waitForSelector('.c-message-banner__message', { state: 'detached' }); + await page.getByRole('button', { name: 'Save' }).click(); + await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); // hover over plot for plot controls await page.getByLabel('Plot Canvas').hover(); // click on pause control diff --git a/e2e/tests/functional/plugins/plot/plotRendering.e2e.spec.js b/e2e/tests/functional/plugins/plot/plotRendering.e2e.spec.js index d178b79b8b..d90310d22c 100644 --- a/e2e/tests/functional/plugins/plot/plotRendering.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/plotRendering.e2e.spec.js @@ -78,17 +78,13 @@ test.describe('Plot Rendering', () => { // click on synchronize with time conductor await page.getByTitle('Synchronize Time Conductor').click(); - await page.getByRole('button', { name: 'OK', exact: true }).click(); + await page.getByRole('button', { name: 'Ok', exact: true }).click(); //confirm that you're now in fixed mode with the correct range await expect(page.getByLabel('Time Conductor Mode')).toHaveText('Fixed Timespan'); }); - test.fixme('Plot is rendered when infinity values exist', async ({ page }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/7421' - }); + test('Plot is rendered when infinity values exist', async ({ page }) => { // Edit Plot await editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject); diff --git a/e2e/tests/functional/plugins/plot/scatterPlot.e2e.spec.js b/e2e/tests/functional/plugins/plot/scatterPlot.e2e.spec.js index c392c26095..ed25ff5747 100644 --- a/e2e/tests/functional/plugins/plot/scatterPlot.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/scatterPlot.e2e.spec.js @@ -53,7 +53,7 @@ test.describe('Scatter Plot', () => { await page.goto(scatterPlot.url); await page.getByLabel('Edit Object').click(); await page.getByRole('tab', { name: 'Elements' }).click(); - await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible(); + await expect(page.getByLabel(`Preview ${swg1.name}`)).toBeVisible(); await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); @@ -65,14 +65,12 @@ test.describe('Scatter Plot', () => { }); // Verify that the 'Replace telemetry source' modal appears and accept it - await expect - .soft( - page.locator( - 'text=This action will replace the current telemetry source. Do you want to continue?' - ) + await expect( + page.getByText( + 'This action will replace the current telemetry source. Do you want to continue?' ) - .toBeVisible(); - await page.click('text=Ok'); + ).toBeVisible(); + await page.getByRole('button', { name: 'Ok', exact: true }).click(); // Navigate to the scatter plot and verify that the new SWG // appears in the elements pool and the old one is gone @@ -81,27 +79,25 @@ test.describe('Scatter Plot', () => { // Click the "Elements" tab await page.getByRole('tab', { name: 'Elements' }).click(); - await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeHidden(); - await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg2.name}`)).toBeVisible(); + await expect(page.getByLabel(`Preview ${swg1.name}`)).toBeHidden(); + await expect(page.getByLabel(`Preview ${swg2.name}`)).toBeVisible(); await page.getByRole('button', { name: 'Save' }).click(); // Right click on the new SWG in the elements pool and delete it - await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({ + await page.getByLabel(`Preview ${swg2.name}`).click({ button: 'right' }); - await page.locator('li[title="Remove this object from its containing object."]').click(); + await page.getByLabel('Remove').click(); // Verify that the 'Remove object' confirmation modal appears and accept it - await expect - .soft( - page.locator( - 'text=Warning! This action will remove this object. Are you sure you want to continue?' - ) + await expect( + page.getByText( + 'Warning! This action will remove this object. Are you sure you want to continue?' ) - .toBeVisible(); - await page.click('text=Ok'); + ).toBeVisible(); + await page.getByRole('button', { name: 'Ok', exact: true }).click(); // Verify that the elements pool shows no elements - await expect(page.locator('text="No contained elements"')).toBeVisible(); + await expect(page.getByText('No contained elements')).toBeVisible(); }); }); diff --git a/e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js b/e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js index aed64e7e8c..6926097c8f 100644 --- a/e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js @@ -82,7 +82,7 @@ test.describe('Stacked Plot', () => { .getByRole('menuitem') .filter({ hasText: /Remove/ }) .click(); - await page.getByRole('button').filter({ hasText: 'OK' }).click(); + await page.getByRole('button').filter({ hasText: 'Ok' }).click(); await expect(page.locator('#inspector-elements-tree .js-elements-pool__item')).toHaveCount(2); @@ -239,7 +239,7 @@ test.describe('Stacked Plot', () => { await page.getByLabel(`Stacked Plot Item ${swgA.name}`).click(); // Expand config for the series - await page.getByLabel('Expand Sine Wave Generator').click(); + await page.getByLabel('Expand Sine Wave Generator A Plot Series Options').click(); // turn off alarm markers await page.getByLabel('Alarm Markers').uncheck(); @@ -256,8 +256,7 @@ test.describe('Stacked Plot', () => { await page.getByLabel(`Stacked Plot Item ${swgA.name}`).click(); // Expand config for the series - //TODO Fix this locator - await page.getByLabel('Expand Sine Wave Generator A generator').click(); + await page.getByLabel('Expand Sine Wave Generator A Plot Series Options').click(); // Assert that alarm markers are still turned off await expect( @@ -277,7 +276,7 @@ test.describe('Stacked Plot', () => { await page.getByRole('tab', { name: 'Config' }).click(); - let legendProperties = await page.locator('[aria-label="Legend Properties"]'); + const legendProperties = page.getByLabel('Legend Properties'); await legendProperties.locator('[title="Display legends per sub plot."]~div input').uncheck(); await assertAggregateLegendIsVisible(page); @@ -356,11 +355,9 @@ async function assertAggregateLegendIsVisible(page) { // Wait for plot series data to load await waitForPlotsToRender(page); // Wait for plot legend to be shown - await page.waitForSelector('.js-stacked-plot-legend', { state: 'attached' }); + await expect(page.locator('.js-stacked-plot-legend')).toBeVisible(); // There should be 3 legend items - expect( - await page - .locator('.js-stacked-plot-legend .c-plot-legend__wrapper div.plot-legend-item') - .count() - ).toBe(3); + await expect( + page.locator('.js-stacked-plot-legend .c-plot-legend__wrapper div.plot-legend-item') + ).toHaveCount(3); } diff --git a/e2e/tests/functional/plugins/plot/tagging.e2e.spec.js b/e2e/tests/functional/plugins/plot/tagging.e2e.spec.js index 51c2bbbd1f..e62c65772a 100644 --- a/e2e/tests/functional/plugins/plot/tagging.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/tagging.e2e.spec.js @@ -89,7 +89,7 @@ test.describe('Plot Tagging', () => { await setRealTimeMode(page); // Search for Science Tag - await page.getByRole('searchbox', { name: 'Search Input' }); + await page.getByRole('searchbox', { name: 'Search Input' }).click(); await page.getByRole('searchbox', { name: 'Search Input' }).fill('sc'); // Click on the search object result diff --git a/e2e/tests/functional/plugins/reloadAction/reloadAction.e2e.spec.js b/e2e/tests/functional/plugins/reloadAction/reloadAction.e2e.spec.js index 0855d2efc8..499454bb2a 100644 --- a/e2e/tests/functional/plugins/reloadAction/reloadAction.e2e.spec.js +++ b/e2e/tests/functional/plugins/reloadAction/reloadAction.e2e.spec.js @@ -42,19 +42,21 @@ test.describe('Reload action', () => { await createDomainObjectWithDefaults(page, { type: 'Sine Wave Generator', - parent: alphaTable.uuid, - customParameters: { - '[aria-label="Data Rate (hz)"]': '0.001' - } + parent: alphaTable.uuid }); + await page.getByLabel('More actions').click(); + await page.getByRole('menuitem', { name: /Edit Properties/ }).click(); + await page.getByLabel('Data Rate (hz)', { exact: true }).fill('0.001'); + await page.getByLabel('Save').click(); await createDomainObjectWithDefaults(page, { type: 'Sine Wave Generator', - parent: betaTable.uuid, - customParameters: { - '[aria-label="Data Rate (hz)"]': '0.001' - } + parent: betaTable.uuid }); + await page.getByLabel('More actions').click(); + await page.getByRole('menuitem', { name: /Edit Properties/ }).click(); + await page.getByLabel('Data Rate (hz)', { exact: true }).fill('0.001'); + await page.getByLabel('Save').click(); await page.goto(displayLayout.url); @@ -63,20 +65,26 @@ test.describe('Reload action', () => { await page.getByLabel('Edit Object', { exact: true }).click(); - await page.dragAndDrop(`text='Alpha Table'`, '.l-layout__grid-holder', { - targetPosition: { x: 0, y: 0 } - }); + await page + .getByLabel('Main Tree') + .getByLabel(`Preview ${alphaTable.name}`) + .dragTo(page.getByLabel('Layout Grid'), { + targetPosition: { x: 0, y: 0 } + }); - await page.dragAndDrop(`text='Beta Table'`, '.l-layout__grid-holder', { - targetPosition: { x: 0, y: 250 } - }); + await page + .getByLabel('Main Tree') + .getByLabel(`Preview ${betaTable.name}`) + .dragTo(page.getByLabel('Layout Grid'), { + targetPosition: { x: 0, y: 250 } + }); - await page.locator('button[title="Save"]').click(); + await page.getByLabel('Save').click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); }); test('can reload display layout and its children', async ({ page }) => { - const beforeReloadAlphaTelemetryValue = await page + const beforeReloadAlphaTelemetryValue = page .getByLabel('Alpha Table table content') .getByLabel('wavelengths table cell') .first() diff --git a/e2e/tests/functional/plugins/styling/flexLayoutStyling.e2e.spec.js b/e2e/tests/functional/plugins/styling/flexLayoutStyling.e2e.spec.js index 27129822fd..e74accfe82 100644 --- a/e2e/tests/functional/plugins/styling/flexLayoutStyling.e2e.spec.js +++ b/e2e/tests/functional/plugins/styling/flexLayoutStyling.e2e.spec.js @@ -466,7 +466,7 @@ test.describe('Flexible Layout styling', () => { page.getByLabel('Flexible Layout Column') ); await page.getByLabel('Cancel Editing').click(); - await page.getByRole('button', { name: 'OK', exact: true }).click(); + await page.getByRole('button', { name: 'Ok', exact: true }).click(); await checkStyles( hexToRGB(defaultBorderTargetColor), NO_STYLE_RGBA, diff --git a/e2e/tests/functional/plugins/tabs/tabs.e2e.spec.js b/e2e/tests/functional/plugins/tabs/tabs.e2e.spec.js index 508a08a75b..8e9a3bb586 100644 --- a/e2e/tests/functional/plugins/tabs/tabs.e2e.spec.js +++ b/e2e/tests/functional/plugins/tabs/tabs.e2e.spec.js @@ -114,8 +114,8 @@ test.describe('Tabs View CRUD', () => { await page.getByLabel('Edit Object').click(); await page.getByLabel('More actions').click(); await page.getByLabel('Edit Properties...').click(); - await expect(await page.getByLabel('Eager Load Tabs')).not.toBeChecked(); + await expect(page.getByLabel('Eager Load Tabs')).not.toBeChecked(); await page.getByLabel('Eager Load Tabs').setChecked(true); - await expect(await page.getByLabel('Eager Load Tabs')).toBeChecked(); + await expect(page.getByLabel('Eager Load Tabs')).toBeChecked(); }); }); diff --git a/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js b/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js index 3c94b06952..9aea26f395 100644 --- a/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js +++ b/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js @@ -155,8 +155,7 @@ test.describe('Telemetry Table', () => { expect(cells.length).toBeGreaterThan(1); // ensure the text content of each cell contains the search term for (const cell of cells) { - const text = await cell.textContent(); - expect(text).toContain('Roger'); + await expect(cell).toHaveText(/Roger/); } await page.getByRole('searchbox', { name: 'message filter input' }).click(); @@ -167,15 +166,14 @@ test.describe('Telemetry Table', () => { .getByText(/Dodger/) .all(); // ensure we've got more than one cell - expect(cells.length).toBe(0); + expect(cells).toHaveLength(0); // ensure the text content of each cell contains the search term for (const cell of cells) { - const text = await cell.textContent(); - expect(text).not.toContain('Dodger'); + await expect(cell).not.toHaveText(/Dodger/); } // Click pause button - await page.click('button[title="Pause"]'); + await page.getByLabel('Pause').click(); }); test('Supports filtering using Regex', async ({ page }) => { @@ -201,8 +199,7 @@ test.describe('Telemetry Table', () => { expect(cells.length).toBeGreaterThan(1); // ensure the text content of each cell contains the search term for (const cell of cells) { - const text = await cell.textContent(); - expect(text).toContain('Roger'); + await expect(cell).toHaveText(/Roger/); } await page.getByRole('searchbox', { name: 'message filter input' }).click(); @@ -213,15 +210,14 @@ test.describe('Telemetry Table', () => { .getByText(/Dodger/) .all(); // ensure we've got more than one cell - expect(cells.length).toBe(0); + expect(cells).toHaveLength(0); // ensure the text content of each cell contains the search term for (const cell of cells) { - const text = await cell.textContent(); - expect(text).not.toContain('Dodger'); + await expect(cell).not.toHaveText(/Dodger/); } // Click pause button - await page.click('button[title="Pause"]'); + await page.getByLabel('Pause').click(); }); }); @@ -256,7 +252,6 @@ async function getScrollPosition(page, top = true) { scrollHeight: node.scrollHeight })); - // eslint-disable-next-line playwright/no-conditional-in-test if (top) { return scrollTop; } else { diff --git a/e2e/tests/functional/plugins/timeConductor/datepicker.e2e.spec.js b/e2e/tests/functional/plugins/timeConductor/datepicker.e2e.spec.js index e65de8c465..0da324bb98 100644 --- a/e2e/tests/functional/plugins/timeConductor/datepicker.e2e.spec.js +++ b/e2e/tests/functional/plugins/timeConductor/datepicker.e2e.spec.js @@ -22,16 +22,18 @@ import { createDomainObjectWithDefaults, - setIndependentTimeConductorBounds + navigateToObjectWithFixedTimeBounds, + setFixedIndependentTimeConductorBounds } from '../../../../appActions.js'; import { expect, test } from '../../../../pluginFixtures.js'; -const FIXED_TIME = - './#/browse/mine?tc.mode=fixed&tc.startBound=1693592063607&tc.endBound=1693593893607&tc.timeSystem=utc&view=grid&hideInspector=true&hideTree=true'; +const FIXED_TIME_URL = './#/browse/mine'; + test.describe('Datepicker operations', () => { test.beforeEach(async ({ page }) => { - await page.goto(FIXED_TIME); + await navigateToObjectWithFixedTimeBounds(page, FIXED_TIME_URL, 1693592063607, 1693593893607); }); + test('Verify that user can use the datepicker in the TC', async ({ page }) => { await page.getByLabel('Time Conductor Mode').click(); // Click on the date picker that is left-most on the screen @@ -42,12 +44,13 @@ test.describe('Datepicker operations', () => { // Expect datepicker to close and time conductor date setting to be changed await expect(page.getByRole('dialog')).toHaveCount(0); }); + test('Verify that user can use the datepicker in the ITC', async ({ page }) => { const createdTimeList = await createDomainObjectWithDefaults(page, { type: 'Time List' }); await page.goto(createdTimeList.url, { waitUntil: 'domcontentloaded' }); - await setIndependentTimeConductorBounds(page, { + await setFixedIndependentTimeConductorBounds(page, { start: '2024-11-12 19:11:11.000Z', end: '2024-11-12 20:11:11.000Z' }); diff --git a/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js b/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js index a873b7c306..79346f382e 100644 --- a/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js +++ b/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js @@ -281,29 +281,3 @@ test.describe('Global Time Conductor', () => { // select an option and verify the offsets are updated correctly }); }); - -test.describe('Time Conductor History', () => { - test('shows milliseconds on hover @unstable', async ({ page }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/4386' - }); - // Navigate to Open MCT in Fixed Time Mode, UTC Time System - // with startBound at 2022-01-01 00:00:00.000Z - // and endBound at 2022-01-01 00:00:00.200Z - await page.goto( - './#/browse/mine?view=grid&tc.mode=fixed&tc.startBound=1640995200000&tc.endBound=1640995200200&tc.timeSystem=utc&hideInspector=true' - ); - await page.getByRole('button', { name: 'Time Conductor Settings' }).click(); - await page.getByRole('button', { name: 'Time Conductor History' }).hover({ trial: true }); - await page.getByRole('button', { name: 'Time Conductor History' }).click(); - - // Validate history item format - const historyItem = page.locator('text="2022-01-01 00:00:00 + 200ms"'); - await expect(historyItem).toBeEnabled(); - await expect(historyItem).toHaveAttribute( - 'title', - '2022-01-01 00:00:00.000 - 2022-01-01 00:00:00.200' - ); - }); -}); diff --git a/e2e/tests/functional/plugins/timer/timer.e2e.spec.js b/e2e/tests/functional/plugins/timer/timer.e2e.spec.js index 1e0393545f..f63cd4c1e7 100644 --- a/e2e/tests/functional/plugins/timer/timer.e2e.spec.js +++ b/e2e/tests/functional/plugins/timer/timer.e2e.spec.js @@ -20,10 +20,7 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { - createDomainObjectWithDefaults, - openObjectTreeContextMenu -} from '../../../../appActions.js'; +import { createDomainObjectWithDefaults } from '../../../../appActions.js'; import { MISSION_TIME } from '../../../../constants.js'; import { expect, test } from '../../../../pluginFixtures.js'; @@ -33,7 +30,6 @@ test.describe('Timer', () => { test.beforeEach(async ({ page }) => { await page.goto('./', { waitUntil: 'domcontentloaded' }); timer = await createDomainObjectWithDefaults(page, { type: 'timer' }); - await assertTimerElements(page, timer); }); test('Can perform actions on the Timer', async ({ page }) => { @@ -42,13 +38,11 @@ test.describe('Timer', () => { description: 'https://github.com/nasa/openmct/issues/4313' }); - const timerUrl = timer.url; - await test.step('From the tree context menu', async () => { - await triggerTimerContextMenuAction(page, timerUrl, 'Start'); - await triggerTimerContextMenuAction(page, timerUrl, 'Pause'); - await triggerTimerContextMenuAction(page, timerUrl, 'Restart at 0'); - await triggerTimerContextMenuAction(page, timerUrl, 'Stop'); + await triggerTimerContextMenuAction(page, timer.url, 'Start'); + await triggerTimerContextMenuAction(page, timer.url, 'Pause'); + await triggerTimerContextMenuAction(page, timer.url, 'Restart at 0'); + await triggerTimerContextMenuAction(page, timer.url, 'Stop'); }); await test.step('From the 3dot menu', async () => { @@ -67,26 +61,18 @@ test.describe('Timer', () => { }); test.describe('Timer with target date @clock', () => { - let timer; - test.beforeEach(async ({ page }) => { + await page.clock.install({ time: MISSION_TIME }); + await page.clock.resume(); await page.goto('./', { waitUntil: 'domcontentloaded' }); - timer = await createDomainObjectWithDefaults(page, { type: 'timer' }); - await assertTimerElements(page, timer); - }); - - // Override clock - test.use({ - clockOptions: { - now: MISSION_TIME, - shouldAdvanceTime: true - } + await createDomainObjectWithDefaults(page, { type: 'timer' }); }); test('Can count down to a target date', async ({ page }) => { // Set the target date to 2024-11-24 03:30:00 await page.getByTitle('More actions').click(); await page.getByRole('menuitem', { name: /Edit Properties.../ }).click(); + await page.getByPlaceholder('YYYY-MM-DD').fill('2024-11-24'); await page.locator('input[name="hour"]').fill('3'); await page.locator('input[name="min"]').fill('30'); @@ -159,14 +145,13 @@ async function triggerTimerContextMenuAction(page, timerUrl, action) { */ async function triggerTimer3dotMenuAction(page, action) { const menuAction = `.c-menu ul li >> text="${action}"`; - const threeDotMenuButton = 'button[title="More actions"]'; let isActionAvailable = false; let iterations = 0; // Dismiss/open the 3dot menu until the action is available // or a maximum number of iterations is reached while (!isActionAvailable && iterations <= 20) { - await page.click('.c-object-view'); - await page.click(threeDotMenuButton); + await page.getByLabel('Object View').click(); + await page.getByLabel('More actions').click(); isActionAvailable = await page.locator(menuAction).isVisible(); iterations++; } @@ -183,7 +168,7 @@ async function triggerTimer3dotMenuAction(page, action) { async function triggerTimerViewAction(page, action) { await page.locator('.c-timer').hover({ trial: true }); const buttonTitle = buttonTitleFromAction(action); - await page.click(`button[title="${buttonTitle}"]`); + await page.getByLabel(buttonTitle, { exact: true }).click(); assertTimerStateAfterAction(page, action); } @@ -214,11 +199,11 @@ async function assertTimerStateAfterAction(page, action) { case 'Start': case 'Restart at 0': timerStateClass = 'is-started'; - expect(await timerValue.innerText()).toBe('0D 00:00:00'); + await expect(timerValue).toHaveText('0D 00:00:00'); break; case 'Stop': timerStateClass = 'is-stopped'; - expect(await timerValue.innerText()).toBe('--:--:--'); + await expect(timerValue).toHaveText('--:--:--'); break; case 'Pause': timerStateClass = 'is-paused'; @@ -229,23 +214,16 @@ async function assertTimerStateAfterAction(page, action) { } /** - * Assert that all the major components of a timer are present in the DOM. + * Open the given `domainObject`'s context menu from the object tree. + * Expands the path to the object and scrolls to it if necessary. + * * @param {import('@playwright/test').Page} page - * @param {import('../../../../appActions').CreatedObjectInfo} timer + * @param {string} url the url to the object */ -async function assertTimerElements(page, timer) { - const timerElement = page.locator('.c-timer'); - const resetButton = page.getByRole('button', { name: 'Reset' }); - const pausePlayButton = page - .getByRole('button', { name: 'Pause' }) - .or(page.getByRole('button', { name: 'Start' })); - const timerDirectionIcon = page.locator('.c-timer__direction'); - const timerValue = page.locator('.c-timer__value'); - - expect(await page.locator('.l-browse-bar__object-name').innerText()).toBe(timer.name); - expect(timerElement).toBeAttached(); - expect(resetButton).toBeAttached(); - expect(pausePlayButton).toBeAttached(); - expect(timerDirectionIcon).toBeAttached(); - expect(timerValue).toBeAttached(); +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' + }); } diff --git a/e2e/tests/functional/recentObjects.e2e.spec.js b/e2e/tests/functional/recentObjects.e2e.spec.js index 6b18d6eb02..1a5cc7afd7 100644 --- a/e2e/tests/functional/recentObjects.e2e.spec.js +++ b/e2e/tests/functional/recentObjects.e2e.spec.js @@ -95,7 +95,7 @@ test.describe('Recent Objects', () => { ).toBeGreaterThan(0); expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy(); - await page.click('button[title="Show selected item in tree"]'); + await page.getByLabel('Show selected item in tree').click(); // Delete the folder via the left tree pane treeitem context menu await page .getByRole('treeitem', { name: new RegExp(folderA.name) }) @@ -104,7 +104,7 @@ test.describe('Recent Objects', () => { button: 'right' }); await page.getByRole('menuitem', { name: /Remove/ }).click(); - await page.getByRole('button', { name: 'OK', exact: true }).click(); + await page.getByRole('button', { name: 'Ok', exact: true }).click(); // Verify that the folder and clock are no longer in the recent objects list await expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeHidden(); @@ -227,24 +227,22 @@ test.describe('Recent Objects', () => { .click(); // Assert that two recent objects are displayed and one of them is an alias - expect(await recentObjectsList.getByRole('listitem', { name: clock.name }).count()).toBe(2); - expect(await recentObjectsList.locator('.is-alias').count()).toBe(1); + await expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toHaveCount(2); + await expect(recentObjectsList.locator('.is-alias')).toHaveCount(1); // Assert that the alias and the original's breadcrumbs are different const clockBreadcrumbs = recentObjectsList .getByRole('listitem', { name: clock.name }) .getByRole('navigation'); - expect(await clockBreadcrumbs.count()).toBe(2); - expect(await clockBreadcrumbs.nth(0).innerText()).not.toEqual( - await clockBreadcrumbs.nth(1).innerText() - ); + await expect(clockBreadcrumbs).toHaveCount(2); + await expect(clockBreadcrumbs.nth(0)).not.toHaveText(await clockBreadcrumbs.nth(1).innerText()); }); test('Enforces a limit of 20 recent objects and clears the recent objects', async ({ page }) => { // Creating 21 objects takes a while, so increase the timeout test.slow(); // Assert that the list initially contains 3 objects (clock, folder, my items) - expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(3); + await expect(recentObjectsList.locator('.c-recentobjects-listitem')).toHaveCount(3); let lastFolder; let lastClock; @@ -261,7 +259,7 @@ test.describe('Recent Objects', () => { } // Assert that the list contains 20 objects - expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(20); + await expect(recentObjectsList.locator('.c-recentobjects-listitem')).toHaveCount(20); // Collapse the tree await page.getByTitle('Collapse all tree items').click(); @@ -293,44 +291,38 @@ test.describe('Recent Objects', () => { await page.getByRole('button', { name: 'Clear Recently Viewed' }).click(); // Click on the "OK" button in the confirmation dialog - await page.getByRole('button', { name: 'OK', exact: true }).click(); + await page.getByRole('button', { name: 'Ok', exact: true }).click(); // Assert that the list is empty - expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(0); + await expect(recentObjectsList.locator('.c-recentobjects-listitem')).toHaveCount(0); }); test('Verify functionality of "clear" and "collapse pane" buttons', async ({ page }) => { // Assert that the list initially contains 3 objects (clock, folder, my items) - expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(3); + await expect(recentObjectsList.locator('.c-recentobjects-listitem')).toHaveCount(3); // Assert that the button is enabled - expect(await page.getByRole('button', { name: 'Clear Recently Viewed' }).isEnabled()).toBe( - true - ); + await expect(page.getByRole('button', { name: 'Clear Recently Viewed' })).toBeEnabled(); // Click the aria-label="Clear Recently Viewed" button await page.getByRole('button', { name: 'Clear Recently Viewed' }).click(); // Click on the "OK" button in the confirmation dialog - await page.getByRole('button', { name: 'OK', exact: true }).click(); + await page.getByRole('button', { name: 'Ok', exact: true }).click(); // Assert that the list is empty - expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(0); + await expect(recentObjectsList.locator('.c-recentobjects-listitem')).toHaveCount(0); // Assert that the button is disabled - expect(await page.getByRole('button', { name: 'Clear Recently Viewed' }).isEnabled()).toBe( - false - ); + await expect(page.getByRole('button', { name: 'Clear Recently Viewed' })).toBeDisabled(); // Navigate to folder object await page.goto(folderA.url); // Assert that the list contains 1 object - expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(1); + await expect(recentObjectsList.locator('.c-recentobjects-listitem')).toHaveCount(1); // Assert that the button is enabled - expect(await page.getByRole('button', { name: 'Clear Recently Viewed' }).isEnabled()).toBe( - true - ); + await expect(page.getByRole('button', { name: 'Clear Recently Viewed' })).toBeEnabled(); // Assert initial state of pane and collapse the Recent Objects panel await expect(page.getByLabel('Expand Recently Viewed Pane')).toBeHidden(); diff --git a/e2e/tests/functional/renaming.e2e.spec.js b/e2e/tests/functional/renaming.e2e.spec.js index f201c07028..80641fb29b 100644 --- a/e2e/tests/functional/renaming.e2e.spec.js +++ b/e2e/tests/functional/renaming.e2e.spec.js @@ -24,13 +24,13 @@ This test suite is dedicated to tests for renaming objects, and their global application effects. */ -import { createDomainObjectWithDefaults, renameObjectFromContextMenu } from '../../appActions.js'; +import { createDomainObjectWithDefaults } from '../../appActions.js'; import { expect, test } from '../../baseFixtures.js'; test.describe('Renaming objects', () => { test.beforeEach(async ({ page }) => { // Go to baseURL - await page.goto('./', { waitUntil: 'networkidle' }); + await page.goto('./', { waitUntil: 'domcontentloaded' }); }); test('When renaming objects, the browse bar and various components all update', async ({ @@ -73,3 +73,33 @@ test.describe('Renaming objects', () => { expect(title).toBe(clock.name); }); }); + +/** + * @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.locator('li:text("Edit Properties")').click(); + const nameInput = page.getByLabel('Title', { exact: true }); + await nameInput.fill(''); + await nameInput.fill(newName); + await page.locator('[aria-label="Save"]').click(); +} + +/** + * Open the given `domainObject`'s context menu from the object tree. + * Expands the path to the object and scrolls to it if necessary. + * + * @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' + }); +} diff --git a/e2e/tests/functional/search.e2e.spec.js b/e2e/tests/functional/search.e2e.spec.js index ccc0163b18..4b7073aef9 100644 --- a/e2e/tests/functional/search.e2e.spec.js +++ b/e2e/tests/functional/search.e2e.spec.js @@ -162,7 +162,7 @@ test.describe('Grand Search', () => { const searchResults = page.getByRole('listitem', { name: 'Object Search Result' }); // Verify that no results are found - expect(await searchResults.count()).toBe(0); + await expect(searchResults).toHaveCount(0); // Verify proper message appears await expect(page.getByText('No results found')).toBeVisible(); @@ -187,7 +187,7 @@ test.describe('Grand Search', () => { // Verify that one result is found await expect(searchResults).toBeVisible(); - expect(await searchResults.count()).toBe(1); + await expect(searchResults).toHaveCount(1); await expect(searchResults).toContainText(folderName); }); @@ -216,7 +216,7 @@ test.describe('Grand Search', () => { // Network requests for the composite telemetry with multiple items should be: // 1. batched request for latest telemetry using the bulk API - expect(networkRequests.length).toBe(1); + await expect.poll(() => networkRequests, { timeout: 10000 }).toHaveLength(1); await expect(page.getByRole('list', { name: 'Object Results' })).toContainText('Clock A'); }); @@ -282,7 +282,7 @@ test.describe('Grand Search', () => { // Get the search results const objectSearchResults = page.getByLabel('Object Search Result'); // Verify that two results are found - expect(await objectSearchResults.count()).toBe(2); + await expect(objectSearchResults).toHaveCount(2); }); }); diff --git a/e2e/tests/functional/staleness.e2e.spec.js b/e2e/tests/functional/staleness.e2e.spec.js index 0b2a00a536..d247674cae 100644 --- a/e2e/tests/functional/staleness.e2e.spec.js +++ b/e2e/tests/functional/staleness.e2e.spec.js @@ -29,8 +29,6 @@ test.describe('Staleness', () => { }); test('Does not show staleness after navigating from a stale object', async ({ page }) => { - const objectViewSelector = '.c-object-view'; - const isStaleClass = 'is-stale'; const staleSWG = await createDomainObjectWithDefaults(page, { type: 'Sine Wave Generator', name: 'SWG' @@ -51,7 +49,7 @@ test.describe('Staleness', () => { await navigateToObjectWithRealTime(page, staleSWG.url); // Assert that staleness is shown - await expect(page.locator(`${objectViewSelector} .${isStaleClass}`)).toBeAttached({ + await expect(page.getByLabel('Object View')).toHaveClass(/is-stale/, { timeout: 30 * 1000 // Give 30 seconds for the staleness to be updated }); @@ -59,6 +57,6 @@ test.describe('Staleness', () => { await page.goto(folder.url); // Verify that staleness is not shown - await expect(page.locator(`${objectViewSelector} .${isStaleClass}`)).not.toBeAttached(); + await expect(page.getByLabel('Object View')).not.toHaveClass(/is-stale/); }); }); diff --git a/e2e/tests/functional/tooltips.e2e.spec.js b/e2e/tests/functional/tooltips.e2e.spec.js index 8782871a56..42d831196c 100644 --- a/e2e/tests/functional/tooltips.e2e.spec.js +++ b/e2e/tests/functional/tooltips.e2e.spec.js @@ -21,19 +21,11 @@ *****************************************************************************/ /* -This test suite is dedicated to tests which can quickly verify that any openmct installation is -operable and that any type of testing can proceed. - -Ideally, smoke tests should make zero assumptions about how and where they are run. This makes them -more resilient to change and therefor a better indicator of failure. Smoke tests will also run quickly -as they cover a very "thin surface" of functionality. - -When deciding between authoring new smoke tests or functional tests, ask yourself "would I feel -comfortable running this test during a live mission?" Avoid creating or deleting Domain Objects. -Make no assumptions about the order that elements appear in the DOM. +This suite is dedicated to tests which verify that tooltips are displayed correctly. */ import { createDomainObjectWithDefaults, expandEntireTree } from '../../appActions.js'; +import { MISSION_TIME } from '../../constants.js'; import { expect, test } from '../../pluginFixtures.js'; test.describe('Verify tooltips', () => { @@ -48,7 +40,7 @@ test.describe('Verify tooltips', () => { const swg2Path = 'My Items / Folder Foo / Folder Bar / SWG 2'; const swg3Path = 'My Items / Folder Foo / Folder Bar / Folder Baz / SWG 3'; - test.beforeEach(async ({ page, openmctConfig }) => { + test.beforeEach(async ({ page }) => { await page.goto('./', { waitUntil: 'domcontentloaded' }); folder1 = await createDomainObjectWithDefaults(page, { @@ -89,7 +81,7 @@ test.describe('Verify tooltips', () => { await expandEntireTree(page); }); - test('display correct paths for LAD tables', async ({ page, openmctConfig }) => { + test('display correct paths for LAD tables', async ({ page }) => { // Create LAD table await createDomainObjectWithDefaults(page, { type: 'LAD Table', @@ -98,25 +90,32 @@ test.describe('Verify tooltips', () => { // Edit LAD table await page.getByLabel('Edit Object').click(); - // Add the Sine Wave Generator to the LAD table and save changes - await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-lad-table-wrapper'); - await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.c-lad-table-wrapper'); - await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-lad-table-wrapper'); - await page.locator('button[title="Save"]').click(); + // Add the Sine Wave Generator to the LAD table and save changes. + //TODO Follow up with https://github.com/nasa/openmct/issues/7773 + await page.getByLabel(`Preview ${sineWaveObject1.name}`).dragTo(page.getByLabel('Object View')); + await page.getByLabel(`Preview ${sineWaveObject2.name}`).dragTo(page.getByLabel('Object View')); + await page.getByLabel(`Preview ${sineWaveObject3.name}`).dragTo(page.getByLabel('Object View')); + await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); await page.keyboard.down('Control'); + //Hover on something else + await page.getByRole('button', { name: 'Create' }).hover(); + //Hover over the first + await page.getByLabel('lad name').getByText(sineWaveObject1.name).hover(); + await expect(page.getByRole('tooltip', { name: sineWaveObject1.path })).toBeVisible(); - async function getToolTip(object) { - await page.locator('.c-create-button').hover(); - await page.getByLabel('lad name').getByText(object.name).hover(); - let tooltipText = await page.locator('.c-tooltip').textContent(); - return tooltipText.replace('\n', '').trim(); - } + //Hover on something else + await page.getByRole('button', { name: 'Create' }).hover(); + //Hover over second object + await page.getByLabel('lad name').getByText(sineWaveObject2.name).hover(); + await expect(page.getByRole('tooltip', { name: sineWaveObject2.path })).toBeVisible(); - expect(await getToolTip(sineWaveObject1)).toBe(sineWaveObject1.path); - expect(await getToolTip(sineWaveObject2)).toBe(sineWaveObject2.path); - expect(await getToolTip(sineWaveObject3)).toBe(sineWaveObject3.path); + //Hover on something else + await page.getByRole('button', { name: 'Create' }).hover(); + //Hover over third object + await page.getByLabel('lad name').getByText(sineWaveObject3.name).hover(); + await expect(page.getByRole('tooltip', { name: sineWaveObject3.path })).toBeVisible(); }); test('display correct paths for expanded and collapsed plot legend items', async ({ page }) => { @@ -128,66 +127,74 @@ test.describe('Verify tooltips', () => { // Edit Overlay Plot await page.getByLabel('Edit Object').click(); - // Add the Sine Wave Generator to the LAD table and save changes - await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.gl-plot'); - await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.gl-plot'); - await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.gl-plot'); - await page.locator('button[title="Save"]').click(); + // Add the Sine Wave Generators to the and save changes + await page + .getByLabel('Preview SWG 1 generator Object') + .dragTo(page.getByLabel('Plot Container Style Target')); + await page + .getByLabel('Preview SWG 2 generator Object') + .dragTo(page.getByLabel('Plot Container Style Target')); + await page + .getByLabel('Preview SWG 3 generator Object') + .dragTo(page.getByLabel('Plot Container Style Target')); + + await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); + //Hover over Collapsed Plot Legend Components with the Control Key pressed await page.keyboard.down('Control'); - - async function getCollapsedLegendToolTip(object) { - await page.locator('.c-create-button').hover(); - await page - .locator('.plot-series-name', { has: page.locator(`text="${object.name} Hz"`) }) - .hover(); - let tooltipText = await page.locator('.c-tooltip').textContent(); - return tooltipText.replace('\n', '').trim(); - } - - async function getExpandedLegendToolTip(object) { - await page.locator('.c-create-button').hover(); - await page - .locator('.plot-series-name', { has: page.locator(`text="${object.name}"`) }) - .hover(); - let tooltipText = await page.locator('.c-tooltip').textContent(); - return tooltipText.replace('\n', '').trim(); - } - - expect(await getCollapsedLegendToolTip(sineWaveObject1)).toBe(sineWaveObject1.path); - expect(await getCollapsedLegendToolTip(sineWaveObject2)).toBe(sineWaveObject2.path); - expect(await getCollapsedLegendToolTip(sineWaveObject3)).toBe(sineWaveObject3.path); - + //Hover over first object + await page.getByText('SWG 1 Hz').hover(); + await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path); + //Hover over another object to clear + await page.getByRole('button', { name: 'create' }).hover(); + //Hover over second object + await page.getByText('SWG 2 Hz').hover(); + await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject2.path); + //Hover over another object to clear + await page.getByRole('button', { name: 'create' }).hover(); + //Hover over third object + await page.getByText('SWG 3 Hz').hover(); + await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path); + //Release the Control Key await page.keyboard.up('Control'); + + //Expand the legend await page.locator('.gl-plot-legend__view-control.c-disclosure-triangle').click(); + + //Hover over Expanded Plot Legend Components with the Control Key pressed await page.keyboard.down('Control'); - expect(await getExpandedLegendToolTip(sineWaveObject1)).toBe(sineWaveObject1.path); - expect(await getExpandedLegendToolTip(sineWaveObject2)).toBe(sineWaveObject2.path); - expect(await getExpandedLegendToolTip(sineWaveObject3)).toBe(sineWaveObject3.path); + await page.getByLabel('Plot Legend Expanded').getByText('SWG 1').hover(); + await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path); + //Hover over another object to clear + await page.getByRole('button', { name: 'create' }).hover(); + //Hover over second object + await page.getByLabel('Plot Legend Expanded').getByText('SWG 2').hover(); + await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject2.path); + //Hover over another object to clear + await page.getByRole('button', { name: 'create' }).hover(); + //Hover over third object + await page.getByLabel('Plot Legend Expanded').getByText('SWG 3').hover(); + await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path); }); test('display correct paths when hovering over object labels', async ({ page }) => { - async function getObjectLabelTooltip(object) { - await page - .locator('.c-tree__item__name.c-object-label__name', { - has: page.locator(`text="${object.name}"`) - }) - .click(); - await page.keyboard.down('Control'); - await page - .locator('.l-browse-bar__object-name.c-object-label__name', { - has: page.locator(`text="${object.name}"`) - }) - .hover(); - const tooltipText = await page.locator('.c-tooltip').textContent(); - await page.keyboard.up('Control'); - return tooltipText.replace('\n', '').trim(); - } + //Navigate to SWG 1 in Tree + await page.getByLabel('Navigate to SWG 1 generator').click(); - expect(await getObjectLabelTooltip(sineWaveObject1)).toBe(sineWaveObject1.path); - expect(await getObjectLabelTooltip(sineWaveObject3)).toBe(sineWaveObject3.path); + //Expect tooltip to be the path of SWG 1 + await page.keyboard.down('Control'); + await page.getByRole('main').getByText('SWG 1', { exact: true }).hover(); + await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path); + await page.keyboard.up('Control'); + + //Navigate to SWG 3 in Tree + await page.getByLabel('Navigate to SWG 3 generator').click(); + //Expect tooltip to be the path of SWG 3 + await page.keyboard.down('Control'); + await page.getByRole('main').getByText('SWG 3', { exact: true }).hover(); + await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path); }); test('display correct paths when hovering over display layout pane headers', async ({ page }) => { @@ -198,8 +205,11 @@ test.describe('Verify tooltips', () => { }); // Edit Overlay Plot await page.getByLabel('Edit Object').click(); - await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.gl-plot'); - await page.locator('button[title="Save"]').click(); + + await page + .getByLabel('Preview SWG 1 generator Object') + .dragTo(page.getByLabel('Plot Container Style Target')); + await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); // Create Stacked Plot @@ -209,8 +219,9 @@ test.describe('Verify tooltips', () => { }); // Edit Stacked Plot await page.getByLabel('Edit Object').click(); - await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.c-plot--stacked.holder'); - await page.locator('button[title="Save"]').click(); + + await page.getByLabel(`Preview ${sineWaveObject2.name}`).dragTo(page.getByLabel('Object View')); + await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); // Create Display Layout @@ -221,66 +232,77 @@ test.describe('Verify tooltips', () => { // Edit Display Layout await page.getByLabel('Edit Object').click(); - await page.dragAndDrop("text='Test Overlay Plot'", '.l-layout__grid-holder', { - targetPosition: { x: 0, y: 0 } - }); - await page.dragAndDrop("text='Test Stacked Plot'", '.l-layout__grid-holder', { - targetPosition: { x: 0, y: 250 } - }); - await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.l-layout__grid-holder', { - targetPosition: { x: 500, y: 200 } - }); - await page.locator('button[title="Save"]').click(); + await page + .getByLabel('Preview Test Overlay Plot') + .dragTo(page.locator('#display-layout-drop-area'), { + targetPosition: { x: 0, y: 0 } + }); + + //Add Display Layout below Overlay Plot + await page + .getByLabel('Preview Test Stacked Plot') + .dragTo(page.locator('#display-layout-drop-area'), { + targetPosition: { x: 0, y: 250 } + }); + + //Drag the SWG3 Object to the Display off to the right + await page + .getByLabel('Preview SWG 3 generator Object') + .dragTo(page.locator('#display-layout-drop-area'), { + targetPosition: { x: 500, y: 200 } + }); + + await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); + //Hover over Overlay Plot with the Control Key pressed await page.keyboard.down('Control'); - await page.getByText('Test Overlay Plot').nth(2).hover(); - let tooltipText = await page.locator('.c-tooltip').textContent(); - tooltipText = tooltipText.replace('\n', '').trim(); - expect(tooltipText).toBe('My Items / Test Overlay Plot'); - + //Hover Overlay Plot + await page.getByTitle('Test Overlay Plot').hover(); + await expect(page.getByRole('tooltip')).toHaveText('My Items / Test Overlay Plot'); await page.keyboard.up('Control'); - await page.locator('.c-plot-legend__view-control >> nth=0').click(); + + //Expand the Overlay Plot Legend and hover over the first legend item + await page.getByLabel('Expand Test Overlay Plot Legend').click(); + await page.keyboard.down('Control'); - await page.locator('.plot-wrapper-expanded-legend .plot-series-name').first().hover(); - tooltipText = await page.locator('.c-tooltip').textContent(); - tooltipText = tooltipText.replace('\n', '').trim(); - expect(tooltipText).toBe(sineWaveObject1.path); + await page.getByLabel('Plot Legend Item for Test').getByText('SWG').hover(); + await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path); - await page.getByText('Test Stacked Plot').nth(2).hover(); - tooltipText = await page.locator('.c-tooltip').textContent(); - tooltipText = tooltipText.replace('\n', '').trim(); - expect(tooltipText).toBe('My Items / Test Stacked Plot'); + //Hover over Stacked Plot Title + await page.getByTitle('Test Stacked Plot').hover(); + await expect(page.getByRole('tooltip')).toHaveText('My Items / Test Stacked Plot'); - await page.getByText('SWG 3').nth(2).hover(); - tooltipText = await page.locator('.c-tooltip').textContent(); - tooltipText = tooltipText.replace('\n', '').trim(); - expect(sineWaveObject3.path).toBe(tooltipText); + //Hover over SWG3 Object + await page.getByLabel('Alpha-numeric telemetry name for SWG').hover(); + await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path); }); test('display correct paths when hovering over flexible object labels', async ({ page }) => { + //Create Flexible Layout await createDomainObjectWithDefaults(page, { type: 'Flexible Layout', name: 'Test Flexible Layout' }); - await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-fl-container >> nth=0'); - await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-fl-container >> nth=1'); + //Add SWG1 and SWG3 to Flexible Layout + await page.getByLabel('Navigate to SWG 1 generator').dragTo(page.getByRole('row').nth(0)); + await page + .getByLabel('Preview SWG 3 generator Object') + .dragTo(page.getByLabel('Container Handle 2')); - await page.locator('button[title="Save"]').click(); + await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); + //Hover over SWG1 Object await page.keyboard.down('Control'); - await page.getByText('SWG 1').nth(2).hover(); - let tooltipText = await page.locator('.c-tooltip').textContent(); - tooltipText = tooltipText.replace('\n', '').trim(); - expect(tooltipText).toBe(sineWaveObject1.path); + await page.getByTitle('SWG 1').hover(); + await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path); - await page.getByText('SWG 3').nth(2).hover(); - tooltipText = await page.locator('.c-tooltip').textContent(); - tooltipText = tooltipText.replace('\n', '').trim(); - expect(tooltipText).toBe(sineWaveObject3.path); + //Hover over SWG3 Object + await page.getByTitle('SWG 3').hover(); + await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path); }); test('display correct paths when hovering over tab view labels', async ({ page }) => { @@ -289,46 +311,40 @@ test.describe('Verify tooltips', () => { name: 'Test Tabs View' }); - await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-tabs-view__tabs-holder'); - await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-tabs-view__tabs-holder'); + //Add SWG1 and SWG3 to Flexible Layout + await page + .getByLabel('Navigate to SWG 1 generator') + .dragTo(page.getByText('Drag objects here to add them')); + await page.getByLabel('Preview SWG 3 generator Object').dragTo(page.getByLabel('SWG 1 tab')); - await page.locator('button[title="Save"]').click(); + await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); await page.keyboard.down('Control'); - await page.getByText('SWG 1').nth(2).hover(); - let tooltipText = await page.locator('.c-tooltip').textContent(); - tooltipText = tooltipText.replace('\n', '').trim(); - expect(tooltipText).toBe(sineWaveObject1.path); + await page.getByLabel('SWG 1 tab').getByText('SWG').hover(); - await page.getByText('SWG 3').nth(2).hover(); - tooltipText = await page.locator('.c-tooltip').textContent(); - tooltipText = tooltipText.replace('\n', '').trim(); - expect(tooltipText).toBe(sineWaveObject3.path); + await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path); + + await page.getByLabel('SWG 3 tab').getByText('SWG').hover(); + await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path); }); test('display correct paths when hovering tree items', async ({ page }) => { await page.keyboard.down('Control'); - await page.getByText('SWG 1').nth(0).hover(); - let tooltipText = await page.locator('.c-tooltip').textContent(); - tooltipText = tooltipText.replace('\n', '').trim(); - expect(tooltipText).toBe(sineWaveObject1.path); + await page.getByText('SWG 1').first().hover(); + await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path); - await page.getByText('SWG 3').nth(0).hover(); - tooltipText = await page.locator('.c-tooltip').textContent(); - tooltipText = tooltipText.replace('\n', '').trim(); - expect(tooltipText).toBe(sineWaveObject3.path); + await page.getByText('SWG 3').first().hover(); + await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path); }); test('display correct paths when hovering search items', async ({ page }) => { await page.getByRole('searchbox', { name: 'Search Input' }).click(); - await page.fill('.c-search__input', 'SWG 3'); + await page.getByRole('searchbox', { name: 'Search Input' }).fill('SWG 3'); await page.keyboard.down('Control'); - await page.locator('.c-gsearch-result__title').hover(); - let tooltipText = await page.locator('.c-tooltip').textContent(); - tooltipText = tooltipText.replace('\n', '').trim(); - expect(tooltipText).toBe(sineWaveObject3.path); + await page.getByLabel('Object Results').getByText('SWG').hover(); + await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path); }); test('display path for source telemetry when hovering over gauge', async ({ page }) => { @@ -336,13 +352,14 @@ test.describe('Verify tooltips', () => { type: 'Gauge', name: 'Test Gauge' }); - await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-gauge__wrapper'); + + await page.getByLabel('Navigate to SWG 3 generator').dragTo(page.getByRole('meter')); await page.keyboard.down('Control'); + // FIXME: We shouldn't need a `force: true` here, but the parent + // element blocks // eslint-disable-next-line playwright/no-force-option - await page.locator('.c-gauge.c-dial').hover({ position: { x: 0, y: 0 }, force: true }); - let tooltipText = await page.locator('.c-tooltip').textContent(); - tooltipText = tooltipText.replace('\n', '').trim(); - expect(tooltipText).toBe(sineWaveObject3.path); + await page.getByRole('meter').hover({ position: { x: 0, y: 0 }, force: true }); + await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path); }); test('display tooltip path for notebook embeds', async ({ page }) => { @@ -351,78 +368,72 @@ test.describe('Verify tooltips', () => { name: 'Test Notebook' }); - await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-notebook__drag-area'); + await page + .getByLabel('Navigate to SWG 3 generator') + .dragTo(page.getByLabel('To start a new entry, click')); await page.keyboard.down('Control'); - await page.locator('.c-ne__embed').hover(); - let tooltipText = await page.locator('.c-tooltip').textContent(); - tooltipText = tooltipText.replace('\n', '').trim(); - expect(tooltipText).toBe(sineWaveObject3.path); + await page.getByLabel('SWG 3 Notebook Embed').hover(); + await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path); }); - test.fixme('display tooltip path for telemetry table names', async ({ page }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/7421' - }); - // set endBound to 10 seconds after start bound - const url = await page.url(); - const parsedUrl = new URL(url.replace('#', '!')); - const startBound = Number(parsedUrl.searchParams.get('tc.startBound')); - const tenSecondsInMilliseconds = 10 * 1000; - const endBound = startBound + tenSecondsInMilliseconds; - parsedUrl.searchParams.set('tc.endBound', endBound); - await page.goto(parsedUrl.href.replace('!', '#')); - + test('display tooltip path for telemetry table names @clock', async ({ page }) => { + await page.clock.install({ time: MISSION_TIME }); + await page.clock.resume(); await createDomainObjectWithDefaults(page, { type: 'Telemetry Table', name: 'Test Telemetry Table' }); - await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-telemetry-table'); - await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-telemetry-table'); + await page + .getByLabel(`Navigate to ${sineWaveObject1.name}`) + .dragTo(page.getByLabel('Object View')); + await page.getByLabel(`Preview ${sineWaveObject3.name}`).dragTo(page.getByLabel('Object View')); - await page.locator('button[title="Save"]').click(); + await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); + + // Confirm that telemetry rows exist for SWG 1 and 3 and are in view + await expect(page.getByLabel('name table cell SWG 1').first()).toBeInViewport(); + await expect(page.getByLabel('name table cell SWG 3').first()).toBeInViewport(); + + // Pause to prevent more telemetry from streaming in + await page.clock.pauseAt(MISSION_TIME + 30 * 1000); + // Run for 30 seconds to allow SOME telemetry to stream in + await page.clock.runFor(30 * 1000); + await page.keyboard.down('Control'); + // Hover over SWG3 in Telemetry Table + await page.getByLabel('name table cell SWG 3').first().hover(); + await expect(page.getByRole('tooltip', { name: sineWaveObject3.path })).toBeVisible(); - await page.locator('.noselect > [title="SWG 3"]').first().hover(); - let tooltipText = await page.locator('.c-tooltip').textContent(); - tooltipText = tooltipText.replace('\n', '').trim(); - expect(tooltipText).toBe(sineWaveObject3.path); + // Release Control Key + await page.keyboard.up('Control'); + // Hover somewhere else so the tooltip goes away + await page.getByLabel('Navigate to Test Telemetry Table').hover(); + await expect(page.getByRole('tooltip')).toBeHidden(); - await page.locator('.noselect > [title="SWG 1"]').first().hover(); - tooltipText = await page.locator('.c-tooltip').textContent(); - tooltipText = tooltipText.replace('\n', '').trim(); - expect(tooltipText).toBe(sineWaveObject1.path); + await page.keyboard.down('Control'); + // Hover over SWG1 in Telemetry Table + await page.getByLabel('name table cell SWG 1').first().hover(); + await expect(page.getByRole('tooltip', { name: sineWaveObject1.path })).toBeVisible(); }); test('display tooltip path for recently viewed items', async ({ page }) => { // drag up Recently Viewed pane - await page - .locator('.l-pane.l-pane--vertical-handle-before', { - hasText: 'Recently Viewed' - }) - .locator('.l-pane__handle') - .hover(); + await page.getByLabel('Resize Recently Viewed Pane').hover(); await page.mouse.down(); await page.mouse.move(0, 300); await page.mouse.up(); await page.keyboard.down('Control'); await page.getByLabel('Recent Objects').getByText(sineWaveObject3.name).hover(); - let tooltipText = await page.locator('.c-tooltip').textContent(); - tooltipText = tooltipText.replace('\n', '').trim(); - expect(tooltipText).toBe(sineWaveObject3.path); + await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path); await page.getByLabel('Recent Objects').getByText(sineWaveObject2.name).hover(); - tooltipText = await page.locator('.c-tooltip').textContent(); - tooltipText = tooltipText.replace('\n', '').trim(); - expect(tooltipText).toBe(sineWaveObject2.path); + await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject2.path); await page.getByLabel('Recent Objects').getByText(sineWaveObject1.name).hover(); - tooltipText = await page.locator('.c-tooltip').textContent(); - tooltipText = tooltipText.replace('\n', '').trim(); - expect(tooltipText).toBe(sineWaveObject1.path); + await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path); }); test('display tooltip path for time strips', async ({ page }) => { @@ -433,35 +444,27 @@ test.describe('Verify tooltips', () => { }); // Edit Overlay Plot await page.getByLabel('Edit Object').click(); - await page.dragAndDrop( - `text=${sineWaveObject1.name}`, - '.c-object-view.is-object-type-time-strip' - ); - await page.dragAndDrop( - `text=${sineWaveObject2.name}`, - '.c-object-view.is-object-type-time-strip' - ); - await page.dragAndDrop( - `text=${sineWaveObject3.name}`, - '.c-object-view.is-object-type-time-strip' - ); - await page.locator('button[title="Save"]').click(); + await page + .getByLabel(`Preview ${sineWaveObject1.name}`) + .dragTo(page.getByLabel('Test Time Strip Object View')); + await page + .getByLabel(`Preview ${sineWaveObject2.name}`) + .dragTo(page.getByLabel('Test Time Strip Object View')); + await page + .getByLabel(`Preview ${sineWaveObject3.name}`) + .dragTo(page.getByLabel('Test Time Strip Object View')); + + await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); await page.keyboard.down('Control'); await page.getByText(sineWaveObject1.name).nth(2).hover(); - let tooltipText = await page.locator('.c-tooltip').textContent(); - tooltipText = tooltipText.replace('\n', '').trim(); - expect(tooltipText).toBe(sineWaveObject1.path); + await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path); await page.getByText(sineWaveObject2.name).nth(2).hover(); - tooltipText = await page.locator('.c-tooltip').textContent(); - tooltipText = tooltipText.replace('\n', '').trim(); - expect(tooltipText).toBe(sineWaveObject2.path); + await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject2.path); await page.getByText(sineWaveObject3.name).nth(2).hover(); - tooltipText = await page.locator('.c-tooltip').textContent(); - tooltipText = tooltipText.replace('\n', '').trim(); - expect(tooltipText).toBe(sineWaveObject3.path); + await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path); }); }); diff --git a/e2e/tests/functional/tree.e2e.spec.js b/e2e/tests/functional/tree.e2e.spec.js index 0db239205f..5bd6ca9086 100644 --- a/e2e/tests/functional/tree.e2e.spec.js +++ b/e2e/tests/functional/tree.e2e.spec.js @@ -20,7 +20,7 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { createDomainObjectWithDefaults, renameObjectFromContextMenu } from '../../appActions.js'; +import { createDomainObjectWithDefaults } from '../../appActions.js'; import { expect, test } from '../../pluginFixtures.js'; test.describe('Main Tree', () => { @@ -47,123 +47,139 @@ test.describe('Main Tree', () => { parent: folder.uuid }); - await expandTreePaneItemByName(page, folder.name); - await assertTreeItemIsVisible(page, clock.name); + await page.getByLabel(`Expand ${folder.name} folder`).click(); + + await expect( + page.getByRole('tree', { name: 'Main Tree' }).getByRole('treeitem', { name: clock.name }) + ).toBeVisible(); }); test('Creating a child object on one tab and expanding its parent on the other shows the correct composition @2p', async ({ - page, - openmctConfig + page }) => { test.info().annotations.push({ type: 'issue', description: 'https://github.com/nasa/openmct/issues/6391' }); - const { myItemsFolderName } = openmctConfig; const page2 = await page.context().newPage(); // Both pages: Go to baseURL await Promise.all([ - page.goto('./', { waitUntil: 'networkidle' }), - page2.goto('./', { waitUntil: 'networkidle' }) + page.goto('./', { waitUntil: 'domcontentloaded' }), + page2.goto('./', { waitUntil: 'domcontentloaded' }) + ]); + + await Promise.all([ + page.waitForURL('**/browse/mine?**'), + page2.waitForURL('**/browse/mine?**') ]); const page1Folder = await createDomainObjectWithDefaults(page, { type: 'Folder' }); - await expandTreePaneItemByName(page2, myItemsFolderName); - await assertTreeItemIsVisible(page2, page1Folder.name); + await page2.getByLabel('Expand My Items folder').click(); + + await expect( + page2 + .getByRole('tree', { name: 'Main Tree' }) + .getByRole('treeitem', { name: page1Folder.name }) + ).toBeVisible(); }); test('Creating a child object on one tab and expanding its parent on the other shows the correct composition @couchdb @2p', async ({ - page, - openmctConfig + page }) => { test.info().annotations.push({ type: 'issue', description: 'https://github.com/nasa/openmct/issues/6391' }); - const { myItemsFolderName } = openmctConfig; const page2 = await page.context().newPage(); // Both pages: Go to baseURL await Promise.all([ - page.goto('./', { waitUntil: 'networkidle' }), - page2.goto('./', { waitUntil: 'networkidle' }) + page.goto('./', { waitUntil: 'domcontentloaded' }), + page2.goto('./', { waitUntil: 'domcontentloaded' }) + ]); + + await Promise.all([ + page.waitForURL('**/browse/mine?**'), + page2.waitForURL('**/browse/mine?**') ]); const page1Folder = await createDomainObjectWithDefaults(page, { type: 'Folder' }); - await expandTreePaneItemByName(page2, myItemsFolderName); - await assertTreeItemIsVisible(page2, page1Folder.name); + await page2.getByLabel('Expand My Items folder').click(); + await expect( + page2 + .getByRole('tree', { name: 'Main Tree' }) + .getByRole('treeitem', { name: page1Folder.name }) + ).toBeVisible(); }); - test('Renaming an object reorders the tree @unstable', async ({ page, openmctConfig }) => { - const { myItemsFolderName } = openmctConfig; - - await createDomainObjectWithDefaults(page, { + test('Renaming an object reorders the tree', async ({ page }) => { + const foo = await createDomainObjectWithDefaults(page, { type: 'Folder', name: 'Foo' }); - await createDomainObjectWithDefaults(page, { + const bar = await createDomainObjectWithDefaults(page, { type: 'Folder', name: 'Bar' }); - await createDomainObjectWithDefaults(page, { + const baz = await createDomainObjectWithDefaults(page, { type: 'Folder', name: 'Baz' }); - const clock1 = await createDomainObjectWithDefaults(page, { + let clock1 = await createDomainObjectWithDefaults(page, { type: 'Clock', name: 'aaa' }); - await createDomainObjectWithDefaults(page, { + const www = await createDomainObjectWithDefaults(page, { type: 'Clock', name: 'www' }); // Expand the root folder - await expandTreePaneItemByName(page, myItemsFolderName); + await page.getByLabel('Expand My Items folder').click(); await test.step('Reorders objects with the same tree depth', async () => { - await getAndAssertTreeItems(page, ['aaa', 'Bar', 'Baz', 'Foo', 'www']); - await renameObjectFromContextMenu(page, clock1.url, 'zzz'); - await getAndAssertTreeItems(page, ['Bar', 'Baz', 'Foo', 'www', 'zzz']); + await getAndAssertTreeItems(page, ['My Items', 'aaa', 'Bar', 'Baz', 'Foo', 'www']); + clock1.name = 'zzz'; + await renameObjectFromContextMenu(page, clock1.url, clock1.name); + await getAndAssertTreeItems(page, ['My Items', 'Bar', 'Baz', 'Foo', 'www', 'zzz']); }); await test.step('Reorders links to objects as well as original objects', async () => { - await page.click('role=treeitem[name=/Bar/]'); - await page.dragAndDrop('role=treeitem[name=/www/]', '.c-object-view'); - await page.dragAndDrop('role=treeitem[name=/zzz/]', '.c-object-view'); - await page.click('role=treeitem[name=/Baz/]'); - await page.dragAndDrop('role=treeitem[name=/www/]', '.c-object-view'); - await page.dragAndDrop('role=treeitem[name=/zzz/]', '.c-object-view'); - await page.click('role=treeitem[name=/Foo/]'); - await page.dragAndDrop('role=treeitem[name=/www/]', '.c-object-view'); - await page.dragAndDrop('role=treeitem[name=/zzz/]', '.c-object-view'); + await page.getByLabel(`Navigate to ${bar.name}`).dragTo(page.getByLabel('Object View')); + await page.getByLabel(`Navigate to ${www.name}`).dragTo(page.getByLabel('Object View')); + await page.getByLabel(`Navigate to ${clock1.name}`).dragTo(page.getByLabel('Object View')); + await page.getByLabel(`Navigate to ${baz.name}`).dragTo(page.getByLabel('Object View')); + await page.getByLabel(`Navigate to ${www.name}`).dragTo(page.getByLabel('Object View')); + await page.getByLabel(`Navigate to ${clock1.name}`).dragTo(page.getByLabel('Object View')); + await page.goto(foo.url); + await page.getByLabel(`Navigate to ${www.name}`).dragTo(page.getByLabel('Object View')); + await page.getByLabel(`Navigate to ${clock1.name}`).dragTo(page.getByLabel('Object View')); // Expand the unopened folders - await expandTreePaneItemByName(page, 'Bar'); - await expandTreePaneItemByName(page, 'Baz'); - await expandTreePaneItemByName(page, 'Foo'); + await page.getByLabel(`Expand Bar folder`).click(); + await page.getByLabel(`Expand Baz folder`).click(); + await page.getByLabel(`Expand Foo folder`).click(); - await renameObjectFromContextMenu(page, clock1.url, '___'); + clock1.name = '___'; + await renameObjectFromContextMenu(page, clock1.url, clock1.name); + await expect(page.getByLabel('Navigate to ' + clock1.name)).toHaveCount(2); await getAndAssertTreeItems(page, [ + 'My Items', '___', 'Bar', - '___', - 'www', 'Baz', - '___', - 'www', 'Foo', '___', 'www', @@ -172,10 +188,8 @@ test.describe('Main Tree', () => { }); }); test('Opening and closing an item before the request has been fulfilled will abort the request @couchdb', async ({ - page, - openmctConfig + page }) => { - const { myItemsFolderName } = openmctConfig; let requestWasAborted = false; page.on('requestfailed', (request) => { @@ -201,7 +215,7 @@ test.describe('Main Tree', () => { // Quickly Expand/close the root folder await page .getByRole('button', { - name: `Expand ${myItemsFolderName} folder` + name: `Expand My Items folder` }) .dblclick({ delay: 400 }); @@ -214,35 +228,36 @@ test.describe('Main Tree', () => { * @param {Array} expected */ async function getAndAssertTreeItems(page, expected) { - const treeItems = page.locator('[role="treeitem"]'); - const allTexts = await treeItems.allInnerTexts(); - // Get rid of root folder ('My Items') as its position will not change - allTexts.shift(); - expect(allTexts).toEqual(expected); -} - -async function assertTreeItemIsVisible(page, name) { - const mainTree = page.getByRole('tree', { - name: 'Main Tree' - }); - const treeItem = mainTree.getByRole('treeitem', { - name - }); - - await expect(treeItem).toBeVisible(); + const treeItems = page.getByRole('treeitem'); + await expect(treeItems).toHaveCount(expected.length); + await expect(treeItems).toHaveText(expected, { useInnerText: true }); } /** * @param {import('@playwright/test').Page} page - * @param {string} name + * @param {string} myItemsFolderName + * @param {string} url + * @param {string} newName */ -async function expandTreePaneItemByName(page, name) { - const mainTree = page.getByRole('tree', { - name: 'Main Tree' - }); - const treeItem = mainTree.getByRole('treeitem', { - name, - expanded: false - }); - await treeItem.locator('.c-disclosure-triangle').click(); +async function renameObjectFromContextMenu(page, url, newName) { + await openObjectTreeContextMenu(page, url); + await page.getByLabel('Edit Properties...').click(); + const nameInput = page.getByLabel('Title', { exact: true }); + await nameInput.fill(newName); + await page.getByLabel('Save').click(); +} + +/** + * Open the given `domainObject`'s context menu from the object tree. + * Expands the path to the object and scrolls to it if necessary. + * + * @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' + }); } diff --git a/e2e/tests/mobile/smoke.e2e.spec.js b/e2e/tests/mobile/smoke.e2e.spec.js index cd0ad707a4..adb39820c0 100644 --- a/e2e/tests/mobile/smoke.e2e.spec.js +++ b/e2e/tests/mobile/smoke.e2e.spec.js @@ -62,7 +62,7 @@ test.describe('Smoke tests for @mobile', () => { test('Verify that user can change time conductor @mobile', async ({ page }) => { //Collapse Browse Pane to get more Time Conductor space await page.getByLabel('Collapse Browse Pane').click(); - //Open Time Conductor and change to Real Time Mode and set offset hour by 1 hour + // Open Time Conductor and change to Real Time Mode and set offset hour by 1 hour // Disabling line because we're intentionally obscuring the text // eslint-disable-next-line playwright/no-force-option await page.getByLabel('Time Conductor Mode').click({ force: true }); @@ -82,14 +82,14 @@ test.describe('Smoke tests for @mobile', () => { await page.getByTitle('Collapse Browse Pane').click(); await expect(page.getByRole('main').getByText('Parent Display Layout')).toBeVisible(); //Verify both objects are in view - await expect(await page.getByLabel('Child Layout 1 Layout')).toBeVisible(); - await expect(await page.getByLabel('Child Layout 2 Layout')).toBeVisible(); + await expect(page.getByLabel('Child Layout 1 Layout')).toBeVisible(); + await expect(page.getByLabel('Child Layout 2 Layout')).toBeVisible(); //Remove First Object to bring up confirmation dialog await page.getByLabel('View menu items').nth(1).click(); await page.getByLabel('Remove').click(); - await page.getByRole('button', { name: 'OK' }).click(); + await page.getByRole('button', { name: 'Ok' }).click(); //Verify that the object is removed - await expect(await page.getByLabel('Child Layout 1 Layout')).toBeVisible(); - expect(await page.getByLabel('Child Layout 2 Layout').count()).toBe(0); + await expect(page.getByLabel('Child Layout 1 Layout')).toBeVisible(); + await expect(page.getByLabel('Child Layout 2 Layout')).toHaveCount(0); }); }); diff --git a/e2e/tests/performance/contract/imagery.contract.perf.spec.js b/e2e/tests/performance/contract/imagery.contract.perf.spec.js index 7b3b00b6cf..c1f64dd890 100644 --- a/e2e/tests/performance/contract/imagery.contract.perf.spec.js +++ b/e2e/tests/performance/contract/imagery.contract.perf.spec.js @@ -39,7 +39,7 @@ const filePath = 'test-data/PerformanceDisplayLayout.json'; test.describe('Performance tests', () => { test.beforeEach(async ({ page, browser }, testInfo) => { // Go to baseURL - await page.goto('./', { waitUntil: 'networkidle' }); + await page.goto('./', { waitUntil: 'domcontentloaded' }); // Click a:has-text("My Items") await page.locator('a:has-text("My Items")').click({ @@ -129,12 +129,12 @@ test.describe('Performance tests', () => { ]); //Time to Example Imagery Frame loads within Display Layout - await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' }); + await page.locator('.c-imagery__main-image__bg').waitFor({ state: 'visible' }); //Time to Example Imagery object loads - await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' }); + await page.locator('.c-imagery__main-image__background-image').waitFor({ state: 'visible' }); //Get background-image url from background-image css prop - const backgroundImage = await page.locator('.c-imagery__main-image__background-image'); + const backgroundImage = page.locator('.c-imagery__main-image__background-image'); let backgroundImageUrl = await backgroundImage.evaluate((el) => { return window .getComputedStyle(el) @@ -156,15 +156,15 @@ test.describe('Performance tests', () => { await page.evaluate(() => window.performance.mark('viewLarge.start.test')); //This is a mark only to compare evaluate timing //Time to Imagery Rendered in Large Frame - await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' }); + await page.locator('.c-imagery__main-image__bg').waitFor({ state: 'visible' }); await page.evaluate(() => window.performance.mark('background-image-frame')); //Time to Example Imagery object loads - await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' }); + await page.locator('.c-imagery__main-image__background-image').waitFor({ state: 'visible' }); await page.evaluate(() => window.performance.mark('background-image-visible')); // Get Current number of images in thumbstrip - await page.waitForSelector('.c-imagery__thumb'); + await page.locator('.c-imagery__thumb').waitFor({ state: 'visible' }); const thumbCount = await page.locator('.c-imagery__thumb').count(); console.log('number of thumbs rendered ' + thumbCount); await page.locator('.c-imagery__thumb').last().click(); diff --git a/e2e/tests/performance/contract/notebook.contract.perf.spec.js b/e2e/tests/performance/contract/notebook.contract.perf.spec.js index 5a63d73ed3..27e7a1a703 100644 --- a/e2e/tests/performance/contract/notebook.contract.perf.spec.js +++ b/e2e/tests/performance/contract/notebook.contract.perf.spec.js @@ -38,7 +38,7 @@ const notebookFilePath = 'test-data/PerformanceNotebook.json'; test.describe('Performance tests', () => { test.beforeEach(async ({ page, browser }, testInfo) => { // Go to baseURL - await page.goto('./', { waitUntil: 'networkidle' }); + await page.goto('./', { waitUntil: 'domcontentloaded' }); // Click a:has-text("My Items") await page.locator('a:has-text("My Items")').click({ @@ -110,20 +110,19 @@ test.describe('Performance tests', () => { await page.evaluate(() => window.performance.mark('search-entered')); //Search Result Appears and is clicked await Promise.all([ - page.waitForNavigation(), page.locator('a:has-text("Performance Notebook")').first().click(), page.evaluate(() => window.performance.mark('click-search-result')) ]); - await page.waitForSelector('.c-tree__item c-tree-and-search__loading loading', { - state: 'hidden' - }); + await page + .locator('.c-tree__item c-tree-and-search__loading loading') + .waitFor({ state: 'hidden' }); await page.evaluate(() => window.performance.mark('search-spinner-gone')); - await page.waitForSelector('.l-browse-bar__object-name', { state: 'visible' }); + await page.locator('.l-browse-bar__object-name').waitFor({ state: 'visible' }); await page.evaluate(() => window.performance.mark('object-title-appears')); - await page.waitForSelector('.c-notebook__entry >> nth=0', { state: 'visible' }); + await page.locator('.c-notebook__entry >> nth=0').waitFor({ state: 'visible' }); await page.evaluate(() => window.performance.mark('notebook-entry-appears')); // Click Add new Notebook Entry @@ -139,9 +138,9 @@ test.describe('Performance tests', () => { await page.evaluate(() => window.performance.mark('notebook-search-start')); await page.locator('.c-notebook__search >> input').fill('Existing Entry'); await page.evaluate(() => window.performance.mark('notebook-search-filled')); - await page.waitForSelector('text=Search Results (3)', { state: 'visible' }); + await page.locator('text=Search Results (3)').waitFor({ state: 'visible' }); await page.evaluate(() => window.performance.mark('notebook-search-processed')); - await page.waitForSelector('.c-notebook__entry >> nth=2', { state: 'visible' }); + await page.locator('.c-notebook__entry >> nth=2').waitFor({ state: 'visible' }); await page.evaluate(() => window.performance.mark('notebook-search-processed')); //Clear Search @@ -154,7 +153,7 @@ test.describe('Performance tests', () => { await page.locator('div.c-ne__time-and-content').last().hover(); await page.locator('button[title="Delete this entry"]').last().click(); await page.locator('button:has-text("Ok")').click(); - await page.waitForSelector('.c-notebook__entry >> nth=3', { state: 'detached' }); + await page.locator('.c-notebook__entry >> nth=3').waitFor({ state: 'detached' }); await page.evaluate(() => window.performance.mark('new-notebook-entry-deleted')); //await client.send('HeapProfiler.enable'); diff --git a/e2e/tests/performance/memory/navigation.memory.perf.spec.js b/e2e/tests/performance/memory/navigation.memory.perf.spec.js index cb74b6a11a..022c0bb1c2 100644 --- a/e2e/tests/performance/memory/navigation.memory.perf.spec.js +++ b/e2e/tests/performance/memory/navigation.memory.perf.spec.js @@ -228,9 +228,7 @@ test.describe('Navigation memory leak is not detected in', () => { expect(result).toBe(true); }); - test('display layout with plots of swgs, alphanumerics, and condition sets, ', async ({ - page - }) => { + test('display layout with plots of swgs, alphanumerics, and condition sets', async ({ page }) => { const result = await navigateToObjectAndDetectMemoryLeak( page, 'display-layout-simple-telemetry' diff --git a/e2e/tests/performance/tabs.perf.spec.js b/e2e/tests/performance/tabs.perf.spec.js index 3db219f1da..6141bf61ee 100644 --- a/e2e/tests/performance/tabs.perf.spec.js +++ b/e2e/tests/performance/tabs.perf.spec.js @@ -78,7 +78,7 @@ test.describe('Tabs View', () => { await page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true }).click(); // ensure sine wave generator visible - expect(await page.locator('.c-plot').isVisible()).toBe(true); + await expect(page.locator('.c-plot')).toBeVisible(); // now select notebook and clear animation calls await page.getByLabel(`${notebook.name} tab`, { exact: true }).click(); diff --git a/e2e/tests/performance/tagging.perf.spec.js b/e2e/tests/performance/tagging.perf.spec.js index 495026f7a8..4fa19db2f9 100644 --- a/e2e/tests/performance/tagging.perf.spec.js +++ b/e2e/tests/performance/tagging.perf.spec.js @@ -84,7 +84,7 @@ test.describe('Plot Tagging Performance', () => { await setRealTimeMode(page); // Search for Science - await page.getByRole('searchbox', { name: 'Search Input' }); + await page.getByRole('searchbox', { name: 'Search Input' }).click(); await page.getByRole('searchbox', { name: 'Search Input' }).fill('sc'); // click on the search result diff --git a/e2e/tests/visual-a11y/components/header.visual.spec.js b/e2e/tests/visual-a11y/components/header.visual.spec.js index bbbdc5b4a6..f1dd4e0adc 100644 --- a/e2e/tests/visual-a11y/components/header.visual.spec.js +++ b/e2e/tests/visual-a11y/components/header.visual.spec.js @@ -89,6 +89,7 @@ test.describe('Visual - Header @a11y', () => { await percySnapshot(page, `Notebook Snapshot Show button (theme: '${theme}')`, { scope: header }); + await expect(page.getByLabel('Show Snapshots')).toBeVisible(); }); }); diff --git a/e2e/tests/visual-a11y/components/inspector.visual.spec.js b/e2e/tests/visual-a11y/components/inspector.visual.spec.js index bf573e5a51..5f1dd2be00 100644 --- a/e2e/tests/visual-a11y/components/inspector.visual.spec.js +++ b/e2e/tests/visual-a11y/components/inspector.visual.spec.js @@ -29,15 +29,14 @@ import { MISSION_TIME, VISUAL_FIXED_URL } from '../../../constants.js'; const inspectorPane = '.l-shell__pane-inspector'; test.describe('Visual - Inspector @ally @clock', () => { - test.beforeEach(async ({ page }) => { - await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' }); - }); test.use({ - storageState: 'test-data/overlay_plot_with_delay_storage.json', - clockOptions: { - now: MISSION_TIME, - shouldAdvanceTime: true - } + storageState: 'test-data/overlay_plot_with_delay_storage.json' + }); + + test.beforeEach(async ({ page }) => { + await page.clock.install({ time: MISSION_TIME }); + await page.clock.resume(); + await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' }); }); test('Inspector from overlay_plot_with_delay_storage @localStorage', async ({ page, theme }) => { diff --git a/e2e/tests/visual-a11y/components/timeConductor.visual.spec.js b/e2e/tests/visual-a11y/components/timeConductor.visual.spec.js index bd089bbb6c..e880bd6642 100644 --- a/e2e/tests/visual-a11y/components/timeConductor.visual.spec.js +++ b/e2e/tests/visual-a11y/components/timeConductor.visual.spec.js @@ -33,13 +33,9 @@ import { } from '../../../constants.js'; test.describe('Visual - Time Conductor', () => { - test.use({ - clockOptions: { - now: MISSION_TIME, - shouldAdvanceTime: false - } - }); test.beforeEach(async ({ page }) => { + await page.clock.install({ time: MISSION_TIME }); + await page.clock.pauseAt(MISSION_TIME); await page.goto('./', { waitUntil: 'domcontentloaded' }); }); @@ -87,7 +83,7 @@ test.describe('Visual - Time Conductor', () => { test( 'Visual - Time Conductor Axis Resized @clock @snapshot', { annotation: [{ type: 'issue', description: 'https://github.com/nasa/openmct/issues/7623' }] }, - async ({ page, tick }) => { + async ({ page }) => { const VISUAL_REALTIME_WITH_PANES = VISUAL_REALTIME_URL.replace( 'hideTree=true', 'hideTree=false' @@ -108,7 +104,7 @@ test.describe('Visual - Time Conductor', () => { await page.getByLabel('Collapse Browse Pane').click(); // manually tick the clock to trigger the resize / re-render - await tick(1000 * 2); + await page.clock.runFor(1000 * 2); const mask = []; diff --git a/e2e/tests/visual-a11y/components/timeConductor.visual.spec.js-snapshots/time-conductor-axis-resized-chrome-linux.png b/e2e/tests/visual-a11y/components/timeConductor.visual.spec.js-snapshots/time-conductor-axis-resized-chrome-linux.png index b86cc7eade7308660709b6893a76fc09068068c1..fd7007028957736f58352dca7f6d2add3805dba0 100644 GIT binary patch literal 31101 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 H{>T3V(LqoI literal 33614 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

& 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 literal 29757 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“p-z#Lf6+sFhkc6j*jbut;mZ3lI$AY=Q1c>RrffdKF;1dn6!j0gA5q)L z8|>)Z9d^DMnc8!}MxQ8Z=LdN7B19#s>)v(Do2*3~mW_=qtWonD1ME0JCC(@JtE;Oa zW?r!($~|_i<;IK6daqr(W}D!ot^-?&lZ%TL)&n{VAfUCQncXcgq^}Euy zDK>FQEaQ`v9Hwv-r%!RhT9eeL;-dGlXh>$_SDC6EBH9pH)^MpE+6<*6!+Jj=fa7xd zh2~1~Y2EWTGp6^}XMPXnu|O58o?$*q%kNuWE()Z8+=B8zcSr`){5$!gGYx*e^fhAW*cW~hA&h98XY6O6x>BD&SmK{1s`cH6&x@+Qg0 z`uY2=Z5P9=_|OhCV&(@|s6$&sp4o4a<=H4XA7v|v9hw{;e{#&IT)3;-+NN&8J=|WT zv8_KhyZ+(TNa;>{4(tCgWyn3z6xst3l5R|COkaDWpx6if+-9p5P+T07dE_NViv!^ zawRLV>W01(Gg3mnWtD(0-hcSe*q5oobXW#$ z8T>T~e+vh^{Qdl50rE0QB$Iy!ZXz-zWmS03Mf$*^Xn3f^f(F2gWA=jQ+T?92wGCSJDj$q=m-X|tLdianOn(Kza z!CF^C2=V4b?=j`)WR>iI~~(b0u1W)&Kt&gPbX$6odZej%)p4yenHcbD(RN_mv36oI&eQNCmwP98nbt4G3wzYXL8HLUOV3$8L)(gr zH=HclNr`1{9~}qKqWnz zr&e2Bo4T&4{0#HNq;zEMSb<9YlX)r-FOdle3ILMdgoUN2ruxBs2aY-@H1vyw#$n_v zq%@lBa6I^r@%wi9JqDZ73mm@V*TakVHL{=Hj^WkW8%1M@>mbmVZ*gi~Wc<-84$B7(^Q%{{%E`8+?0=nN0R|}auCJh=puD2u^ZkukrQ0dl+1wC&C}I(lb44QEbKf>sM_B=~r3SCu|1m*rkXYSUNTDJCFKI8ZSGjD8=&QA!J6s zON#pfCp6=otKPd3vf<2%nwoJ&t_MKR%RwGvT0O0Lw!wL~@^^O{2{gTzRf!vc2MQ=F z6M>!37AF|kpQTX*&(OEBva&M8WkKhJhqutn!6KaaaGFghXZl&>Yu)gE^XChFnQe(; zy~~ICu*yJo&GGr{pFuE)SUVa^)UG!%Jc_#!0J*TjY7^k#IyC7PUQ*K$pRGeP@?8*`|+qxb`1PN&5IJJoc(JkkGKSev#w$?b}e43XGfIyO*HXks`fi4z5yKevO7~b_d8|_p7~+oeL%cgKyvU zp+Q^?3ozrmZFPp-Br(79^wY1LA3n#Gtdgo=Ur=DZ_PDoLp0k{Pc9-nqS=VV zNjj%K3v)^PtKqC&Ihb)YC)Kq>W<%vVdG`c%bPrQmFPCb4!lbADRrb?vn-OUTHeBg~j- zX&(T>1i8dM&i=BvIK7OFOcprpH^vU$@5Utsd@4S% zyu`pTHR$tq2o*Vy)DU=ZkBz0(y?D|JX4ro{r;vFfX}Di+S;$BFr1$%!D?E*=-hh=ZGmabJ%s2zfhJDNn zZIX`Vcxj0cK9{rAEkYHZ1^?trsd|pVxA*u4e;`+|C&ryVzzCHQi^f;9D_45nn^Pc|hrK7nuY4FA+gw_*Bt3IR=(EPJ^M=9+oZQ@3CA1y% z@G%z@^q>_yMP(%~FWYXb1}$3naPwqbqX=7h^?Y+V{I5@i501aR{69Mp<^KKNZ|DE| zAov=?PlWjQg}er&h_`1^-1W=*IuIcXA@G z{qu45rv(wEO-u93%ZiU4kpWa{M|1u40q~XJBd9BVeX7^5U#BneNh9>dbA67B1MOBd zpyY{)(gK!OwT2Y{&2`D_-;>5#fSci5WOLvAu~dTnMx~OZ-Sj>9K=W+qJ*Pl1pa4-# z@weYU*Y%SvhqGQr%l+&vmwFjr$jc7nPJ@*6^ds+e{&i#R=Q-b|AponC%DF%Oy5L{u z4G6QDb(LgFiTi>dw0?CsWeZ*ba2No;?FYoHum=gKR&;>K@Y;C6gr%^<^p7`M7_!8h zX6I*6;T(d3kr^r8BUV+W$!-A1#Mhvv6af#xzU={N0Wd@;Ui{_JF1`8t>EFv0`(~x5D4{Q_OZ6Q$R}#!Yn`&i;&vf<9Ch}z?hp+!`&Fg*?3&Q9=-TJEJ zD^s`ifR*@S-^ythCt^feYAU43E0U7c_fPn$nw&VxEIDLV(`Yz#xQfI;AucHe3(!Zf z`Y=j+4t=S;8+g`nzKft}c+B!RmlrxNyq1hUUUA`ZKKs|biU?NQ1``aNhb6Oa7>akB zQULSC24_`x818fuhsd6&{Y6Hrkl>{Etjk-z^(&XR%k4nKU<@?yuwEsl_->bnC-l?Z zz=5YJQBn7_wCJgWzXZ6SwzIS2akl*XGjQzBb4DX>tPAW&ZSn0$Z(I&sbq&b&HX1AK zq<3ZF$g4m$RYtuDj~p~ASJ8Zol4dN&DOwU^x`N0FNz!YS9zU|#k=h_gS2t*PypIWQ zY>ipM=I&mvLep~V)|RX;E~A<)RxowNB15=Q7cs{{nV-3{wPjJqNUxZU$x`wWl*!hm zJXAjQVlSU&D4j1f18g98`<7ZQi%9VDc06_lWg(1NN()mD`4g}3%0v5aULu_9pPURT z-+2}mJqrn_XwJ(cjnIfa&DSRYDJ@JK)Nyb(XjVD%4C;F1qKnj6xNkcgZ2iUyCBFEu07ufsLRQ|1t|C)qeCrO?hd|VB`lJkYj}=d)-1K8!;6>^5fLDnueXb!^DPE>@tzns8ets()5Kf>fGqNn?A_29XqA}Xx3d!f zI~t~y4|!Ea2B~|DPwDWec&b`ViW%bZx#GA>n(B?E?VOf;$XaE)F>+Yo=1N&Gs~zK| zwYqmYp#bZqM8N66|CHxK<5sF#%dlDVTO)Ua{NYm%#ubi`-r;g@4mq27*@e?c?lV#4 zyB|KeFn%W3=A!~bu+6348WXaUZ@g%^T24J04ya#9y!OGn(P}7!`3F*lDJ?UxaE&$$ zy?0?kEaSz8!q(YXn#jVn7aQ(m9LBi#GvgmNQ!lSq(G&U(K640F(1iAswSh-Usz#Tf z4lu4gTypxeGXQ#lCRYJo$U*`ZA2do@5o^ZdgKZ=MpPw$Ds~vP_c;H<_q*1BTERns_{K3y9=9NIiI43!Xs(Tq**@ya{ys z2)wzW*u4Mk{a|WPi0=Yn28ja`A;&z`>4SA3Tq*r4h5ZbOk+PK};Mk$8SfG;Ayfq>% z&gV+(pGoGMjh(p`9+Y0J$bDO`Oh5ZtTD6nM2%lHN+mEM|LU2sfIDOB~t45n!_F{nq z43`fzBGC@zDhU2O6#t!_-0!}jcXw+a4(_s`viKAHhP)H$yak2%^r|-mgRod3(rMy9 z7{?>$S#(Jz*K~Y1=GWLqa`(rQ<@dU(wp@=VHtjo~a6HP$Z@q=NlSPEirqVBZOif8B zOJRQ)#$LAi0-?n;aSvsKohnvvbG?~Uh70O84!77d`O>rBD>JQWPVs8R;{~&o2H(eP zxiS|oTb&**iw_FbBYfBEaZ=Nt!%)zDQs>?MKo(v6*|ohKd_2HTosdCeg1kf~%*9lj z60BjztVx*1$}t77aE-eot$`wMCMkvB)?-V(Cx^QJjePI&mu18|B3Wc!g?whHd-0=4 zG{w0U8(?a1HZ#&hf1Ui-P^dE1VdEJdpI*NF*FRRx$q*|OH4HvJhob92L8kyL|7>e> za76o8PA3S_$Pl`AT{KjLKT*&OU zo9!Zk#y`(xAoOwAV}P;bT4Q+L4;V~K;e=i{>*{q`r6lo2=rdPny{m1|(%7{?8Ga~d zVDB&~qNJv7lEJQ16c!eKyy5E(@$@b!X}KH=^p7pRgt4%6_Iy!T-`&js4C=YvuK^IB z*%#0K#K%9}T^R?e!0sgL-*r>#J^D9ml&q88_04wCuzEv#$y{mWk2dawt#%JbfAqMc zp2XO9zAs7}BU3*^RB$|BJxS-eA^4NtH7SkPYmevTXQoT`+Zj>!f7osf(3~1xrPnw$ zyeTMWlM>qRz%1(^rmk#5T6vazB~4Rl`O(W9F|nP?hPY!6PeGOChEO@QLSH9KU+5BPDxo-6x+14zldV@%KBb0qd1WQ#7S)nU>^h9>D9AhvT-9sf z;A8UL=(?s9;t^Z;v#2-bViUQom{M0n`{(d-!Xh93oF~pxkq)G*er-iw#hL;nuY1T(uru1e268P!*^cE7@6PLCcCnjmi} z=?aFHD+h}EV9atYW~(NC%b;vBChks~2H%Xq z^(+@n6``x%8b*jU6pN3eWbMv~h5UmD`oMqSO(wUQ=peU%z!F+>76C-^{74B`V`HP` zM0G{S4ui1eqz3ZfBGAn2VDADMj<;&*M(y!{kW@L~De$`V=IP0R?+lD_MEZ5U1WOY& z3BbUyZ%ctI4uD9$>$)L$Ba@R&ZFVPWJSyO`lTJ7SW&)$@-$&1IgD(V{aNqJC}3Jnh%qukJPiJu@~fauOhY2=j>*G{W~Tw z*XtA?3t`K}?#apyWQ{}h$6ILbz9usO!w(rPKLT_IVXF~UqeJkSN5;nf^d%sCT4X{F zbP9+JuYr;(Z1y^P`ZR(5Sh=0j-SEzf^z;wiE2Zf5ykZ^tt6xDN@y@EzeSkU%uAa!M zmC9g_6wmkI`^9q|bL|O9bF_cA0cK{x{*)FaQ>C&+&6J|wvTEOIJU*LV_Xo;(Z_dW7 z#)uON%zK~B?6k48;<^2O?%J2GQAx+#%a7-z>p6^9J9i9_hlB6X4x*PmG%A@pQ7P}8 z*BPsKh2-MuRbLP1T_ni#;C!?2P##t(eEG)(MkTWZrKsXnrJoaOyr^%SwEe#(^|md< z^GlnI+&=uGD)b>AK1K~x`oPvd$ut~7PRO=gWw#M;M$2t-Mc9YXw_w_GC%)dK;WdHw z>>cg^JHk4XhG~Y$D1zUQy@ltd9zTBDD1u0<@EhX8KAijbW@o8fu(?=UxHF>Ug`{c9 zx6P+V{z-GsZ)!cFZNC_M>}Ot+9$mRwtI2WA3{7q+>{--Huj(w_Plw1Reb_HVqNhkN zu~f#1YU%96K-(XNm;^=+ZwUY*PpA})^pOF}J&@h6ge(U5fuV)1lpy3<3IbTh;o%&YFTVsi2vmdNS}zHJ6ef}3e1EkCPgvyAYaapl&bYB!E$EoE z>w!3nmI{1(4r6?IHBlujr1mL%4op93NdJ#q3P1;1|7e)O;6h@j6DzdS| zGDB7={uH?!$1S6r6rri*IwInpB7*bN~lmIUFQlu^U4ma2~s>m~ovc>u@ zBC`Y$a)G zVM}X!nGW(5HS%^VPU_$Quw)kb;Joq57rSzD8>sMaf`hXL{r;X(xuU2hwMX&?We;^i zV9ZQ`g4d!m@AD~bB*ympHKowXgtd0`v|7|%&3ke^foMM)lZQ#iYcfu;N6Zat+Q*!1 z5jxkx=jTgv0-(ZG81CW+NYMZ`G7!z)$gIb1nkI`jo1 z#C~b0o-AWC+`IxQsl|Wv9h&D&*LbUiREF543#)#Xa4+)rPU82XI-j1Lm)+rt>dE^6 zu;g#=UWF8;a=!mV_HC;9wPOof9(GTAm@;D4S#&$0;^J`djWdd*tdRppCgzpbgzLL4 z`RF-qHZ`}j_YlHA;W**2)+36hw{oOr{p+<%4NZq{T;7AJcYXPypc1KQX|;rkOau*E z;$1i;rFv!7On|~?niy>GgDC)HZZ1>{Cy+OYp=B>zxbQAIx(T1nz~zbWohmJID~OAR z+cstu4`*E#`rdS>$!#`sq=?>k+l5pGcZwAe$Q7wE|=C+|#6Y;nV_eCC!U&hvX@SnF{yyys3eX`!g+E+as)tpg zL&&$}8#D}oe=jL((!|)Rh)R3tp1=g~3o=xbOpPzIf+)*C$*p zN6BBE%h84##`N4~&7$qtbpwg$bEQUaj3hi;UBXb(6&b%bntQjUHS`y(-EqezF&oJa ztB#V^AF7kon@4p<5I;1|+NIkMS$5)TvL-OroSc!&9G;7YerHEBu5!)CKN#?hALI~? zQT8SlKp!<&d4-FtI8-qK_EK_EyIr1*HG+6dJ9f#dWaKlS^-NW-3>(W{mQ-`NppH>{ zuUzKB;yk*GGvOtu^Y!)Ba`{?&sj<-l=7s(&xc)yu z7b={^6wFwAy zeVZWgEYmb7R5^lsoPRSuNYAE?C)v`N>{p9Z@&l7^jLDPdy;DdBr#kG-ZXlEdO~B$l zqjZ}UP#&r@Jel4c zb8jjT0dq4_zLv6Vr0*<4xp% zL`+hQzOKrIXU2+en)`iTv7=49BS}m%IzygVvoAmYd{dcl>L0|uqHKP?W z8?O|C@t$X1+x;MTtiw2o$f4NZmkcauWLcR@&Nh2wv)L#c;>At+PIY&8@GAMGq_Dt~ znYq%?SmAjZFeNEv%?Q70H2jpE{k-Y(?tHg@rb@>9xVYxA3Ww&OKVL*_{zE1P-Hc(d zsV-=;x3RGS?*?9VoKs!7K{EO3G!?ixXP_yB@}u&{AGe?ZE6~belyDB2ZH*2f=eldpXL+SjAU&-+ z{?CgE@iv6--|v7eomSlOtHQCwYV}O<@_9rX2r`nw3GGUw>RBcG9^8M&I*b+V0IyMW zdpUIFI>EEsY0qpdwPhTnq?JrCWxESHKPh#1ekeYIDwv&&1{T9?^*jRR;$*;ZhTo^KdW2oy z)VP{L=o+Bn4uMU2zx;2u7s$ZO`iwu=TGqXWhVyN-9I?s(} z4wz>W721N60RvYx6Tvr`{91;HP~C9v~Q7QWTH)=Z**69GBW1 zQ?WRPL8HV{ofE!*1R&9Y0W_(_%hNMH>rcV|&!y~n5KPTuFPVJP?0xUfujWXd5!fi; zZ0P_Hpjl}AhIO$MhQh#VY75+Y^kT)oAJGTobL%=7q$I4gbJQV{2=R3>X8bVvbswx+;AUXEa>dUFyP2Y7RZN=y&q+RL z&9u5W2@b5LFeU|6)eF!z;C<)DCMMz~cBc`g=KU>{Yr${ce1h3xhzVdeA1Ej^z$G!= z`k%}b5v`()$d`Wh8X*I7S)i8|z<^XVug#`ojWQD^SkSN!{ z$_K`0nB$5713$Pdp*<90&WCZd_AV=kgufn{^0Kn8dv~nL#$SW;6MU&;jG|rZ<>np$ z=9HFoZXkA-hG!mdk)1h{1}y4TcJ}ks)Ur?m;HH`XwmARKVJJo3zJ1FmQ0cK!f#TGK z7}~8U7O9kY2gig22ifV|xpTgdg};9N`sLss{C(vLPaPW}Ka5X<@A`j6Y)f>l&YvpY zp9hc7yA*@ahb{8^_ixz9agaIf?d^FVb^cooJ~uz3CID&n4ESGRdI&bxJ$O01+0{cq zttsUnFMtOS$9^E}iu+O%{CnDFV*NA(r%#=_BqVeZuMEJ{A)^y4l(DI)M2QHZbVTXg zg$wsUoQH{_E3nmp>2J@h1?yM49n-&y|HS_rkw+G6@k>{(obUw?N6amQ55Q%^6d6oz zRNfS(4}F@Wo^j*<$a7js+lY^_IN-|q40i-ZG3c}U9|Fq^6_s177(^aY%uYWAEfrM* zlpj~H)dJ`91@6Ezn4AN5W`X1HC*ZW!K1%)HGMH%Sy0JZf{v5wu!={jbnQCntFKR$# zxRJ5bv_L{DY)OmT^})%RnQ?+W5KIO5d2l?Agn97}^oM`KHYg|<=2F&yFBfY+HZ2-{ zT4B!y`N9sLYPL2ut_TWJ`GQXcEM}b`{fL5@1=1+bxX-`Kni&8Kt@)Lv&b$P4PMsEf z1{fXYBrq~G6!kvZ8*Y+~nfrJ7?3tk@=uTH;h}i&c7IFl<{)#(Hau!<*o`quUzOy86 zZ+`;;Ll)lz3jY-#uVC=@jaXn%@IxC>L?=($!X#v&_c5->1!JexzxxS=?BwQV<@a3* zPCb=sN&5BL*Y2Y1N||dGpQVEcT{* zR6@Mh?%3XL3{X31toPc0+GGw}&)+7<5JVPlgI*mBrV;E+#7H7HXUO>G8deicy{nc- zYfj_cB`h(z?q-n8f2b%+T?EhwaRJ%u$G8i6e0+uk{>utP9;rRxO{Q?Cx9Xo-3nTnU9|;Wmk;f(n68p-{ois<{pxoWq|V27#W;fx6!gX zLf3UFKxgI0^;`H?xEj$57~ZQwjf@l80z*__)|-xR4!ThwgujKG&5~Jg0{=QDT2Wj5 z45YX_7225#VPRom1pE^vr2sgkqQhCQfa$RNpEI?Ep=+0e5*X|$_Q=*joW#F>`#Nr|=q&mkw6>7kO4@CTA-A8)>uXUJZC*zp^fFW_b}#mH;L#W*N>M!9=p~x(ut?$4 zA1~hc7L@7TH?>+sMozoEFvnJJOFA(U5QmFOw|J=CbIW^6E7`+9vpguAx>V+(e#Qf{ z??lINuQje5=Q*}JkG)pDDoKR!T~UAXg#NDV@^M^Ywp<>qxXB$dCc(=s?pvV5T~2F~ z{bc(?Wo%-d@i)y^watosH!o;)#8+_ym&gAHfHpVQv7)Y8sWwUy)SlFueA_h=9IAq83oU^ovQL!)Vf)7tM{Tr zy%q$z%al8Zs!POId>l2$+y_s*&e@IBTa8}dT~T9BIsTzN{Sp<8V%nALogKpMwe+Ud)(Tix zKR4)}8T6V;THp5ZxuirdQrzlbG!@BL!r1VcDo{>d{)a=WNK?CuG@U|>)eFsT^=nc0 zR8)*MF&;A-F0?nkDtqqLm^K!q5jUl`tF0%o$k2E4plK;7Rg{@29rzL`3AsE_!CWGZ zA~v3)rOPjHrL(4rEL0l0o)1-bmYG^LY{C|OL}dnS#^@+1SIb{Ksh&-wO3Ti^N-bF{ zUV9ix#FRXfWHspSX)$0Ox;nn~l-tKlfn&3;kN3n`+8fso&eC3xC|j0FuO%dIOg@Mo zs+^kDkkWAIkP(}3*$>&9Pz-NO+La#t?X1&2I+Yk+>Z6+zN+-oH?Kn#(VDI|ea#)>7 zWUQJ$+113Vd+)mYhsO6`7H4FvHEFB9EowDJ)fCMBG2N0)XEc4JIb(S1`F+cAT_&X4 ze1PkVsg|>4HAfWbO$nw+=cHO%4cB}5P{`x?h_9LBRI{^R?6%beXQCoy?}10*XfQAO zhZKc03nissdPrCnZyYrt7nk8|JkL6xOf1Wy@Y1!B)r$`4O#!Ci;aRwibS0FS$yVK- z9*ejLv(Mb@-by%4v7|19eb$5k4{xo$q3660PSjbw-?=ipqX|7?D((H)60J$4cidq! zDupXyHg(*r80gI+ zR@HpUbs42Os3i3#ndy{vuu!1=wlpY_>xRxQH|&TiUyr^|Nm<}M(?KZvbvT2G1zB!m zmMCNKEGj03`hKIo-J*_iV~&ediHqcF3KL(>I5loNi;YK*MY!Cidq=3mWBQlf-84Bj zvpylDx|x~$BSx9*aSNYcxUn&VJ#p19U)a_&!*yvTzM0Sbzq+n7s)?>!M^Uk$hze4J zqJn}*?^UF{HV}~_RfrU6q4y9Fr9-TMND)w^BS>$N7L*nc2#}COi1ZR5qyQng!~5Ou z{=2g>KPGE3bF$B~_w$^!_TC3txf|#?Wy=dOJekbhI-ksM^_YjflZ(*T?+Ta61}l6O z5gKQ&zYcNiMyZDB8ZZ%kE?p9pk!BIo%k-KRa_u0#=61mh1RfDIydib1N!5AgXGZYY2h*$PW~L3K z9LB2jDcsCl#viq+zuQ#KWH={DcOLbF#k;ZXK@_BGRbOY#K*{kXcC~V_-l7mTuL2NM z1`%U2(8VW*pXA{zK#@<^%BmUd()+uvY!!Mzzn(g=;HLWqm7B)%t3GI>Wu3~{SX4fY z+0E9JVFBH?Hhw&xm4j^-F&Lcagne{hm63iXKm?*f4{q?|X(*iXYlEZ#hSqnK3^9k> zZS5aFyxP^nVyvk8?_^j}H!%#P1IwAYoKE0lo$LF0l5=x-wQioEor3WOFk8W%57sF^&+FbW%kYGBb307iHq57YdU+& zd(yk!0BfIcK_G@N?bLT6uUgCCcmCzV+Q_lr{93Hh1sr=Jzo)M+TtAduP*8kr=Dqn{ zhxDpe?d4O5PpcY1Jxl{BNPkguIz+kg}x#U~NstL?VNpdp(|Ruda3c zWr%stnSoN-(vsyFz=eW#n&L*fq1tM|R9y3J2zB(Fu;m?7qHf@f(tA#-$RuWBcPykP z^Jn9*d%2?a?_uHH8KxXF6}xGM5HmB)jehJz9VGZ3zsBXlEJd-jO02L<*~Gb&l>5Ft zc$1#wUj+p%Wr~KUN|X1QT3>(VnOicPMc}9kTdi~RbWb!oUzzy&meeINr@7)DR4X@< z9~ZQe3cQoi4ZBO~e-=*a@6ib{ZlQRYHBD8lGIFe&ZUn8Bq(BV0+-l{kLuS!!p-cF$ z@<$MwZ6gp-O+U{q?UK!J=x3IcpoaMZYObYz7Zo0Cus^`Ccj+huIYbj|^-WVuj8Cgp zytnTTL>Ec$LXGp&wHbQAO{#b9CL(2y8_+?TKDME_P>Jpj1JmX1mR>7GB?%~0tEnID zSx@&z*AyviBx%lWW0j0XNaIQX(O|+{TTF*AUbRX0u)l}rBsA^BPBb)1g6(VM2UV#P zfTW?(MN{X~pvi@W@kXK!_{}WXz64_aoj1|93|c8mFoAEsI9`zg0-YV%-ytUc7d#V@~$ zXl_j;X25n>7vwxpzH)HWo^+Im@xCT{Y0`Es99wo8`>j!O9~NffT!^y>(KEQ9(XJ$^ zBuo7q(SuOrb6wY)rzE>9EijLn^KZ!B@)PcSN5)O zZ>*^auH0<+0~mMWt&D+w*ZM_R60HhK5kil_u&=fs< zUxJ7AGj0?-%R4Q%HL)>F0zs_;#;%Oabo!$cxzSouh4-<|nLgOhucZ-o)YNANxL_q{ zT(~xNjnpIrP6QOMfgSAD6nmjMehwLaLp?mA4CZX)IrVVNCE{gK1^#TahfqG0vV?-s z&`a%jj&b-=(ze1oq<9@z^M0}`s$YQ$=TP=#-6uh zMXdE#RfXaztTv~bZuC?G@?dU)KLf`ZLQ*D4af&JlTmuS@}!dG{EK0B+QoA%)+vI~dWx9Qn$U zJ#HGW=SW`4ZC-Q0`u%d&?;5%ZBXK(H7Ua+W8_6($8^y;{1~v1Xo%?^ESLs@((^7gP zTe`zOzq6h+tiCTJVas|OBd|GJj2tlzo(YATYDim8V!vf8RFLn#^@8ZwF>fs zw{ME{jlMO~S2TEJK5p>r_C-N^{QLg6vhN$K$Y6XPkz-vbx4Ep3_e&u4i@vHM<}_I{kev!426>4>-4lx{q4D5ZXFCFi&w4y5|MTA;Y_{pchi768g+)Em zgWK*bze&O()rRFRadrV`O$4hG7dgJ^`=6k=s!e-20V^~C^TJsY^WAB4Zlj~4K@s7k zUpYHkz_2bSnk|(!*JHm~loDy#eJ66P@YN+AZXX*)uz*Mgdd1vk?&O`%uc^7*3q;E~ zA0o3>nfjT>cX(0)^%WUmsZU9426*V2Hf7cU>{AwJv%FzMfj{CBno?;CBz4O9TkrDv z4-30+&#KzM$s$XQb-w=Q`}%RXyD{h+Y1zHGVsN|yRgazmbYkL|4P@huk~G~6kK9CS zfewIFt5MnV&9kJXY+QboBRl^&Dt4!c&?qg50An{j#(oTeF{fYt%QI3UQ$u^YG(|;J{#Q&_|1(HFKo6RY0d{e8vDge;-;-59P8NW*R;Ql9%&X^)b84o!u`V)B04j1{-UD*CzhO;B-TtuZ zZVf5F=JRDiK_OCmkA=P&#Oz(bY0{m$$A^a|CWdb$1}Khq(<_!2?4r2WHtUu$cZycT zfHG4%%dOD+Ua+ktbLOpJbdGaXbK#A!t*c%t<67ZqflYo(C>y?RlSrDrZ3vCQ=$9az zfwQ_bu&Z!l8*a(7xejsuIM3|tm4WF`q)_9JurZ0)cqiHn;S7dGC(B zSr~6o_g;}R>e&b4zWS*i3y-nG?KY2LRu=;Zr|1y@wFtMiEqaMZo9J7BAdovhw>K;u zn!XP(XsEZai^bH(G1MX>1%Qw%A!CuSk=GZFn%-}lh^O-SbMm+ zX?`YomTRDEV+4rNsRgAJsxC<*aOS5fa5Da$hm!x=MbQm(_p^ik{0x~+h;#Z`bT)r; zAA2-c{N_Z)FbC<9ziXgC zl|^fdQpH}!Di4n-a(*a1Q45fP4J+kuA(nx5eeSPsRngvyZ1fhkN~vNAOk}yMy_G11 zprF7a&Al{e_Z^BVm}wX`>QRA)sO}3cd#ygF-wB=j9$-^O&*+$w3csOjcSMlMGMx6yY*U4P{vRoH}c@Ny| z)_4?k+dXtKzuEcD>+({A$2V3nYnVE(s&HfP=H1rwZ6-+&1bmVU>WtN#3iNm`pQ>>s z-MgLKG2L-zt2m&+k9K)?qUh+g=UveYH7=(nFLxV>T~|ChORT?Ds_@6h@yc40)>$6X z^IG_N)%Q_z~7zmj)Lkb24TbsI}0 z?ZMgw6<4qr>M#U1sP`3Jg`wHs%Q=@oF2Uh3i%U{xUbkxU!Y4Np%)~cZYV<#&Q+lxG zi>EuKy6iEq6%wRjuYJ50^#uKhiusZ$ z5VfCBs(40F(&SUYbolj_Ko0$x3;hGu(IsYSo0{L{)ATN%svfOiFP&DD?2bp1-xMDx zGM6&_Hm{boy>qli@=CwU$gAgYWzG|luigm9)I9c$OmV2Y&~2aFU)Wa6dGTS@NSd~# zVZkx^MnT{MlSEG{vx-!4sToD5FJIB0x%Zx|Zr;Y9BEArCWFa8-07sMrTCt$@ZnVx{ zX#-}L&(W6$FH7i8Cva98wO6?Z1W0{)xa{-o(?Mh-UH$22@e5XBdQ%MFkn7NE9eU3{ z4GSJ=4XU6CGx0>+kTuh=WIvD4;#_Yo@bI~alTtrcemml*>0GyM z$+50??VWX2`Ep0E(Ae5e1(R^!pj1J$!HpCm@$fZaK`Zom3T z0Evrf@^jDL=y<{3$A?~?GkIy77k#h7aJcx$b(>eu0k8OchR3@xQn^jr@z|mJcN6Qz z&r*~J10zo;yuEn(=dnxEfvRyU(n;e!S{47T@2_lkb|86S|aTb4lW~-oAJE6RK;myDTO~bV@AY%yfBUd7Qi;}Bj8cJ+Em`>rT`TpI`f%;jDVZd3z0CY~~=w2djaQacg6 zp!m6cCaErn*Hv0+22<8@X1QA7)9{k1#boS#oX&IWg1Dh3BnDUO88!zgLl6F-NmyQz zeGp%0ArRF1`0u%_tw8CVQ5E_hYOu-7aLV0RNa`ya<#z>ZIoh6~8w&fs*wg8Iey9A_ zuSM~ez4m%-!iPwhq=>Q>g=eiv`Yevsa3)H?Tq%2kY9VK>5f=A|!$&r#&Gbc_-+9N` zlm0`5FF$B_hmNPpx5|e}+iY-WtP+Js0Gv$rXkHn3n1AWZzJFp^J4#sCEV;_F$7UDF{FTQ^SMB6@m_n>c6{>jR=@F_=)FX6H8b=rBIbNNg> z)n0`A%}rys%`P10iXuGes(Pc|(!_pY^9MHh{M@}%L{}*e?yZ9v6~D#8++zG0A-}3$ z6KwgW#wl11u3Xhg00=Ej`k$KtPmcz9TCZ+db2FcbzSGY1YG{&h1*gg69LswEiq?%1 z;xjgCw{)$d4?Szy+7bbQ@*YHG!|NmV{=6kO9p;7&5FKI~2iIj2_QIjx=vZ@q@#)O1 z%gohn6Id!mS;tD_m`ZK$1L73PKdXLs3>ON?*L(FLEo!TAqruBS5I}JlulrA4YhlI~ zmvQ#916CZb_S60i6TH$tRMRz|e|9{b&;O(&rlEa;2<;r>i{z#p}6+iYm;A;)X0=gZX;5Xgv# zI~-#lGr>5Q1uc*H^>>^6iMENicW7>z3irPQPYyZq$)Z5-mHu(A(-Zy?{e}gbQ^jd+ zW#11u**1gdP!3xCkw3FP8f~imPwIvobvdAG=^=ga#Q|F7FH~z?=Dr@b ziK6TQ572(?%kUK-Qs(zz|JJT zw_)Ra(Rx-}TAg=Yrya+7kIN_TU-tgd{Vx0|N6-j6%%MG8@pO)(^KwP+kU|Rhf1}gg z@nB+vJl5=+&<5zq{Ygt%{(yh7UZ-BrIU(x)=#HXAdVbZ?-*0Hd!o$B=`1xyZ2dc}d zegu}sbVKWSI7)Q82Zs$+?+-^9#?<$_&KGd8{;knSxpVf_P9mwO(g(^n9be1ZH|f7e ztg&jAjJG_482Ep+Tnm1RQ*t#If#Mah*XW+wJKo3kcPsh#*mVBHkJXiHh~`oUUFAyx zDsTS!C%2UIyH7smKe^PN6Z_xCy4{l(9DUF?s7J6^@wWw5(_Ky*pU{{70UDnMdj9um x`Eu@7|1s;oV;o*^0{sik{fD@5LH^&1T^)W$@nLB0@Dd0(ZW|cuSLi)?{y()V { - test('Tree pane in various states', async ({ page, theme, openmctConfig }) => { - const { myItemsFolderName } = openmctConfig; + test('Tree pane in various states', async ({ page, theme }) => { await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' }); //Open Tree @@ -69,28 +68,28 @@ test.describe('Visual - Tree Pane', () => { scope: treePane }); - await expandTreePaneItemByName(page, myItemsFolderName); + await page.getByLabel('Expand My Items folder').click(); await page.goto(foo.url); - await page.dragAndDrop('role=treeitem[name=/A Clock/]', '.c-object-view'); - await page.dragAndDrop('role=treeitem[name=/Z Clock/]', '.c-object-view'); + await page.getByLabel('Navigate to A Clock').dragTo(page.getByLabel('Object View')); + await page.getByLabel('Navigate to Z Clock').dragTo(page.getByLabel('Object View')); await page.goto(bar.url); - await page.dragAndDrop('role=treeitem[name=/A Clock/]', '.c-object-view'); - await page.dragAndDrop('role=treeitem[name=/Z Clock/]', '.c-object-view'); + await page.getByLabel('Navigate to A Clock').dragTo(page.getByLabel('Object View')); + await page.getByLabel('Navigate to Z Clock').dragTo(page.getByLabel('Object View')); await page.goto(baz.url); - await page.dragAndDrop('role=treeitem[name=/A Clock/]', '.c-object-view'); - await page.dragAndDrop('role=treeitem[name=/Z Clock/]', '.c-object-view'); + await page.getByLabel('Navigate to A Clock').dragTo(page.getByLabel('Object View')); + await page.getByLabel('Navigate to Z Clock').dragTo(page.getByLabel('Object View')); await percySnapshot(page, `Tree Pane w/ single level expanded (theme: ${theme})`, { scope: treePane }); - await expandTreePaneItemByName(page, foo.name); - await expandTreePaneItemByName(page, bar.name); - await expandTreePaneItemByName(page, baz.name); + await page.getByLabel(`Expand ${foo.name} folder`).click(); + await page.getByLabel(`Expand ${bar.name} folder`).click(); + await page.getByLabel(`Expand ${baz.name} folder`).click(); // eslint-disable-next-line playwright/no-wait-for-timeout - await page.waitForTimeout(1 * 1000); //https://github.com/nasa/openmct/issues/7059 + await page.waitForTimeout(3 * 1000); //https://github.com/nasa/openmct/issues/7059 await percySnapshot(page, `Tree Pane w/ multiple levels expanded (theme: ${theme})`, { scope: treePane diff --git a/e2e/tests/visual-a11y/controlledClock.visual.spec.js b/e2e/tests/visual-a11y/controlledClock.visual.spec.js index 8778713107..c175a776ab 100644 --- a/e2e/tests/visual-a11y/controlledClock.visual.spec.js +++ b/e2e/tests/visual-a11y/controlledClock.visual.spec.js @@ -32,18 +32,14 @@ import { expect, test } from '../../pluginFixtures.js'; test.describe('Visual - Controlled Clock @clock', () => { test.beforeEach(async ({ page }) => { + await page.clock.install({ time: MISSION_TIME }); await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' }); }); test.use({ - storageState: 'test-data/overlay_plot_with_delay_storage.json', - clockOptions: { - now: MISSION_TIME, - shouldAdvanceTime: false //Don't advance the clock - } + storageState: 'test-data/overlay_plot_with_delay_storage.json' }); test('Overlay Plot Loading Indicator @localStorage', async ({ page, theme }) => { - await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' }); await page .getByRole('gridcell', { hasText: 'Overlay Plot with 5s Delay Overlay Plot' }) .click(); diff --git a/e2e/tests/visual-a11y/displayLayout.visual.spec.js b/e2e/tests/visual-a11y/displayLayout.visual.spec.js index e937d7b7af..401c5b243c 100644 --- a/e2e/tests/visual-a11y/displayLayout.visual.spec.js +++ b/e2e/tests/visual-a11y/displayLayout.visual.spec.js @@ -27,13 +27,9 @@ import { MISSION_TIME, VISUAL_FIXED_URL } from '../../constants.js'; import { test } from '../../pluginFixtures.js'; test.describe('Visual - Display Layout @clock', () => { - test.use({ - clockOptions: { - now: MISSION_TIME, - shouldAdvanceTime: true - } - }); test.beforeEach(async ({ page }) => { + await page.clock.install({ time: MISSION_TIME }); + await page.clock.resume(); await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' }); const parentLayout = await createDomainObjectWithDefaults(page, { diff --git a/e2e/tests/visual-a11y/notebook.visual.spec.js b/e2e/tests/visual-a11y/notebook.visual.spec.js index ddf7da39c4..3532953fbf 100644 --- a/e2e/tests/visual-a11y/notebook.visual.spec.js +++ b/e2e/tests/visual-a11y/notebook.visual.spec.js @@ -22,7 +22,7 @@ import percySnapshot from '@percy/playwright'; -import { createDomainObjectWithDefaults, expandTreePaneItemByName } from '../../appActions.js'; +import { createDomainObjectWithDefaults } from '../../appActions.js'; import { expect, scanForA11yViolations, test } from '../../avpFixtures.js'; import { VISUAL_FIXED_URL } from '../../constants.js'; import { enterTextEntry, startAndAddRestrictedNotebookObject } from '../../helper/notebookUtils.js'; @@ -86,9 +86,7 @@ test.describe('Visual - Notebook @a11y', () => { name: 'Test Notebook' }); }); - test('Accepts dropped objects as embeds', async ({ page, theme, openmctConfig }) => { - const { myItemsFolderName } = openmctConfig; - + test('Accepts dropped objects as embeds', async ({ page, theme }) => { // Create Overlay Plot await createDomainObjectWithDefaults(page, { type: 'Overlay Plot', @@ -98,11 +96,13 @@ test.describe('Visual - Notebook @a11y', () => { //Open Tree to perform drag await page.getByRole('button', { name: 'Browse' }).click(); - await expandTreePaneItemByName(page, myItemsFolderName); + await page.getByLabel('Expand My Items folder').click(); await page.goto(notebook.url); - await page.dragAndDrop('role=treeitem[name=/Dropped Overlay Plot/]', '.c-notebook__drag-area'); + await page + .getByLabel('Navigate to Dropped Overlay Plot') + .dragTo(page.getByLabel('To start a new entry, click here or drag and drop any object')); await percySnapshot(page, `Notebook w/ dropped embed (theme: ${theme})`); }); diff --git a/e2e/tests/visual-a11y/planning-timelist.visual.spec.js b/e2e/tests/visual-a11y/planning-timelist.visual.spec.js index 2889b1a571..baf23a02a9 100644 --- a/e2e/tests/visual-a11y/planning-timelist.visual.spec.js +++ b/e2e/tests/visual-a11y/planning-timelist.visual.spec.js @@ -33,17 +33,12 @@ const examplePlanSmall1 = JSON.parse( fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url)) ); +const FIRST_ACTIVITY_SMALL_1 = getFirstActivity(examplePlanSmall1); + test.describe('Visual - Timelist progress bar @clock @a11y', () => { - const firstActivity = getFirstActivity(examplePlanSmall1); - - test.use({ - clockOptions: { - now: firstActivity.end + 10000, - shouldAdvanceTime: true - } - }); - test.beforeEach(async ({ page }) => { + await page.clock.install({ time: FIRST_ACTIVITY_SMALL_1.end + 10000 }); + await page.clock.resume(); await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1); await page.getByLabel('Click to collapse items').click(); }); diff --git a/e2e/tests/visual-a11y/planning-timestrip.visual.spec.js b/e2e/tests/visual-a11y/planning-timestrip.visual.spec.js index 897fa60213..f0a6c9a90c 100644 --- a/e2e/tests/visual-a11y/planning-timestrip.visual.spec.js +++ b/e2e/tests/visual-a11y/planning-timestrip.visual.spec.js @@ -44,11 +44,13 @@ test.describe('Visual - Time Strip @a11y', () => { }); await createPlanFromJSON(page, { json: examplePlanSmall2, - parent: timeStrip.uuid + parent: timeStrip.uuid, + name: 'examplePlanSmall2' }); await createDomainObjectWithDefaults(page, { type: 'Sine Wave Generator', - parent: timeStrip.uuid + parent: timeStrip.uuid, + name: 'Sine Wave Generator' }); await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' }); diff --git a/e2e/tests/visual-a11y/planning-view.visual.spec.js b/e2e/tests/visual-a11y/planning-view.visual.spec.js index 7966fa8026..7ca1200c03 100644 --- a/e2e/tests/visual-a11y/planning-view.visual.spec.js +++ b/e2e/tests/visual-a11y/planning-view.visual.spec.js @@ -41,17 +41,12 @@ const examplePlanSmall2 = JSON.parse( fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url)) ); +const FIRST_ACTIVITY_SMALL_1 = getFirstActivity(examplePlanSmall1); + test.describe('Visual - Timelist progress bar @clock @a11y', () => { - const firstActivity = getFirstActivity(examplePlanSmall1); - - test.use({ - clockOptions: { - now: firstActivity.end + 10000, - shouldAdvanceTime: true - } - }); - test.beforeEach(async ({ page }) => { + await page.clock.install({ time: FIRST_ACTIVITY_SMALL_1.end + 10000 }); + await page.clock.resume(); await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1); await page.getByLabel('Click to collapse items').click(); }); diff --git a/package-lock.json b/package-lock.json index 002dd4c0a9..b15818b907 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,7 @@ "eslint-config-prettier": "9.1.0", "eslint-plugin-compat": "4.2.0", "eslint-plugin-no-unsanitized": "4.0.2", - "eslint-plugin-playwright": "0.12.0", + "eslint-plugin-playwright": "1.5.2", "eslint-plugin-prettier": "5.1.3", "eslint-plugin-simple-import-sort": "10.0.0", "eslint-plugin-unicorn": "49.0.0", @@ -104,9 +104,7 @@ "@axe-core/playwright": "4.8.5", "@percy/cli": "1.27.4", "@percy/playwright": "1.0.4", - "@playwright/test": "1.45.2", - "@types/sinonjs__fake-timers": "8.1.5", - "sinon": "17.0.0" + "@playwright/test": "1.45.2" } }, "e2e/node_modules/@percy/cli": { @@ -1582,50 +1580,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", - "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^3.0.0" - } - }, - "node_modules/@sinonjs/samsam": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", - "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^2.0.0", - "lodash.get": "^4.4.2", - "type-detect": "^4.0.8" - } - }, - "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/text-encoding": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", - "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", - "dev": true - }, "node_modules/@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -1906,12 +1860,6 @@ "@types/node": "*" } }, - "node_modules/@types/sinonjs__fake-timers": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", - "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", - "dev": true - }, "node_modules/@types/sockjs": { "version": "0.3.36", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", @@ -4235,15 +4183,6 @@ "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", "dev": true }, - "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -4756,13 +4695,22 @@ } }, "node_modules/eslint-plugin-playwright": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-0.12.0.tgz", - "integrity": "sha512-KXuzQjVzca5irMT/7rvzJKsVDGbQr43oQPc8i+SLEBqmfrTxlwMwRqfv9vtZqh4hpU0jmrnA/EOfwtls+5QC1w==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-1.5.2.tgz", + "integrity": "sha512-TMzLrLGQMccngU8GogtzIc9u5RzXGnfsQEUjLfEfshINuVR2fS4SHfDtU7xYP90Vwm5vflHECf610KTdGvO53w==", "dev": true, + "workspaces": [ + "examples" + ], + "dependencies": { + "globals": "^13.23.0" + }, + "engines": { + "node": ">=16.6.0" + }, "peerDependencies": { - "eslint": ">=7", - "eslint-plugin-jest": ">=24" + "eslint": ">=8.40.0", + "eslint-plugin-jest": ">=25" }, "peerDependenciesMeta": { "eslint-plugin-jest": { @@ -4770,6 +4718,33 @@ } } }, + "node_modules/eslint-plugin-playwright/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-playwright/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-plugin-prettier": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", @@ -7250,12 +7225,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/just-extend": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", - "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", - "dev": true - }, "node_modules/karma": { "version": "6.4.2", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.2.tgz", @@ -7636,12 +7605,6 @@ "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", "dev": true }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "dev": true - }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -8073,19 +8036,6 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, - "node_modules/nise": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", - "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^3.0.0", - "@sinonjs/fake-timers": "^11.2.2", - "@sinonjs/text-encoding": "^0.7.2", - "just-extend": "^6.2.0", - "path-to-regexp": "^6.2.1" - } - }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -10200,45 +10150,6 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, - "node_modules/sinon": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.0.tgz", - "integrity": "sha512-p4lJiYKBoOEVUxxVIC9H1MM2znG1/c8gud++I2BauJA5hsz7hHsst35eurNWXTusBsIq66FzOQbZ/uMdpvbPIQ==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^3.0.0", - "@sinonjs/fake-timers": "^11.2.2", - "@sinonjs/samsam": "^8.0.0", - "diff": "^5.1.0", - "nise": "^5.1.5", - "supports-color": "^7.2.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/sinon" - } - }, - "node_modules/sinon/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/sinon/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/slash": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", @@ -10944,15 +10855,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", diff --git a/package.json b/package.json index 14f0b37fa3..654ddac942 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "eslint-config-prettier": "9.1.0", "eslint-plugin-compat": "4.2.0", "eslint-plugin-no-unsanitized": "4.0.2", - "eslint-plugin-playwright": "0.12.0", + "eslint-plugin-playwright": "1.5.2", "eslint-plugin-prettier": "5.1.3", "eslint-plugin-simple-import-sort": "10.0.0", "eslint-plugin-unicorn": "49.0.0", @@ -116,14 +116,13 @@ "test:e2e:a11y": "npm test --workspace e2e -- --config=playwright-visual-a11y.config.js --project=chrome --grep @a11y", "test:e2e:mobile": "npm test --workspace e2e -- --config=playwright-mobile.config.js", "test:e2e:couchdb": "npm test --workspace e2e -- --config=playwright-ci.config.js --project=chrome --grep @couchdb --workers=1", - "test:e2e:stable": "npm test --workspace e2e -- --config=playwright-ci.config.js --project=chrome --grep-invert \"@unstable|@couchdb|@generatedata\"", - "test:e2e:unstable": "npm test --workspace e2e -- --config=playwright-ci.config.js --project=chrome --grep @unstable", + "test:e2e:ci": "npm test --workspace e2e -- --config=playwright-ci.config.js --project=chrome --grep-invert \"@couchdb|@generatedata\"", "test:e2e:local": "npm test --workspace e2e -- --config=playwright-local.config.js --project=chrome", "test:e2e:generatedata": "npm test --workspace e2e -- --config=playwright-ci.config.js --project=chrome --grep @generatedata", "test:e2e:checksnapshots": "npm test --workspace e2e -- --config=playwright-ci.config.js --project=chrome --grep @snapshot --retries=0", "test:e2e:updatesnapshots": "npm test --workspace e2e -- --config=playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots", - "test:e2e:visual:ci": "npm run test:visual --workspace e2e -- --config .percy.ci.yml --partial -- npx playwright test --config=playwright-visual-a11y.config.js --project=chrome --grep-invert @unstable", - "test:e2e:visual:full": "npm run test:visual --workspace e2e -- --config .percy.nightly.yml -- npx playwright test --config=playwright-visual-a11y.config.js --grep-invert @unstable", + "test:e2e:visual:ci": "npm run test:visual --workspace e2e -- --config .percy.ci.yml --partial -- npx playwright test --config=playwright-visual-a11y.config.js --project=chrome", + "test:e2e:visual:full": "npm run test:visual --workspace e2e -- --config .percy.nightly.yml -- npx playwright test --config=playwright-visual-a11y.config.js", "test:e2e:full": "npm test --workspace e2e -- --config=playwright-ci.config.js --grep-invert @couchdb", "test:e2e:watch": "npm test --workspace e2e -- --ui --config=playwright-watch.config.js", "test:perf:contract": "npm test --workspace e2e -- --config=playwright-performance-dev.config.js", @@ -133,7 +132,7 @@ "update-copyright-date": "npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\s\\(c\\)\\s20\\d\\d\\-20\\d\\d/Copyright \\(c\\)\\ 2014\\-2024/gm'", "cov:e2e:report": "nyc report --reporter=lcovonly --report-dir=./coverage/e2e", "cov:e2e:full:publish": "codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-full", - "cov:e2e:stable:publish": "codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-stable", + "cov:e2e:ci:publish": "codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-ci", "cov:unit:publish": "codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit", "prepare": "npm run build:prod && npx tsc" }, diff --git a/src/api/forms/components/FormProperties.vue b/src/api/forms/components/FormProperties.vue index f4ac6b23af..1e4bdf30bb 100644 --- a/src/api/forms/components/FormProperties.vue +++ b/src/api/forms/components/FormProperties.vue @@ -115,7 +115,7 @@ export default { return this.model.buttons.submit.label; } - return 'OK'; + return 'Ok'; }, cancelLabel() { if (this.model.buttons && this.model.buttons.cancel && this.model.buttons.cancel.label) { diff --git a/src/api/forms/components/controls/FileInput.vue b/src/api/forms/components/controls/FileInput.vue index 911fdb945a..dd27d6ea3a 100644 --- a/src/api/forms/components/controls/FileInput.vue +++ b/src/api/forms/components/controls/FileInput.vue @@ -29,6 +29,7 @@ type="file" :accept="acceptableFileTypes" style="display: none" + aria-labelledby="fileSelect" />