Compare commits

...

124 Commits

Author SHA1 Message Date
379a466d4d remove choochoo steps 2024-07-24 13:44:38 -07:00
75552cc708 new local storage 2024-07-24 13:44:28 -07:00
5377382a97 Merge branch 'master' into eslint_update 2024-07-24 09:58:07 -07:00
689f7cc815 test(e2e): stabilize flaky imagery tests (#7765) 2024-07-23 20:41:07 -07:00
ac911cc2ae force 2024-07-23 13:28:40 -07:00
d811af4bbc update more locators 2024-07-23 11:57:46 -07:00
0fa8095462 move role around 2024-07-23 11:57:38 -07:00
512cbe4127 add expand component 2024-07-23 11:57:29 -07:00
31b0f00233 update to remove expect expect given our use of check functions 2024-07-23 11:57:19 -07:00
c213952f42 Merge branch 'master' of https://github.com/nasa/openmct into eslint_update 2024-07-23 07:35:43 -07:00
7bc49c84c3 Updating more tests 2024-07-23 07:35:20 -07:00
1fae0a6ad5 fix(#6812): Align Plot and Plan X-Axes in Time Strips (#7744)
* DRAFT - alignment for axes

* Use alignmentContext to manage tick widths instead of passing around as props

* Remove log statements

* Add ability to remove alignment widths for a given y axis

* Fix computation of left margin and width of plan when in the timestrip

* Remove excess padding when there is no left y axis

* Use alignment composable to adjust left margin and width of time system axis

* Fix now marker visibility

* refactor: use built in `Map()` data structure

* refactor: improve readability and conciseness

* docs: improve jsdocs

* refactor: move jsdoc typedefs to bottom of file

* refactor: axis to use vue reactivity

* fix: return alignment as an object of refs

* alignmentMap needs to be shared state, move it out of the useAlignment composable.

* Fix now marker offset

* Add new visual test for time strips

* update with animation stabilization

* Fix failing test due to changed injected property (path -> objectPath)

* change injected property from path to objectPath

* Fix spelling

* Remove unused arguments to function call

---------

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Hill, John (ARC-TI)[KBR Wyle Services, LLC] <john.c.hill@nasa.gov>
2024-07-22 16:05:21 -07:00
9202fa3fde review comments 2024-07-22 09:48:25 -07:00
2c0bacf3cc Merge branch 'master' of https://github.com/nasa/openmct into eslint_update 2024-07-22 09:19:19 -07:00
762762945d chore: bump playwright to 1.45.2 (#7785)
* chore: bump playwright to `1.45.2`

* chore: update package-lock
2024-07-16 18:51:44 +00:00
c763937455 chore: disable codeql scanning for e2e/ folder (#7784) 2024-07-16 11:26:35 -07:00
db808b4d54 Plots correctly use configuration set on the parent if they can't their own (#7770)
* For telemetry that cannot have it's own configuration, ensure that it is correct initialized by the parent.

* stacked plot test checks that config properties for immutable telemetry points are applied correctly

* add tab navigation to inspector tabs

* add accessibility metadata to this component

* Update title to be on the correct component. Add expand/collapse logic

* clean up test

* refactor: better a11y for plot forms, fix "expand by default" test, refactor out `plotActions.js`

* a11y: aria label for plotOptionsItem

* refactor(a11y): PlotOptionsBrowse structure to have better a11y

* fixed tests

* address comment

* reverted to match previous commit

---------

Co-authored-by: Hill, John (ARC-TI)[KBR Wyle Services, LLC] <john.c.hill@nasa.gov>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2024-07-16 10:57:12 -07:00
6983148aba fix(package.json): consistent props (#7768) 2024-07-15 22:09:42 +00:00
92e5cba6d3 test(visual): skip flaky visual test (#7776) 2024-07-15 13:28:29 -07:00
3ec9ee3ab7 first pass at fixing tooltip selectors 2024-07-12 20:39:40 -07:00
3920aaff6e this should be a button 2024-07-12 20:16:48 -07:00
12fbc3d562 adds an id for dragondrops 2024-07-12 20:16:37 -07:00
9e7d042eb6 tells a screenreader that this is a row and a cell 2024-07-12 20:16:20 -07:00
e2aff7b7a1 unneeded 2024-07-12 20:15:57 -07:00
567ab8a581 unneeded 2024-07-12 20:15:37 -07:00
478b57fb7a driveby to remove dead code 2024-07-12 20:14:59 -07:00
558ef62eaf update package 2024-07-12 20:14:47 -07:00
cd0c654f3b Merge branch 'master' of https://github.com/nasa/openmct into eslint_update 2024-07-09 21:08:12 -07:00
0215a5b693 chore: bump playwright to version 1.45.0 (#7762) 2024-07-02 21:04:39 +00:00
291c0997a6 chore(cspell): grammar fixes (#7735)
* chore(cspell): grammar fixes

* chore: remove valid words

* fix(cspell): resolve linting errors

* fix(naming): change filename

* fix(cspell): resolve linting errors

* fix(spelling): remove wrong word

* fix(review): redundant wording
2024-07-01 10:38:26 -07:00
393da7dc62 chore(deps): bump ws, engine.io and socket.io-adapter (#7761)
Bumps [ws](https://github.com/websockets/ws), [engine.io](https://github.com/socketio/engine.io) and [socket.io-adapter](https://github.com/socketio/socket.io-adapter). These dependencies needed to be updated together.

Updates `ws` from 8.11.0 to 8.17.1
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.11.0...8.17.1)

Updates `engine.io` from 6.5.4 to 6.5.5
- [Release notes](https://github.com/socketio/engine.io/releases)
- [Changelog](https://github.com/socketio/engine.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/engine.io/compare/6.5.4...6.5.5)

Updates `socket.io-adapter` from 2.5.4 to 2.5.5
- [Release notes](https://github.com/socketio/socket.io-adapter/releases)
- [Changelog](https://github.com/socketio/socket.io-adapter/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io-adapter/compare/2.5.4...2.5.5)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
- dependency-name: engine.io
  dependency-type: indirect
- dependency-name: socket.io-adapter
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-25 05:36:49 -07:00
448750ca59 Fix some flaky percy behavior (#7756)
* update imagery to be static

* bump versions

* doesn't do anything

* does this do anything

* undo only

* update the hardcoded urls

* add ignore CI logs push

* downgrade to test

* update the package lock

* Add some more noise reduction

* move to elements tab so that there is no time-based component

* stability
2024-06-25 05:08:01 +00:00
554f77c42f [a11y] re-enable and fix all failing plan tests (#7753)
* enable planning a11y tests

* Fix all a11y failing tests

* drive-by: missed clean

* test: attempt to fix parallelization

* split the test 3 ways

* lint

---------

Co-authored-by: Hill, John (ARC-TI)[KBR Wyle Services, LLC] <john.c.hill@nasa.gov>
2024-06-21 16:37:36 -07:00
a5770817cc chore: re-enable a11y checks in visual suite (#7747)
* re-enable main test

* Enable tests and fix some failing ones

* revert accidental changes

* test(a11y): on failure, take a screenshot to disk

* test(visual): wait for the snapshot indicator to stop flashing

* Fix all failing tests

* test: disable a11y checks for planning suite

* lint:fix

* fix pathing issues

* build: increase visual-a11y parallelism from 2 to 4

* test: darkmatter theme a11y checks + snapshots-- ACTIVATE!

* test: fix file name and path

* fix(#7317): scott bell prophecy

* jk no credit for u

* chore: disable parallelism until we figure out what's going on

* chore: set parallelism to 2 in hopes it fixes percy

---------

Co-authored-by: Hill, John (ARC-TI)[KBR Wyle Services, LLC] <john.c.hill@nasa.gov>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2024-06-19 01:16:54 +00:00
34b4091204 fix(#7680): no index.html on npm pack (#7699) 2024-06-05 19:11:54 +00:00
6360bc4b6c Ensure time conductor mode is set when synchronizing time range (#7731)
* Use setMode API to set the time span as well as the bounds instead of the old bounds time API.

* Add test for synchronized time conductor via plots

* Fix linting issue
2024-06-03 16:17:41 +00:00
c354e1c2f1 If display bounds are out of sync with time conductor, don't purge data out of bounds (#7732)
* If we're paused, don't purge data out of bounds
* Refactor auto scale utility functions for reuse
* Create new test spec for plot controls
* Move plot related actions into it's own file
* Fix typo for imports
* Remove named exposedFunction as it is causing errors when the method is used on the same page more than once.
* Fix spelling
2024-06-03 15:46:05 +00:00
eba6f0f505 fix(#7670): Ensure objects unsubscribe from staleness when destroyed (#7736)
* clear any staleness subscriptions before viewing a new object

* add checkbox to options for createDomainObjectWithDefaults, update swg staleness provider to use new time api, add test

* revert

* modified for simplification and to remove the need to update helper methods

* consistent naming convention

* Update e2e/tests/functional/staleness.e2e.spec.js

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>

* adding back vars that were removed

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2024-05-31 20:46:59 +00:00
017380bb6a chore: upgrade eventemitter to v5.0.2 (#7709)
* chore: upgrade eventemitter to v5.0.2
* fix: pass context to eventHelpers
* fix: no need to destroy router as it is destroyed during openmct teardown
* fix: register `CreateAction` and retrieve it from the registry
* test: fix tests
* refactor: import action key consts
* fix: update usage. don't use getters
Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>

---------

Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
2024-05-14 21:51:33 +00:00
810d580b18 [Telemetry Tables] Make sure tables auto scroll correctly on first load (#7720)
* run scroll method to scroll to top after initial load of historical data

* clarifying comment

* added e2e test to make sure tables auto scroll on mount

* adding descriptive comments

* adding in ascending check as well

* added new appAction for navigating to/in realtime, using it in table scroll test

* lint

---------

Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
2024-05-09 11:53:11 -07:00
977792fae8 this is 2024. * observers no more. (#7715)
* this is 2024. `*` observers no more.

* add edit and save domain object helper functions

* add aria-labels and fix e2e tests to use new labels

* generate and save in local storage a condition set with telemetry and condition

* rename const

* move creation code out of generateLocalStorage since it is immutable

* remove function abstractions

* remove @localStorage test label

* Update e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>

* Update e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>

* Update e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>

* Update e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>

* remove unneccesary aria text

* remove unneccessary aria text

* use recommended playwright locators

* lint fix

* remove unneccesary steps now that child created directly in parent

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2024-05-09 11:19:55 -07:00
a69e300f1c chore: bump playwright to v1.44.0 (#7716) 2024-05-09 08:20:31 -07:00
17bc6cb722 chore(webpack): destruct version from pkgJson (#7713) 2024-05-07 08:52:41 -07:00
b3d3465734 [Darkmatter] Create new darkmatter theme (#7682)
* initial theme plugin setup, changes to layout frames

* update visual tests

* Changes to gauge, layout borders, and background

* Make background image a DIY theme variable. Fixes made to gauges. Deleted custom font.

* More changes to overall background colors. Added glass layer effect to menus

* changes to menu

* Fix to make theme easy to run

* Fix tab colors and add glass background to menus

* make highlightd corners longer

* Initial changes to font styles

* Add temporary numeric font style. Test numeric font in gauges.

* Initial changes to alphanumerics in layouts

* Updated variables

* update plugin.js file

* Fix highlighted corners on frames such that it uses outermost frame

* renaming theme plugin and rename branch

* fix button colors to be more readable

* change background image

* Fix bad merges from other theme files. Fix gauge and alphanumerics such that they dont have darkmatter borders

* more fixes

* Fix where mixin is used such that when an object's frame is hidden, highlgihts disappear

* remove blur from meter gauges

* Add comment about this theme being in beta mode

* Delete draft .scss file that is no longer needed

* Fix major accessibility issues

* Fix PR review comments

*  fix: Correct import file name for DarkMatter theme.

* Fix other theme code that was failing e2e tests

* Revert index.html

* Fix linting error

* Fix for failing percy test regarding padding

* Fix for failing percy test regarding padding part 2

* Fix for failing percy test regarding padding part 3

* Remove mixin that may be causing percy issue

* Another fix to resolve percy issue

* Add back some code that was deleted during debugging, and create new variables for the object padding

* Fix gradient clipping in inspector

* Restructure all constants-.scss files

* Change bg image to be square and NASA official picture

* Final fixes to darkmatter variable layouts

* Address PR comments

* Change darkmatter to darkmatterTheme

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2024-04-25 16:06:07 -07:00
fb0d74e87f chore(deps-dev): bump vue from 3.4.19 to 3.4.24 (#7702)
Bumps [vue](https://github.com/vuejs/core) from 3.4.19 to 3.4.24.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.4.19...v3.4.24)

---
updated-dependencies:
- dependency-name: vue
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-22 13:19:00 -07:00
a961d7e3bf Fix nested Flexible Layout direction problem (#7637)
* Closes #7635
- More specific approach to CSS class application for column vs. row layouts.
- Added layout direction CSS classing to `c-fl-container__frames-holder`.
- Switched toolbar icon and titling for better parity with
'toggle' approach used elsewhere.
- Cleaned up duped property def in mixin.

* Addressing PR change requests
- Updated e2e test.
- New computed properties for layout direction.
- CSS code cleanup.

* fix selector in test

* fix more bad selectors

* fix changed title

---------

Co-authored-by: David Tsay <david.e.tsay@nasa.gov>
2024-04-18 23:38:11 +00:00
5a06b51c5a refactor: remove the final .bounds() call (#7695)
 refactor: Use getBounds method instead of bounds in IndependentTimeContext.
2024-04-17 16:19:21 +00:00
ef8b353d01 Improve performance of JSON import and add progress bar (#7654)
* add progress bar

* improves performance a lot

* fix test

* remove old function

* fix legit bug from test - well done tests

* add constant, better comments

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2024-04-16 21:23:31 +00:00
6c5b925454 chore: remove all usage of deprecated Time API methods (#7688)
* chore: remove all usage of deprecated Time API methods

* test: update unit test

* docs: Fix spacing and add clarity to TimeConductorBounds definition.

* test: add unit test coverage for new time api methods
2024-04-16 21:12:09 +00:00
e91aba2e37 Handle paste events for images and text properly (#7679)
* enable eval source maps for debugging

* split image and text paste handling
better event handling

* change back source maps

* image takes precedence over text

* break up notebook entry functions for re-use

* create hotkeys utils
add clipboard functions

* add notebook paste test

* add test for pasting to selected but not editing entry

* link tests to issue

* jsdoc addition

* jsdocs

* no need to import then export

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>

* fix changed path

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2024-04-16 13:54:40 -07:00
b18aa48141 Revert "Handle the case where the pasted data is not an image" (#7668)
Revert "Handle the case where the pasted data is not an image (#7628)"

This reverts commit d33da65dae.
2024-04-04 15:03:45 -07:00
d33da65dae Handle the case where the pasted data is not an image (#7628)
* Handle the case where the pasted data is not an image or it is image AND text
* Change method name for paste handling

---------

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2024-04-04 21:30:12 +00:00
e3adeb6a75 Do not add unused created attribute to metadata of couch documents on create (#7656)
this isn't used anywhere

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2024-04-04 13:33:36 -07:00
de3dad02b5 [Telemetry Tables] Don't mutate configuration if object is not able to be persisted (#7626)
* source maps

* do not persist if obj is not persistable

* nope

* prevent mutation of configuration

* static roots are read only by nature

* helps to use functions correctly

* update persistModeChange logic

* remove debug

* remove unnecessary change
2024-04-04 00:38:59 +00:00
311ad0b87a fix(e2e): specify .nyc_output path as custom config setting (#7658)
* fix: specify .nyc_output path as custom config setting

* fix: coverage for mobile suite

* fix: pathing in playwright configs
2024-04-01 18:29:47 +00:00
f98eb31956 fix: move file to correct folder (#7652) 2024-03-28 17:32:46 -07:00
1671a585fb Mct7636 (#7645)
* setting order for sort to descending if in performance mode and no sort set

---------

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2024-03-28 17:32:03 -07:00
a3fb84ad43 chore: remove type: module, create openmct-e2e subpackage (#7590)
* fix: remove mystery webpack code

* fix: remove type:module and specify exports

- we aren't a module... yet

* fix: rename webpack*.js to webpack*.mjs so we can use import/export. fix imports

* fix: exports format

* fix: woops, need to add `start` script back

* chore: split e2e into its own module

* fix: use normal Painterro import

* fix: update e2e pathing

* fix: copy over helper functions

* chore: specify `cwd` for playwright configs so that openmct npm commands work as intended in any environment

* chore: add pretest script to e2e package.json

* chore: don't package e2e

* refactor: tidy up webpack common config

* chore: compile types to a single file

* chore: fix visual test npm scripts

* chore: fix import pathing

* chore: define package exports, move test specific dependencies to the subpackage

* chore: export test framework from openmct-e2e

* chore: export baseFixtures also

* chore: let `openmct` and `openmct-e2e` share `node_modules/`

* chore: use `--workspace`, remove pretest script

* Revert "fix: remove mystery webpack code"

This reverts commit eb14d52569ffa27ab1a090b883694f4707b59cd0.

* chore: update package-lock

* chore: add `.npmignore`

* fix: *js -> mjs
2024-03-28 14:49:00 -07:00
a5c6b141a6 chore(deps-dev): bump express from 4.18.3 to 4.19.2 (#7646)
Bumps [express](https://github.com/expressjs/express) from 4.18.3 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.3...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-27 13:12:48 -07:00
46823ec225 chore(gha): run prcop workflow on milestone (#7647)
chore: run prcop workflow on milestone
2024-03-27 09:09:57 -07:00
5b4ee1949f fix(#7623): Resize ConductorAxis properly (#7624)
* fix: resize conductor properly

* refactor: more computed properties, unregister listener

* fix: beforeUnmounted hook

* test(visual): add time conductor visual test for fixed mode

* fix: initialize to `null`

* feat: extend the base `screenshot` function to mask elements which will always create variance in an Open MCT screenshot

* docs: add types for fixtures

* fix: remove unneeded await

* chore: add sinon timers types package back

* docs: remove unused docs

* doc: remove unused docs

* test: add visual realtime url, update imports

* feat: provide wrapped page.screenshot fixture that applies defaults

* test: add basic timeConductor snapshot tests

* chore: update eslint config

* lint: remove unused disable directives

* test: remove redundant navigation

* fix: remove listeners

* fix: maybe stabilize unit tests

* docs: remove

* fix: provide sourcemaps in unit tests

* test: add regression snapshot test for time conductor axis

* lint: remove unused imports

* feat(e2e): add fixture to manually tick the clock and use it

* test: reactivate test now that we don't use deploysentinel :(

* test: update snapshots

* test: add test for clockOptions and tick fixtures

* test: add afterEach stub and fixme

* test: try and stabilize fault management flake

* lint: defy the word gods

* chore: ignore `*-darwin.png` screenshots

* chore: remove darwin screenshot binaries

* docs: markdownlint

* docs: remove MacOS specific instructions from snapshot testing

* fix: remove a11y
2024-03-26 23:52:33 +00:00
7e926ccbb7 chore(deps-dev): bump webpack-dev-middleware from 7.0.0 to 7.1.1 (#7634)
Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) from 7.0.0 to 7.1.1.
- [Release notes](https://github.com/webpack/webpack-dev-middleware/releases)
- [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v7.0.0...v7.1.1)

---
updated-dependencies:
- dependency-name: webpack-dev-middleware
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-26 15:23:17 -07:00
6e264517f8 docs: Mission Status and more (#7521)
* chore: update tsconfig to target modern ES

* docs: update UserProvider

* docs: update UserAPI, make openmct private

* docs: update StatusAPI

* refactor: convert ViewRegistry to ES6 class

* docs: finish type imports for openmct api

* docs: minor doc improvements

* docs: add UserIndicator readme

* docs: add User API section to API docs

* docs: document Mission Status

* docs(JSDoc): primitive types should be lowercase, otherwise TitleCase
2024-03-26 19:11:00 +00:00
986da5782b Disable reload in preview (#7639)
disable reload in preview

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2024-03-26 16:58:47 +00:00
539138437b For an existing View in a Preview, ensure we pull the same ActionCollection (#7632)
* ensure action collection returned is the cached one from the same view

* add test

* use async await
2024-03-26 14:11:23 +00:00
493b31d0b9 fix(#7633): add missing await (#7643)
* fix(#7633): add missing `await`

* test: add LAD table e2e suite + test
2024-03-26 10:14:25 +00:00
d68ac31ab5 chore: bump @playwright/test to 1.42.1 (#7627)
* chore: bump `@playwright/test` to `1.42.1`

* chore(circleci): don't try to re-run individual percy tests
2024-03-21 09:27:41 -07:00
f504ee29cc fix: 🤖 beep boop beep, you forgot an await 🤖 (#7630)
* fix: 🤖 beep boop beep, you forgot an `await` 🤖

* add e2e test

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
2024-03-20 20:20:48 +00:00
1d5ddc545e chore(deps-dev): bump @types/lodash from 4.14.192 to 4.17.0 (#7610)
Bumps [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) from 4.14.192 to 4.17.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash)

---
updated-dependencies:
- dependency-name: "@types/lodash"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-19 22:57:38 -07:00
42085a4b70 [CI] Parallelize visual test runs (#7618)
* rename suite and add parallelism

* use test sharding

* expect 2 parallel runs

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
2024-03-19 20:50:14 -07:00
b2b0837592 Handle empty namespaces in import (#7619)
* handle blank namespaces in import

---------

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2024-03-19 13:05:14 -07:00
e305b46d88 Ensure a request for telemetry happens in Condition Sets (#7592)
* request telemetry when subscribing to data in case we have cached subscription

* change back to >=

* revert

* update tests

* fixing tests

* add metadata

* fix test

* another mock required

* one more function needed

* attempt to fix some afterall errors

* add fixme for e2e test

* fail fast on request

---------

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2024-03-19 10:44:50 -07:00
fb396ac194 [Telemetry Table] Telemetry mode bug fixes (#7601)
* source maps

* any tables without configuration will default to either default options or configured options

* prevent double unsubscribese

* remove source maps

* update coment

* moving defaults to plugin level

* whoops

* missed a spot, updated omment

* adding config values

* lint

* typos

* fixing broken ref

* fixing broken ref

* actually fixing ref

* setting rowLimit so initial change does not trigger a resubscribe of telemetry that was not subscribed yet
2024-03-19 02:34:00 +00:00
a01f21017f For the setTimeConductorMode, use the close time popup button rather than the submit button to dismiss time popup (#7613)
Use the close time popup button rather than the submit button as the submit button triggers network requests.
2024-03-18 23:48:33 +00:00
b7b9ccbe65 [TC Popup] Fix Calendar so it is not cutoff (#7596) 2024-03-18 23:28:09 +00:00
f189a4d602 Resize plans properly (#7597)
* resize firing

* ensure watcher fires

* remove unneeded const

* add small visual test resizing plan

* use browser with null viewport

* lint
2024-03-18 15:13:19 -07:00
4027eae299 chore(deps-dev): bump follow-redirects from 1.15.5 to 1.15.6 (#7603)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.5 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-17 06:53:20 -07:00
d4695178bc remove reference to LGTM (#7591) 2024-03-16 11:33:55 -07:00
5fc5c13314 [Plot] Fix plot swatch behavior when vertical space is small (#7493)
Add overflow: hidden such that when vertical space is small, no autoscroll happens on the axis
2024-03-14 16:49:36 +00:00
ceeb761d94 [build] Re-enable package lock (#7584)
* include package lock
* migrate to npm ci
* remove cache busting doc and replace with npm run clean

---------

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2024-03-14 16:27:31 +00:00
10eb749d32 Prevent Metadata Time System Error for Missing Objects (#7565)
https://github.com/nasa/openmct/pull/7565 Modified Stacked Plots to not show Missing Objects. Added a check in Telemetry Collections for missing objects before displaying telemetry metadata time system error.
2024-03-14 09:05:23 -07:00
349be42275 add draggable true to tree items 2024-03-13 16:16:56 -07:00
6edb3b2dc4 Update plot aria 2024-03-13 16:16:43 -07:00
444c9ff33e update notebook snapshot drop area 2024-03-13 16:16:20 -07:00
b7b7bc2d41 update gauge component 2024-03-13 16:16:06 -07:00
6e1272dfe9 start to factor out bad locators 2024-03-13 16:15:34 -07:00
d8df0e15e8 driveby 2024-03-13 16:15:17 -07:00
faed27c143 fix(#7552): Fix notebook snapshot image annotations (#7555)
* fix: painterro import

* test(snapshotAnnotation): add minimal e2e test

* chore: add e2e test annotation

* fix: notebook snapshot test

* refactor: put `v-else` on template

* small changes to the test and a visual one

* additional a11y

* fix: html structure

* test(e2e): fix notebook snapshot tests

* Update documentation for file download and JSON testing

* Update stubs and add jpg/png export

* refactor(TimelistComponent): tidy up

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-13 20:27:49 +00:00
18e976ad12 allow inspector pane content to scroll vertically (#7567)
* allow content to scroll vertically

* add framework for inspector content scrollable e2e test

* fix paths and spelling error

* add aria-label to properties list

* add scroll check to test

* use click, which scrolls if needed

* use scrollbar to scroll

* Closes #7566
- Fixed scrolling to only apply to the area below the Inspector tabs.
- Removed unneeded padding in pane.scss.
- Alignment fixes to related scroll elements in tree.

* cspell: ignore this file because it doesn't understand latin

* fix selectors and test

* lint fix

* driveby: wait for thumbnail bar to finish scrolling before taking snapshot

---------

Co-authored-by: Charles Hacskaylo <charles.f.hacskaylo@nasa.gov>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2024-03-13 19:04:02 +00:00
64862634f3 [Telemetry Table] Address issues found during testing Table Performance (#7529)
Fix exporting from Limited Mode: #7268 (comment)
Fix UI issues: #7268 (comment)
Apply configuration changes made in Edit Properties.

---------

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2024-03-13 09:25:51 -07:00
cb4c59a464 fix(#7015): Generate source maps for generating code coverage metrics (#7582)
* fix(?): the robot says to do this...

* refactor: remove unused env var

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-12 17:02:41 -07:00
5f0bd10c61 Request batch when idle (#7526)
See https://github.com/nasa/openmct/pull/7526 for details
2024-03-12 20:46:06 +00:00
a393bfb87a update component tests to match linting rules 2024-03-12 11:24:45 -07:00
8933baf103 change ruleset 2024-03-12 11:24:14 -07:00
82326dc658 update package 2024-03-12 11:24:02 -07:00
df2d9ab133 Merge branch 'master' of https://github.com/nasa/openmct into eslint_update 2024-03-11 18:44:06 -07:00
8c2558bfe0 fix(#7524): Open in New Tab action from a sub-object in a layout (#7542)
* refactor: url tools use named exports

* fix: refactor method and remove customUrlParams

* test(e2e): verify bounds are preserved in data pivoting

* test: remove test as feature is no longer needed

- dataVisualization logic has moved from MMGIS plugin to the open source. As such, we can just use the time conductor bounds

* refactor: autoformat keeps changing this so i'mma just commit it

* refactor: remove unnecessary code

* refactor: simplify, add docs

* Revert "refactor: remove unnecessary code"

This reverts commit 87aef35c510230835fb682b80e89a6006ef2d923.

* a11y: improve aria labels for ITC

* fix: simplify url method

* fix: update ITC app actions

* test: add test to generate test data for display layout w/ overlay plot + ITC enabled

* test(e2e): add suite + test for open in new tab from subobject

- needs cleanup

* a11y: various a11y improvement drivebys

* a11y: clock indicator needs to be quiet

* a11y: add `aria-live` to SuperMenu details

* a11y: greatly improve a11y of Menus and SuperMenus

* test(e2e): clean up test

* fix: improve a11y for context menus, fix test

* chore: remove nop-longer-recommended extension

* feat: provide one more bound option for example data viz

* fix: no need for `mount`, use dynamic rendering instead

* Revert "fix: simplify url method"

This reverts commit b24c7dabc783a9a1c3f2460eada99f452259f566.

* fix: correct time conductor bounds when opening in a new tab from a plot in the inspector

* test: fix e2e tests

* Revert "test: remove test as feature is no longer needed"

This reverts commit 759ebd4667bffb1979d5f62af6b47f349dcd9f77.

* test: move 2p annotation to test

* test: fix e2e

* fix: no words for the word god today

* test: fix e2e

* fix: e2e test

* test: fix test

* driveby: fix perf test

* fix: revert required prop change

---------

Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-11 16:39:38 -07:00
0eadc7a4ae chore(deps-dev): bump copy-webpack-plugin from 11.0.0 to 12.0.2 (#7574)
Bumps [copy-webpack-plugin](https://github.com/webpack-contrib/copy-webpack-plugin) from 11.0.0 to 12.0.2.
- [Release notes](https://github.com/webpack-contrib/copy-webpack-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/copy-webpack-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/copy-webpack-plugin/compare/v11.0.0...v12.0.2)

---
updated-dependencies:
- dependency-name: copy-webpack-plugin
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-11 16:33:15 -07:00
cad4652a08 chore(deps-dev): bump karma-webpack from 5.0.0 to 5.0.1 (#7575)
Bumps [karma-webpack](https://github.com/webpack-contrib/karma-webpack) from 5.0.0 to 5.0.1.
- [Release notes](https://github.com/webpack-contrib/karma-webpack/releases)
- [Changelog](https://github.com/codymikol/karma-webpack/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/karma-webpack/compare/v5.0.0...v5.0.1)

---
updated-dependencies:
- dependency-name: karma-webpack
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-11 16:03:50 -07:00
8379f2d073 chore(deps-dev): bump imports-loader from 4.0.1 to 5.0.0 (#7576)
Bumps [imports-loader](https://github.com/webpack-contrib/imports-loader) from 4.0.1 to 5.0.0.
- [Release notes](https://github.com/webpack-contrib/imports-loader/releases)
- [Changelog](https://github.com/webpack-contrib/imports-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/imports-loader/compare/v4.0.1...v5.0.0)

---
updated-dependencies:
- dependency-name: imports-loader
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-11 16:03:12 -07:00
8ed112a4a8 chore(deps-dev): bump sass from 1.68.0 to 1.71.1 (#7577)
Bumps [sass](https://github.com/sass/dart-sass) from 1.68.0 to 1.71.1.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.68.0...1.71.1)

---
updated-dependencies:
- dependency-name: sass
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-11 15:42:05 -07:00
14c58c4410 chore(deps-dev): bump npm-run-all2 from 6.1.1 to 6.1.2 (#7579)
Bumps [npm-run-all2](https://github.com/bcomnes/npm-run-all2) from 6.1.1 to 6.1.2.
- [Release notes](https://github.com/bcomnes/npm-run-all2/releases)
- [Changelog](https://github.com/bcomnes/npm-run-all2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bcomnes/npm-run-all2/compare/v6.1.1...v6.1.2)

---
updated-dependencies:
- dependency-name: npm-run-all2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-11 15:40:13 -07:00
87ba9fcbc0 [Mobile] Make Time Conductor Usable Again (#7515)
* Initial changes to refactor Time Conductor

* Finish refactor using grid-template

* Finish total refactor of Time Conductor

* Initial mobile changes

* Fix TC on mobile by changing grid template

* Fix more mobile stuff

* Add ellipsize to TC popup options and rearrange popup inputs and labels

* Small final changes so TC is adaptive to extreme cases

* Add e2e mobile test

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-08 12:22:09 -08:00
ab49f3f3a1 fix(#7215)[Fault Management]: Fix shelving and acknowledging faults with single and bulk actions (#7559)
* refactor: merge FaultManagementListView into FaultManagementView

* refactor: make `selectedFaults` a computed property

* refactor: use named exports

* fix: reset fault map AFTER selectedFaults have been acknowledged

* a11y: add aria labels for fault management toolbar buttons

* refactor: use named import/exports

* a11y: add label

* a11y: add aria label for checkboxes

* fix: acknowledging or shelving single fault from context menu should only apply to selected fault

* refactor: use change event instead of input event for checkbox

* test: fix e2e tests, remove expect.softs

* test: stabilize fault management e2e tests
2024-03-06 15:28:38 -08:00
df969722d1 d3 implementation of progress pie chart (#7485)
* d3 implementation of progress pie chart

* Handle 0% and 100% cases

* PR #7485
- Minor tweaks to `s-selected` styling.

* add in-progress class for compact view

* Fix issue where updating progress for pie chart wasn't working till at least one clock tick.
Write tests for progress pie

* update documentation for clock annotation

* Update clock annotation in tests

* split long testfile

* driveby missing test

* driveby fix flake

* temp: fix flake and prep for visual test

* Fix linting errors

* this should be resolved

* these keep popping up

* moving some of this around

* moving this around

* the test

* Fix imports for tests

* no longer need constant

* move to front

* Stabalize name

* test(missionStatus): fix visual test

---------

Co-authored-by: Charles Hacskaylo <charles.f.hacskaylo@nasa.gov>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2024-03-05 14:45:28 -08:00
ef62633df1 chore(deps-dev): bump sass-loader from 14.0.0 to 14.1.1 (#7512)
Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 14.0.0 to 14.1.1.
- [Release notes](https://github.com/webpack-contrib/sass-loader/releases)
- [Changelog](https://github.com/webpack-contrib/sass-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/sass-loader/compare/v14.0.0...v14.1.1)

---
updated-dependencies:
- dependency-name: sass-loader
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-04 19:10:15 -08:00
597dc58eb7 chore: minor formatting fix for PR template (#7550)
Minor formatting nitpick
2024-03-04 19:09:33 -08:00
5d00d642f3 [CI] Temp disable flakefinder as we it's generating too many GHA minutes (#7548)
Temp disable flakefinder as we it's generating too many GHA minutes
2024-03-04 18:32:12 -08:00
39ab81c3d0 fix(overlay plot): legend updates correctly when removing element via remove action (#7531)
* fix: remove duplicate listeners
* fix: if no series found, don't splice
* test(e2e): update legend a11y and add test
2024-03-05 02:07:44 +00:00
0bdd0963a4 Limit lines handle plot resizing (#7151)
* Fix error when removing staleness subscription due to incorrect parameter

* On resize, clear the drawing API to reset the height and width for point calculation.

* Add e2e test to test limit lines after resizing the plot view.

* We need to update viewport when drawing limits in case there is no data for plots.

* Address review comments. change event naming convention and reduce debounce time.

* Use limit line and label seriesKeys to make ids unique

* Improve locator for limit lines checkbox

* Add a check for network requests when limit lines are redrawn
2024-03-04 17:02:30 -08:00
eae51356c8 chore(deps-dev): bump plotly.js-basic-dist-min from 2.20.0 to 2.29.1 (#7523)
Bumps [plotly.js-basic-dist-min](https://github.com/plotly/plotly.js) from 2.20.0 to 2.29.1.
- [Release notes](https://github.com/plotly/plotly.js/releases)
- [Changelog](https://github.com/plotly/plotly.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/plotly/plotly.js/compare/v2.20.0...v2.29.1)

---
updated-dependencies:
- dependency-name: plotly.js-basic-dist-min
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-04 16:22:18 -08:00
a36ad3f5e7 chore(deps-dev): bump webpack-dev-server from 4.15.1 to 5.0.2 (#7544)
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 4.15.1 to 5.0.2.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v4.15.1...v5.0.2)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-04 16:11:50 -08:00
307ededd19 chore(deps-dev): bump eslint-plugin-vue from 9.18.1 to 9.22.0 (#7545)
Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 9.18.1 to 9.22.0.
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v9.18.1...v9.22.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-vue
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-04 15:57:12 -08:00
d7ecfdf10f chore(deps-dev): bump webpack from 5.89.0 to 5.90.3 (#7546)
Bumps [webpack](https://github.com/webpack/webpack) from 5.89.0 to 5.90.3.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.89.0...v5.90.3)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-04 13:42:26 -08:00
a1c36f314d chore(deps-dev): bump sanitize-html from 2.11.0 to 2.12.1 (#7539)
Bumps [sanitize-html](https://github.com/apostrophecms/sanitize-html) from 2.11.0 to 2.12.1.
- [Changelog](https://github.com/apostrophecms/sanitize-html/blob/main/CHANGELOG.md)
- [Commits](https://github.com/apostrophecms/sanitize-html/compare/2.11.0...2.12.1)

---
updated-dependencies:
- dependency-name: sanitize-html
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-04 13:20:17 -08:00
86e636cbce chore(deps-dev): bump @axe-core/playwright from 4.8.2 to 4.8.5 (#7496)
Bumps [@axe-core/playwright](https://github.com/dequelabs/axe-core-npm) from 4.8.2 to 4.8.5.
- [Release notes](https://github.com/dequelabs/axe-core-npm/releases)
- [Changelog](https://github.com/dequelabs/axe-core-npm/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/dequelabs/axe-core-npm/commits/v4.8.5)

---
updated-dependencies:
- dependency-name: "@axe-core/playwright"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-03 09:06:49 -08:00
0d2b36ae82 chore(deps-dev): bump vue-eslint-parser from 9.3.2 to 9.4.2 (#7471)
Bumps [vue-eslint-parser](https://github.com/vuejs/vue-eslint-parser) from 9.3.2 to 9.4.2.
- [Release notes](https://github.com/vuejs/vue-eslint-parser/releases)
- [Commits](https://github.com/vuejs/vue-eslint-parser/compare/v9.3.2...v9.4.2)

---
updated-dependencies:
- dependency-name: vue-eslint-parser
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-03 08:55:45 -08:00
95072da257 chore(deps-dev): bump marked from 11.2.0 to 12.0.0 (#7467)
Bumps [marked](https://github.com/markedjs/marked) from 11.2.0 to 12.0.0.
- [Release notes](https://github.com/markedjs/marked/releases)
- [Changelog](https://github.com/markedjs/marked/blob/master/.releaserc.json)
- [Commits](https://github.com/markedjs/marked/compare/v11.2.0...v12.0.0)

---
updated-dependencies:
- dependency-name: marked
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-03 08:41:49 -08:00
faa2621e26 chore(deps-dev): bump css-loader from 6.8.1 to 6.10.0 (#7466)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 6.8.1 to 6.10.0.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v6.8.1...v6.10.0)

---
updated-dependencies:
- dependency-name: css-loader
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-03 08:29:37 -08:00
73eead6b72 [Mobile] Center Confirmation Dialog Texts (#7492)
* Make overlay messages centered

* Fix changes so that only dialogs and not forms are affected

* Fix buttons such that they are right-aligned

* Reduce to one worker for stability

* Add test to cover new capabilities

* lint fixes

* Closes #7343
- Fixed an oversight that caused the top of form dialogs to
be scrolled out of view by default.
- Fixed approach to vertical centering for `-fit` type confirmation dialogs.
- Reduced size of confirmation dialog icons.
- Smoke tested in Chrome mobile emulator in a large variety of mobile
viewport sizes and orientations.

* Closes #7343
- Removes extra margin unintentionally added to `l-overlay-large`.

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Charles Hacskaylo <charles.f.hacskaylo@nasa.gov>
2024-03-03 16:14:45 +00:00
e449fd0eda Fix visible toolbar overflow (#7037)
* Closes #7036
- CSS fix to prevent toolbar overrun of main view area.

* a11y: add label to pane handles

* test(visual): toolbar overflow

---------

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-02-29 11:26:51 -08:00
7d25c967a5 Allow test data toggle to be clicked (#7479)
* Add the disabled class instead of the disabled property

* Add test for condition set test data

* Use computed property for css class

* Use array syntax for class instead of computed value

* Fix .getByTitle locators to use .getByLabel instead

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-02-28 19:44:37 +00:00
2897ca65b3 first pass of lint fixes 2024-02-26 08:59:20 -08:00
0590b50d59 update eslint package 2024-02-26 04:28:24 -08:00
445 changed files with 24187 additions and 5712 deletions

View File

@ -1,59 +1,33 @@
version: 2.1 version: 2.1
orbs:
node: circleci/node@5.2.0
browser-tools: circleci/browser-tools@1.3.0
executors: executors:
pw-focal-development: pw-focal-development:
docker: docker:
- image: mcr.microsoft.com/playwright:v1.39.0-focal - image: mcr.microsoft.com/playwright:v1.45.2-focal
environment: environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
PERCY_POSTINSTALL_BROWSER: "true" # Needed to store the percy browser in cache deps PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
PERCY_LOGLEVEL: "debug" # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742) PERCY_LOGLEVEL: 'debug' # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742)
PERCY_PARALLEL_TOTAL: 2
ubuntu: ubuntu:
machine: machine:
image: ubuntu-2204:current image: ubuntu-2204:current
docker_layer_caching: true docker_layer_caching: true
parameters:
BUST_CACHE:
description: "Set this with the CircleCI UI Trigger Workflow button (boolean = true) to bust the cache!"
default: false
type: boolean
commands: commands:
build_and_install: build_and_install:
description: "All steps used to build and install. Will use cache if found" description: 'All steps used to build and install.'
parameters: parameters:
node-version: node-version:
type: string type: string
steps: steps:
- checkout - checkout
- restore_cache_cmd:
node-version: << parameters.node-version >>
- node/install: - node/install:
node-version: << parameters.node-version >> node-version: << parameters.node-version >>
- run: npm install --no-audit --progress=false - node/install-packages
restore_cache_cmd:
description: "Custom command for restoring cache with the ability to bust cache. When BUST_CACHE is set to true, jobs will not restore cache"
parameters:
node-version:
type: string
steps:
- when:
condition:
equal: [false, << pipeline.parameters.BUST_CACHE >>]
steps:
- restore_cache:
key: deps--{{ arch }}--{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
save_cache_cmd:
description: "Custom command for saving cache."
parameters:
node-version:
type: string
steps:
- save_cache:
key: deps--{{ arch }}--{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
paths:
- ~/.npm
- node_modules
generate_and_store_version_and_filesystem_artifacts: generate_and_store_version_and_filesystem_artifacts:
description: "Track important packages and files" description: 'Track important packages and files'
steps: steps:
- run: | - run: |
[[ $EUID -ne 0 ]] && (sudo mkdir -p /tmp/artifacts && sudo chmod 777 /tmp/artifacts) || (mkdir -p /tmp/artifacts && chmod 777 /tmp/artifacts) [[ $EUID -ne 0 ]] && (sudo mkdir -p /tmp/artifacts && sudo chmod 777 /tmp/artifacts) || (mkdir -p /tmp/artifacts && chmod 777 /tmp/artifacts)
@ -64,16 +38,13 @@ commands:
- store_artifacts: - store_artifacts:
path: /tmp/artifacts/ path: /tmp/artifacts/
generate_e2e_code_cov_report: generate_e2e_code_cov_report:
description: "Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test" description: 'Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test'
parameters: parameters:
suite: suite:
type: string type: string
steps: steps:
- run: npm run cov:e2e:report || true - run: npm run cov:e2e:report || true
- run: npm run cov:e2e:<<parameters.suite>>:publish - run: npm run cov:e2e:<<parameters.suite>>:publish
orbs:
node: circleci/node@5.1.0
browser-tools: circleci/browser-tools@1.3.0
jobs: jobs:
npm-audit: npm-audit:
parameters: parameters:
@ -111,8 +82,6 @@ jobs:
TESTFILES=$(circleci tests glob "src/**/*Spec.js") TESTFILES=$(circleci tests glob "src/**/*Spec.js")
echo "$TESTFILES" | circleci tests run --command="xargs npm run test" --verbose echo "$TESTFILES" | circleci tests run --command="xargs npm run test" --verbose
- run: npm run cov:unit:publish - run: npm run cov:unit:publish
- save_cache_cmd:
node-version: <<parameters.node-version>>
- store_test_results: - store_test_results:
path: dist/reports/tests/ path: dist/reports/tests/
- store_artifacts: - store_artifacts:
@ -133,7 +102,7 @@ jobs:
node-version: lts/hydrogen node-version: lts/hydrogen
- when: #Only install chrome-beta when running the 'full' suite to save $$$ - when: #Only install chrome-beta when running the 'full' suite to save $$$
condition: condition:
equal: ["full", <<parameters.suite>>] equal: ['full', <<parameters.suite>>]
steps: steps:
- run: npx playwright install chrome-beta - run: npx playwright install chrome-beta
- run: - run:
@ -190,7 +159,7 @@ jobs:
steps: steps:
- build_and_install: - build_and_install:
node-version: lts/hydrogen node-version: lts/hydrogen
- run: npx playwright@1.39.0 install #Necessary for bare ubuntu machine - run: npx playwright@1.45.2 install #Necessary for bare ubuntu machine
- run: | - run: |
export $(cat src/plugins/persistence/couch/.env.ci | xargs) 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
@ -252,15 +221,16 @@ jobs:
equal: [42, 42] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 equal: [42, 42] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps: steps:
- generate_and_store_version_and_filesystem_artifacts - generate_and_store_version_and_filesystem_artifacts
visual-a11y-tests: visual-a11y:
parameters: parameters:
suite: suite:
type: string # ci or full type: string # ci or full
executor: pw-focal-development executor: pw-focal-development
parallelism: 2
steps: steps:
- build_and_install: - build_and_install:
node-version: lts/hydrogen node-version: lts/iron
- run: npm run test:e2e:visual:<<parameters.suite>> - run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:visual:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
- store_test_results: - store_test_results:
path: test-results/results.xml path: test-results/results.xml
- store_artifacts: - store_artifacts:
@ -286,8 +256,8 @@ workflows:
name: e2e-stable name: e2e-stable
suite: stable suite: stable
- e2e-mobile - e2e-mobile
- visual-a11y-tests: - visual-a11y:
name: visual-a11y-test-ci name: visual-a11y-ci
suite: ci suite: ci
the-nightly: #These jobs do not run on PRs, but against master at night the-nightly: #These jobs do not run on PRs, but against master at night
@ -306,13 +276,13 @@ workflows:
- e2e-mobile - e2e-mobile
- perf-test - perf-test
- mem-test - mem-test
- visual-a11y-tests: - visual-a11y:
name: visual-a11y-test-nightly name: visual-a11y-nightly
suite: full suite: full
- e2e-couchdb - e2e-couchdb
triggers: triggers:
- schedule: - schedule:
cron: "0 0 * * *" cron: '0 0 * * *'
filters: filters:
branches: branches:
only: only:

View File

@ -7,24 +7,18 @@
"minmax", "minmax",
"openmct", "openmct",
"datasources", "datasources",
"recieved",
"evalute",
"Sinewave", "Sinewave",
"deregistration", "deregistration",
"unregisters", "unregisters",
"configutation",
"configuation",
"codecov", "codecov",
"carryforward", "carryforward",
"Chacon", "Chacon",
"Straub", "Straub",
"OWASP", "OWASP",
"Testathon", "Testathon",
"exploratorily",
"Testathons", "Testathons",
"testathon", "testathon",
"npmjs", "npmjs",
"publishj",
"treeitem", "treeitem",
"timespan", "timespan",
"Timespan", "Timespan",
@ -41,14 +35,10 @@
"faultname", "faultname",
"gantt", "gantt",
"sharded", "sharded",
"perfromance",
"MMOC", "MMOC",
"codegen", "codegen",
"Unfortuantely",
"viewports", "viewports",
"updatesnapshots", "updatesnapshots",
"excercised",
"Circel",
"browsercontexts", "browsercontexts",
"miminum", "miminum",
"testcase", "testcase",
@ -135,9 +125,7 @@
"tortor", "tortor",
"faucibus", "faucibus",
"euismod", "euismod",
"pratices",
"pathing", "pathing",
"pases",
"testcases", "testcases",
"Noneditable", "Noneditable",
"listitem", "listitem",
@ -206,16 +194,12 @@
"unlisten", "unlisten",
"symbolsfont", "symbolsfont",
"ellipsize", "ellipsize",
"dismissable",
"TIMESYSTEM", "TIMESYSTEM",
"Metadatas", "Metadatas",
"stalenes",
"receieves",
"unsub", "unsub",
"callbacktwo", "callbacktwo",
"unsubscribetwo", "unsubscribetwo",
"telem", "telem",
"Telemetery",
"unemitted", "unemitted",
"granually", "granually",
"timesystem", "timesystem",
@ -457,7 +441,6 @@
"Userand", "Userand",
"Userbefore", "Userbefore",
"brdr", "brdr",
"pushs",
"ALPH", "ALPH",
"Recents", "Recents",
"Qbert", "Qbert",
@ -497,7 +480,8 @@
"checksnapshots", "checksnapshots",
"specced", "specced",
"composables", "composables",
"countup" "countup",
"darkmatter"
], ],
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US", "en-gb", "misc"], "dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US", "en-gb", "misc"],
"ignorePaths": [ "ignorePaths": [

View File

@ -1,10 +1,13 @@
const LEGACY_FILES = ['example/**']; const LEGACY_FILES = ['example/**'];
module.exports = { /** @type {import('eslint').Linter.Config} */
const config = {
env: { env: {
browser: true, browser: true,
es6: true, es2024: true,
jasmine: true, jasmine: true,
amd: true node: true,
worker: true,
serviceworker: true
}, },
globals: { globals: {
_: 'readonly' _: 'readonly'
@ -23,10 +26,11 @@ module.exports = {
parser: '@babel/eslint-parser', parser: '@babel/eslint-parser',
requireConfigFile: false, requireConfigFile: false,
allowImportExportEverywhere: true, allowImportExportEverywhere: true,
ecmaVersion: 2015, ecmaVersion: 'latest',
ecmaFeatures: { ecmaFeatures: {
impliedStrict: true impliedStrict: true
} },
sourceType: 'module'
}, },
rules: { rules: {
'simple-import-sort/imports': 'warn', 'simple-import-sort/imports': 'warn',
@ -35,6 +39,7 @@ module.exports = {
'vue/no-deprecated-events-api': 'warn', 'vue/no-deprecated-events-api': 'warn',
'vue/no-v-for-template-key': 'off', 'vue/no-v-for-template-key': 'off',
'vue/no-v-for-template-key-on-child': 'error', 'vue/no-v-for-template-key-on-child': 'error',
'vue/component-name-in-template-casing': ['error', 'PascalCase'],
'prettier/prettier': 'error', 'prettier/prettier': 'error',
'you-dont-need-lodash-underscore/omit': 'off', 'you-dont-need-lodash-underscore/omit': 'off',
'you-dont-need-lodash-underscore/throttle': 'off', 'you-dont-need-lodash-underscore/throttle': 'off',
@ -150,9 +155,10 @@ module.exports = {
'error', 'error',
{ {
cases: { cases: {
camelCase: true,
pascalCase: true pascalCase: true
}, },
ignore: ['^.*\\.js$'] ignore: ['^.*\\.(js|cjs|mjs)$']
} }
], ],
'vue/first-attribute-linebreak': 'error', 'vue/first-attribute-linebreak': 'error',
@ -179,3 +185,5 @@ module.exports = {
} }
] ]
}; };
module.exports = config;

View File

@ -8,7 +8,7 @@ Closes <!--- Insert Issue Number(s) this PR addresses. Start by typing # will op
* [ ] Have you followed the guidelines in our [Contributing document](https://github.com/nasa/openmct/blob/master/CONTRIBUTING.md)? * [ ] Have you followed the guidelines in our [Contributing document](https://github.com/nasa/openmct/blob/master/CONTRIBUTING.md)?
* [ ] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/nasa/openmct/pulls) for the same update/change? * [ ] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/nasa/openmct/pulls) for the same update/change?
* [ ] Is this a notable change that will require a special callout in the release notes [Notable Change](../docs/src/process/release.md) ? For example, will this break compatibility with existing APIs or projects which source these plugins? * [ ] Is this a [notable change](../docs/src/process/release.md) that will require a special callout in the release notes? For example, will this break compatibility with existing APIs or projects that consume these plugins?
### Author Checklist ### Author Checklist
@ -17,7 +17,6 @@ Closes <!--- Insert Issue Number(s) this PR addresses. Start by typing # will op
* [ ] Has this been smoke tested? * [ ] Has this been smoke tested?
* [ ] Have you associated this PR with a `type:` label? Note: this is not necessarily the same as the original issue. * [ ] Have you associated this PR with a `type:` label? Note: this is not necessarily the same as the original issue.
* [ ] Have you associated a milestone with this PR? Note: leave blank if unsure. * [ ] Have you associated a milestone with this PR? Note: leave blank if unsure.
* [ ] Is this a breaking change to be called out in the release notes?
* [ ] Testing instructions included in associated issue OR is this a dependency/testcase change? * [ ] Testing instructions included in associated issue OR is this a dependency/testcase change?
### Reviewer Checklist ### Reviewer Checklist

View File

@ -1 +1,5 @@
name: 'Custom CodeQL config' name: 'Custom CodeQL config'
paths-ignore:
# Ignore e2e tests and framework
- e2e

View File

@ -28,7 +28,7 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-node- ${{ runner.os }}-node-
- run: npm install --cache ~/.npm --no-audit --progress=false - run: npm ci --no-audit --progress=false
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v3 uses: docker/login-action@v3
@ -37,7 +37,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- run: npx playwright@1.39.0 install - run: npx playwright@1.45.2 install
- name: Start CouchDB Docker Container and Init with Setup Scripts - name: Start CouchDB Docker Container and Init with Setup Scripts
run: | run: |

View File

@ -1,15 +1,15 @@
name: 'pr:e2e:flakefinder' name: 'pr:e2e:flakefinder'
on: on:
push: # push:
branches: master # branches: master
workflow_dispatch: workflow_dispatch:
pull_request: # pull_request:
types: # types:
- labeled # - labeled
- opened # - opened
schedule: # schedule:
- cron: '0 0 * * *' # - cron: '0 0 * * *'
jobs: jobs:
e2e-flakefinder: e2e-flakefinder:
@ -30,8 +30,8 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-node- ${{ runner.os }}-node-
- run: npx playwright@1.39.0 install - run: npx playwright@1.45.2 install
- run: npm install --cache ~/.npm --no-audit --progress=false - run: npm ci --no-audit --progress=false
- name: Run E2E Tests (Repeated 10 Times) - 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:stable -- --retries=0 --repeat-each=10 --max-failures=50

View File

@ -28,8 +28,8 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-node- ${{ runner.os }}-node-
- run: npx playwright@1.39.0 install - run: npx playwright@1.45.2 install
- run: npm install --cache ~/.npm --no-audit --progress=false - run: npm ci --no-audit --progress=false
- run: npm run test:perf:localhost - run: npm run test:perf:localhost
- run: npm run test:perf:contract - run: npm run test:perf:contract
- run: npm run test:perf:memory - run: npm run test:perf:memory

View File

@ -33,9 +33,9 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-node- ${{ runner.os }}-node-
- run: npx playwright@1.39.0 install - run: npx playwright@1.45.2 install
- run: npx playwright install chrome-beta - run: npx playwright install chrome-beta
- run: npm install --cache ~/.npm --no-audit --progress=false - run: npm ci --no-audit --progress=false
- run: npm run test:e2e:full -- --max-failures=40 - run: npm run test:e2e:full -- --max-failures=40
- run: npm run cov:e2e:report || true - run: npm run cov:e2e:report || true
- shell: bash - shell: bash

View File

@ -15,7 +15,7 @@ jobs:
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: lts/hydrogen node-version: lts/hydrogen
- run: npm install - run: npm ci
- run: | - run: |
echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" >> ~/.npmrc echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" >> ~/.npmrc
npm whoami npm whoami
@ -31,7 +31,7 @@ jobs:
with: with:
node-version: lts/hydrogen node-version: lts/hydrogen
registry-url: https://registry.npmjs.org/ registry-url: https://registry.npmjs.org/
- run: npm install - run: npm ci
- run: npm publish --access=public --tag unstable - run: npm publish --access=public --tag unstable
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@ -45,7 +45,7 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-${{ matrix.node_version }}- ${{ runner.os }}-${{ matrix.node_version }}-
- run: npm install --cache ~/.npm --no-audit --progress=false - run: npm ci --no-audit --progress=false
- run: npm test - run: npm test

View File

@ -5,6 +5,8 @@ on:
types: types:
- labeled - labeled
- unlabeled - unlabeled
- milestoned
- demilestoned
- opened - opened
- reopened - reopened
- synchronize - synchronize

4
.gitignore vendored
View File

@ -48,5 +48,5 @@ index.html.bak
coverage coverage
codecov codecov
# :( # Don't commit MacOS screenshots
package-lock.json *-darwin.png

View File

@ -23,8 +23,5 @@
!openmct.js !openmct.js
!SECURITY.md !SECURITY.md
# Add e2e tests to npm package # Dont include the example html
!/e2e/**/* dist/index.html
# ... except our test-data folder files.
/e2e/test-data/*.json

3
.npmrc
View File

@ -2,6 +2,3 @@ loglevel=warn
#Prevent folks from ignoring an important error when building from source #Prevent folks from ignoring an important error when building from source
engine-strict=true engine-strict=true
# Dont include lockfile
package-lock=false

View File

@ -5,7 +5,6 @@
// List of extensions which should be recommended for users of this workspace. // List of extensions which should be recommended for users of this workspace.
"recommendations": [ "recommendations": [
"Vue.volar", "Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"rvest.vs-code-prettier-eslint" "rvest.vs-code-prettier-eslint"
], ],

View File

@ -1,8 +1,8 @@
/* /*
This is the OpenMCT common webpack file. It is imported by the other three webpack configurations: This is the OpenMCT common webpack file. It is imported by the other three webpack configurations:
- webpack.prod.js - the production configuration for OpenMCT (default) - webpack.prod.mjs - the production configuration for OpenMCT (default)
- webpack.dev.js - the development configuration for OpenMCT - webpack.dev.mjs - the development configuration for OpenMCT
- webpack.coverage.js - imports webpack.dev.js and adds code coverage - webpack.coverage.mjs - imports webpack.dev.js and adds code coverage
There are separate npm scripts to use these configurations, though simply running `npm install` There are separate npm scripts to use these configurations, though simply running `npm install`
will use the default production configuration. will use the default production configuration.
*/ */
@ -15,10 +15,11 @@ import CopyWebpackPlugin from 'copy-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import { VueLoaderPlugin } from 'vue-loader'; import { VueLoaderPlugin } from 'vue-loader';
import webpack from 'webpack'; import webpack from 'webpack';
import { merge } from 'webpack-merge';
let gitRevision = 'error-retrieving-revision'; let gitRevision = 'error-retrieving-revision';
let gitBranch = 'error-retrieving-branch'; let gitBranch = 'error-retrieving-branch';
const packageDefinition = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url))); const { version } = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url)));
try { try {
gitRevision = execSync('git rev-parse HEAD').toString().trim(); gitRevision = execSync('git rev-parse HEAD').toString().trim();
@ -48,15 +49,18 @@ const config = {
couchDBChangesFeed: './src/plugins/persistence/couch/CouchChangesFeed.js', couchDBChangesFeed: './src/plugins/persistence/couch/CouchChangesFeed.js',
inMemorySearchWorker: './src/api/objects/InMemorySearchWorker.js', inMemorySearchWorker: './src/api/objects/InMemorySearchWorker.js',
espressoTheme: './src/plugins/themes/espresso-theme.scss', espressoTheme: './src/plugins/themes/espresso-theme.scss',
snowTheme: './src/plugins/themes/snow-theme.scss' snowTheme: './src/plugins/themes/snow-theme.scss',
darkmatterTheme: './src/plugins/themes/darkmatter-theme.scss'
}, },
output: { output: {
globalObject: 'this', globalObject: 'this',
filename: '[name].js', filename: '[name].js',
path: path.resolve(projectRootDir, 'dist'), path: path.resolve(projectRootDir, 'dist'),
library: 'openmct', library: {
libraryExport: 'default', name: 'openmct',
libraryTarget: 'umd', type: 'umd',
export: 'default'
},
publicPath: '', publicPath: '',
hashFunction: 'xxhash64', hashFunction: 'xxhash64',
clean: true clean: true
@ -81,7 +85,7 @@ const config = {
}, },
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({
__OPENMCT_VERSION__: `'${packageDefinition.version}'`, __OPENMCT_VERSION__: `'${version}'`,
__OPENMCT_BUILD_DATE__: `'${new Date()}'`, __OPENMCT_BUILD_DATE__: `'${new Date()}'`,
__OPENMCT_REVISION__: `'${gitRevision}'`, __OPENMCT_REVISION__: `'${gitRevision}'`,
__OPENMCT_BUILD_BRANCH__: `'${gitBranch}'`, __OPENMCT_BUILD_BRANCH__: `'${gitBranch}'`,

View File

@ -1,15 +1,12 @@
/* /*
This file extends the webpack.dev.js config to add babel istanbul coverage. This file extends the webpack.dev.mjs config to add babel istanbul coverage.
OpenMCT Continuous Integration servers use this configuration to add code coverage OpenMCT Continuous Integration servers use this configuration to add code coverage
information to pull requests. information to pull requests.
*/ */
import config from './webpack.dev.js'; import config from './webpack.dev.mjs';
// eslint-disable-next-line no-undef
const CI = process.env.CI === 'true';
config.devtool = CI ? false : undefined;
config.devtool = 'inline-source-map';
config.devServer.hot = false; config.devServer.hot = false;
config.module.rules.push({ config.module.rules.push({
@ -19,7 +16,6 @@ config.module.rules.push({
loader: 'babel-loader', loader: 'babel-loader',
options: { options: {
retainLines: true, retainLines: true,
// eslint-disable-next-line no-undef
plugins: [ plugins: [
[ [
'babel-plugin-istanbul', 'babel-plugin-istanbul',

View File

@ -1,14 +1,15 @@
/* /*
This configuration should be used for development purposes. It contains full source map, a This configuration should be used for development purposes. It contains full source map, a
devServer (which be invoked using by `npm start`), and a non-minified Vue.js distribution. devServer (which be invoked using by `npm start`), and a non-minified Vue.js distribution.
If OpenMCT is to be used for a production server, use webpack.prod.js instead. If OpenMCT is to be used for a production server, use webpack.prod.mjs instead.
*/ */
import { fileURLToPath } from 'node:url';
import path from 'path'; import path from 'path';
import webpack from 'webpack'; import webpack from 'webpack';
import { merge } from 'webpack-merge'; import { merge } from 'webpack-merge';
import { fileURLToPath } from 'node:url';
import common from './webpack.common.js'; import common from './webpack.common.mjs';
export default merge(common, { export default merge(common, {
mode: 'development', mode: 'development',
@ -38,7 +39,7 @@ export default merge(common, {
return shouldWrite; return shouldWrite;
} }
}, },
watchFiles: ['**/*.css'], watchFiles: ['src/**/*.css', 'example/**/*.css'],
static: { static: {
directory: fileURLToPath(new URL('../dist', import.meta.url)), directory: fileURLToPath(new URL('../dist', import.meta.url)),
publicPath: '/dist', publicPath: '/dist',

View File

@ -6,7 +6,7 @@ It is the default webpack configuration.
import webpack from 'webpack'; import webpack from 'webpack';
import { merge } from 'webpack-merge'; import { merge } from 'webpack-merge';
import common from './webpack.common.js'; import common from './webpack.common.mjs';
export default merge(common, { export default merge(common, {
mode: 'production', mode: 'production',

18
API.md
View File

@ -713,7 +713,7 @@ openmct.telemetry.addFormat({
A single telemetry point is considered a Datum, and is represented by a standard A single telemetry point is considered a Datum, and is represented by a standard
javascript object. Realtime subscriptions (obtained via **subscribe**) will javascript object. Realtime subscriptions (obtained via **subscribe**) will
invoke the supplied callback once for each telemetry datum recieved. Telemetry invoke the supplied callback once for each telemetry datum received. Telemetry
requests (obtained via **request**) will return a promise for an array of requests (obtained via **request**) will return a promise for an array of
telemetry datums. telemetry datums.
@ -738,7 +738,7 @@ section.
Limit evaluators allow a telemetry integrator to define which limits exist for a Limit evaluators allow a telemetry integrator to define which limits exist for a
telemetry endpoint and how limits should be applied to telemetry from a given domain object. telemetry endpoint and how limits should be applied to telemetry from a given domain object.
A limit evaluator can implement the `evalute` method which is used to define how limits A limit evaluator can implement the `evaluate` method which is used to define how limits
should be applied to telemetry and the `getLimits` method which is used to specify should be applied to telemetry and the `getLimits` method which is used to specify
what the limit values are for different limit levels. what the limit values are for different limit levels.
@ -1180,7 +1180,7 @@ An example time conductor configuration is provided below. It sets up some
default options for the [UTCTimeSystem](https://github.com/nasa/openmct/blob/master/src/plugins/utcTimeSystem/UTCTimeSystem.js) default options for the [UTCTimeSystem](https://github.com/nasa/openmct/blob/master/src/plugins/utcTimeSystem/UTCTimeSystem.js)
and [LocalTimeSystem](https://github.com/nasa/openmct/blob/master/src/plugins/localTimeSystem/LocalTimeSystem.js), and [LocalTimeSystem](https://github.com/nasa/openmct/blob/master/src/plugins/localTimeSystem/LocalTimeSystem.js),
in both fixed mode, and for the [LocalClock](https://github.com/nasa/openmct/blob/master/src/plugins/utcTimeSystem/LocalClock.js) in both fixed mode, and for the [LocalClock](https://github.com/nasa/openmct/blob/master/src/plugins/utcTimeSystem/LocalClock.js)
source. In this configutation, the local clock supports both the UTCTimeSystem source. In this configuration, the local clock supports both the UTCTimeSystem
and LocalTimeSystem. Configuration for fixed bounds mode is specified by omitting and LocalTimeSystem. Configuration for fixed bounds mode is specified by omitting
a clock key. a clock key.
@ -1190,7 +1190,7 @@ const ONE_MINUTE = 60 * 1000;
openmct.install(openmct.plugins.Conductor({ openmct.install(openmct.plugins.Conductor({
menuOptions: [ menuOptions: [
// 'Fixed' bounds mode configuation for the UTCTimeSystem // 'Fixed' bounds mode configuration for the UTCTimeSystem
{ {
timeSystem: 'utc', timeSystem: 'utc',
bounds: {start: Date.now() - 30 * ONE_MINUTE, end: Date.now()}, bounds: {start: Date.now() - 30 * ONE_MINUTE, end: Date.now()},
@ -1305,6 +1305,16 @@ View provider Example:
} }
``` ```
## User API
Open MCT provides a User API which can be used to define providers for user information. The API
can be used to manage user information and roles.
### Example
Open MCT provides an example [user](example/exampleUser/exampleUserCreator.js) and [user provider](example/exampleUser/ExampleUserProvider.js) which
can be used as a starting point for creating a custom user provider.
## Visibility-Based Rendering in View Providers ## Visibility-Based Rendering in View Providers
To enhance performance and resource efficiency in OpenMCT, a visibility-based rendering feature has been added. This feature is designed to defer the execution of rendering logic for views that are not currently visible. It ensures that views are only updated when they are in the viewport, similar to how modern browsers handle rendering of inactive tabs but optimized for the OpenMCT tabbed display. It also works when views are scrolled outside the viewport (e.g., in a Display Layout). To enhance performance and resource efficiency in OpenMCT, a visibility-based rendering feature has been added. This feature is designed to defer the execution of rendering logic for views that are not currently visible. It ensures that views are only updated when they are in the viewport, similar to how modern browsers handle rendering of inactive tabs but optimized for the OpenMCT tabbed display. It also works when views are scrolled outside the viewport (e.g., in a Display Layout).

View File

@ -16,8 +16,6 @@ The [CodeQL GitHub Actions workflow](https://github.com/nasa/openmct/blob/master
CodeQL is run for every pull-request in GitHub Actions. CodeQL is run for every pull-request in GitHub Actions.
The project is also monitored by [LGTM](https://lgtm.com/projects/g/nasa/openmct/) and is available to public.
### ESLint ### ESLint
Static analysis is run for every push on the master branch and every pull request on all branches in Github Actions. Static analysis is run for every push on the master branch and every pull request on all branches in Github Actions.

View File

@ -63,7 +63,7 @@ Once the file is generated, it can be published to codecov with
### e2e ### e2e
The e2e line coverage is a bit more complex than the karma implementation. This is the general sequence of events: The e2e line coverage is a bit more complex than the karma implementation. This is the general sequence of events:
1. Each e2e suite will start webpack with the ```npm run start:coverage``` command with config `webpack.coverage.js` and the `babel-plugin-istanbul` plugin to generate code coverage during e2e test execution using our custom [baseFixture](./baseFixtures.js). 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. 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. [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. 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`.
@ -91,12 +91,14 @@ There are a few reasons that your GitHub PR could be failing beyond simple faile
### Local=Pass and CI=Fail ### Local=Pass and CI=Fail
Although rare, it is possible that your test can pass locally but fail in CI. Although rare, it is possible that your test can pass locally but fail in CI.
#### Busting Cache ### Reset your workspace
In certain circumstances, the CircleCI cache can become stale. In order to bust the cache, we've implemented a runtime boolean parameter in Circle CI creatively name BUST_CACHE. To execute: It's possible that you're running with dependencies or a local environment which is out of sync with the branch you're working on. Make sure to execute the following:
1. Navigate to the branch in Circle CI believed to have stale cache.
1. Click on the 'Trigger Pipeline' button. ```sh
1. Add Parameter -> Parameter Type = boolean , Name = BUST_CACHE ,Value = true nvm use
1. Click 'Trigger Pipeline' npm run clean
npm install
```
#### Run tests in the same container as CI #### Run tests in the same container as CI

View File

@ -133,7 +133,7 @@ emphasis on testing.
Multi-user testing, involving as many users as Multi-user testing, involving as many users as
is feasible, plus development team. Open-ended; should verify is feasible, plus development team. Open-ended; should verify
completed work from this sprint using the sprint branch, test completed work from this sprint using the sprint branch, test
exploratorily for regressions, et cetera. exploratory for regressions, et cetera.
* [__Long-Duration Test.__](testing/plan.md#long-duration-testing) A * [__Long-Duration Test.__](testing/plan.md#long-duration-testing) A
test to verify that the software remains test to verify that the software remains
stable after running for longer durations. May include some stable after running for longer durations. May include some

View File

@ -132,7 +132,7 @@ numbers by the following process:
4. Test the package before publishing by doing `npm publish --dry-run` 4. Test the package before publishing by doing `npm publish --dry-run`
if necessary. if necessary.
5. Publish the package to the npmjs registry (e.g. `npm publish --access public`) 5. Publish the package to the npmjs registry (e.g. `npm publish --access public`)
NOTE: Use the `--tag unstable` flag to the npm publishj if this is a prerelease. NOTE: Use the `--tag unstable` flag to the npm publish if this is a prerelease.
6. Confirm the package has been published (e.g. `https://www.npmjs.com/package/openmct`) 6. Confirm the package has been published (e.g. `https://www.npmjs.com/package/openmct`)
5. Update snapshot status in `package.json` 5. Update snapshot status in `package.json`
1. Create a new branch off the `master` branch. 1. Create a new branch off the `master` branch.

View File

@ -1,14 +1,14 @@
/* eslint-disable no-undef */ /* eslint-disable no-undef */
module.exports = { module.exports = {
extends: ['plugin:playwright/playwright-test'], extends: ['plugin:playwright/recommended'],
rules: { rules: {
'playwright/max-nested-describe': ['error', { max: 1 }] 'playwright/max-nested-describe': ['error', { max: 1 }]
}, },
overrides: [ overrides: [
{ {
files: ['tests/visual/*.spec.js'], files: ['**/*.spec.js'], // Added the 'files' property
rules: { rules: {
'playwright/no-wait-for-timeout': 'off' 'playwright/expect-expect': 'off'
} }
} }
] ]

7
e2e/.npmignore Normal file
View File

@ -0,0 +1,7 @@
*
!appActions.js
!baseFixtures.js
!pluginFixtures.js
!avpFixtures.js
!index.js
!*.md

View File

@ -30,4 +30,15 @@ snapshot:
.gl-plot-chart-area{ .gl-plot-chart-area{
opacity: 0 !important; opacity: 0 !important;
} }
/* SWG Time values on plot */
.gl-plot-x{
opacity: 0 !important;
}
/* Notification Time in modal */
.c-ne__time{
opacity: 0 !important;
}
/* Snapshot name with embedded time */
.l-browse-bar__snapshot-datetime{
opacity: 0 !important;
}

View File

@ -29,4 +29,16 @@ snapshot:
/* Chart Area for Plots */ /* Chart Area for Plots */
.gl-plot-chart-area{ .gl-plot-chart-area{
opacity: 0 !important; opacity: 0 !important;
} }
/* SWG Time values on plot */
.gl-plot-x{
opacity: 0 !important;
}
/* Notification Time in modal */
.c-ne__time{
opacity: 0 !important;
}
/* Snapshot name with embedded time */
.l-browse-bar__snapshot-datetime{
opacity: 0 !important;
}

View File

@ -76,28 +76,30 @@ To read about how to write a good visual test, please see [How to write a great
`npm run test:e2e:visual` commands will run all of the visual tests against a local instance of Open MCT. If no `PERCY_TOKEN` API key is found in the terminal or command line environment variables, no visual comparisons will be made. `npm run test:e2e:visual` commands will run all of the visual tests against a local instance of Open MCT. If no `PERCY_TOKEN` API key is found in the terminal or command line environment variables, no visual comparisons will be made.
- `npm run test:e2e:visual:ci` will run against every commit and PR. - `npm run test:e2e:visual:ci` will run against every commit and PR.
- `npm run test:e2e:visual:full` will run every night with additional comparisons made for Larger Displays and with the `snow` theme. - `npm run test:e2e:visual:full` will run every night with additional comparisons made for Larger Displays and with the `snow` theme.
#### Percy.io #### Percy.io
To make this possible, we're leveraging a 3rd party service, [Percy](https://percy.io/). This service maintains a copy of all changes, users, scm-metadata, and baselines to verify that the application looks and feels the same _unless approved by a Open MCT developer_. To request a Percy API token, please reach out to the Open MCT Dev team on GitHub. For more information, please see the official [Percy documentation](https://docs.percy.io/docs/visual-testing-basics). To make this possible, we're leveraging a 3rd party service, [Percy](https://percy.io/). This service maintains a copy of all changes, users, scm-metadata, and baselines to verify that the application looks and feels the same _unless approved by a Open MCT developer_. To request a Percy API token, please reach out to the Open MCT Dev team on GitHub. For more information, please see the official [Percy documentation](https://docs.percy.io/docs/visual-testing-basics).
At present, we are using percy with two configuration files: `./e2e/.percy.nightly.yml` and `./e2e/.percy.ci.yml`. This is mainly to reduce the number of snapshots. At present, we are using percy with two configuration files: `./e2e/.percy.nightly.yml` and `./e2e/.percy.ci.yml`. This is mainly to reduce the number of snapshots.
### Advanced: Snapshot Testing (Not Recommended) ### Advanced: Snapshot Testing (Not Recommended)
While snapshot testing offers a precise way to detect changes in your application without relying on third-party services like Percy.io, we've found that it doesn't offer any advantages over visual testing in our use-cases. Therefore, snapshot testing is **not recommended** for further implementation. While snapshot testing offers a precise way to detect changes in your application without relying on third-party services like Percy.io, we've found that it doesn't offer any advantages over visual testing in our use-cases. Therefore, snapshot testing is **not recommended** for further implementation.
#### CI vs Manual Checks #### CI vs Manual Checks
Snapshot tests can be reliably executed in Continuous Integration (CI) environments but lack the manual oversight provided by visual testing platforms like Percy.io. This means they may miss issues that a human reviewer could catch during manual checks. Snapshot tests can be reliably executed in Continuous Integration (CI) environments but lack the manual oversight provided by visual testing platforms like Percy.io. This means they may miss issues that a human reviewer could catch during manual checks.
#### Example #### Example
A single visual test assertion in Percy.io can be executed across 10 different browser and resolution combinations without additional setup, providing comprehensive testing with minimal configuration. In contrast, a snapshot test is restricted to a single OS and browser resolution, requiring more effort to achieve the same level of coverage. A single visual test assertion in Percy.io can be executed across 10 different browser and resolution combinations without additional setup, providing comprehensive testing with minimal configuration. In contrast, a snapshot test is restricted to a single OS and browser resolution, requiring more effort to achieve the same level of coverage.
#### Further Reading #### Further Reading
For those interested in the mechanics of snapshot testing with Playwright, you can refer to the [Playwright Snapshots Documentation](https://playwright.dev/docs/test-snapshots). However, keep in mind that we do not recommend using this approach.
For those interested in the mechanics of snapshot testing with Playwright, you can refer to the [Playwright Snapshots Documentation](https://playwright.dev/docs/test-snapshots). However, keep in mind that we do not recommend using this approach.
#### Open MCT's implementation #### Open MCT's implementation
@ -118,14 +120,6 @@ When the `@snapshot` tests fail, they will need to be evaluated to determine if
To compare a snapshot, run a test and open the html report with the 'Expected' vs 'Actual' screenshot. If the actual screenshot is preferred, then the source-controlled 'Expected' snapshots will need to be updated with the following scripts. To compare a snapshot, run a test and open the html report with the 'Expected' vs 'Actual' screenshot. If the actual screenshot is preferred, then the source-controlled 'Expected' snapshots will need to be updated with the following scripts.
MacOS
```
npm run test:e2e:updatesnapshots
```
Linux/CI
```sh ```sh
// Replace {X.X.X} with the current Playwright version // Replace {X.X.X} with the current Playwright version
// from our package.json or circleCI configuration file // from our package.json or circleCI configuration file
@ -173,9 +167,9 @@ When an a11y test fails, the result must be interpreted in the html test report
The open source performance tests function in three ways which match their naming and folder structure: The open source performance tests function in three ways which match their naming and folder structure:
`./e2e/tests/performance` - The tests at the root of this folder path detect functional changes which are mostly apparent with large performance regressions like [this](https://github.com/nasa/openmct/issues/6879). These tests run against openmct webpack in `production-mode` with the `npm run test:perf:localhost` script. `tests/performance` - The tests at the root of this folder path detect functional changes which are mostly apparent with large performance regressions like [this](https://github.com/nasa/openmct/issues/6879). These tests run against openmct webpack in `production-mode` with the `npm run test:perf:localhost` script.
`./e2e/tests/performance/contract/` - These tests serve as [contracts](https://martinfowler.com/bliki/ContractTest.html) for the locator logic, functionality, and assumptions will work in our downstream, closed source test suites. These tests run against openmct webpack in `dev-mode` with the `npm run test:perf:contract` script. `tests/performance/contract/` - These tests serve as [contracts](https://martinfowler.com/bliki/ContractTest.html) for the locator logic, functionality, and assumptions will work in our downstream, closed source test suites. These tests run against openmct webpack in `dev-mode` with the `npm run test:perf:contract` script.
`./e2e/tests/performance/memory/` - These tests execute memory leak detection checks in various ways. This is expected to evolve as we move to the `memlab` project. These tests run against openmct webpack in `production-mode` with the `npm run test:perf:memory` script. `tests/performance/memory/` - These tests execute memory leak detection checks in various ways. This is expected to evolve as we move to the `memlab` project. These tests run against openmct webpack in `production-mode` with the `npm run test:perf:memory` script.
These tests are expected to become blocking and gating with assertions as we extend the capabilities of Playwright. These tests are expected to become blocking and gating with assertions as we extend the capabilities of Playwright.
@ -187,7 +181,7 @@ In addition to the explicit definition of performance tests, we also ensure that
### File Structure ### File Structure
Our file structure follows the type of type of testing being excercised at the e2e layer and files containing test suites which matcher application behavior or our `src` and `example` layout. This area is not well refined as we figure out what works best for closed source and downstream projects. This may change altogether if we move `e2e` to it's own npm package. Our file structure follows the type of type of testing being exercised at the e2e layer and files containing test suites which matcher application behavior or our `src` and `example` layout. This area is not well refined as we figure out what works best for closed source and downstream projects. This may change altogether if we move `e2e` to it's own npm package.
|File Path|Description| |File Path|Description|
|:-:|-| |:-:|-|
@ -238,10 +232,11 @@ Current list of test tags:
|`@unstable` | A new test or test which is known to be flaky.| |`@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.| |`@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.| |`@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.
### Continuous Integration ### Continuous Integration
The cheapest time to catch a bug is pre-merge. Unfortuantely, this is the most expensive time to run all of the tests since each merge event can consist of hundreds of commits. For this reason, we're selective in _what we run_ as much as _when we run it_. The cheapest time to catch a bug is pre-merge. Unfortunately, this is the most expensive time to run all of the tests since each merge event can consist of hundreds of commits. For this reason, we're selective in _what we run_ as much as _when we run it_.
We leverage CircleCI to run tests against each commit and inject the Test Reports which are generated by Playwright so that they team can keep track of flaky and [historical test trends](https://app.circleci.com/insights/github/nasa/openmct/workflows/overall-circleci-commit-status/tests?branch=master&reporting-window=last-30-days) We leverage CircleCI to run tests against each commit and inject the Test Reports which are generated by Playwright so that they team can keep track of flaky and [historical test trends](https://app.circleci.com/insights/github/nasa/openmct/workflows/overall-circleci-commit-status/tests?branch=master&reporting-window=last-30-days)
@ -286,7 +281,7 @@ Playwright has native support for semi-intelligent sharding. Read about it [here
We will be adjusting the parallelization of the Per-Commit tests to keep below the 5 minute total runtime threshold. We will be adjusting the parallelization of the Per-Commit tests to keep below the 5 minute total runtime threshold.
In addition to the Parallelization of Test Runners (Sharding), we're also running two concurrent threads on every Shard. This is the functional limit of what CircelCI Agents can support from a memory and CPU resource constraint. In addition to the Parallelization of Test Runners (Sharding), we're also running two concurrent threads on every Shard. This is the functional limit of what CircleCI Agents can support from a memory and CPU resource constraint.
So for every commit, Playwright is effectively running 4 x 2 concurrent browsercontexts to keep the overall runtime to a miminum. So for every commit, Playwright is effectively running 4 x 2 concurrent browsercontexts to keep the overall runtime to a miminum.
@ -334,9 +329,11 @@ We have a Mission-need to support iPad and mobile devices. To run our test suite
In general, our test suite is not designed to run against mobile devices as the mobile experience is a focused version of the application. Core functionality is missing (chiefly the 'Create' button). To bypass the object creation, we leverage the `storageState` properties for starting the mobile tests with localstorage. In general, our test suite is not designed to run against mobile devices as the mobile experience is a focused version of the application. Core functionality is missing (chiefly the 'Create' button). To bypass the object creation, we leverage the `storageState` properties for starting the mobile tests with localstorage.
For now, the mobile tests will exist in the /tests/mobile/ suites and be executed with the For now, the mobile tests will exist in the /tests/mobile/ suites and be executed with the
```sh ```sh
npm run test:e2e:mobile npm run test:e2e:mobile
``` ```
command. command.
#### **Skipping or executing tests based on browser, os, and/os browser version:** #### **Skipping or executing tests based on browser, os, and/os browser version:**
@ -376,13 +373,14 @@ In general, strive to test only through the UI as a user would. As stated in the
By adhering to this principle, we can create tests that are both robust and reflective of actual user experiences. By adhering to this principle, we can create tests that are both robust and reflective of actual user experiences.
#### How to make tests robust to function in other contexts (VISTA, COUCHDB, YAMCS, VIPER, etc.) #### How to make tests robust to function in other contexts (VISTA, COUCHDB, YAMCS, VIPER, etc.)
1. Leverage the use of `appActions.js` methods such as `createDomainObjectWithDefaults()`. This ensures that your tests will create unique instances of objects for your test to interact with. 1. Leverage the use of `appActions.js` methods such as `createDomainObjectWithDefaults()`. This ensures that your tests will create unique instances of objects for your test to interact with.
1. Do not assert on the order or structure of objects available unless you created them yourself. These tests may be used against a persistent datastore like couchdb with many objects in the tree. 1. Do not assert on the order or structure of objects available unless you created them yourself. These tests may be used against a persistent datastore like couchdb with many objects in the tree.
1. Do not search for your created objects. Open MCT does not performance uniqueness checks so it's possible that your tests will break when run twice. 1. Do not search for your created objects. Open MCT does not performance uniqueness checks so it's possible that your tests will break when run twice.
1. Avoid creating locator aliases. This likely means that you're compensating for a bad locator. Improve the application instead. 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. 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: 1. Avoid app interaction when possible. The best way of doing this is to navigate directly by URL:
```js ```js
@ -395,10 +393,21 @@ By adhering to this principle, we can create tests that are both robust and refl
1. Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });` 1. Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });`
- Initial navigation should _almost_ always use the `{ waitUntil: 'domcontentloaded' }` option. - 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. 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. 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 ##### Utilizing LocalStorage
1. In order to save test runtime in the case of tests that require a decent amount of initial setup (such as in the case of testing complex displays), you may use [Playwright's `storageState` feature](https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state) to generate and load localStorage states. 1. In order to save test runtime in the case of tests that require a decent amount of initial setup (such as in the case of testing complex displays), you may use [Playwright's `storageState` feature](https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state) to generate and load localStorage states.
1. To generate a localStorage state to be used in a test: 1. To generate a localStorage state to be used in a test:
- Add an e2e test to our generateLocalStorageData suite which sets the initial state (creating/configuring objects, etc.), saving it in the `test-data` folder: - Add an e2e test to our generateLocalStorageData suite which sets the initial state (creating/configuring objects, etc.), saving it in the `test-data` folder:
@ -419,7 +428,6 @@ By adhering to this principle, we can create tests that are both robust and refl
}); });
``` ```
### How to write a great test ### How to write a great test
- Avoid using css locators to find elements to the page. Use modern web accessible locators like `getByRole` - Avoid using css locators to find elements to the page. Use modern web accessible locators like `getByRole`
@ -435,7 +443,7 @@ By adhering to this principle, we can create tests that are both robust and refl
await notesInput.fill(testNotes); await notesInput.fill(testNotes);
``` ```
#### How to Write a Great Visual Test #### How to Write a Great Visual Test
1. **Look for the Unknown Unknowns**: Avoid asserting on specific differences in the visual diff. Visual tests are most effective for identifying unknown unknowns. 1. **Look for the Unknown Unknowns**: Avoid asserting on specific differences in the visual diff. Visual tests are most effective for identifying unknown unknowns.
@ -444,22 +452,27 @@ By adhering to this principle, we can create tests that are both robust and refl
3. **Expect the Unexpected**: Use functional expect statements only to verify assumptions about the state between steps. A great visual test doesn't fail during the test itself, but rather when changes are reviewed in Percy.io. 3. **Expect the Unexpected**: Use functional expect statements only to verify assumptions about the state between steps. A great visual test doesn't fail during the test itself, but rather when changes are reviewed in Percy.io.
4. **Control Variability**: Account for variations inherent in working with time-based telemetry and clocks. 4. **Control Variability**: Account for variations inherent in working with time-based telemetry and clocks.
- Utilize `percyCSS` to ignore time-based elements. For more details, consult our [percyCSS file](./.percy.ci.yml).
- Use Open MCT's fixed-time mode unless explicitly testing realtime clock - Utilize `percyCSS` to ignore time-based elements. For more details, consult our [percyCSS file](./.percy.ci.yml).
- Employ the `createExampleTelemetryObject` appAction to source telemetry and specify a `name` to avoid autogenerated names. - 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.
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: 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')` - `await page.goto('./#/browse/mine?hideTree=true&hideInspector=true')`
6. **Component-Specific Tests**: If you wish to focus on a particular component, use the `/visual-a11y/component/` folder and limit the scope of the comparison to that component. For instance: 6. **Component-Specific Tests**: If you wish to focus on a particular component, use the `/visual-a11y/component/` folder and limit the scope of the comparison to that component. For instance:
```js ```js
await percySnapshot(page, `Tree Pane w/ single level expanded (theme: ${theme})`, { await percySnapshot(page, `Tree Pane w/ single level expanded (theme: ${theme})`, {
scope: treePane scope: treePane
}); });
``` ```
- Note: The `scope` variable can be any valid CSS selector. - Note: The `scope` variable can be any valid CSS selector.
7. **Write many `percySnapshot` commands in a single test**: In line with our approach to longer functional tests, we recommend that many test percySnapshots are taken in a single test. For instance: 7. **Write many `percySnapshot` commands in a single test**: In line with our approach to longer functional tests, we recommend that many test percySnapshots are taken in a single test. For instance:
```js ```js
//<Some interesting state> //<Some interesting state>
await percySnapshot(page, `Before object expanded (theme: ${theme})`); await percySnapshot(page, `Before object expanded (theme: ${theme})`);
@ -509,11 +522,35 @@ test.describe('foo test suite', () => {
}); });
}); });
``` ```
More info and options for `overrideClock` can be found in [baseFixtures.js](baseFixtures.js) More info and options for `overrideClock` can be found in [baseFixtures.js](baseFixtures.js)
- Working with multiple pages - 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. 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.
- Working with file downloads and JSON data
Open MCT has the capability of exporting certain objects in the form of a JSON file handled by the chrome browser. The best example of this type of test can be found in the exportAsJson test.
```js
const [download] = await Promise.all([
page.waitForEvent('download'), // Waits for the download event
page.getByLabel('Export as JSON').click() // Triggers the download
]);
// Wait for the download process to complete
const path = await download.path();
// Read the contents of the downloaded file using readFile from fs/promises
const fileContents = await fs.readFile(path, 'utf8');
const jsonData = JSON.parse(fileContents);
// Use the function to retrieve the key
const key = getFirstKeyFromOpenMctJson(jsonData);
// Verify the contents of the JSON file
expect(jsonData.openmct[key]).toHaveProperty('name', 'e2e folder');
```
### Reporting ### Reporting
Test Reporting is done through official Playwright reporters and the CI Systems which execute them. Test Reporting is done through official Playwright reporters and the CI Systems which execute them.
@ -589,6 +626,7 @@ A single e2e test in Open MCT is extended to run:
### Writing Tests ### Writing Tests
Playwright provides 3 supported methods of debugging and authoring tests: Playwright provides 3 supported methods of debugging and authoring tests:
- A 'watch mode' for running tests locally and debugging on the fly - A 'watch mode' for running tests locally and debugging on the fly
- A 'debug mode' for debugging tests and writing assertions against tests - A 'debug mode' for debugging tests and writing assertions against tests
- A 'VSCode plugin' for debugging tests within the VSCode IDE. - A 'VSCode plugin' for debugging tests within the VSCode IDE.

View File

@ -78,13 +78,13 @@ async function createDomainObjectWithDefaults(
// Navigate to the parent object. This is necessary to create the object // Navigate to the parent object. This is necessary to create the object
// in the correct location, such as a folder, layout, or plot. // in the correct location, such as a folder, layout, or plot.
await page.goto(`${parentUrl}`); await page.goto(parentUrl);
//Click the Create button // Click the Create button
await page.getByRole('button', { name: 'Create' }).click(); await page.getByRole('button', { name: 'Create', exact: true }).click();
// Click the object specified by 'type' // Click the object specified by 'type'-- case insensitive
await page.click(`li[role='menuitem']:text("${type}")`); await page.getByRole('menuitem', { name: new RegExp(`^${type}$`, 'i') }).click();
// Modify the name input field of the domain object to accept 'name' // Modify the name input field of the domain object to accept 'name'
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]'); const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
@ -275,6 +275,17 @@ async function navigateToObjectWithFixedTimeBounds(page, url, start, end) {
); );
} }
/**
* Navigates directly to a given object url, in real-time mode.
* @param {import('@playwright/test').Page} page
* @param {string} url The url to the domainObject
*/
async function navigateToObjectWithRealTime(page, url, start = '1800000', end = '30000') {
await page.goto(
`${url}?tc.mode=local&tc.startDelta=${start}&tc.endDelta=${end}&tc.timeSystem=utc`
);
}
/** /**
* Open the given `domainObject`'s context menu from the object tree. * 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 path to the object and scrolls to it if necessary.
@ -342,7 +353,7 @@ async function getFocusedObjectUuid(page) {
* @returns {Promise<string>} the url of the object * @returns {Promise<string>} the url of the object
*/ */
async function getHashUrlToDomainObject(page, identifier) { async function getHashUrlToDomainObject(page, identifier) {
await page.waitForLoadState('load'); await page.waitForLoadState('domcontentloaded');
const hashUrl = await page.evaluate(async (objectIdentifier) => { const hashUrl = await page.evaluate(async (objectIdentifier) => {
const path = await window.openmct.objects.getOriginalPath(objectIdentifier); const path = await window.openmct.objects.getOriginalPath(objectIdentifier);
let url = let url =
@ -392,6 +403,8 @@ async function setTimeConductorMode(page, isFixedTimespan = true) {
await page.getByRole('menuitem', { name: /Real-Time/ }).click(); await page.getByRole('menuitem', { name: /Real-Time/ }).click();
await page.waitForURL(/tc\.mode=local/); await page.waitForURL(/tc\.mode=local/);
} }
//dismiss the time conductor popup
await page.getByLabel('Discard changes and close time popup').click();
} }
/** /**
@ -505,15 +518,14 @@ async function setTimeConductorBounds(page, startDate, endDate) {
* @param {string} startDate * @param {string} startDate
* @param {string} endDate * @param {string} endDate
*/ */
async function setIndependentTimeConductorBounds(page, startDate, endDate) { async function setIndependentTimeConductorBounds(page, { start, end }) {
// Activate Independent Time Conductor in Fixed Time Mode // Activate Independent Time Conductor
await page.getByRole('switch').click(); await page.getByLabel('Enable Independent Time Conductor').click();
// Bring up the time conductor popup // Bring up the time conductor popup
await page.click('.c-conductor-holder--compact .c-compact-tc'); await page.getByLabel('Independent Time Conductor Settings').click();
await expect(page.locator('.itc-popout')).toBeInViewport(); await expect(page.locator('.itc-popout')).toBeInViewport();
await setTimeBounds(page, start, end);
await setTimeBounds(page, startDate, endDate);
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
} }
@ -580,9 +592,6 @@ async function waitForPlotsToRender(page) {
* @return {Promise<PlotPixel[]>} * @return {Promise<PlotPixel[]>}
*/ */
async function getCanvasPixels(page, canvasSelector) { async function getCanvasPixels(page, canvasSelector) {
const getTelemValuePromise = new Promise((resolve) =>
page.exposeFunction('getCanvasValue', resolve)
);
const canvasHandle = await page.evaluateHandle( const canvasHandle = await page.evaluateHandle(
(canvas) => document.querySelector(canvas), (canvas) => document.querySelector(canvas),
canvasSelector canvasSelector
@ -593,7 +602,7 @@ async function getCanvasPixels(page, canvasSelector) {
); );
await waitForPlotsToRender(page); await waitForPlotsToRender(page);
await page.evaluate( return page.evaluate(
([canvas, ctx]) => { ([canvas, ctx]) => {
// The document canvas is where the plot points and lines are drawn. // The document canvas is where the plot points and lines are drawn.
// The only way to access the canvas is using document (using page.evaluate) // The only way to access the canvas is using document (using page.evaluate)
@ -621,12 +630,10 @@ async function getCanvasPixels(page, canvasSelector) {
i = i + 4; i = i + 4;
} }
window.getCanvasValue(plotPixels); return plotPixels;
}, },
[canvasHandle, canvasContextHandle] [canvasHandle, canvasContextHandle]
); );
return getTelemValuePromise;
} }
/** /**
@ -655,6 +662,7 @@ export {
getFocusedObjectUuid, getFocusedObjectUuid,
getHashUrlToDomainObject, getHashUrlToDomainObject,
navigateToObjectWithFixedTimeBounds, navigateToObjectWithFixedTimeBounds,
navigateToObjectWithRealTime,
openObjectTreeContextMenu, openObjectTreeContextMenu,
renameObjectFromContextMenu, renameObjectFromContextMenu,
setEndOffset, setEndOffset,
@ -663,5 +671,6 @@ export {
setRealTimeMode, setRealTimeMode,
setStartOffset, setStartOffset,
setTimeConductorBounds, setTimeConductorBounds,
setTimeConductorMode,
waitForPlotsToRender waitForPlotsToRender
}; };

View File

@ -34,29 +34,90 @@
*/ */
import AxeBuilder from '@axe-core/playwright'; import AxeBuilder from '@axe-core/playwright';
import fs from 'fs'; import fs from 'fs/promises';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url';
import { expect, test } from './pluginFixtures.js'; import { expect, test } from './pluginFixtures.js';
// Constants for repeated values // Constants for repeated values
const TEST_RESULTS_DIR = './test-results'; const __dirname = path.dirname(fileURLToPath(import.meta.url));
const TEST_RESULTS_DIR = path.join(__dirname, './test-results');
const extendedTest = test.extend({
/**
* Overrides the default screenshot function to apply default options that should apply to all
* screenshots taken in the AVP tests.
*
* @param {import('@playwright/test').PlaywrightTestArgs} args - The Playwright test arguments.
* @param {Function} use - The function to use the page object.
* Defaults:
* - Disables animations
* - Masks the clock indicator
* - Masks the time conductor last update time in realtime mode
* - Masks the time conductor start bounds in fixed mode
* - Masks the time conductor end bounds in fixed mode
*/
page: async ({ page }, use) => {
const playwrightScreenshot = page.screenshot;
/**
* Override the screenshot function to always mask a given set of locators which will always
* show variance across screenshots. Defaults may be overridden by passing in options to the
* screenshot function.
* @param {import('@playwright/test').PageScreenshotOptions} options - The options for the screenshot.
* @returns {Promise<Buffer>} Returns the screenshot as a buffer.
*/
page.screenshot = async function (options = {}) {
const mask = [
this.getByLabel('Clock Indicator'), // Mask the clock indicator
this.getByLabel('Last update'), // Mask the time conductor last update time in realtime mode
this.getByLabel('Start bounds'), // Mask the time conductor start bounds in fixed mode
this.getByLabel('End bounds') // Mask the time conductor end bounds in fixed mode
];
const result = await playwrightScreenshot.call(this, {
animations: 'disabled',
mask,
...options // Pass through or override any options
});
return result;
};
await use(page);
}
});
/**
* Writes the accessibility report to the specified path.
*
* @param {string} reportPath - The path to write the report to.
* @param {Object} accessibilityScanResults - The results of the accessibility scan.
* @returns {Promise<Object>} The accessibility scan results.
* @throws Will throw an error if writing the report fails.
*/
async function writeAccessibilityReport(reportPath, accessibilityScanResults) {
try {
await fs.mkdir(path.dirname(reportPath), { recursive: true });
const data = JSON.stringify(accessibilityScanResults, null, 2);
await fs.writeFile(reportPath, data);
console.log(`Accessibility report with violations saved successfully as ${reportPath}`);
return accessibilityScanResults;
} catch (err) {
console.error(`Error writing the accessibility report to file ${reportPath}:`, err);
throw err;
}
}
/** /**
* Scans for accessibility violations on a page and writes a report to disk if violations are found. * Scans for accessibility violations on a page and writes a report to disk if violations are found.
* Automatically asserts that no violations should be present. * Automatically asserts that no violations should be present.
* *
* @typedef {object} GenerateReportOptions
* @property {string} [reportName] - The name for the report file.
*
* @param {import('playwright').Page} page - The page object from Playwright. * @param {import('playwright').Page} page - The page object from Playwright.
* @param {string} testCaseName - The name of the test case. * @param {string} testCaseName - The name of the test case.
* @param {GenerateReportOptions} [options={}] - The options for the report generation. * @param {{ reportName?: string }} [options={}] - The options for the report generation.
* * @returns {Promise<Object|null>} Returns the accessibility scan results if violations are found, otherwise returns null.
* @returns {Promise<object|null>} Returns the accessibility scan results if violations are found,
* otherwise returns null.
*/ */
/* eslint-disable no-undef */
export async function scanForA11yViolations(page, testCaseName, options = {}) { export async function scanForA11yViolations(page, testCaseName, options = {}) {
const builder = new AxeBuilder({ page }); const builder = new AxeBuilder({ page });
builder.withTags(['wcag2aa']); builder.withTags(['wcag2aa']);
@ -64,25 +125,29 @@ export async function scanForA11yViolations(page, testCaseName, options = {}) {
const accessibilityScanResults = await builder.analyze(); const accessibilityScanResults = await builder.analyze();
// Assert that no violations should be present // Assert that no violations should be present
expect( expect
accessibilityScanResults.violations, .soft(
`Accessibility violations found in test case: ${testCaseName}` accessibilityScanResults.violations,
).toEqual([]); `Accessibility violations found in test case: ${testCaseName}`
)
.toEqual([]);
// Check if there are any violations // Check if there are any violations
if (accessibilityScanResults.violations.length > 0) { if (accessibilityScanResults.violations.length > 0) {
let reportName = options.reportName || testCaseName; const reportName = options.reportName || testCaseName;
let sanitizedReportName = reportName.replace(/\//g, '_'); const sanitizedReportName = reportName.replace(/\//g, '_');
const reportPath = path.join(TEST_RESULTS_DIR, `${sanitizedReportName}.json`); const reportPath = path.join(
TEST_RESULTS_DIR,
'a11y-json-reports',
`${sanitizedReportName}.json`
);
try { try {
if (!fs.existsSync(TEST_RESULTS_DIR)) { await page.screenshot({
fs.mkdirSync(TEST_RESULTS_DIR); path: path.join(TEST_RESULTS_DIR, 'a11y-screenshots', `${sanitizedReportName}.png`)
} });
fs.writeFileSync(reportPath, JSON.stringify(accessibilityScanResults, null, 2)); return await writeAccessibilityReport(reportPath, accessibilityScanResults);
console.log(`Accessibility report with violations saved successfully as ${reportPath}`);
return accessibilityScanResults;
} catch (err) { } catch (err) {
console.error(`Error writing the accessibility report to file ${reportPath}:`, err); console.error(`Error writing the accessibility report to file ${reportPath}:`, err);
throw err; throw err;
@ -93,4 +158,4 @@ export async function scanForA11yViolations(page, testCaseName, options = {}) {
} }
} }
export { expect, test }; export { expect, extendedTest as test };

View File

@ -1,4 +1,3 @@
/* eslint-disable no-undef */
/***************************************************************************** /*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government * Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space * as represented by the Administrator of the National Aeronautics and Space
@ -40,7 +39,7 @@ import { v4 as uuid } from 'uuid';
* @see {@link https://github.com/microsoft/playwright/discussions/11690 Github Discussion} * @see {@link https://github.com/microsoft/playwright/discussions/11690 Github Discussion}
* @private * @private
* @param {import('@playwright/test').ConsoleMessage} msg * @param {import('@playwright/test').ConsoleMessage} msg
* @returns {String} formatted string with message type, text, url, and line and column numbers * @returns {string} formatted string with message type, text, url, and line and column numbers
*/ */
function _consoleMessageToString(msg) { function _consoleMessageToString(msg) {
const { url, lineNumber, columnNumber } = msg.location(); const { url, lineNumber, columnNumber } = msg.location();
@ -61,14 +60,16 @@ function waitForAnimations(locator) {
); );
} }
/** const istanbulCLIOutput = fileURLToPath(new URL('.nyc_output', import.meta.url));
* This is part of our codecoverage shim.
* @see {@link https://github.com/mxschmitt/playwright-test-coverage Github Example Project}
* @constant {string}
*/
const istanbulCLIOutput = path.join(process.cwd(), '.nyc_output');
const extendedTest = test.extend({ const extendedTest = test.extend({
/**
* Path to output raw coverage files. Can be overridden in Playwright config file.
* @see {@link https://github.com/mxschmitt/playwright-test-coverage Github Example Project}
* @constant {string}
*/
coveragePath: [istanbulCLIOutput, { option: true }],
/** /**
* This allows the test to manipulate the browser clock. This is useful for Visual and Snapshot tests which need * 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. * the Time Indicator Clock to be in a specific state.
@ -111,21 +112,55 @@ const extendedTest = test.extend({
scope: 'test' 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. * Extends the base context class to add codecoverage shim.
* @see {@link https://github.com/mxschmitt/playwright-test-coverage Github Project} * @see {@link https://github.com/mxschmitt/playwright-test-coverage Github Project}
*/ */
context: async ({ context }, use) => { context: async ({ context, coveragePath }, use) => {
await context.addInitScript(() => await context.addInitScript(() =>
window.addEventListener('beforeunload', () => window.addEventListener('beforeunload', () =>
window.collectIstanbulCoverage(JSON.stringify(window.__coverage__)) window.collectIstanbulCoverage(JSON.stringify(window.__coverage__))
) )
); );
await fs.promises.mkdir(istanbulCLIOutput, { recursive: true }); await fs.promises.mkdir(coveragePath, { recursive: true });
await context.exposeFunction('collectIstanbulCoverage', (coverageJSON) => { await context.exposeFunction('collectIstanbulCoverage', (coverageJSON) => {
if (coverageJSON) { if (coverageJSON) {
fs.writeFileSync( fs.writeFileSync(
path.join(istanbulCLIOutput, `playwright_coverage_${uuid()}.json`), path.join(coveragePath, `playwright_coverage_${uuid()}.json`),
coverageJSON coverageJSON
); );
} }
@ -133,9 +168,9 @@ const extendedTest = test.extend({
await use(context); await use(context);
for (const page of context.pages()) { for (const page of context.pages()) {
await page.evaluate(() => await page.evaluate(() => {
window.collectIstanbulCoverage(JSON.stringify(window.__coverage__)) window.collectIstanbulCoverage(JSON.stringify(window.__coverage__));
); });
} }
}, },
/** /**
@ -154,17 +189,13 @@ const extendedTest = test.extend({
// function in the generatorWorker context. This is necessary // function in the generatorWorker context. This is necessary
// to ensure that example telemetry data is generated for the new clock time. // to ensure that example telemetry data is generated for the new clock time.
if (clockOptions?.now !== undefined) { if (clockOptions?.now !== undefined) {
page.on( page.on('worker', (worker) => {
'worker', if (worker.url().includes('generatorWorker')) {
(worker) => { worker.evaluate((time) => {
if (worker.url().includes('generatorWorker')) { self.Date.now = () => time;
worker.evaluate((time) => { }, clockOptions.now);
self.Date.now = () => time; }
}); });
}
},
clockOptions.now
);
} }
// Capture any console errors during test execution // Capture any console errors during test execution
@ -181,23 +212,6 @@ const extendedTest = test.extend({
.not.toEqual('error') .not.toEqual('error')
); );
} }
},
/**
* Extends the base browser class to enable CDP connection definition in playwright.config.js. Once
* that RFE is implemented, this function can be removed.
* @see {@link https://github.com/microsoft/playwright/issues/8379 Github RFE}
*/
browser: async ({ playwright, browser }, use, workerInfo) => {
// Use browserless if configured
if (workerInfo.project.name.match(/browserless/)) {
const vBrowser = await playwright.chromium.connectOverCDP({
endpointURL: 'ws://localhost:3003'
});
await use(vBrowser);
} else {
// Use Local Browser for testing.
await use(browser);
}
} }
}); });

View File

@ -1,4 +1,3 @@
/* eslint-disable prettier/prettier */
/** /**
* Constants which may be used across all e2e tests. * Constants which may be used across all e2e tests.
*/ */
@ -8,12 +7,30 @@
* - Used for overriding the browser clock in tests. * - Used for overriding the browser clock in tests.
*/ */
export const MISSION_TIME = 1732413600000; // Saturday, November 23, 2024 6:00:00 PM GMT-08:00 (Thanksgiving Dinner Time) export const MISSION_TIME = 1732413600000; // Saturday, November 23, 2024 6:00:00 PM GMT-08:00 (Thanksgiving Dinner Time)
// Subtracting 30 minutes from MISSION_TIME
export const MISSION_TIME_FIXED_START = 1732413600000 - 1800000; // 1732411800000
// Adding 1 minute to MISSION_TIME
export const MISSION_TIME_FIXED_END = 1732413600000 + 60000; // 1732413660000
/** /**
* URL Constants * URL Constants
* - This is the URL that the browser will be directed to when running visual tests. This URL * These constants are used for initial navigation in visual tests, in either fixed or realtime mode.
* - hides the tree and inspector to prevent visual noise * They navigate to the 'My Items' folder at MISSION_TIME.
* - sets the time bounds to a fixed range * They set the following url parameters:
* - tc.mode - The time conductor mode ('fixed' or 'local')
* - tc.startBound - The time conductor start bound (when in fixed mode)
* - tc.endBound - The time conductor end bound (when in fixed mode)
* - tc.startDelta - The time conductor start delta (when in realtime mode)
* - tc.endDelta - The time conductor end delta (when in realtime mode)
* - tc.timeSystem - The time conductor time system ('utc')
* - view - The view to display ('grid')
* - hideInspector - Whether to hide the inspector (true)
* - hideTree - Whether to hide the tree (true)
* @typedef {string} VisualUrl
*/ */
export const VISUAL_URL =
'./#/browse/mine?tc.mode=fixed&tc.startBound=1693592063607&tc.endBound=1693593893607&tc.timeSystem=utc&view=grid&hideInspector=true&hideTree=true'; /** @type {VisualUrl} */
export const VISUAL_FIXED_URL = `./#/browse/mine?tc.mode=fixed&tc.startBound=${MISSION_TIME_FIXED_START}&tc.endBound=${MISSION_TIME_FIXED_END}&tc.timeSystem=utc&view=grid&hideInspector=true&hideTree=true`;
/** @type {VisualUrl} */
export const VISUAL_REALTIME_URL =
'./#/browse/mine?tc.mode=local&tc.timeSystem=utc&view=grid&tc.startDelta=1800000&tc.endDelta=30000&hideTree=true&hideInspector=true';

View File

@ -21,10 +21,12 @@
*****************************************************************************/ *****************************************************************************/
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { expect } from '../pluginFixtures.js';
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function navigateToFaultManagementWithExample(page) { export async function navigateToFaultManagementWithExample(page) {
await page.addInitScript({ await page.addInitScript({
path: fileURLToPath(new URL('./addInitExampleFaultProvider.js', import.meta.url)) path: fileURLToPath(new URL('./addInitExampleFaultProvider.js', import.meta.url))
}); });
@ -35,7 +37,7 @@ async function navigateToFaultManagementWithExample(page) {
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function navigateToFaultManagementWithStaticExample(page) { export async function navigateToFaultManagementWithStaticExample(page) {
await page.addInitScript({ await page.addInitScript({
path: fileURLToPath(new URL('./addInitExampleFaultProviderStatic.js', import.meta.url)) path: fileURLToPath(new URL('./addInitExampleFaultProviderStatic.js', import.meta.url))
}); });
@ -46,7 +48,7 @@ async function navigateToFaultManagementWithStaticExample(page) {
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function navigateToFaultManagementWithoutExample(page) { export async function navigateToFaultManagementWithoutExample(page) {
await page.addInitScript({ await page.addInitScript({
path: fileURLToPath(new URL('./addInitFaultManagementPlugin.js', import.meta.url)) path: fileURLToPath(new URL('./addInitFaultManagementPlugin.js', import.meta.url))
}); });
@ -57,7 +59,7 @@ async function navigateToFaultManagementWithoutExample(page) {
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function navigateToFaultItemInTree(page) { export async function navigateToFaultItemInTree(page) {
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
const faultManagementTreeItem = page const faultManagementTreeItem = page
@ -75,88 +77,95 @@ async function navigateToFaultItemInTree(page) {
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function acknowledgeFault(page, rowNumber) { export async function acknowledgeFault(page, rowNumber) {
await openFaultRowMenu(page, rowNumber); await openFaultRowMenu(page, rowNumber);
await page.locator('.c-menu >> text="Acknowledge"').click(); await page.getByLabel('Acknowledge', { exact: true }).click();
// Click [aria-label="Save"] await page.getByLabel('Save').click();
await page.locator('[aria-label="Save"]').click();
} }
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function shelveMultipleFaults(page, ...nums) { export async function shelveMultipleFaults(page, ...nums) {
const selectRows = nums.map((num) => { const selectRows = nums.map((num) => {
return selectFaultItem(page, num); return selectFaultItem(page, num);
}); });
await Promise.all(selectRows); await Promise.all(selectRows);
await page.locator('button:has-text("Shelve")').click(); await page.getByLabel('Shelve selected faults').click();
await page.locator('[aria-label="Save"]').click(); await page.getByLabel('Save').click();
} }
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function acknowledgeMultipleFaults(page, ...nums) { export async function acknowledgeMultipleFaults(page, ...nums) {
const selectRows = nums.map((num) => { const selectRows = nums.map((num) => {
return selectFaultItem(page, num); return selectFaultItem(page, num);
}); });
await Promise.all(selectRows); await Promise.all(selectRows);
await page.locator('button:has-text("Acknowledge")').click(); await page.locator('button:has-text("Acknowledge")').click();
await page.locator('[aria-label="Save"]').click(); await page.getByLabel('Save').click();
} }
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function shelveFault(page, rowNumber) { export async function shelveFault(page, rowNumber) {
await openFaultRowMenu(page, rowNumber); await openFaultRowMenu(page, rowNumber);
await page.locator('.c-menu >> text="Shelve"').click(); await page.locator('.c-menu >> text="Shelve"').click();
// Click [aria-label="Save"] // Click [aria-label="Save"]
await page.locator('[aria-label="Save"]').click(); await page.getByLabel('Save').click();
} }
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function changeViewTo(page, view) { export async function changeViewTo(page, view) {
await page.locator('.c-fault-mgmt__search-row select').first().selectOption(view); await page.locator('.c-fault-mgmt__search-row select').first().selectOption(view);
} }
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function sortFaultsBy(page, sort) { export async function sortFaultsBy(page, sort) {
await page.locator('.c-fault-mgmt__list-header-sortButton select').selectOption(sort); await page.locator('.c-fault-mgmt__list-header-sortButton select').selectOption(sort);
} }
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function enterSearchTerm(page, term) { export async function enterSearchTerm(page, term) {
await page.locator('.c-fault-mgmt-search [aria-label="Search Input"]').fill(term); await page.locator('.c-fault-mgmt-search [aria-label="Search Input"]').fill(term);
} }
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function clearSearch(page) { export async function clearSearch(page) {
await enterSearchTerm(page, ''); await enterSearchTerm(page, '');
} }
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function selectFaultItem(page, rowNumber) { export async function selectFaultItem(page, rowNumber) {
await page.locator(`.c-fault-mgmt-item > input >> nth=${rowNumber - 1}`).check(); await page
.getByLabel('Select fault')
.nth(rowNumber - 1)
.check({
// Need force here because checkbox state is changed by an event emitted by the checkbox
// eslint-disable-next-line playwright/no-force-option
force: true
});
await expect(page.getByLabel('Select fault').nth(rowNumber - 1)).toBeChecked();
} }
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function getHighestSeverity(page) { export async function getHighestSeverity(page) {
const criticalCount = await page.locator('[title=CRITICAL]').count(); const criticalCount = await page.locator('[title=CRITICAL]').count();
const warningCount = await page.locator('[title=WARNING]').count(); const warningCount = await page.locator('[title=WARNING]').count();
@ -172,7 +181,7 @@ async function getHighestSeverity(page) {
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function getLowestSeverity(page) { export async function getLowestSeverity(page) {
const warningCount = await page.locator('[title=WARNING]').count(); const warningCount = await page.locator('[title=WARNING]').count();
const watchCount = await page.locator('[title=WATCH]').count(); const watchCount = await page.locator('[title=WATCH]').count();
@ -188,7 +197,7 @@ async function getLowestSeverity(page) {
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function getFaultResultCount(page) { export async function getFaultResultCount(page) {
const count = await page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').count(); const count = await page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').count();
return count; return count;
@ -197,7 +206,7 @@ async function getFaultResultCount(page) {
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
function getFault(page, rowNumber) { export function getFault(page, rowNumber) {
const fault = page.locator( const fault = page.locator(
`.c-faults-list-view-item-body > .c-fault-mgmt__list >> nth=${rowNumber - 1}` `.c-faults-list-view-item-body > .c-fault-mgmt__list >> nth=${rowNumber - 1}`
); );
@ -208,7 +217,7 @@ function getFault(page, rowNumber) {
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
function getFaultByName(page, name) { export function getFaultByName(page, name) {
const fault = page.locator(`.c-fault-mgmt__list-faultname:has-text("${name}")`); const fault = page.locator(`.c-fault-mgmt__list-faultname:has-text("${name}")`);
return fault; return fault;
@ -217,7 +226,7 @@ function getFaultByName(page, name) {
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function getFaultName(page, rowNumber) { export async function getFaultName(page, rowNumber) {
const faultName = await page const faultName = await page
.locator(`.c-fault-mgmt__list-faultname >> nth=${rowNumber - 1}`) .locator(`.c-fault-mgmt__list-faultname >> nth=${rowNumber - 1}`)
.textContent(); .textContent();
@ -228,7 +237,7 @@ async function getFaultName(page, rowNumber) {
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function getFaultSeverity(page, rowNumber) { export async function getFaultSeverity(page, rowNumber) {
const faultSeverity = await page const faultSeverity = await page
.locator(`.c-faults-list-view-item-body .c-fault-mgmt__list-severity >> nth=${rowNumber - 1}`) .locator(`.c-faults-list-view-item-body .c-fault-mgmt__list-severity >> nth=${rowNumber - 1}`)
.getAttribute('title'); .getAttribute('title');
@ -239,7 +248,7 @@ async function getFaultSeverity(page, rowNumber) {
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function getFaultNamespace(page, rowNumber) { export async function getFaultNamespace(page, rowNumber) {
const faultNamespace = await page const faultNamespace = await page
.locator(`.c-fault-mgmt__list-path >> nth=${rowNumber - 1}`) .locator(`.c-fault-mgmt__list-path >> nth=${rowNumber - 1}`)
.textContent(); .textContent();
@ -250,7 +259,7 @@ async function getFaultNamespace(page, rowNumber) {
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function getFaultTriggerTime(page, rowNumber) { export async function getFaultTriggerTime(page, rowNumber) {
const faultTriggerTime = await page const faultTriggerTime = await page
.locator(`.c-fault-mgmt__list-trigTime >> nth=${rowNumber - 1} >> .c-fault-mgmt-item__value`) .locator(`.c-fault-mgmt__list-trigTime >> nth=${rowNumber - 1} >> .c-fault-mgmt-item__value`)
.textContent(); .textContent();
@ -261,35 +270,10 @@ async function getFaultTriggerTime(page, rowNumber) {
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function openFaultRowMenu(page, rowNumber) { export async function openFaultRowMenu(page, rowNumber) {
// select // select
await page await page
.locator(`.c-fault-mgmt-item > .c-fault-mgmt__list-action-button >> nth=${rowNumber - 1}`) .getByLabel('Disposition actions')
.nth(rowNumber - 1)
.click(); .click();
} }
export {
acknowledgeFault,
acknowledgeMultipleFaults,
changeViewTo,
clearSearch,
enterSearchTerm,
getFault,
getFaultByName,
getFaultName,
getFaultNamespace,
getFaultResultCount,
getFaultSeverity,
getFaultTriggerTime,
getHighestSeverity,
getLowestSeverity,
navigateToFaultItemInTree,
navigateToFaultManagementWithExample,
navigateToFaultManagementWithoutExample,
navigateToFaultManagementWithStaticExample,
openFaultRowMenu,
selectFaultItem,
shelveFault,
shelveMultipleFaults,
sortFaultsBy
};

View File

@ -0,0 +1,47 @@
/*****************************************************************************
* 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.
*****************************************************************************/
const isMac = process.platform === 'darwin';
const modifier = isMac ? 'Meta' : 'Control';
/**
* @param {import('@playwright/test').Page} page
*/
async function selectAll(page) {
await page.keyboard.press(`${modifier}+KeyA`);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function copy(page) {
await page.keyboard.press(`${modifier}+KeyC`);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function paste(page) {
await page.keyboard.press(`${modifier}+KeyV`);
}
export { copy, paste, selectAll };

View File

@ -0,0 +1,23 @@
/*****************************************************************************
* 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.
*****************************************************************************/
export * from './clipboard.js';

View File

@ -28,16 +28,28 @@ import { fileURLToPath } from 'url';
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {string} text
*/ */
async function enterTextEntry(page, text) { async function enterTextEntry(page, text) {
// Click the 'Add Notebook Entry' area await addNotebookEntry(page);
await page.locator(NOTEBOOK_DROP_AREA).click(); await enterTextInLastEntry(page, text);
// enter text
await page.getByLabel('Notebook Entry Input').last().fill(text);
await commitEntry(page); await commitEntry(page);
} }
/**
* @param {import('@playwright/test').Page} page
*/
async function addNotebookEntry(page) {
await page.locator(NOTEBOOK_DROP_AREA).click();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function enterTextInLastEntry(page, text) {
await page.getByLabel('Notebook Entry Input').last().fill(text);
}
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
@ -68,7 +80,6 @@ async function commitEntry(page) {
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function startAndAddRestrictedNotebookObject(page) { async function startAndAddRestrictedNotebookObject(page) {
// eslint-disable-next-line no-undef
await page.addInitScript({ await page.addInitScript({
path: fileURLToPath(new URL('./addInitRestrictedNotebook.js', import.meta.url)) path: fileURLToPath(new URL('./addInitRestrictedNotebook.js', import.meta.url))
}); });
@ -141,10 +152,13 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
} }
export { export {
addNotebookEntry,
commitEntry,
createNotebookAndEntry, createNotebookAndEntry,
createNotebookEntryAndTags, createNotebookEntryAndTags,
dragAndDropEmbed, dragAndDropEmbed,
enterTextEntry, enterTextEntry,
enterTextInLastEntry,
lockPage, lockPage,
startAndAddRestrictedNotebookObject startAndAddRestrictedNotebookObject
}; };

View File

@ -20,6 +20,7 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../appActions.js';
import { expect } from '../pluginFixtures.js'; import { expect } from '../pluginFixtures.js';
/** /**
@ -28,7 +29,7 @@ import { expect } from '../pluginFixtures.js';
* for each activity in the plan data per group, using the earliest activity's * for each activity in the plan data per group, using the earliest activity's
* start time as the start bound and the current activity's end time as the end bound. * 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 {import('@playwright/test').Page} page the page
* @param {object} plan The raw plan json to assert against * @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} objectUrl 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, objectUrl) {
@ -85,7 +86,7 @@ function activitiesWithinTimeBounds(start1, end1, start2, end2) {
* Asserts that the swim lanes / groups in the plan view matches the order of * Asserts that the swim lanes / groups in the plan view matches the order of
* groups in the plan data. * groups in the plan data.
* @param {import('@playwright/test').Page} page the page * @param {import('@playwright/test').Page} page the page
* @param {object} plan The raw plan json to assert against * @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} objectUrl The URL of the object to assert against (plan or gantt chart)
*/ */
export async function assertPlanOrderedSwimLanes(page, plan, objectUrl) { export async function assertPlanOrderedSwimLanes(page, plan, objectUrl) {
@ -109,7 +110,7 @@ export async function assertPlanOrderedSwimLanes(page, plan, objectUrl) {
* Navigate to the plan view, switch to fixed time mode, * Navigate to the plan view, switch to fixed time mode,
* and set the bounds to span all activities. * and set the bounds to span all activities.
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {object} planJson * @param {Object} planJson
* @param {string} planObjectUrl * @param {string} planObjectUrl
*/ */
export async function setBoundsToSpanAllActivities(page, planJson, planObjectUrl) { export async function setBoundsToSpanAllActivities(page, planJson, planObjectUrl) {
@ -124,7 +125,7 @@ export async function setBoundsToSpanAllActivities(page, planJson, planObjectUrl
} }
/** /**
* @param {object} planJson * @param {Object} planJson
* @returns {number} * @returns {number}
*/ */
export function getEarliestStartTime(planJson) { export function getEarliestStartTime(planJson) {
@ -134,7 +135,7 @@ export function getEarliestStartTime(planJson) {
/** /**
* *
* @param {object} planJson * @param {Object} planJson
* @returns {number} * @returns {number}
*/ */
export function getLatestEndTime(planJson) { export function getLatestEndTime(planJson) {
@ -142,6 +143,18 @@ export function getLatestEndTime(planJson) {
return Math.max(...activities.map((activity) => activity.end)); return Math.max(...activities.map((activity) => activity.end));
} }
/**
*
* @param {object} planJson
* @returns {object}
*/
export function getFirstActivity(planJson) {
const groups = Object.keys(planJson);
const firstGroupKey = groups[0];
const firstGroupItems = planJson[firstGroupKey];
return firstGroupItems[0];
}
/** /**
* Uses the Open MCT API to set the status of a plan to 'draft'. * Uses the Open MCT API to set the status of a plan to 'draft'.
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
@ -172,3 +185,55 @@ export async function addPlanGetInterceptor(page) {
}); });
}); });
} }
/**
* Create a Plan from JSON and add it to a Timelist and Navigate to the Plan view
* @param {import('@playwright/test').Page} page
*/
export async function createTimelistWithPlanAndSetActivityInProgress(page, planJson) {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const timelist = await createDomainObjectWithDefaults(page, {
name: 'Time List',
type: 'Time List'
});
await createPlanFromJSON(page, {
name: 'Test Plan',
json: planJson,
parent: timelist.uuid
});
// Ensure that all activities are shown in the expanded view
const groups = Object.keys(planJson);
const firstGroupKey = groups[0];
const firstGroupItems = planJson[firstGroupKey];
const firstActivityForPlan = firstGroupItems[0];
const lastActivity = firstGroupItems[firstGroupItems.length - 1];
const startBound = firstActivityForPlan.start;
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`
);
// Change the object to edit mode
await page.getByRole('button', { name: 'Edit Object' }).click();
// Find the display properties section in the inspector
await page.getByRole('tab', { name: 'View Properties' }).click();
// Switch to expanded view and save the setting
await page.getByLabel('Display Style').selectOption({ label: 'Expanded' });
// Click on the "Save" button
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
const anActivity = page.getByRole('row').nth(0);
// Set the activity to in progress
await anActivity.click();
await page.getByRole('tab', { name: 'Activity' }).click();
await page.getByLabel('Activity Status', { exact: true }).selectOption({ label: 'In progress' });
}

View File

@ -27,8 +27,8 @@ import { expect } from '../pluginFixtures.js';
* Given a canvas and a set of points, tags the points on the canvas. * Given a canvas and a set of points, tags the points on the canvas.
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {HTMLCanvasElement} canvas a telemetry item with a plot * @param {HTMLCanvasElement} canvas a telemetry item with a plot
* @param {Number} xEnd a telemetry item with a plot * @param {number} xEnd a telemetry item with a plot
* @param {Number} yEnd a telemetry item with a plot * @param {number} yEnd a telemetry item with a plot
* @returns {Promise} * @returns {Promise}
*/ */
export async function createTags({ page, canvas, xEnd = 700, yEnd = 520 }) { export async function createTags({ page, canvas, xEnd = 700, yEnd = 520 }) {

View File

@ -0,0 +1,29 @@
/*****************************************************************************
* 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 should be used to install the Darkmatter theme for Open MCT.
// e.g.
// await page.addInitScript({ path: path.join(__dirname, 'useDarkmatterTheme.js') });
document.addEventListener('DOMContentLoaded', () => {
const openmct = window.openmct;
openmct.install(openmct.plugins.DarkmatterTheme());
});

8
e2e/index.js Normal file
View File

@ -0,0 +1,8 @@
// Import everything from the specific fixture files
import * as appActions from './appActions.js';
import * as avpFixtures from './avpFixtures.js';
import * as baseFixtures from './baseFixtures.js';
import * as pluginFixtures from './pluginFixtures.js';
// Export these as named exports
export { appActions, avpFixtures, baseFixtures, pluginFixtures };

1449
e2e/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
e2e/package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "openmct-e2e",
"version": "4.0.0-next",
"description": "The Open MCT e2e framework",
"type": "module",
"module": "index.js",
"exports": {
".": {
"import": "./index.js"
}
},
"scripts": {
"test": "npx playwright test",
"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"
},
"author": {
"name": "National Aeronautics and Space Administration",
"url": "https://www.nasa.gov"
},
"license": "Apache-2.0"
}

View File

@ -3,6 +3,7 @@
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
import { devices } from '@playwright/test'; import { devices } from '@playwright/test';
import { fileURLToPath } from 'url';
const MAX_FAILURES = 5; const MAX_FAILURES = 5;
const NUM_WORKERS = 2; const NUM_WORKERS = 2;
@ -11,10 +12,11 @@ const config = {
retries: 2, //Retries 2 times for a total of 3 runs. When running sharded and with max-failures=5, this should ensure that flake is managed without failing the full suite retries: 2, //Retries 2 times for a total of 3 runs. When running sharded and with max-failures=5, this should ensure that flake is managed without failing the full suite
testDir: 'tests', testDir: 'tests',
grepInvert: /@mobile/, //Ignore mobile tests grepInvert: /@mobile/, //Ignore mobile tests
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-performance.config.js
timeout: 60 * 1000, timeout: 60 * 1000,
webServer: { webServer: {
command: 'npm run start:coverage', command: 'npm run start:coverage',
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
url: 'http://localhost:8080/#', url: 'http://localhost:8080/#',
timeout: 200 * 1000, timeout: 200 * 1000,
reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging. reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.
@ -27,7 +29,9 @@ const config = {
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
screenshot: 'only-on-failure', screenshot: 'only-on-failure',
trace: 'on-first-retry', trace: 'on-first-retry',
video: 'off' video: 'off',
// @ts-ignore - custom configuration option for nyc codecoverage output path
coveragePath: fileURLToPath(new URL('../.nyc_output', import.meta.url))
}, },
projects: [ projects: [
{ {

View File

@ -1,6 +1,6 @@
// playwright.config.js // playwright.config.js
// @ts-check // @ts-check
import { fileURLToPath } from 'url';
/** @type {import('@playwright/test').PlaywrightTestConfig} */ /** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = { const config = {
retries: 0, retries: 0,
@ -10,6 +10,7 @@ const config = {
timeout: 30 * 1000, timeout: 30 * 1000,
webServer: { webServer: {
command: 'npm run start:coverage', command: 'npm run start:coverage',
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
url: 'http://localhost:8080/#', url: 'http://localhost:8080/#',
timeout: 120 * 1000, timeout: 120 * 1000,
reuseExistingServer: true reuseExistingServer: true

View File

@ -3,7 +3,6 @@
import { devices } from '@playwright/test'; import { devices } from '@playwright/test';
const MAX_FAILURES = 5; const MAX_FAILURES = 5;
const NUM_WORKERS = 2;
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
@ -11,23 +10,27 @@ import { fileURLToPath } from 'url';
const config = { const config = {
retries: 1, //Retries 2 times for a total of 3 runs. When running sharded and with max-failures=5, this should ensure that flake is managed without failing the full suite retries: 1, //Retries 2 times for a total of 3 runs. When running sharded and with max-failures=5, this should ensure that flake is managed without failing the full suite
testDir: 'tests', testDir: 'tests',
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-performance.config.js
timeout: 30 * 1000, timeout: 30 * 1000,
webServer: { webServer: {
command: 'npm run start:coverage', command: 'npm run start:coverage',
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
url: 'http://localhost:8080/#', url: 'http://localhost:8080/#',
timeout: 200 * 1000, timeout: 200 * 1000,
reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging. reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.
}, },
maxFailures: MAX_FAILURES, //Limits failures to 5 to reduce CI Waste maxFailures: MAX_FAILURES, //Limits failures to 5 to reduce CI Waste
workers: NUM_WORKERS, //Limit to 2 for CircleCI Agent workers: 1, //Limit to 1 due to resource constraints similar to https://github.com/percy/cli/discussions/1067
use: { use: {
baseURL: 'http://localhost:8080/', baseURL: 'http://localhost:8080/',
headless: true, headless: true,
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
screenshot: 'only-on-failure', screenshot: 'only-on-failure',
trace: 'on-first-retry', trace: 'on-first-retry',
video: 'off' video: 'off',
// @ts-ignore - custom configuration option for nyc codecoverage output path
coveragePath: fileURLToPath(new URL('../.nyc_output', import.meta.url))
}, },
projects: [ projects: [
{ {

View File

@ -1,6 +1,6 @@
// playwright.config.js // playwright.config.js
// @ts-check // @ts-check
import { fileURLToPath } from 'url';
/** @type {import('@playwright/test').PlaywrightTestConfig} */ /** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = { const config = {
retries: 1, //Only for debugging purposes for trace: 'on-first-retry' retries: 1, //Only for debugging purposes for trace: 'on-first-retry'
@ -10,6 +10,7 @@ const config = {
workers: 1, //Only run in serial with 1 worker workers: 1, //Only run in serial with 1 worker
webServer: { webServer: {
command: 'npm run start', //need development mode for performance.marks and others command: 'npm run start', //need development mode for performance.marks and others
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
url: 'http://localhost:8080/#', url: 'http://localhost:8080/#',
timeout: 200 * 1000, timeout: 200 * 1000,
reuseExistingServer: false reuseExistingServer: false

View File

@ -1,6 +1,6 @@
// playwright.config.js // playwright.config.js
// @ts-check // @ts-check
import { fileURLToPath } from 'url';
/** @type {import('@playwright/test').PlaywrightTestConfig} */ /** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = { const config = {
retries: 0, //Only for debugging purposes for trace: 'on-first-retry' retries: 0, //Only for debugging purposes for trace: 'on-first-retry'
@ -10,6 +10,7 @@ const config = {
workers: 1, //Only run in serial with 1 worker workers: 1, //Only run in serial with 1 worker
webServer: { webServer: {
command: 'npm run start:prod', //Production mode command: 'npm run start:prod', //Production mode
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
url: 'http://localhost:8080/#', url: 'http://localhost:8080/#',
timeout: 200 * 1000, timeout: 200 * 1000,
reuseExistingServer: false //Must be run with this option to prevent dev mode reuseExistingServer: false //Must be run with this option to prevent dev mode

View File

@ -1,7 +1,6 @@
/* eslint-disable no-undef */
// playwright.config.js // playwright.config.js
// @ts-check // @ts-check
import { fileURLToPath } from 'url';
/** @type {import('@playwright/test').PlaywrightTestConfig<{ theme: string }>} */ /** @type {import('@playwright/test').PlaywrightTestConfig<{ theme: string }>} */
const config = { const config = {
retries: 0, // Visual tests should never retry due to snapshot comparison errors. Leaving as a shim retries: 0, // Visual tests should never retry due to snapshot comparison errors. Leaving as a shim
@ -11,9 +10,10 @@ const config = {
workers: 1, //Lower stress on Circle CI Agent for Visual tests https://github.com/percy/cli/discussions/1067 workers: 1, //Lower stress on Circle CI Agent for Visual tests https://github.com/percy/cli/discussions/1067
webServer: { webServer: {
command: 'npm run start:coverage', command: 'npm run start:coverage',
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
url: 'http://localhost:8080/#', url: 'http://localhost:8080/#',
timeout: 200 * 1000, timeout: 200 * 1000,
reuseExistingServer: !process.env.CI reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.
}, },
use: { use: {
baseURL: 'http://localhost:8080/', baseURL: 'http://localhost:8080/',
@ -36,6 +36,13 @@ const config = {
browserName: 'chromium', browserName: 'chromium',
theme: 'snow' theme: 'snow'
} }
},
{
name: 'darkmatter-theme', //Runs the same visual tests but with darkmatter-theme
use: {
browserName: 'chromium',
theme: 'darkmatter'
}
} }
], ],
reporter: [ reporter: [

View File

@ -1,6 +1,5 @@
// playwright.config.js // playwright.config.js
// @ts-check // @ts-check
import { devices } from '@playwright/test'; import { devices } from '@playwright/test';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
@ -11,6 +10,7 @@ const config = {
timeout: 60 * 1000, timeout: 60 * 1000,
webServer: { webServer: {
command: 'npm run start', //Start in dev mode for hot reloading command: 'npm run start', //Start in dev mode for hot reloading
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
url: 'http://localhost:8080/#', url: 'http://localhost:8080/#',
timeout: 200 * 1000, timeout: 200 * 1000,
reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging. reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.

View File

@ -1,4 +1,3 @@
/* eslint-disable no-undef */
/***************************************************************************** /*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government * Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space * as represented by the Administrator of the National Aeronautics and Space
@ -123,12 +122,16 @@ const extendedTest = test.extend({
theme: [theme, { option: true }], theme: [theme, { option: true }],
// eslint-disable-next-line no-shadow // eslint-disable-next-line no-shadow
page: async ({ page, theme }, use, testInfo) => { page: async ({ page, theme }, use, testInfo) => {
// eslint-disable-next-line playwright/no-conditional-in-test
if (theme === 'snow') { if (theme === 'snow') {
//inject snow theme //inject snow theme
await page.addInitScript({ await page.addInitScript({
path: fileURLToPath(new URL('./helper/useSnowTheme.js', import.meta.url)) path: fileURLToPath(new URL('./helper/useSnowTheme.js', import.meta.url))
}); });
} else if (theme === 'darkmatter') {
//inject darkmatter theme
await page.addInitScript({
path: fileURLToPath(new URL('./helper/useDarkmatterTheme.js', import.meta.url))
});
} }
// Attach info about the currently running test and its project. // Attach info about the currently running test and its project.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -174,6 +174,6 @@ test.describe('AppActions', () => {
type: 'Folder' type: 'Folder'
}); });
await openObjectTreeContextMenu(page, folder.url); await openObjectTreeContextMenu(page, folder.url);
await expect(page.getByLabel('Menu')).toBeVisible(); await expect(page.getByLabel(`${folder.name} Context Menu`)).toBeVisible();
}); });
}); });

View File

@ -26,11 +26,12 @@ relates to how we've extended it (i.e. ./e2e/baseFixtures.js) and assumptions ma
(`npm start` and ./e2e/webpack-dev-middleware.js) (`npm start` and ./e2e/webpack-dev-middleware.js)
*/ */
import { test } from '../../baseFixtures.js'; import { expect, test } from '../../baseFixtures.js';
import { MISSION_TIME } from '../../constants.js';
test.describe('baseFixtures tests', () => { test.describe('baseFixtures tests', () => {
//Skip this test for now https://github.com/nasa/openmct/issues/6785 //Skip this test for now https://github.com/nasa/openmct/issues/6785
test.fixme('Verify that tests fail if console.error is thrown', async ({ page }) => { test('Verify that tests fail if console.error is thrown', async ({ page }) => {
test.fail(); test.fail();
//Go to baseURL //Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' }); await page.goto('./', { waitUntil: 'domcontentloaded' });
@ -52,3 +53,27 @@ 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);
});
});

View File

@ -22,7 +22,7 @@
/* /*
* This test suite template is to be used when creating new test suites. It will be kept up to date with the latest improvements * This test suite template is to be used when creating new test suites. It will be kept up to date with the latest improvements
* made by the Open MCT team. It will also follow our best pratices as those evolve. Please use this structure as a _reference_ and clear * made by the Open MCT team. It will also follow our best practices as those evolve. Please use this structure as a _reference_ and clear
* or update any references when creating a new test suite! * or update any references when creating a new test suite!
* *
* To illustrate current best practices, we've included a mocked up test suite for Renaming a Timer domain object. * To illustrate current best practices, we've included a mocked up test suite for Renaming a Timer domain object.
@ -81,7 +81,7 @@ test.describe('Renaming Timer Object', () => {
test('An existing Timer object can be renamed via the 3dot actions menu', async ({ page }) => { test('An existing Timer object can be renamed via the 3dot actions menu', async ({ page }) => {
const newObjectName = 'Renamed Timer'; const newObjectName = 'Renamed Timer';
// We've created an example of a shared function which pases the page and newObjectName values // We've created an example of a shared function which passes the page and newObjectName values
await renameTimerFrom3DotMenu(page, timer.url, newObjectName); await renameTimerFrom3DotMenu(page, timer.url, newObjectName);
// Assert that the name has changed in the browser bar to the value we assigned above // Assert that the name has changed in the browser bar to the value we assigned above

View File

@ -33,13 +33,18 @@
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { createDomainObjectWithDefaults, createExampleTelemetryObject } from '../../appActions.js'; import {
createDomainObjectWithDefaults,
createExampleTelemetryObject,
setIndependentTimeConductorBounds,
setTimeConductorBounds
} from '../../appActions.js';
import { MISSION_TIME } from '../../constants.js'; import { MISSION_TIME } from '../../constants.js';
import { expect, test } from '../../pluginFixtures.js'; import { expect, test } from '../../pluginFixtures.js';
const overlayPlotName = 'Overlay Plot with Telemetry Object'; const overlayPlotName = 'Overlay Plot with Telemetry Object';
test.describe('Generate Visual Test Data @localStorage @generatedata', () => { test.describe('Generate Visual Test Data @localStorage @generatedata @clock', () => {
test.use({ test.use({
clockOptions: { clockOptions: {
now: MISSION_TIME, now: MISSION_TIME,
@ -89,6 +94,53 @@ test.describe('Generate Visual Test Data @localStorage @generatedata', () => {
}); });
}); });
test('Generate display layout with 1 child overlay plot', async ({ page, context }) => {
const parent = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Parent Display Layout'
});
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
name: 'Child Overlay Plot 1',
parent: parent.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Child SWG 1',
parent: overlayPlot.uuid
});
await page.goto(parent.url, { waitUntil: 'domcontentloaded' });
await setIndependentTimeConductorBounds(page, {
start: '2024-11-12 19:11:11.000Z',
end: '2024-11-12 20:11:11.000Z'
});
const NEW_GLOBAL_START_BOUNDS = '2024-11-11 19:11:11.000Z';
const NEW_GLOBAL_END_BOUNDS = '2024-11-11 20:11:11.000Z';
await setTimeConductorBounds(page, NEW_GLOBAL_START_BOUNDS, NEW_GLOBAL_END_BOUNDS);
// Verify that the global time conductor bounds have been updated
expect(
await page.getByLabel('Global Time Conductor').getByLabel('Start bounds').textContent()
).toEqual(NEW_GLOBAL_START_BOUNDS);
expect(
await page.getByLabel('Global Time Conductor').getByLabel('End bounds').textContent()
).toEqual(NEW_GLOBAL_END_BOUNDS);
//Save localStorage for future test execution
await context.storageState({
path: fileURLToPath(
new URL(
'../../../e2e/test-data/display_layout_with_child_overlay_plot.json',
import.meta.url
)
)
});
});
test('Generate flexible layout with 2 child display layouts', async ({ page, context }) => { test('Generate flexible layout with 2 child display layouts', async ({ page, context }) => {
// Create Display Layout // Create Display Layout
const parent = await createDomainObjectWithDefaults(page, { const parent = await createDomainObjectWithDefaults(page, {

View File

@ -41,7 +41,7 @@ test.describe('CouchDB Status Indicator with mocked responses @couchdb', () => {
//Go to baseURL //Go to baseURL
await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', { 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(); 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 //Go to baseURL
await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', { 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(); 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 //Go to baseURL
await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', { 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(); await expect(page.locator('div:has-text("CouchDB connectivity unknown")').nth(3)).toBeVisible();
}); });

View File

@ -31,8 +31,8 @@ import { createDomainObjectWithDefaults } from '../../appActions.js';
import { expect, test } from '../../pluginFixtures.js'; import { expect, test } from '../../pluginFixtures.js';
const TEST_FOLDER = 'test folder'; const TEST_FOLDER = 'test folder';
const jsonFilePath = 'e2e/test-data/ExampleLayouts.json'; const jsonFilePath = 'test-data/ExampleLayouts.json';
const imageFilePath = 'e2e/test-data/rick.jpg'; const imageFilePath = 'test-data/rick.jpg';
test.describe('Form Validation Behavior', () => { test.describe('Form Validation Behavior', () => {
test('Required Field indicators appear if title is empty and can be corrected', async ({ test('Required Field indicators appear if title is empty and can be corrected', async ({
@ -188,8 +188,8 @@ test.describe('Persistence operations @couchdb', () => {
// Both pages: Go to baseURL // Both pages: Go to baseURL
await Promise.all([ await Promise.all([
page.goto('./', { waitUntil: 'networkidle' }), page.goto('./', { waitUntil: 'domcontentloaded' }),
page2.goto('./', { waitUntil: 'networkidle' }) page2.goto('./', { waitUntil: 'domcontentloaded' })
]); ]);
//Slow down the test a bit //Slow down the test a bit

View File

@ -21,7 +21,6 @@
*****************************************************************************/ *****************************************************************************/
import fs from 'fs'; import fs from 'fs';
import { getPreciseDuration } from '../../../../src/utils/duration.js';
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../../appActions.js'; import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../../appActions.js';
import { import {
assertPlanActivities, assertPlanActivities,
@ -132,3 +131,58 @@ test.describe('Gantt Chart', () => {
); );
}); });
}); });
const ONE_SECOND = 1000;
const ONE_MINUTE = 60 * ONE_SECOND;
const ONE_HOUR = ONE_MINUTE * 60;
const ONE_DAY = ONE_HOUR * 24;
function normalizeAge(num) {
const hundredtized = num * 100;
const isWhole = hundredtized % 100 === 0;
return isWhole ? hundredtized / 100 : num;
}
function padLeadingZeros(num, numOfLeadingZeros) {
return num.toString().padStart(numOfLeadingZeros, '0');
}
function toDoubleDigits(num) {
return padLeadingZeros(num, 2);
}
function toTripleDigits(num) {
return padLeadingZeros(num, 3);
}
function getPreciseDuration(value, { excludeMilliSeconds, useDayFormat } = {}) {
let preciseDuration;
const ms = value || 0;
const duration = [
Math.floor(normalizeAge(ms / ONE_DAY)),
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR))),
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE))),
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)))
];
if (!excludeMilliSeconds) {
duration.push(toTripleDigits(Math.floor(normalizeAge(ms % ONE_SECOND))));
}
if (useDayFormat) {
// Format days as XD
const days = duration.shift();
if (days > 0) {
preciseDuration = `${days}D ${duration.join(':')}`;
} else {
preciseDuration = duration.join(':');
}
} else {
const days = toDoubleDigits(duration.shift());
duration.unshift(days);
preciseDuration = duration.join(':');
}
return preciseDuration;
}

View File

@ -22,29 +22,13 @@
import fs from 'fs'; import fs from 'fs';
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../../appActions.js'; import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../../appActions.js';
import { getEarliestStartTime } from '../../../helper/planningUtils';
import { expect, test } from '../../../pluginFixtures.js'; import { expect, test } from '../../../pluginFixtures.js';
const examplePlanSmall3 = JSON.parse(
fs.readFileSync(
new URL('../../../test-data/examplePlans/ExamplePlan_Small3.json', import.meta.url)
)
);
const examplePlanSmall1 = JSON.parse( const examplePlanSmall1 = JSON.parse(
fs.readFileSync( fs.readFileSync(
new URL('../../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url) new URL('../../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url)
) )
); );
// eslint-disable-next-line no-unused-vars
const START_TIME_COLUMN = 0;
// eslint-disable-next-line no-unused-vars
const END_TIME_COLUMN = 1;
const TIME_TO_FROM_COLUMN = 2;
// eslint-disable-next-line no-unused-vars
const ACTIVITY_COLUMN = 3;
const HEADER_ROW = 0;
const NUM_COLUMNS = 5;
test.describe('Time List', () => { test.describe('Time List', () => {
test("Create a Time List, add a single Plan to it, verify all the activities are displayed with no milliseconds and selecting an activity shows it's properties", async ({ test("Create a Time List, add a single Plan to it, verify all the activities are displayed with no milliseconds and selecting an activity shows it's properties", async ({
page page
@ -161,7 +145,7 @@ test("View a timelist in expanded view, verify all the activities are displayed
await expect(eventCount).toEqual(firstGroupItems.length); await expect(eventCount).toEqual(firstGroupItems.length);
}); });
await test.step('Shows activity properties when a row is selected', async () => { await test.step('Shows activity properties when a row is selected in the expanded view', async () => {
await page.getByRole('row').nth(2).click(); await page.getByRole('row').nth(2).click();
// Find the activity state section in the inspector // Find the activity state section in the inspector
@ -171,167 +155,10 @@ test("View a timelist in expanded view, verify all the activities are displayed
'Not started' 'Not started'
); );
}); });
});
/** await test.step("Verify absence of progress indication for an activity that's not in progress", async () => {
* The regular expression used to parse the countdown string. // When an activity is not in progress, the progress pie is not visible
* Some examples of valid Countdown strings: const hidden = await page.getByRole('row').locator('path').nth(1).isHidden();
* ``` await expect(hidden).toBe(true);
* '35D 02:03:04'
* '-1D 01:02:03'
* '01:02:03'
* '-05:06:07'
* ```
*/
const COUNTDOWN_REGEXP = /(-)?(\d+D\s)?(\d{2}):(\d{2}):(\d{2})/;
/**
* @typedef {Object} CountdownOrUpObject
* @property {string} sign - The sign of the countdown ('-' if the countdown is negative, '+' otherwise).
* @property {string} days - The number of days in the countdown (undefined if there are no days).
* @property {string} hours - The number of hours in the countdown.
* @property {string} minutes - The number of minutes in the countdown.
* @property {string} seconds - The number of seconds in the countdown.
* @property {string} toString - The countdown string.
*/
/**
* Object representing the indices of the capture groups in a countdown regex match.
*
* @typedef {{ SIGN: number, DAYS: number, HOURS: number, MINUTES: number, SECONDS: number, REGEXP: RegExp }}
* @property {number} SIGN - The index for the sign capture group (1 if a '-' sign is present, otherwise undefined).
* @property {number} DAYS - The index for the days capture group (2 for the number of days, otherwise undefined).
* @property {number} HOURS - The index for the hours capture group (3 for the hour part of the time).
* @property {number} MINUTES - The index for the minutes capture group (4 for the minute part of the time).
* @property {number} SECONDS - The index for the seconds capture group (5 for the second part of the time).
*/
const COUNTDOWN = Object.freeze({
SIGN: 1,
DAYS: 2,
HOURS: 3,
MINUTES: 4,
SECONDS: 5
});
test.describe('Time List with controlled clock', () => {
test.use({
clockOptions: {
now: getEarliestStartTime(examplePlanSmall3),
shouldAdvanceTime: true
}
});
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Time List shows current events and counts down correctly in real-time mode', async ({
page
}) => {
await test.step('Create a Time List, add a Plan to it, and switch to real-time mode', async () => {
// Create Time List
const timelist = await createDomainObjectWithDefaults(page, {
type: 'Time List'
});
// Create a Plan with events that count down and up.
// Add it as a child to the Time List.
await createPlanFromJSON(page, {
json: examplePlanSmall3,
parent: timelist.uuid
});
// 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`
);
});
const countUpCells = [
getCellByIndex(page, 1, TIME_TO_FROM_COLUMN),
getCellByIndex(page, 2, TIME_TO_FROM_COLUMN)
];
const countdownCells = [
getCellByIndex(page, 3, TIME_TO_FROM_COLUMN),
getCellByIndex(page, 4, TIME_TO_FROM_COLUMN)
];
// Verify that the countdown cells are counting down
for (let i = 0; i < countdownCells.length; i++) {
await test.step(`Countdown cell ${i + 1} counts down`, async () => {
const countdownCell = countdownCells[i];
// Get the initial countdown timestamp object
const beforeCountdown = await getAndAssertCountdownOrUpObject(page, i + 3);
// should not have a '-' sign
await expect(countdownCell).not.toHaveText('-');
// Wait until it changes
await expect(countdownCell).not.toHaveText(beforeCountdown.toString());
// Get the new countdown timestamp object
const afterCountdown = await getAndAssertCountdownOrUpObject(page, i + 3);
// Verify that the new countdown timestamp object is less than the old one
expect(Number(afterCountdown.seconds)).toBeLessThan(Number(beforeCountdown.seconds));
});
}
// Verify that the count-up cells are counting up
for (let i = 0; i < countUpCells.length; i++) {
await test.step(`Count-up cell ${i + 1} counts up`, async () => {
const countUpCell = countUpCells[i];
// Get the initial count-up timestamp object
const beforeCountUp = await getAndAssertCountdownOrUpObject(page, i + 1);
// should not have a '+' sign
await expect(countUpCell).not.toHaveText('+');
// Wait until it changes
await expect(countUpCell).not.toHaveText(beforeCountUp.toString());
// Get the new count-up timestamp object
const afterCountUp = await getAndAssertCountdownOrUpObject(page, i + 1);
// Verify that the new count-up timestamp object is greater than the old one
expect(Number(afterCountUp.seconds)).toBeGreaterThan(Number(beforeCountUp.seconds));
});
}
}); });
}); });
/**
* Get the cell at the given row and column indices.
* @param {import('@playwright/test').Page} page
* @param {number} rowIndex
* @param {number} columnIndex
* @returns {import('@playwright/test').Locator} cell
*/
function getCellByIndex(page, rowIndex, columnIndex) {
return page.getByRole('cell').nth(rowIndex * NUM_COLUMNS + columnIndex);
}
/**
* Return the innerText of the cell at the given row and column indices.
* @param {import('@playwright/test').Page} page
* @param {number} rowIndex
* @param {number} columnIndex
* @returns {Promise<string>} text
*/
async function getCellTextByIndex(page, rowIndex, columnIndex) {
const text = await getCellByIndex(page, rowIndex, columnIndex).innerText();
return text;
}
/**
* Get the text from the countdown (or countup) cell in the given row, assert that it matches the countdown/countup
* regex, and return an object representing the countdown.
* @param {import('@playwright/test').Page} page
* @param {number} rowIndex the row index
* @returns {Promise<CountdownOrUpObject>} The countdown (or countup) object
*/
async function getAndAssertCountdownOrUpObject(page, rowIndex) {
const timeToFrom = await getCellTextByIndex(page, HEADER_ROW + rowIndex, TIME_TO_FROM_COLUMN);
expect(timeToFrom).toMatch(COUNTDOWN_REGEXP);
const match = timeToFrom.match(COUNTDOWN_REGEXP);
return {
sign: match[COUNTDOWN.SIGN],
days: match[COUNTDOWN.DAYS],
hours: match[COUNTDOWN.HOURS],
minutes: match[COUNTDOWN.MINUTES],
seconds: match[COUNTDOWN.SECONDS],
toString: () => timeToFrom
};
}

View File

@ -0,0 +1,290 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
Collection of Time List tests set to run with browser clock manipulate made possible with the
clockOptions plugin fixture.
*/
import fs from 'fs';
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../../appActions.js';
import {
createTimelistWithPlanAndSetActivityInProgress,
getEarliestStartTime,
getFirstActivity
} from '../../../helper/planningUtils';
import { expect, test } from '../../../pluginFixtures.js';
const examplePlanSmall3 = JSON.parse(
fs.readFileSync(
new URL('../../../test-data/examplePlans/ExamplePlan_Small3.json', import.meta.url)
)
);
const examplePlanSmall1 = JSON.parse(
fs.readFileSync(
new URL('../../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url)
)
);
const TIME_TO_FROM_COLUMN = 2;
const HEADER_ROW = 0;
const NUM_COLUMNS = 5;
const FULL_CIRCLE_PATH =
'M3.061616997868383e-15,-50A50,50,0,1,1,-3.061616997868383e-15,50A50,50,0,1,1,3.061616997868383e-15,-50Z';
/**
* The regular expression used to parse the countdown string.
* Some examples of valid Countdown strings:
* ```
* '35D 02:03:04'
* '-1D 01:02:03'
* '01:02:03'
* '-05:06:07'
* ```
*/
const COUNTDOWN_REGEXP = /(-)?(\d+D\s)?(\d{2}):(\d{2}):(\d{2})/;
/**
* @typedef {Object} CountdownOrUpObject
* @property {string} sign - The sign of the countdown ('-' if the countdown is negative, '+' otherwise).
* @property {string} days - The number of days in the countdown (undefined if there are no days).
* @property {string} hours - The number of hours in the countdown.
* @property {string} minutes - The number of minutes in the countdown.
* @property {string} seconds - The number of seconds in the countdown.
* @property {string} toString - The countdown string.
*/
/**
* Object representing the indices of the capture groups in a countdown regex match.
*
* @typedef {{ SIGN: number, DAYS: number, HOURS: number, MINUTES: number, SECONDS: number, REGEXP: RegExp }}
* @property {number} SIGN - The index for the sign capture group (1 if a '-' sign is present, otherwise undefined).
* @property {number} DAYS - The index for the days capture group (2 for the number of days, otherwise undefined).
* @property {number} HOURS - The index for the hours capture group (3 for the hour part of the time).
* @property {number} MINUTES - The index for the minutes capture group (4 for the minute part of the time).
* @property {number} SECONDS - The index for the seconds capture group (5 for the second part of the time).
*/
const COUNTDOWN = Object.freeze({
SIGN: 1,
DAYS: 2,
HOURS: 3,
MINUTES: 4,
SECONDS: 5
});
test.describe('Time List with controlled clock @clock', () => {
test.use({
clockOptions: {
now: getEarliestStartTime(examplePlanSmall3),
shouldAdvanceTime: true
}
});
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create Time List
const timelist = await createDomainObjectWithDefaults(page, {
type: 'Time List'
});
// Create a Plan with events that count down and up.
// Add it as a child to the Time List.
await createPlanFromJSON(page, {
json: examplePlanSmall3,
parent: timelist.uuid
});
// 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`
);
//Expand the viewport to show the entire time list
await page.getByLabel('Collapse Inspect Pane').click();
await page.getByLabel('Collapse Browse Pane').click();
});
test('Time List shows current events and counts down correctly in real-time mode', async ({
page
}) => {
const countUpCells = [
getTimeListCellByIndex(page, 1, TIME_TO_FROM_COLUMN),
getTimeListCellByIndex(page, 2, TIME_TO_FROM_COLUMN)
];
const countdownCells = [
getTimeListCellByIndex(page, 3, TIME_TO_FROM_COLUMN),
getTimeListCellByIndex(page, 4, TIME_TO_FROM_COLUMN)
];
// Verify that the countdown cells are counting down
for (let i = 0; i < countdownCells.length; i++) {
await test.step(`Countdown cell ${i + 1} counts down`, async () => {
const countdownCell = countdownCells[i];
// Get the initial countdown timestamp object
const beforeCountdown = await getAndAssertCountdownOrUpObject(page, i + 3);
// should not have a '-' sign
await expect(countdownCell).not.toHaveText('-');
// Wait until it changes
await expect(countdownCell).not.toHaveText(beforeCountdown.toString());
// Get the new countdown timestamp object
const afterCountdown = await getAndAssertCountdownOrUpObject(page, i + 3);
// Verify that the new countdown timestamp object is less than the old one
expect(Number(afterCountdown.seconds)).toBeLessThan(Number(beforeCountdown.seconds));
});
}
// Verify that the count-up cells are counting up
for (let i = 0; i < countUpCells.length; i++) {
await test.step(`Count-up cell ${i + 1} counts up`, async () => {
const countUpCell = countUpCells[i];
// Get the initial count-up timestamp object
const beforeCountUp = await getAndAssertCountdownOrUpObject(page, i + 1);
// should not have a '+' sign
await expect(countUpCell).not.toHaveText('+');
// Wait until it changes
await expect(countUpCell).not.toHaveText(beforeCountUp.toString());
// Get the new count-up timestamp object
const afterCountUp = await getAndAssertCountdownOrUpObject(page, i + 1);
// Verify that the new count-up timestamp object is greater than the old one
expect(Number(afterCountUp.seconds)).toBeGreaterThan(Number(beforeCountUp.seconds));
});
}
});
});
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 createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);
});
test('progress pie is empty', async ({ page }) => {
const anActivity = page.getByRole('row').nth(0);
// Progress pie shows no progress when now is less than the start time
await expect(anActivity.getByLabel('Activity in progress').locator('path')).not.toHaveAttribute(
'd'
);
});
});
test.describe('Activity progress when now is between start and end of the activity @clock', () => {
const firstActivity = getFirstActivity(examplePlanSmall1);
test.beforeEach(async ({ page }) => {
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');
// Progress pie shows progress when now is greater than the start time
await expect(pathElement).toHaveAttribute('d');
});
});
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 createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);
});
test('progress pie is full', async ({ page }) => {
const anActivity = page.getByRole('row').nth(0);
// Progress pie is completely full and doesn't update if now is greater than the end time
await expect(anActivity.getByLabel('Activity in progress').locator('path')).toHaveAttribute(
'd',
FULL_CIRCLE_PATH
);
});
});
/**
* Get the cell at the given row and column indices.
* @param {import('@playwright/test').Page} page
* @param {number} rowIndex
* @param {number} columnIndex
* @returns {import('@playwright/test').Locator} cell
*/
function getTimeListCellByIndex(page, rowIndex, columnIndex) {
return page.getByRole('cell').nth(rowIndex * NUM_COLUMNS + columnIndex);
}
/**
* Return the innerText of the cell at the given row and column indices.
* @param {import('@playwright/test').Page} page
* @param {number} rowIndex
* @param {number} columnIndex
* @returns {Promise<string>} text
*/
async function getTimeListCellTextByIndex(page, rowIndex, columnIndex) {
const text = await getTimeListCellByIndex(page, rowIndex, columnIndex).innerText();
return text;
}
/**
* Get the text from the countdown (or countup) cell in the given row, assert that it matches the countdown/countup
* regex, and return an object representing the countdown.
* @param {import('@playwright/test').Page} page
* @param {number} rowIndex the row index
* @returns {Promise<CountdownOrUpObject>} The countdown (or countup) object
*/
async function getAndAssertCountdownOrUpObject(page, rowIndex) {
const timeToFrom = await getTimeListCellTextByIndex(
page,
HEADER_ROW + rowIndex,
TIME_TO_FROM_COLUMN
);
expect(timeToFrom).toMatch(COUNTDOWN_REGEXP);
const match = timeToFrom.match(COUNTDOWN_REGEXP);
return {
sign: match[COUNTDOWN.SIGN],
days: match[COUNTDOWN.DAYS],
hours: match[COUNTDOWN.HOURS],
minutes: match[COUNTDOWN.MINUTES],
seconds: match[COUNTDOWN.SECONDS],
toString: () => timeToFrom
};
}

View File

@ -131,7 +131,10 @@ test.describe('Time Strip', () => {
const startBoundString = new Date(startBound).toISOString().replace('T', ' '); const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
const endBoundString = new Date(endBound).toISOString().replace('T', ' '); const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
await setIndependentTimeConductorBounds(page, startBoundString, endBoundString); await setIndependentTimeConductorBounds(page, {
start: startBoundString,
end: endBoundString
});
expect(await activityBounds.count()).toEqual(1); expect(await activityBounds.count()).toEqual(1);
}); });
@ -160,7 +163,10 @@ test.describe('Time Strip', () => {
const startBoundString = new Date(startBound).toISOString().replace('T', ' '); const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
const endBoundString = new Date(endBound).toISOString().replace('T', ' '); const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
await setIndependentTimeConductorBounds(page, startBoundString, endBoundString); await setIndependentTimeConductorBounds(page, {
start: startBoundString,
end: endBoundString
});
// Verify that two events are displayed // Verify that two events are displayed
expect(await activityBounds.count()).toEqual(2); expect(await activityBounds.count()).toEqual(2);

View File

@ -68,39 +68,36 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage @2p', () =>
}); });
//Begin suite of tests again localStorage //Begin suite of tests again localStorage
test.fixme( test('Condition set object properties persist in main view and inspector after reload @localStorage', async ({
'Condition set object properties persist in main view and inspector @localStorage', page
async ({ page }) => { }) => {
test.info().annotations.push({ //Navigate to baseURL with injected localStorage
type: 'issue', await page.goto(conditionSetUrl, { waitUntil: 'domcontentloaded' });
description: 'https://github.com/nasa/openmct/issues/7421'
});
//Navigate to baseURL with injected localStorage
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
//Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto() //Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
await expect await expect.soft(page.getByRole('main')).toContainText('Unnamed Condition Set');
.soft(page.locator('.l-browse-bar__object-name'))
.toContainText('Unnamed Condition Set');
//Assertions on loaded Condition Set in Inspector //Assertions on loaded Condition Set in Inspector
expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy(); await expect(
page.getByLabel('Title inspector properties').getByLabel('inspector property value')
).toContainText('Unnamed Condition Set');
//Reload Page //Reload Page
await Promise.all([page.reload(), page.waitForLoadState('networkidle')]); await page.reload({ waitUntil: 'domcontentloaded' });
//Re-verify after reload
await expect.soft(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 }) => { test('condition set object can be modified on @localStorage', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = 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() //Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
await expect await expect
@ -151,7 +148,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage @2p', () =>
expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
//Reload Page //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 //Verify Main section reflects updated Name Property
await expect await expect
@ -213,169 +210,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage @2p', () =>
//Feature? //Feature?
//Domain Object is still available by direct URL after delete //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'); await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
}); });
}); });
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
page.click('button[title="Show selected item in tree"]');
// 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.click('button[title="Change the current view"]');
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();
});
test('ConditionSet has correct outputs when telemetry is and is not available', async ({
page
}) => {
const exampleTelemetry = await createExampleTelemetryObject(page);
await page.getByTitle('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.locator('[aria-label="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('---');
});
});

View File

@ -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
page.click('button[title="Show selected item in tree"]');
// 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);
});
});

View File

@ -23,6 +23,7 @@ import { fileURLToPath } from 'url';
import { import {
createDomainObjectWithDefaults, createDomainObjectWithDefaults,
navigateToObjectWithFixedTimeBounds,
setFixedTimeMode, setFixedTimeMode,
setIndependentTimeConductorBounds, setIndependentTimeConductorBounds,
setRealTimeMode, setRealTimeMode,
@ -30,12 +31,120 @@ import {
} from '../../../../appActions.js'; } from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js'; import { expect, test } from '../../../../pluginFixtures.js';
const LOCALSTORAGE_PATH = fileURLToPath( const CHILD_LAYOUT_STORAGE_STATE_PATH = fileURLToPath(
new URL('../../../../test-data/display_layout_with_child_layouts.json', import.meta.url) new URL('../../../../test-data/display_layout_with_child_layouts.json', import.meta.url)
); );
const CHILD_PLOT_STORAGE_STATE_PATH = fileURLToPath(
new URL('../../../../test-data/display_layout_with_child_overlay_plot.json', import.meta.url)
);
const TINY_IMAGE_BASE64 = const TINY_IMAGE_BASE64 =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII'; 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII';
test.describe('Display Layout Sub-object Actions @localStorage', () => {
const INIT_ITC_START_BOUNDS = '2024-11-12 19:11:11.000Z';
const INIT_ITC_END_BOUNDS = '2024-11-12 20:11:11.000Z';
const NEW_GLOBAL_START_BOUNDS = '2024-11-11 19:11:11.000Z';
const NEW_GLOBAL_END_BOUNDS = '2024-11-11 20:11:11.000Z';
test.use({
storageState: CHILD_PLOT_STORAGE_STATE_PATH
});
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.getByLabel('Expand My Items folder').click();
const waitForMyItemsNavigation = page.waitForURL(`**/mine/?*`);
await page
.getByLabel('Main Tree')
.getByLabel('Navigate to Parent Display Layout layout Object')
.click();
// Wait for the URL to change to the display layout
await waitForMyItemsNavigation;
});
test('Open in New Tab action preserves time bounds @2p', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7524'
});
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6982'
});
const TEST_FIXED_START_TIME = 1731352271000; // 2024-11-11 19:11:11.000Z
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);
// 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];
await navigateToObjectWithFixedTimeBounds(
page,
url,
TEST_FIXED_START_TIME,
TEST_FIXED_END_TIME
);
// 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);
// Open the Child Overlay Plot 1 in a new tab
await page.getByLabel('View menu items').click();
const pagePromise = page.context().waitForEvent('page');
await page.getByLabel('Open In New Tab').click();
const newPage = await pagePromise;
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);
// 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);
});
});
test.describe('Display Layout Toolbar Actions @localStorage', () => { test.describe('Display Layout Toolbar Actions @localStorage', () => {
const PARENT_DISPLAY_LAYOUT_NAME = 'Parent Display Layout'; const PARENT_DISPLAY_LAYOUT_NAME = 'Parent Display Layout';
const CHILD_DISPLAY_LAYOUT_NAME1 = 'Child Layout 1'; const CHILD_DISPLAY_LAYOUT_NAME1 = 'Child Layout 1';
@ -50,7 +159,7 @@ test.describe('Display Layout Toolbar Actions @localStorage', () => {
await page.getByLabel('Edit Object').click(); await page.getByLabel('Edit Object').click();
}); });
test.use({ test.use({
storageState: LOCALSTORAGE_PATH storageState: CHILD_LAYOUT_STORAGE_STATE_PATH
}); });
test('can add/remove Text element to a single layout', async ({ page }) => { test('can add/remove Text element to a single layout', async ({ page }) => {
@ -163,7 +272,7 @@ test.describe('Display Layout', () => {
expect(trimmedDisplayValue).toBe(formattedTelemetryValue); expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
// ensure we can right click on the alpha-numeric widget and view historical data // ensure we can right click on the alpha-numeric widget and view historical data
await page.getByLabel('Sine', { exact: true }).click({ await page.getByLabel(/Alpha-numeric telemetry value of.*/).click({
button: 'right' button: 'right'
}); });
await page.getByLabel('View Historical Data').click(); await page.getByLabel('View Historical Data').click();
@ -336,7 +445,7 @@ test.describe('Display Layout', () => {
const startDate = '2021-12-30 01:01:00.000Z'; const startDate = '2021-12-30 01:01:00.000Z';
const endDate = '2021-12-30 01:11:00.000Z'; const endDate = '2021-12-30 01:11:00.000Z';
await setIndependentTimeConductorBounds(page, startDate, endDate); await setIndependentTimeConductorBounds(page, { start: startDate, end: endDate });
// check image date // check image date
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible(); await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
@ -410,7 +519,7 @@ test.describe('Display Layout', () => {
await page.reload(); await page.reload();
// wait for annotations requests to be batched and requested // 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: // Network requests for the composite telemetry with multiple items should be:
// 1. a single batched request for annotations // 1. a single batched request for annotations
expect(networkRequests.length).toBe(1); expect(networkRequests.length).toBe(1);
@ -422,7 +531,7 @@ test.describe('Display Layout', () => {
await page.reload(); await page.reload();
// wait for annotations to not load (if we have any, we've got a problem) // 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 // In real time mode, we don't fetch annotations at all
expect(networkRequests.length).toBe(0); expect(networkRequests.length).toBe(0);

View File

@ -20,25 +20,46 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import * as utils from '../../../../helper/faultUtils.js'; import {
acknowledgeFault,
acknowledgeMultipleFaults,
changeViewTo,
clearSearch,
enterSearchTerm,
getFault,
getFaultByName,
getFaultName,
getFaultNamespace,
getFaultResultCount,
getFaultSeverity,
getFaultTriggerTime,
getHighestSeverity,
getLowestSeverity,
navigateToFaultManagementWithExample,
navigateToFaultManagementWithoutExample,
selectFaultItem,
shelveFault,
shelveMultipleFaults,
sortFaultsBy
} from '../../../../helper/faultUtils.js';
import { expect, test } from '../../../../pluginFixtures.js'; import { expect, test } from '../../../../pluginFixtures.js';
test.describe('The Fault Management Plugin using example faults', () => { test.describe('The Fault Management Plugin using example faults', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await utils.navigateToFaultManagementWithExample(page); await navigateToFaultManagementWithExample(page);
}); });
test('Shows a criticality icon for every fault @unstable', async ({ page }) => { test('Shows a criticality icon for every fault', async ({ page }) => {
const faultCount = await page.locator('c-fault-mgmt__list').count(); const faultCount = await page.locator('c-fault-mgmt__list').count();
const criticalityIconCount = await page.locator('c-fault-mgmt__list-severity').count(); const criticalityIconCount = await page.locator('c-fault-mgmt__list-severity').count();
expect.soft(faultCount).toEqual(criticalityIconCount); expect(faultCount).toEqual(criticalityIconCount);
}); });
test('When selecting a fault, it has an "is-selected" class and it\'s information shows in the inspector @unstable', async ({ test('When selecting a fault, it has an "is-selected" class and it\'s information shows in the inspector', async ({
page page
}) => { }) => {
await utils.selectFaultItem(page, 1); await selectFaultItem(page, 1);
await page.getByRole('tab', { name: 'Config' }).click(); await page.getByRole('tab', { name: 'Config' }).click();
const selectedFaultName = await page const selectedFaultName = await page
@ -48,22 +69,22 @@ test.describe('The Fault Management Plugin using example faults', () => {
.locator(`.c-inspector__properties >> :text("${selectedFaultName}")`) .locator(`.c-inspector__properties >> :text("${selectedFaultName}")`)
.count(); .count();
await expect await expect(
.soft(page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').first()) page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').first()
.toHaveClass(/is-selected/); ).toHaveClass(/is-selected/);
expect.soft(inspectorFaultNameCount).toEqual(1); expect(inspectorFaultNameCount).toEqual(1);
}); });
test('When selecting multiple faults, no specific fault information is shown in the inspector @unstable', async ({ test('When selecting multiple faults, no specific fault information is shown in the inspector', async ({
page page
}) => { }) => {
await utils.selectFaultItem(page, 1); await selectFaultItem(page, 1);
await utils.selectFaultItem(page, 2); await selectFaultItem(page, 2);
const selectedRows = page.locator( const selectedRows = page.locator(
'.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname' '.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname'
); );
expect.soft(await selectedRows.count()).toEqual(2); expect(await selectedRows.count()).toEqual(2);
await page.getByRole('tab', { name: 'Config' }).click(); await page.getByRole('tab', { name: 'Config' }).click();
const firstSelectedFaultName = await selectedRows.nth(0).textContent(); const firstSelectedFaultName = await selectedRows.nth(0).textContent();
@ -75,180 +96,180 @@ test.describe('The Fault Management Plugin using example faults', () => {
.locator(`.c-inspector__properties >> :text("${secondSelectedFaultName}")`) .locator(`.c-inspector__properties >> :text("${secondSelectedFaultName}")`)
.count(); .count();
expect.soft(firstNameInInspectorCount).toEqual(0); expect(firstNameInInspectorCount).toEqual(0);
expect.soft(secondNameInInspectorCount).toEqual(0); expect(secondNameInInspectorCount).toEqual(0);
}); });
test('Allows you to shelve a fault @unstable', async ({ page }) => { test('Allows you to shelve a fault', async ({ page }) => {
const shelvedFaultName = await utils.getFaultName(page, 2); const shelvedFaultName = await getFaultName(page, 2);
const beforeShelvedFault = utils.getFaultByName(page, shelvedFaultName); const beforeShelvedFault = getFaultByName(page, shelvedFaultName);
expect.soft(await beforeShelvedFault.count()).toBe(1); await expect(beforeShelvedFault).toHaveCount(1);
await utils.shelveFault(page, 2); await shelveFault(page, 2);
// check it is removed from standard view // check it is removed from standard view
const afterShelvedFault = utils.getFaultByName(page, shelvedFaultName); const afterShelvedFault = getFaultByName(page, shelvedFaultName);
expect.soft(await afterShelvedFault.count()).toBe(0); expect(await afterShelvedFault.count()).toBe(0);
await utils.changeViewTo(page, 'shelved'); await changeViewTo(page, 'shelved');
const shelvedViewFault = utils.getFaultByName(page, shelvedFaultName); const shelvedViewFault = getFaultByName(page, shelvedFaultName);
expect.soft(await shelvedViewFault.count()).toBe(1); expect(await shelvedViewFault.count()).toBe(1);
}); });
test('Allows you to acknowledge a fault @unstable', async ({ page }) => { test('Allows you to acknowledge a fault', async ({ page }) => {
const acknowledgedFaultName = await utils.getFaultName(page, 3); const acknowledgedFaultName = await getFaultName(page, 3);
await utils.acknowledgeFault(page, 3); await acknowledgeFault(page, 3);
const fault = utils.getFault(page, 3); const fault = getFault(page, 3);
await expect.soft(fault).toHaveClass(/is-acknowledged/); await expect(fault).toHaveClass(/is-acknowledged/);
await utils.changeViewTo(page, 'acknowledged'); await changeViewTo(page, 'acknowledged');
const acknowledgedViewFaultName = await utils.getFaultName(page, 1); const acknowledgedViewFaultName = await getFaultName(page, 1);
expect.soft(acknowledgedFaultName).toEqual(acknowledgedViewFaultName); expect(acknowledgedFaultName).toEqual(acknowledgedViewFaultName);
}); });
test('Allows you to shelve multiple faults @unstable', async ({ page }) => { test('Allows you to shelve multiple faults', async ({ page }) => {
const shelvedFaultNameOne = await utils.getFaultName(page, 1); const shelvedFaultNameOne = await getFaultName(page, 1);
const shelvedFaultNameFour = await utils.getFaultName(page, 4); const shelvedFaultNameFour = await getFaultName(page, 4);
const beforeShelvedFaultOne = utils.getFaultByName(page, shelvedFaultNameOne); const beforeShelvedFaultOne = getFaultByName(page, shelvedFaultNameOne);
const beforeShelvedFaultFour = utils.getFaultByName(page, shelvedFaultNameFour); const beforeShelvedFaultFour = getFaultByName(page, shelvedFaultNameFour);
expect.soft(await beforeShelvedFaultOne.count()).toBe(1); await expect(beforeShelvedFaultOne).toHaveCount(1);
expect.soft(await beforeShelvedFaultFour.count()).toBe(1); await expect(beforeShelvedFaultFour).toHaveCount(1);
await utils.shelveMultipleFaults(page, 1, 4); await shelveMultipleFaults(page, 1, 4);
// check it is removed from standard view // check it is removed from standard view
const afterShelvedFaultOne = utils.getFaultByName(page, shelvedFaultNameOne); const afterShelvedFaultOne = getFaultByName(page, shelvedFaultNameOne);
const afterShelvedFaultFour = utils.getFaultByName(page, shelvedFaultNameFour); const afterShelvedFaultFour = getFaultByName(page, shelvedFaultNameFour);
expect.soft(await afterShelvedFaultOne.count()).toBe(0); await expect(afterShelvedFaultOne).toHaveCount(0);
expect.soft(await afterShelvedFaultFour.count()).toBe(0); await expect(afterShelvedFaultFour).toHaveCount(0);
await utils.changeViewTo(page, 'shelved'); await changeViewTo(page, 'shelved');
const shelvedViewFaultOne = utils.getFaultByName(page, shelvedFaultNameOne); const shelvedViewFaultOne = getFaultByName(page, shelvedFaultNameOne);
const shelvedViewFaultFour = utils.getFaultByName(page, shelvedFaultNameFour); const shelvedViewFaultFour = getFaultByName(page, shelvedFaultNameFour);
expect.soft(await shelvedViewFaultOne.count()).toBe(1); await expect(shelvedViewFaultOne).toHaveCount(1);
expect.soft(await shelvedViewFaultFour.count()).toBe(1); await expect(shelvedViewFaultFour).toHaveCount(1);
}); });
test('Allows you to acknowledge multiple faults @unstable', async ({ page }) => { test('Allows you to acknowledge multiple faults', async ({ page }) => {
const acknowledgedFaultNameTwo = await utils.getFaultName(page, 2); const acknowledgedFaultNameTwo = await getFaultName(page, 2);
const acknowledgedFaultNameFive = await utils.getFaultName(page, 5); const acknowledgedFaultNameFive = await getFaultName(page, 5);
await utils.acknowledgeMultipleFaults(page, 2, 5); await acknowledgeMultipleFaults(page, 2, 5);
const faultTwo = utils.getFault(page, 2); const faultTwo = getFault(page, 2);
const faultFive = utils.getFault(page, 5); const faultFive = getFault(page, 5);
// check they have been acknowledged // check they have been acknowledged
await expect.soft(faultTwo).toHaveClass(/is-acknowledged/); await expect(faultTwo).toHaveClass(/is-acknowledged/);
await expect.soft(faultFive).toHaveClass(/is-acknowledged/); await expect(faultFive).toHaveClass(/is-acknowledged/);
await utils.changeViewTo(page, 'acknowledged'); await changeViewTo(page, 'acknowledged');
const acknowledgedViewFaultTwo = utils.getFaultByName(page, acknowledgedFaultNameTwo); const acknowledgedViewFaultTwo = getFaultByName(page, acknowledgedFaultNameTwo);
const acknowledgedViewFaultFive = utils.getFaultByName(page, acknowledgedFaultNameFive); const acknowledgedViewFaultFive = getFaultByName(page, acknowledgedFaultNameFive);
expect.soft(await acknowledgedViewFaultTwo.count()).toBe(1); await expect(acknowledgedViewFaultTwo).toHaveCount(1);
expect.soft(await acknowledgedViewFaultFive.count()).toBe(1); await expect(acknowledgedViewFaultFive).toHaveCount(1);
}); });
test('Allows you to search faults @unstable', async ({ page }) => { test('Allows you to search faults', async ({ page }) => {
const faultThreeNamespace = await utils.getFaultNamespace(page, 3); const faultThreeNamespace = await getFaultNamespace(page, 3);
const faultTwoName = await utils.getFaultName(page, 2); const faultTwoName = await getFaultName(page, 2);
const faultFiveTriggerTime = await utils.getFaultTriggerTime(page, 5); const faultFiveTriggerTime = await getFaultTriggerTime(page, 5);
// should be all faults (5) // should be all faults (5)
let faultResultCount = await utils.getFaultResultCount(page); let faultResultCount = await getFaultResultCount(page);
expect.soft(faultResultCount).toEqual(5); expect(faultResultCount).toEqual(5);
// search namespace // search namespace
await utils.enterSearchTerm(page, faultThreeNamespace); await enterSearchTerm(page, faultThreeNamespace);
faultResultCount = await utils.getFaultResultCount(page); faultResultCount = await getFaultResultCount(page);
expect.soft(faultResultCount).toEqual(1); expect(faultResultCount).toEqual(1);
expect.soft(await utils.getFaultNamespace(page, 1)).toEqual(faultThreeNamespace); expect(await getFaultNamespace(page, 1)).toEqual(faultThreeNamespace);
// all faults // all faults
await utils.clearSearch(page); await clearSearch(page);
faultResultCount = await utils.getFaultResultCount(page); faultResultCount = await getFaultResultCount(page);
expect.soft(faultResultCount).toEqual(5); expect(faultResultCount).toEqual(5);
// search name // search name
await utils.enterSearchTerm(page, faultTwoName); await enterSearchTerm(page, faultTwoName);
faultResultCount = await utils.getFaultResultCount(page); faultResultCount = await getFaultResultCount(page);
expect.soft(faultResultCount).toEqual(1); expect(faultResultCount).toEqual(1);
expect.soft(await utils.getFaultName(page, 1)).toEqual(faultTwoName); expect(await getFaultName(page, 1)).toEqual(faultTwoName);
// all faults // all faults
await utils.clearSearch(page); await clearSearch(page);
faultResultCount = await utils.getFaultResultCount(page); faultResultCount = await getFaultResultCount(page);
expect.soft(faultResultCount).toEqual(5); expect(faultResultCount).toEqual(5);
// search triggerTime // search triggerTime
await utils.enterSearchTerm(page, faultFiveTriggerTime); await enterSearchTerm(page, faultFiveTriggerTime);
faultResultCount = await utils.getFaultResultCount(page); faultResultCount = await getFaultResultCount(page);
expect.soft(faultResultCount).toEqual(1); expect(faultResultCount).toEqual(1);
expect.soft(await utils.getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime); expect(await getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime);
}); });
test('Allows you to sort faults @unstable', async ({ page }) => { test('Allows you to sort faults', async ({ page }) => {
const highestSeverity = await utils.getHighestSeverity(page); const highestSeverity = await getHighestSeverity(page);
const lowestSeverity = await utils.getLowestSeverity(page); const lowestSeverity = await getLowestSeverity(page);
const faultOneName = 'Example Fault 1'; const faultOneName = 'Example Fault 1';
const faultFiveName = 'Example Fault 5'; const faultFiveName = 'Example Fault 5';
let firstFaultName = await utils.getFaultName(page, 1); let firstFaultName = await getFaultName(page, 1);
expect.soft(firstFaultName).toEqual(faultOneName); expect(firstFaultName).toEqual(faultOneName);
await utils.sortFaultsBy(page, 'oldest-first'); await sortFaultsBy(page, 'oldest-first');
firstFaultName = await utils.getFaultName(page, 1); firstFaultName = await getFaultName(page, 1);
expect.soft(firstFaultName).toEqual(faultFiveName); expect(firstFaultName).toEqual(faultFiveName);
await utils.sortFaultsBy(page, 'severity'); await sortFaultsBy(page, 'severity');
const sortedHighestSeverity = await utils.getFaultSeverity(page, 1); const sortedHighestSeverity = await getFaultSeverity(page, 1);
const sortedLowestSeverity = await utils.getFaultSeverity(page, 5); const sortedLowestSeverity = await getFaultSeverity(page, 5);
expect.soft(sortedHighestSeverity).toEqual(highestSeverity); expect(sortedHighestSeverity).toEqual(highestSeverity);
expect.soft(sortedLowestSeverity).toEqual(lowestSeverity); expect(sortedLowestSeverity).toEqual(lowestSeverity);
}); });
}); });
test.describe('The Fault Management Plugin without using example faults', () => { test.describe('The Fault Management Plugin without using example faults', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await utils.navigateToFaultManagementWithoutExample(page); await navigateToFaultManagementWithoutExample(page);
}); });
test('Shows no faults when no faults are provided @unstable', async ({ page }) => { test('Shows no faults when no faults are provided', async ({ page }) => {
const faultCount = await page.locator('c-fault-mgmt__list').count(); const faultCount = await page.locator('c-fault-mgmt__list').count();
expect.soft(faultCount).toEqual(0); expect(faultCount).toEqual(0);
await utils.changeViewTo(page, 'acknowledged'); await changeViewTo(page, 'acknowledged');
const acknowledgedCount = await page.locator('c-fault-mgmt__list').count(); const acknowledgedCount = await page.locator('c-fault-mgmt__list').count();
expect.soft(acknowledgedCount).toEqual(0); expect(acknowledgedCount).toEqual(0);
await utils.changeViewTo(page, 'shelved'); await changeViewTo(page, 'shelved');
const shelvedCount = await page.locator('c-fault-mgmt__list').count(); const shelvedCount = await page.locator('c-fault-mgmt__list').count();
expect.soft(shelvedCount).toEqual(0); expect(shelvedCount).toEqual(0);
}); });
test('Will return no faults when searching @unstable', async ({ page }) => { test('Will return no faults when searching', async ({ page }) => {
await utils.enterSearchTerm(page, 'fault'); await enterSearchTerm(page, 'fault');
const faultCount = await page.locator('c-fault-mgmt__list').count(); const faultCount = await page.locator('c-fault-mgmt__list').count();
expect.soft(faultCount).toEqual(0); expect(faultCount).toEqual(0);
}); });
}); });

View File

@ -78,8 +78,8 @@ test.describe('Flexible Layout', () => {
// Expand the 'My Items' folder in the left tree // Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click(); await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
// Add the Sine Wave Generator and Clock to the Flexible Layout // Add the Sine Wave Generator and Clock to the Flexible Layout
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first()); await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());
await clockTreeItem.dragTo(page.locator('.c-fl__container.is-empty')); await clockTreeItem.dragTo(page.locator('.c-fl-container.is-empty'));
// Check that panes can be dragged while Flexible Layout is in Edit mode // Check that panes can be dragged while Flexible Layout is in Edit mode
let dragWrapper = page let dragWrapper = page
.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper') .locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper')
@ -105,8 +105,8 @@ test.describe('Flexible Layout', () => {
// Expand the 'My Items' folder in the left tree // Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click(); await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
// Add the Sine Wave Generator and Clock to the Flexible Layout // Add the Sine Wave Generator and Clock to the Flexible Layout
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first()); await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());
await clockTreeItem.dragTo(page.locator('.c-fl__container.is-empty')); await clockTreeItem.dragTo(page.locator('.c-fl-container.is-empty'));
// Click on the first frame to select it // Click on the first frame to select it
await page.locator('.c-fl-container__frame').first().click(); await page.locator('.c-fl-container__frame').first().click();
@ -122,7 +122,7 @@ test.describe('Flexible Layout', () => {
expect(await page.locator('.c-fl--rows').count()).toEqual(0); expect(await page.locator('.c-fl--rows').count()).toEqual(0);
// Change the layout to rows orientation // Change the layout to rows orientation
await page.getByTitle('Columns layout').click(); await page.getByTitle('Switch to rows layout').click();
// Assert the layout is in rows orientation // Assert the layout is in rows orientation
expect(await page.locator('.c-fl--rows').count()).toBeGreaterThan(0); expect(await page.locator('.c-fl--rows').count()).toBeGreaterThan(0);
@ -171,7 +171,7 @@ test.describe('Flexible Layout', () => {
// Expand the 'My Items' folder in the left tree // Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click(); await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
// Add the Sine Wave Generator to the Flexible Layout and save changes // Add the Sine Wave Generator to the Flexible Layout and save changes
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first()); await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());
await page.locator('button[title="Save"]').click(); await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
@ -202,7 +202,7 @@ test.describe('Flexible Layout', () => {
// Expand the 'My Items' folder in the left tree // Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// Add the Sine Wave Generator to the Flexible Layout and save changes // Add the Sine Wave Generator to the Flexible Layout and save changes
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first()); await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());
await page.locator('button[title="Save"]').click(); await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
@ -242,17 +242,16 @@ test.describe('Flexible Layout', () => {
name: new RegExp(exampleImageryObject.name) name: new RegExp(exampleImageryObject.name)
}); });
// Add the Sine Wave Generator to the Flexible Layout and save changes // Add the Sine Wave Generator to the Flexible Layout and save changes
await exampleImageryTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first()); await exampleImageryTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());
await page.locator('button[title="Save"]').click(); await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// flip on independent time conductor // flip on independent time conductor
await setIndependentTimeConductorBounds( await setIndependentTimeConductorBounds(page, {
page, start: '2021-12-30 01:01:00.000Z',
'2021-12-30 01:01:00.000Z', end: '2021-12-30 01:11:00.000Z'
'2021-12-30 01:11:00.000Z' });
);
// check image date // check image date
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible(); await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
@ -290,7 +289,7 @@ test.describe('Flexible Layout Toolbar Actions @localStorage', () => {
await page.getByTitle('Add Container').click(); await page.getByTitle('Add Container').click();
expect(await containerHandles.count()).toEqual(3); expect(await containerHandles.count()).toEqual(3);
await page.getByTitle('Remove Container').click(); await page.getByTitle('Remove Container').click();
await expect(page.getByRole('dialog', { name: 'Overlay' })).toHaveText( await expect(page.getByRole('dialog', { name: 'Overlay' })).toContainText(
'This action will permanently delete this container from this Flexible Layout. Do you want to continue?' '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();
@ -300,7 +299,7 @@ test.describe('Flexible Layout Toolbar Actions @localStorage', () => {
expect(await page.getByRole('group', { name: 'Frame' }).count()).toEqual(2); expect(await page.getByRole('group', { name: 'Frame' }).count()).toEqual(2);
await page.getByRole('group', { name: 'Child Layout 1' }).click(); await page.getByRole('group', { name: 'Child Layout 1' }).click();
await page.getByTitle('Remove Frame').click(); await page.getByTitle('Remove Frame').click();
await expect(page.getByRole('dialog', { name: 'Overlay' })).toHaveText( await expect(page.getByRole('dialog', { name: 'Overlay' })).toContainText(
'This action will remove this frame from this Flexible Layout. Do you want to continue?' '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();
@ -310,9 +309,9 @@ test.describe('Flexible Layout Toolbar Actions @localStorage', () => {
await page.getByRole('columnheader', { name: 'Container Handle 1' }).click(); await page.getByRole('columnheader', { name: 'Container Handle 1' }).click();
const flexRows = page.getByLabel('Flexible Layout Row'); const flexRows = page.getByLabel('Flexible Layout Row');
expect(await flexRows.count()).toEqual(0); expect(await flexRows.count()).toEqual(0);
await page.getByTitle('Columns layout').click(); await page.getByTitle('Switch to rows layout').click();
expect(await flexRows.count()).toEqual(1); expect(await flexRows.count()).toEqual(1);
await page.getByTitle('Rows layout').click(); await page.getByTitle('Switch to columns layout').click();
expect(await flexRows.count()).toEqual(0); expect(await flexRows.count()).toEqual(0);
}); });
}); });

View File

@ -136,14 +136,11 @@ test.describe('Gauge', () => {
// TODO: Verify changes in the UI // TODO: Verify changes in the UI
}); });
test.fixme('Gauge does not display NaN when data not available', async ({ page }) => { test('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'
});
// Create a Gauge // Create a Gauge
const gauge = await createDomainObjectWithDefaults(page, { 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 // Create a Sine Wave Generator in the Gauge with a loading delay
@ -154,7 +151,7 @@ test.describe('Gauge', () => {
await page.getByRole('menuitem', { name: /Edit Properties.../ }).click(); await page.getByRole('menuitem', { name: /Edit Properties.../ }).click();
//Edit Example Telemetry Object to include 5s loading Delay //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(); await page.getByRole('button', { name: 'Save' }).click();
@ -162,9 +159,13 @@ test.describe('Gauge', () => {
await page.waitForURL(`**/${gauge.uuid}/*`); await page.waitForURL(`**/${gauge.uuid}/*`);
// Nav to the Gauge // Nav to the Gauge
await page.goto(gauge.url); await page.goto(gauge.url, { waitUntil: 'domcontentloaded' });
const gaugeNoDataText = await page.locator('.js-dial-current-value tspan').textContent(); // Check that the value is not displayed
expect(gaugeNoDataText).toBe('--'); //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 }) => { test('Gauge enforces composition policy', async ({ page }) => {
@ -175,13 +176,13 @@ test.describe('Gauge', () => {
}); });
// Try to create a Folder into the Gauge. Should be disallowed. // Try to create a Folder into the Gauge. Should be disallowed.
await page.getByRole('button', { name: /Create/ }).click(); await page.getByRole('button', { name: 'Create' }).click();
await page.getByRole('menuitem', { name: /Folder/ }).click(); await page.getByRole('menuitem', { name: /Folder/ }).click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
await page.getByLabel('Cancel').click(); await page.getByLabel('Cancel').click();
// Try to create a Display Layout into the Gauge. Should be disallowed. // Try to create a Display Layout into the Gauge. Should be disallowed.
await page.getByRole('button', { name: /Create/ }).click(); await page.getByRole('button', { name: 'Create' }).click();
await page.getByRole('menuitem', { name: /Display Layout/ }).click(); await page.getByRole('menuitem', { name: /Display Layout/ }).click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
}); });

View File

@ -24,15 +24,22 @@
This test suite is dedicated to tests which verify the basic operations surrounding imagery, This test suite is dedicated to tests which verify the basic operations surrounding imagery,
but only assume that example imagery is present. but only assume that example imagery is present.
*/ */
/* globals process */
import { createDomainObjectWithDefaults, setRealTimeMode } from '../../../../appActions.js'; import {
import { waitForAnimations } from '../../../../baseFixtures.js'; createDomainObjectWithDefaults,
navigateToObjectWithRealTime,
setRealTimeMode
} from '../../../../appActions.js';
import { MISSION_TIME } from '../../../../constants.js';
import { expect, test } from '../../../../pluginFixtures.js'; import { expect, test } from '../../../../pluginFixtures.js';
const backgroundImageSelector = '.c-imagery__main-image__background-image';
const panHotkey = process.platform === 'linux' ? ['Shift', 'Alt'] : ['Alt']; const panHotkey = process.platform === 'linux' ? ['Shift', 'Alt'] : ['Alt'];
const tagHotkey = ['Shift', 'Alt']; const tagHotkey = ['Shift', 'Alt'];
const expectedAltText = process.platform === 'linux' ? 'Shift+Alt drag to pan' : 'Alt drag to pan'; const expectedAltText = process.platform === 'linux' ? 'Shift+Alt drag to pan' : 'Alt drag to pan';
const thumbnailUrlParamsRegexp = /\?w=100&h=100/; const thumbnailUrlParamsRegexp = /\?w=100&h=100/;
const IMAGE_LOAD_DELAY = 5 * 1000;
const MOUSE_WHEEL_DELTA_Y = 120;
const FIVE_MINUTES = 1000 * 60 * 5;
const THIRTY_SECONDS = 1000 * 30;
//The following block of tests verifies the basic functionality of example imagery and serves as a template for Imagery objects embedded in other objects. //The following block of tests verifies the basic functionality of example imagery and serves as a template for Imagery objects embedded in other objects.
test.describe('Example Imagery Object', () => { test.describe('Example Imagery Object', () => {
@ -45,8 +52,7 @@ test.describe('Example Imagery Object', () => {
// Verify that the created object is focused // Verify that the created object is focused
await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name); await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name);
await page.locator('.c-imagery__main-image__bg').hover({ trial: true }); await page.getByLabel('Focused Image Element').hover({ trial: true });
await page.locator(backgroundImageSelector).waitFor();
}); });
test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => { test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
@ -63,7 +69,7 @@ test.describe('Example Imagery Object', () => {
test('Can right click on image and open it in a new tab @2p', async ({ page, context }) => { test('Can right click on image and open it in a new tab @2p', async ({ page, context }) => {
// try to right click on image // try to right click on image
const backgroundImage = await page.locator(backgroundImageSelector); const backgroundImage = page.getByLabel('Focused Image Element');
await backgroundImage.click({ await backgroundImage.click({
button: 'right', button: 'right',
// eslint-disable-next-line playwright/no-force-option // eslint-disable-next-line playwright/no-force-option
@ -80,7 +86,7 @@ test.describe('Example Imagery Object', () => {
const newPage = await pagePromise; const newPage = await pagePromise;
await newPage.waitForLoadState(); await newPage.waitForLoadState();
// expect new tab url to have jpg in it // expect new tab url to have jpg in it
await expect(newPage.url()).toContain('.jpg'); expect(newPage.url()).toContain('.jpg');
}); });
// this requires CORS to be enabled in some fashion // this requires CORS to be enabled in some fashion
@ -105,27 +111,36 @@ test.describe('Example Imagery Object', () => {
type: 'issue', type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6821' description: 'https://github.com/nasa/openmct/issues/6821'
}); });
// Test independent fixed time with global fixed time // Test independent fixed time with global fixed time
// flip on independent time conductor // flip on independent time conductor
await page.getByRole('switch', { name: 'Enable Independent Time Conductor' }).click(); await page.getByLabel('Enable Independent Time Conductor').click();
// Adding in delay to address flakiness of ITC test-- button event handlers not registering in time
await expect(page.locator('#independentTCToggle')).toBeChecked(); await expect(page.locator('#independentTCToggle')).toBeChecked();
await expect(page.locator('.c-compact-tc').first()).toBeVisible(); await expect(page.locator('.c-compact-tc').first()).toBeVisible();
await expect(
page.getByRole('button', { name: 'Independent Time Conductor Settings' })
).toBeEnabled();
await page.getByRole('button', { name: 'Independent Time Conductor Settings' }).click(); await page.getByRole('button', { name: 'Independent Time Conductor Settings' }).click();
await expect(page.getByLabel('Time Conductor Options')).toBeVisible();
await page.getByLabel('Time Conductor Options').hover({ trial: true });
await page.getByRole('textbox', { name: 'Start date' }).hover({ trial: true });
await page.getByRole('textbox', { name: 'Start date' }).fill('2021-12-30'); await page.getByRole('textbox', { name: 'Start date' }).fill('2021-12-30');
await page.keyboard.press('Tab'); await page.keyboard.press('Tab');
await page.getByRole('textbox', { name: 'Start time' }).hover({ trial: true });
await page.getByRole('textbox', { name: 'Start time' }).fill('01:01:00'); await page.getByRole('textbox', { name: 'Start time' }).fill('01:01:00');
await page.keyboard.press('Tab'); await page.keyboard.press('Tab');
await page.getByRole('textbox', { name: 'End date' }).hover({ trial: true });
await page.getByRole('textbox', { name: 'End date' }).fill('2021-12-30'); await page.getByRole('textbox', { name: 'End date' }).fill('2021-12-30');
await page.keyboard.press('Tab'); await page.keyboard.press('Tab');
await page.getByRole('textbox', { name: 'End time' }).hover({ trial: true });
await page.getByRole('textbox', { name: 'End time' }).fill('01:11:00'); await page.getByRole('textbox', { name: 'End time' }).fill('01:11:00');
await page.keyboard.press('Tab'); await page.getByRole('textbox', { name: 'End time' }).fill('01:11:00');
await page.keyboard.press('Enter'); await page.getByLabel('Submit time bounds').click();
// check image date // wait for image thumbnails to stabilize
await page.getByLabel('Image Thumbnails', { exact: true }).hover({ trial: true });
await expect(page.getByText('2021-12-30 01:01:00.000Z').first()).toBeVisible(); await expect(page.getByText('2021-12-30 01:01:00.000Z').first()).toBeVisible();
// flip it off // flip it off
@ -166,14 +181,11 @@ test.describe('Example Imagery Object', () => {
}); });
test('Can use alt+drag to move around image once zoomed in', async ({ page }) => { test('Can use alt+drag to move around image once zoomed in', async ({ page }) => {
const deltaYStep = 100; //equivalent to 1x zoom
await page.locator('.c-imagery__main-image__bg').hover({ trial: true }); await page.locator('.c-imagery__main-image__bg').hover({ trial: true });
// zoom in // zoom in
await page.mouse.wheel(0, deltaYStep * 2); await page.mouse.wheel(0, MOUSE_WHEEL_DELTA_Y * 2);
await page.locator('.c-imagery__main-image__bg').hover({ trial: true }); const zoomedBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
// move to the right // move to the right
@ -195,7 +207,7 @@ test.describe('Example Imagery Object', () => {
await page.mouse.move(imageCenterX - 200, imageCenterY, 10); await page.mouse.move(imageCenterX - 200, imageCenterY, 10);
await page.mouse.up(); await page.mouse.up();
await Promise.all(panHotkey.map((x) => page.keyboard.up(x))); await Promise.all(panHotkey.map((x) => page.keyboard.up(x)));
const afterRightPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const afterRightPanBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x); expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);
// pan left // pan left
@ -204,7 +216,7 @@ test.describe('Example Imagery Object', () => {
await page.mouse.move(imageCenterX, imageCenterY, 10); await page.mouse.move(imageCenterX, imageCenterY, 10);
await page.mouse.up(); await page.mouse.up();
await Promise.all(panHotkey.map((x) => page.keyboard.up(x))); await Promise.all(panHotkey.map((x) => page.keyboard.up(x)));
const afterLeftPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const afterLeftPanBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x); expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);
// pan up // pan up
@ -214,7 +226,7 @@ test.describe('Example Imagery Object', () => {
await page.mouse.move(imageCenterX, imageCenterY + 200, 10); await page.mouse.move(imageCenterX, imageCenterY + 200, 10);
await page.mouse.up(); await page.mouse.up();
await Promise.all(panHotkey.map((x) => page.keyboard.up(x))); await Promise.all(panHotkey.map((x) => page.keyboard.up(x)));
const afterUpPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const afterUpPanBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
expect(afterUpPanBoundingBox.y).toBeGreaterThan(afterLeftPanBoundingBox.y); expect(afterUpPanBoundingBox.y).toBeGreaterThan(afterLeftPanBoundingBox.y);
// pan down // pan down
@ -223,7 +235,7 @@ test.describe('Example Imagery Object', () => {
await page.mouse.move(imageCenterX, imageCenterY - 200, 10); await page.mouse.move(imageCenterX, imageCenterY - 200, 10);
await page.mouse.up(); await page.mouse.up();
await Promise.all(panHotkey.map((x) => page.keyboard.up(x))); await Promise.all(panHotkey.map((x) => page.keyboard.up(x)));
const afterDownPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const afterDownPanBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y); expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);
}); });
@ -282,26 +294,43 @@ test.describe('Example Imagery Object', () => {
await expect(page.getByText('Drilling')).toBeVisible(); await expect(page.getByText('Drilling')).toBeVisible();
}); });
test('Can use + - buttons to zoom on the image @unstable', async ({ page }) => { test('Can use + - buttons to zoom on the image', async ({ page }) => {
await buttonZoomOnImageAndAssert(page); await buttonZoomOnImageAndAssert(page);
}); });
test('Can use the reset button to reset the image @unstable', async ({ page }, testInfo) => { test('Can use the reset button to reset the image', async ({ page }) => {
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
'style.transform',
'scale(1) translate(0px, 0px)'
);
// Get initial image dimensions // Get initial image dimensions
const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const initialBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
// Zoom in twice via button // Zoom in twice via button
await zoomIntoImageryByButton(page); await zoomIntoImageryByButton(page);
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
'style.transform',
'scale(2) translate(0px, 0px)'
);
await zoomIntoImageryByButton(page); await zoomIntoImageryByButton(page);
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
'style.transform',
'scale(3) translate(0px, 0px)'
);
// Get and assert zoomed in image dimensions // Get and assert zoomed in image dimensions
const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const zoomedInBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height); expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width); expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
// Reset pan and zoom and assert against initial image dimensions // Reset pan and zoom and assert against initial image dimensions
await resetImageryPanAndZoom(page); await resetImageryPanAndZoom(page);
const finalBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
'style.transform',
'scale(1) translate(0px, 0px)'
);
const finalBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
expect(finalBoundingBox).toEqual(initialBoundingBox); expect(finalBoundingBox).toEqual(initialBoundingBox);
}); });
@ -324,20 +353,25 @@ test.describe('Example Imagery Object', () => {
}); });
}); });
test.describe('Example Imagery in Display Layout', () => { test.describe('Example Imagery in Display Layout @clock', () => {
let displayLayout; let displayLayout;
test.beforeEach(async ({ page }) => { 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.resume();
// Go to baseURL // Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' }); await page.goto('./', { waitUntil: 'domcontentloaded' });
displayLayout = await createDomainObjectWithDefaults(page, { type: 'Display Layout' }); displayLayout = await createDomainObjectWithDefaults(page, { type: 'Display Layout' });
await page.goto(displayLayout.url);
await createImageryView(page); // Create Example Imagery inside Display Layout
await createImageryViewWithShortDelay(page, {
await expect(page.locator('.l-browse-bar__object-name')).toContainText( name: 'Unnamed Example Imagery',
'Unnamed Example Imagery' parent: displayLayout.uuid
); });
await page.goto(displayLayout.url); await page.goto(displayLayout.url);
}); });
@ -390,7 +424,7 @@ test.describe('Example Imagery in Display Layout', () => {
await expect.soft(pausePlayButton).toHaveClass(/is-paused/); await expect.soft(pausePlayButton).toHaveClass(/is-paused/);
}); });
test('Imagery View operations @unstable', async ({ page }) => { test('Imagery View operations @clock', async ({ page }) => {
test.info().annotations.push({ test.info().annotations.push({
type: 'issue', type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5265' description: 'https://github.com/nasa/openmct/issues/5265'
@ -410,7 +444,7 @@ test.describe('Example Imagery in Display Layout', () => {
await page.locator('div[title="Resize object width"] > input').click(); await page.locator('div[title="Resize object width"] > input').click();
await page.locator('div[title="Resize object width"] > input').fill('50'); await page.locator('div[title="Resize object width"] > input').fill('50');
await performImageryViewOperationsAndAssert(page); await performImageryViewOperationsAndAssert(page, displayLayout);
}); });
test('Resizing the layout changes thumbnail visibility and size', async ({ page }) => { test('Resizing the layout changes thumbnail visibility and size', async ({ page }) => {
@ -454,7 +488,10 @@ test.describe('Example Imagery in Display Layout', () => {
type: 'issue', type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6709' description: 'https://github.com/nasa/openmct/issues/6709'
}); });
await createImageryView(page); await createImageryViewWithShortDelay(page, {
name: 'Unnamed Example Imagery',
parent: displayLayout.uuid
});
await page.goto(displayLayout.url); await page.goto(displayLayout.url);
const imageElements = page.locator('.c-imagery__main-image-wrapper'); const imageElements = page.locator('.c-imagery__main-image-wrapper');
@ -483,16 +520,21 @@ test.describe('Example Imagery in Display Layout', () => {
}); });
}); });
test.describe('Example Imagery in Flexible layout', () => { test.describe('Example Imagery in Flexible layout @clock', () => {
let flexibleLayout; let flexibleLayout;
test.beforeEach(async ({ page }) => { 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.resume();
await page.goto('./', { waitUntil: 'domcontentloaded' }); await page.goto('./', { waitUntil: 'domcontentloaded' });
flexibleLayout = await createDomainObjectWithDefaults(page, { type: 'Flexible Layout' }); flexibleLayout = await createDomainObjectWithDefaults(page, { type: 'Flexible Layout' });
// Create Example Imagery inside the Flexible Layout // Create Example Imagery inside the Flexible Layout
await createDomainObjectWithDefaults(page, { await createImageryViewWithShortDelay(page, {
type: 'Example Imagery', name: 'Unnamed Example Imagery',
parent: flexibleLayout.uuid parent: flexibleLayout.uuid
}); });
@ -502,61 +544,35 @@ test.describe('Example Imagery in Flexible layout', () => {
test('Can double-click on the image to view large image', async ({ page }) => { test('Can double-click on the image to view large image', async ({ page }) => {
// Double-click on the image to open large view // Double-click on the image to open large view
const imageElement = await page.getByRole('button', { name: 'Image Wrapper' }); const imageElement = page.getByRole('button', { name: 'Image Wrapper' });
await imageElement.dblclick(); await imageElement.dblclick();
// Check if the large view is visible // Check if the large view is visible
await page.getByRole('button', { name: 'Background Image', state: 'visible' }); page.getByRole('button', { name: 'Focused Image Element', state: 'visible' });
// Close the large view // Close the large view
await page.getByRole('button', { name: 'Close' }).click(); await page.getByRole('button', { name: 'Close' }).click();
}); });
test.beforeEach(async ({ page }) => { test('Imagery View operations @clock', async ({ page, browserName }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
flexibleLayout = await createDomainObjectWithDefaults(page, { type: 'Flexible Layout' });
await page.goto(flexibleLayout.url);
/* Create Sine Wave Generator with minimum Image Load Delay */
// Click the Create button
await page.getByRole('button', { name: 'Create' }).click();
// Click text=Example Imagery
await page.click('li[role="menuitem"]:has-text("Example Imagery")');
// Clear and set Image load delay to minimum value
await page.locator('input[type="number"]').fill('');
await page.locator('input[type="number"]').fill('5000');
// 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 expect(page.locator('.l-browse-bar__object-name')).toContainText(
'Unnamed Example Imagery'
);
await page.goto(flexibleLayout.url);
});
test('Imagery View operations @unstable', async ({ page, browserName }) => {
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox'); test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
test.info().annotations.push({ test.info().annotations.push({
type: 'issue', type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5326' description: 'https://github.com/nasa/openmct/issues/5326'
}); });
await performImageryViewOperationsAndAssert(page); await performImageryViewOperationsAndAssert(page, flexibleLayout);
}); });
}); });
test.describe('Example Imagery in Tabs View', () => { test.describe('Example Imagery in Tabs View @clock', () => {
let tabsView; let tabsView;
test.beforeEach(async ({ page }) => { 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.resume();
await page.goto('./', { waitUntil: 'domcontentloaded' }); await page.goto('./', { waitUntil: 'domcontentloaded' });
tabsView = await createDomainObjectWithDefaults(page, { type: 'Tabs View' }); tabsView = await createDomainObjectWithDefaults(page, { type: 'Tabs View' });
@ -570,12 +586,12 @@ test.describe('Example Imagery in Tabs View', () => {
await page.click('li[role="menuitem"]:has-text("Example Imagery")'); await page.click('li[role="menuitem"]:has-text("Example Imagery")');
// Clear and set Image load delay to minimum value // Clear and set Image load delay to minimum value
await page.locator('input[type="number"]').fill(''); await page.locator('input[type="number"]').clear();
await page.locator('input[type="number"]').fill('5000'); await page.locator('input[type="number"]').fill(`${IMAGE_LOAD_DELAY}`);
// Click text=OK // Click text=OK
await Promise.all([ await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle' }), page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
page.click('button:has-text("OK")'), page.click('button:has-text("OK")'),
//Wait for Save Banner to appear //Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message') page.waitForSelector('.c-message-banner__message')
@ -587,8 +603,8 @@ test.describe('Example Imagery in Tabs View', () => {
await page.goto(tabsView.url); await page.goto(tabsView.url);
}); });
test('Imagery View operations @unstable', async ({ page }) => { test('Imagery View operations @clock', async ({ page }) => {
await performImageryViewOperationsAndAssert(page); await performImageryViewOperationsAndAssert(page, tabsView);
}); });
}); });
@ -652,20 +668,21 @@ test.describe('Example Imagery in Time Strip', () => {
* 7. Image brightness/contrast can be adjusted by dragging the sliders * 7. Image brightness/contrast can be adjusted by dragging the sliders
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function performImageryViewOperationsAndAssert(page) { async function performImageryViewOperationsAndAssert(page, layoutObject) {
// Verify that imagery thumbnails use a thumbnail url // Verify that imagery thumbnails use a thumbnail url
const thumbnailImages = page.locator('.c-thumb__image'); const thumbnailImages = page.getByLabel('Image thumbnail from').locator('.c-thumb__image');
const mainImage = page.locator('.c-imagery__main-image__image'); const mainImage = page.locator('.c-imagery__main-image__image');
await expect(thumbnailImages.first()).toHaveAttribute('src', thumbnailUrlParamsRegexp); await expect(thumbnailImages.first()).toHaveAttribute('src', thumbnailUrlParamsRegexp);
await expect(mainImage).not.toHaveAttribute('src', thumbnailUrlParamsRegexp); await expect(mainImage).not.toHaveAttribute('src', thumbnailUrlParamsRegexp);
// Click previous image button // Click previous image button
const previousImageButton = page.locator('.c-nav--prev'); const previousImageButton = page.getByLabel('Previous image');
await previousImageButton.click(); await expect(previousImageButton).toBeVisible();
await page.getByLabel('Image Wrapper').hover({ trial: true });
// Verify previous image // Need to force click as the annotation canvas lies on top of the image
const selectedImage = page.locator('.selected'); // and fails the accessibility checks
await expect(selectedImage).toBeVisible(); // eslint-disable-next-line playwright/no-force-option
await previousImageButton.click({ force: true });
// Use the zoom buttons to zoom in and out // Use the zoom buttons to zoom in and out
await buttonZoomOnImageAndAssert(page); await buttonZoomOnImageAndAssert(page);
@ -680,42 +697,51 @@ async function performImageryViewOperationsAndAssert(page) {
await mouseZoomOnImageAndAssert(page, -2); await mouseZoomOnImageAndAssert(page, -2);
// Click next image button // Click next image button
const nextImageButton = page.locator('.c-nav--next'); const nextImageButton = page.getByLabel('Next image');
await nextImageButton.click(); await expect(nextImageButton).toBeVisible();
await page.getByLabel('Image Wrapper').hover({ trial: true });
// eslint-disable-next-line playwright/no-force-option
await nextImageButton.click({ force: true });
// set realtime mode // set realtime mode
await setRealTimeMode(page); await navigateToObjectWithRealTime(
page,
layoutObject.url,
`${FIVE_MINUTES}`,
`${THIRTY_SECONDS}`
);
// Verify previous image
await expect(previousImageButton).toBeVisible();
await page.getByLabel('Image Wrapper').hover({ trial: true });
// eslint-disable-next-line playwright/no-force-option
await previousImageButton.click({ force: true });
await page.locator('.active').click();
const selectedImage = page.locator('.selected');
await expect(selectedImage).toBeVisible();
// Zoom in on next image // Zoom in on next image
await mouseZoomOnImageAndAssert(page, 2); await mouseZoomOnImageAndAssert(page, 2);
// Clicking on the left arrow should pause the imagery and go to previous image // Clicking on the left arrow should pause the imagery and go to previous image
await previousImageButton.click(); await previousImageButton.click();
await expect(page.locator('.c-button.pause-play')).toHaveClass(/is-paused/); await expect(page.getByLabel('Pause automatic scrolling of image thumbnails')).toBeVisible();
await expect(selectedImage).toBeVisible(); await expect(selectedImage).toBeVisible();
// The imagery view should be updated when new images come in
const imageCount = await page.locator('.c-imagery__thumb').count();
await expect
.poll(
async () => {
const newImageCount = await page.locator('.c-imagery__thumb').count();
return newImageCount;
},
{
message: 'verify that old images are discarded',
timeout: 7 * 1000
}
)
.toBe(imageCount);
// Verify selected image is still displayed // Verify selected image is still displayed
await expect(selectedImage).toBeVisible(); await expect(selectedImage).toBeVisible();
// Unpause imagery // Unpause imagery
await page.locator('.pause-play').click(); await page.locator('.pause-play').click();
// verify that old images are discarded
const lastImageInBounds = page.getByLabel('Image thumbnail from').first();
const lastImageTimestamp = await lastImageInBounds.getAttribute('title');
expect(lastImageTimestamp).not.toBeNull();
// go forward in time to ensure old images are discarded
await page.clock.fastForward(IMAGE_LOAD_DELAY);
await page.clock.resume();
await expect(page.getByLabel(lastImageTimestamp)).toBeHidden();
//Get background-image url from background-image css prop //Get background-image url from background-image css prop
await assertBackgroundImageUrlFromBackgroundCss(page); await assertBackgroundImageUrlFromBackgroundCss(page);
@ -773,7 +799,7 @@ async function dragContrastSliderAndAssertFilterValues(page) {
* Gets the filter:brightness value of the current background-image and * Gets the filter:brightness value of the current background-image and
* asserts against an expected value * asserts against an expected value
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {String} expected The expected brightness value * @param {string} expected The expected brightness value
*/ */
async function assertBackgroundImageBrightness(page, expected) { async function assertBackgroundImageBrightness(page, expected) {
const backgroundImage = page.locator('.c-imagery__main-image__background-image'); const backgroundImage = page.locator('.c-imagery__main-image__background-image');
@ -789,38 +815,18 @@ async function assertBackgroundImageBrightness(page, expected) {
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function assertBackgroundImageUrlFromBackgroundCss(page) { async function assertBackgroundImageUrlFromBackgroundCss(page) {
const backgroundImage = page.locator('.c-imagery__main-image__background-image'); const backgroundImage = page.getByLabel('Focused Image Element');
let backgroundImageUrl = await backgroundImage.evaluate((el) => { const backgroundImageUrl = await backgroundImage.evaluate((el) => {
return window return window
.getComputedStyle(el) .getComputedStyle(el)
.getPropertyValue('background-image') .getPropertyValue('background-image')
.match(/url\(([^)]+)\)/)[1]; .match(/url\(([^)]+)\)/)[1];
}); });
let backgroundImageUrl1 = backgroundImageUrl.slice(1, -1); //forgive me, padre
console.log('backgroundImageUrl1 ' + backgroundImageUrl1);
let backgroundImageUrl2; // go forward in time to ensure old images are discarded
await expect await page.clock.fastForward(IMAGE_LOAD_DELAY);
.poll( await page.clock.resume();
async () => { await expect(backgroundImage).not.toHaveJSProperty('background-image', backgroundImageUrl);
// Verify next image has updated
let backgroundImageUrlNext = await backgroundImage.evaluate((el) => {
return window
.getComputedStyle(el)
.getPropertyValue('background-image')
.match(/url\(([^)]+)\)/)[1];
});
backgroundImageUrl2 = backgroundImageUrlNext.slice(1, -1); //forgive me, padre
return backgroundImageUrl2;
},
{
message: 'verify next image has updated',
timeout: 7 * 1000
}
)
.not.toBe(backgroundImageUrl1);
console.log('backgroundImageUrl2 ' + backgroundImageUrl2);
} }
/** /**
@ -829,7 +835,7 @@ async function assertBackgroundImageUrlFromBackgroundCss(page) {
async function panZoomAndAssertImageProperties(page) { async function panZoomAndAssertImageProperties(page) {
const imageryHintsText = await page.locator('.c-imagery__hints').innerText(); const imageryHintsText = await page.locator('.c-imagery__hints').innerText();
expect(expectedAltText).toEqual(imageryHintsText); expect(expectedAltText).toEqual(imageryHintsText);
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const zoomedBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
@ -839,7 +845,7 @@ async function panZoomAndAssertImageProperties(page) {
await page.mouse.move(imageCenterX - 200, imageCenterY, 10); await page.mouse.move(imageCenterX - 200, imageCenterY, 10);
await page.mouse.up(); await page.mouse.up();
await Promise.all(panHotkey.map((x) => page.keyboard.up(x))); await Promise.all(panHotkey.map((x) => page.keyboard.up(x)));
const afterRightPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const afterRightPanBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x); expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);
// Pan left // Pan left
@ -848,7 +854,7 @@ async function panZoomAndAssertImageProperties(page) {
await page.mouse.move(imageCenterX, imageCenterY, 10); await page.mouse.move(imageCenterX, imageCenterY, 10);
await page.mouse.up(); await page.mouse.up();
await Promise.all(panHotkey.map((x) => page.keyboard.up(x))); await Promise.all(panHotkey.map((x) => page.keyboard.up(x)));
const afterLeftPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const afterLeftPanBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x); expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);
// Pan up // Pan up
@ -858,7 +864,7 @@ async function panZoomAndAssertImageProperties(page) {
await page.mouse.move(imageCenterX, imageCenterY + 200, 10); await page.mouse.move(imageCenterX, imageCenterY + 200, 10);
await page.mouse.up(); await page.mouse.up();
await Promise.all(panHotkey.map((x) => page.keyboard.up(x))); await Promise.all(panHotkey.map((x) => page.keyboard.up(x)));
const afterUpPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const afterUpPanBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
expect(afterUpPanBoundingBox.y).toBeGreaterThanOrEqual(afterLeftPanBoundingBox.y); expect(afterUpPanBoundingBox.y).toBeGreaterThanOrEqual(afterLeftPanBoundingBox.y);
// Pan down // Pan down
@ -867,7 +873,7 @@ async function panZoomAndAssertImageProperties(page) {
await page.mouse.move(imageCenterX, imageCenterY - 200, 10); await page.mouse.move(imageCenterX, imageCenterY - 200, 10);
await page.mouse.up(); await page.mouse.up();
await Promise.all(panHotkey.map((x) => page.keyboard.up(x))); await Promise.all(panHotkey.map((x) => page.keyboard.up(x)));
const afterDownPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const afterDownPanBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
expect(afterDownPanBoundingBox.y).toBeLessThanOrEqual(afterUpPanBoundingBox.y); expect(afterDownPanBoundingBox.y).toBeLessThanOrEqual(afterUpPanBoundingBox.y);
} }
@ -879,19 +885,20 @@ async function panZoomAndAssertImageProperties(page) {
*/ */
async function mouseZoomOnImageAndAssert(page, factor = 2) { async function mouseZoomOnImageAndAssert(page, factor = 2) {
// Zoom in // Zoom in
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox(); await page.getByLabel('Focused Image Element').hover({ trial: true });
const deltaYStep = 100; // equivalent to 1x zoom const originalImageDimensions = await page.getByLabel('Focused Image Element').boundingBox();
await page.mouse.wheel(0, deltaYStep * factor); await page.mouse.wheel(0, MOUSE_WHEEL_DELTA_Y * factor);
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); await waitForZoomAndPanTransitions(page);
const zoomedBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
// center the mouse pointer // center the mouse pointer
await page.mouse.move(imageCenterX, imageCenterY); await page.mouse.move(imageCenterX, imageCenterY);
// Wait for zoom animation to finish // Wait for zoom animation to finish and get the new image dimensions
await page.locator('.c-imagery__main-image__bg').hover({ trial: true }); const imageMouseZoomed = await page.getByLabel('Focused Image Element').boundingBox();
const imageMouseZoomed = await page.locator(backgroundImageSelector).boundingBox();
if (factor > 0) { if (factor > 0) {
expect(imageMouseZoomed.height).toBeGreaterThan(originalImageDimensions.height); expect(imageMouseZoomed.height).toBeGreaterThan(originalImageDimensions.height);
@ -908,29 +915,61 @@ async function mouseZoomOnImageAndAssert(page, factor = 2) {
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function buttonZoomOnImageAndAssert(page) { async function buttonZoomOnImageAndAssert(page) {
// Lock the zoom and pan so it doesn't reset if a new image comes in
await page.getByLabel('Focused Image Element').hover({ trial: true });
const lockButton = page.getByRole('button', {
name: 'Lock current zoom and pan across all images'
});
if (!(await lockButton.isVisible())) {
await page.getByLabel('Focused Image Element').hover({ trial: true });
}
await lockButton.click();
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
'style.transform',
'scale(1) translate(0px, 0px)'
);
// Get initial image dimensions // Get initial image dimensions
const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const initialBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
// Zoom in twice via button // Zoom in twice via button
await zoomIntoImageryByButton(page); await zoomIntoImageryByButton(page);
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
'style.transform',
'scale(2) translate(0px, 0px)'
);
await zoomIntoImageryByButton(page); await zoomIntoImageryByButton(page);
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
'style.transform',
'scale(3) translate(0px, 0px)'
);
// Get and assert zoomed in image dimensions // Get and assert zoomed in image dimensions
const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const zoomedInBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height); expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width); expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
// Zoom out once via button // Zoom out once via button
await zoomOutOfImageryByButton(page); await zoomOutOfImageryByButton(page);
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
'style.transform',
'scale(2) translate(0px, 0px)'
);
// Get and assert zoomed out image dimensions // Get and assert zoomed out image dimensions
const zoomedOutBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const zoomedOutBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height); expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width); expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
// Zoom out again via button, assert against the initial image dimensions // Zoom out again via button, assert against the initial image dimensions
await zoomOutOfImageryByButton(page); await zoomOutOfImageryByButton(page);
const finalBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
'style.transform',
'scale(1) translate(0px, 0px)'
);
const finalBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
expect(finalBoundingBox).toEqual(initialBoundingBox); expect(finalBoundingBox).toEqual(initialBoundingBox);
} }
@ -938,7 +977,7 @@ async function buttonZoomOnImageAndAssert(page) {
* Gets the filter:contrast value of the current background-image and * Gets the filter:contrast value of the current background-image and
* asserts against an expected value * asserts against an expected value
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {String} expected The expected contrast value * @param {string} expected The expected contrast value
*/ */
async function assertBackgroundImageContrast(page, expected) { async function assertBackgroundImageContrast(page, expected) {
const backgroundImage = page.locator('.c-imagery__main-image__background-image'); const backgroundImage = page.locator('.c-imagery__main-image__background-image');
@ -957,16 +996,11 @@ async function assertBackgroundImageContrast(page, expected) {
*/ */
async function zoomIntoImageryByButton(page) { async function zoomIntoImageryByButton(page) {
// FIXME: There should only be one set of imagery buttons, but there are two? // FIXME: There should only be one set of imagery buttons, but there are two?
const zoomInBtn = page const zoomInBtn = page.getByRole('button', { name: 'Zoom in' });
.locator("[role='toolbar'][aria-label='Image controls'] .t-btn-zoom-in") const backgroundImage = page.getByLabel('Focused Image Element');
.nth(0); await backgroundImage.hover({ trial: true });
const backgroundImage = page.locator(backgroundImageSelector);
if (!(await zoomInBtn.isVisible())) {
await backgroundImage.hover({ trial: true });
}
await zoomInBtn.click(); await zoomInBtn.click();
await waitForAnimations(backgroundImage); await waitForZoomAndPanTransitions(page);
} }
/** /**
@ -975,17 +1009,11 @@ async function zoomIntoImageryByButton(page) {
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function zoomOutOfImageryByButton(page) { async function zoomOutOfImageryByButton(page) {
// FIXME: There should only be one set of imagery buttons, but there are two? const zoomOutBtn = page.getByRole('button', { name: 'Zoom out' });
const zoomOutBtn = page const backgroundImage = page.getByLabel('Focused Image Element');
.locator("[role='toolbar'][aria-label='Image controls'] .t-btn-zoom-out") await backgroundImage.hover({ trial: true });
.nth(0);
const backgroundImage = page.locator(backgroundImageSelector);
if (!(await zoomOutBtn.isVisible())) {
await backgroundImage.hover({ trial: true });
}
await zoomOutBtn.click(); await zoomOutBtn.click();
await waitForAnimations(backgroundImage); await waitForZoomAndPanTransitions(page);
} }
/** /**
@ -994,38 +1022,43 @@ async function zoomOutOfImageryByButton(page) {
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function resetImageryPanAndZoom(page) { async function resetImageryPanAndZoom(page) {
// FIXME: There should only be one set of imagery buttons, but there are two? const panZoomResetBtn = page.getByRole('button', { name: 'Remove zoom and pan' });
const panZoomResetBtn = page await expect(panZoomResetBtn).toBeVisible();
.locator("[role='toolbar'][aria-label='Image controls'] .t-btn-zoom-reset") await panZoomResetBtn.hover({ trial: true });
.nth(0);
const backgroundImage = page.locator(backgroundImageSelector);
if (!(await panZoomResetBtn.isVisible())) {
await backgroundImage.hover({ trial: true });
}
await panZoomResetBtn.click(); await panZoomResetBtn.click();
await waitForAnimations(backgroundImage);
await waitForZoomAndPanTransitions(page);
await expect(page.getByText('Alt drag to pan')).toBeHidden();
await expect(page.locator('.c-thumb__viewable-area')).toBeHidden();
} }
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function createImageryView(page) { async function createImageryViewWithShortDelay(page, { name, parent }) {
// Click the Create button await createDomainObjectWithDefaults(page, {
await page.getByRole('button', { name: 'Create' }).click(); name,
type: 'Example Imagery',
// Click text=Example Imagery parent
await page.click('li[role="menuitem"]:has-text("Example Imagery")'); });
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
await page.getByLabel('More actions').click();
await page.getByLabel('Edit Properties').click();
// Clear and set Image load delay to minimum value // Clear and set Image load delay to minimum value
await page.locator('input[type="number"]').fill(''); await page.locator('input[type="number"]').fill(`${IMAGE_LOAD_DELAY}`);
await page.locator('input[type="number"]').fill('5000'); await page.getByLabel('Save').click();
}
// Click text=OK
await Promise.all([ /**
page.waitForNavigation({ waitUntil: 'networkidle' }), * @param {import('@playwright/test').Page} page
page.click('button:has-text("OK")'), */
//Wait for Save Banner to appear // eslint-disable-next-line require-await
page.waitForSelector('.c-message-banner__message') async function waitForZoomAndPanTransitions(page) {
]); // Wait for image to stabilize
await page.getByLabel('Focused Image Element').hover({ trial: true });
// Wait for zoom to end
await expect(page.getByLabel('Focused Image Element')).not.toHaveClass(/is-zooming|is-panning/);
// Wait for image to stabilize
await page.getByLabel('Focused Image Element').hover({ trial: true });
} }

View File

@ -152,7 +152,7 @@ test.describe('ExportAsJSON Disabled Actions', () => {
test.describe('ExportAsJSON ProgressBar @couchdb', () => { test.describe('ExportAsJSON ProgressBar @couchdb', () => {
let folder; let folder;
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'domcontentloaded' });
// Perform actions to create the domain object // Perform actions to create the domain object
folder = await createDomainObjectWithDefaults(page, { folder = await createDomainObjectWithDefaults(page, {
type: 'Folder' type: 'Folder'

View File

@ -27,7 +27,6 @@ import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Testing numeric data with inspector data visualization (i.e., data pivoting)', () => { test.describe('Testing numeric data with inspector data visualization (i.e., data pivoting)', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
// eslint-disable-next-line no-undef
await page.addInitScript({ await page.addInitScript({
path: fileURLToPath( path: fileURLToPath(
new URL('../../../../helper/addInitDataVisualization.js', import.meta.url) new URL('../../../../helper/addInitDataVisualization.js', import.meta.url)
@ -37,6 +36,8 @@ 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 }) => { test('Can click on telemetry and see data in inspector @2p', async ({ page, context }) => {
const initStartBounds = await page.getByLabel('Start bounds').textContent();
const initEndBounds = await page.getByLabel('End bounds').textContent();
const exampleDataVisualizationSource = await createDomainObjectWithDefaults(page, { const exampleDataVisualizationSource = await createDomainObjectWithDefaults(page, {
type: 'Example Data Visualization Source' type: 'Example Data Visualization Source'
}); });
@ -78,5 +79,9 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
await newPage.waitForLoadState(); await newPage.waitForLoadState();
// expect new tab title to contain 'Second Sine Wave Generator' // expect new tab title to contain 'Second Sine Wave Generator'
await expect(newPage).toHaveTitle('Second Sine Wave Generator'); 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());
expect(initEndBounds).toEqual(await newPage.getByLabel('End bounds').textContent());
}); });
}); });

View File

@ -0,0 +1,86 @@
/*****************************************************************************
* 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.
*****************************************************************************/
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('LAD Table', () => {
let ladTable;
let swg;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
ladTable = await createDomainObjectWithDefaults(page, {
type: 'LAD Table'
});
swg = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: ladTable.uuid
});
await page.goto(ladTable.url);
});
test('Ensure we have numbers in cells', async ({ page }) => {
// Wait for the initial value to show after mount
await expect(page.getByLabel('lad value').first()).not.toContainText('---');
const valueFromFirstSineWave = await page.getByLabel('lad value').first().innerText();
const firstSineWaveNumber = parseFloat(valueFromFirstSineWave);
// ensure we have a float value in the cell and it's finite
expect(Number.isFinite(firstSineWaveNumber)).toBeTruthy();
const valueFromSecondSineWave = await page.getByLabel('lad value').last().innerText();
const secondSineWaveNumber = parseFloat(valueFromSecondSineWave);
// ensure we have a float value in the cell and it's finite
expect(Number.isFinite(secondSineWaveNumber)).toBeTruthy();
});
test(
'Can remove telemetry from composition',
{
annotation: {
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7633'
}
},
async ({ page }) => {
// Assert that the table is initially populated
await expect(page.getByLabel('lad row')).toHaveCount(1);
// Expand the tree so the SWG is visible
await page.getByLabel('Expand My Items').click();
await page.getByLabel('Expand LAD Table').click();
// 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();
// Assert that the SWG is no longer in the tree and the table is empty
await expect(page.getByRole('treeitem', { name: swg.name })).toBeHidden();
await expect(page.getByLabel('lad row')).toHaveCount(0);
}
);
});

View File

@ -27,6 +27,7 @@ This test suite is dedicated to tests which verify the basic operations surround
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { createDomainObjectWithDefaults } from '../../../../appActions.js'; import { createDomainObjectWithDefaults } from '../../../../appActions.js';
import { copy, paste, selectAll } from '../../../../helper/hotkeys/hotkeys.js';
import * as nbUtils from '../../../../helper/notebookUtils.js'; import * as nbUtils from '../../../../helper/notebookUtils.js';
import { expect, streamToString, test } from '../../../../pluginFixtures.js'; import { expect, streamToString, test } from '../../../../pluginFixtures.js';
@ -277,7 +278,6 @@ test.describe('Notebook entry tests', () => {
// Create Notebook with URL Whitelist // Create Notebook with URL Whitelist
let notebookObject; let notebookObject;
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
// eslint-disable-next-line no-undef
await page.addInitScript({ await page.addInitScript({
path: fileURLToPath(new URL('../../../../helper/addInitNotebookWithUrls.js', import.meta.url)) path: fileURLToPath(new URL('../../../../helper/addInitNotebookWithUrls.js', import.meta.url))
}); });
@ -308,7 +308,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url); await page.goto(notebookObject.url);
// Reveal the notebook in the tree // Reveal the notebook in the tree
await page.getByTitle('Show selected item in tree').click(); await page.getByLabel('Show selected item in tree').click();
await page await page
.getByRole('treeitem', { name: overlayPlot.name }) .getByRole('treeitem', { name: overlayPlot.name })
@ -332,7 +332,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url); await page.goto(notebookObject.url);
// Reveal the notebook in the tree // Reveal the notebook in the tree
await page.getByTitle('Show selected item in tree').click(); await page.getByLabel('Show selected item in tree').click();
await nbUtils.enterTextEntry(page, 'Entry to drop into'); await nbUtils.enterTextEntry(page, 'Entry to drop into');
await page await page
@ -377,7 +377,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url); await page.goto(notebookObject.url);
// Reveal the notebook in the tree // Reveal the notebook in the tree
await page.getByTitle('Show selected item in tree').click(); await page.getByLabel('Show selected item in tree').click();
await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`); await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`);
@ -404,7 +404,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url); await page.goto(notebookObject.url);
// Reveal the notebook in the tree // Reveal the notebook in the tree
await page.getByTitle('Show selected item in tree').click(); await page.getByLabel('Show selected item in tree').click();
await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`); await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`);
@ -421,7 +421,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url); await page.goto(notebookObject.url);
// Reveal the notebook in the tree // Reveal the notebook in the tree
await page.getByTitle('Show selected item in tree').click(); await page.getByLabel('Show selected item in tree').click();
await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`); await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`);
@ -438,7 +438,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url); await page.goto(notebookObject.url);
// Reveal the notebook in the tree // Reveal the notebook in the tree
await page.getByTitle('Show selected item in tree').click(); await page.getByLabel('Show selected item in tree').click();
await nbUtils.enterTextEntry(page, `This should be a link: ${INVALID_TEST_LINK} is it?`); await nbUtils.enterTextEntry(page, `This should be a link: ${INVALID_TEST_LINK} is it?`);
@ -455,7 +455,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url); await page.goto(notebookObject.url);
// Reveal the notebook in the tree // Reveal the notebook in the tree
await page.getByTitle('Show selected item in tree').click(); await page.getByLabel('Show selected item in tree').click();
await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`); await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`);
@ -483,7 +483,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url); await page.goto(notebookObject.url);
// Reveal the notebook in the tree // Reveal the notebook in the tree
await page.getByTitle('Show selected item in tree').click(); await page.getByLabel('Show selected item in tree').click();
await nbUtils.enterTextEntry( await nbUtils.enterTextEntry(
page, page,
@ -547,4 +547,53 @@ test.describe('Notebook entry tests', () => {
); );
await expect(secondLineOfBlockquoteText).toBeVisible(); await expect(secondLineOfBlockquoteText).toBeVisible();
}); });
/**
* Paste into notebook entry tests
*/
test('Can paste text into a notebook entry', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7686'
});
const TEST_TEXT = 'This is a test';
const iterations = 20;
const EXPECTED_TEXT = TEST_TEXT.repeat(iterations);
await page.goto(notebookObject.url);
await nbUtils.addNotebookEntry(page);
await nbUtils.enterTextInLastEntry(page, TEST_TEXT);
await selectAll(page);
await copy(page);
for (let i = 0; i < iterations; i++) {
await paste(page);
}
await nbUtils.commitEntry(page);
await expect(page.locator(`text="${EXPECTED_TEXT}"`)).toBeVisible();
});
test('Prevents pasting text into selected notebook entry if not editing', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7686'
});
const TEST_TEXT = 'This is a test';
await page.goto(notebookObject.url);
await nbUtils.addNotebookEntry(page);
await nbUtils.enterTextInLastEntry(page, TEST_TEXT);
await selectAll(page);
await copy(page);
await paste(page);
await nbUtils.commitEntry(page);
// This should not paste text into the entry
await paste(page);
await expect(await page.locator(`text="${TEST_TEXT.repeat(1)}"`).count()).toEqual(1);
await expect(await page.locator(`text="${TEST_TEXT.repeat(2)}"`).count()).toEqual(0);
});
}); });

View File

@ -71,42 +71,89 @@ test.describe('Snapshot Container tests', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
//Navigate to baseURL //Navigate to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' }); await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.getByLabel('Open the Notebook Snapshot Menu').click();
// Create Notebook
// const notebook = await createDomainObjectWithDefaults(page, {
// type: 'Notebook',
// name: "Test Notebook"
// });
// // Create Overlay Plot
// const snapShotObject = await createDomainObjectWithDefaults(page, {
// type: 'Overlay Plot',
// name: "Dropped Overlay Plot"
// });
await page.getByLabel('Take a Notebook Snapshot').click();
await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click(); await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();
await page.getByLabel('Show Snapshots').click(); await page.getByLabel('Show Snapshots').click();
}); });
test('A snapshot can be Quick Viewed from Container with 3 dot action menu', async ({ page }) => { test('A snapshot can be Quick Viewed from Container with 3 dot action menu', async ({ page }) => {
await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More actions').click(); await page.getByLabel('My Items Notebook Embed').getByLabel('More actions').click();
await page.getByRole('menuitem', { name: 'Quick View' }).click(); await page.getByRole('menuitem', { name: 'Quick View' }).click();
await expect(page.locator('.c-overlay__outer')).toBeVisible(); await expect(page.getByLabel('Modal Overlay')).toBeVisible();
await expect(page.getByLabel('Preview Container')).toBeVisible();
});
test('A snapshot can be Viewed, Annotated, display deleted, and saved from Container with 3 dot action menu', async ({
page
}) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7552'
});
//Open Snapshot Object View
await page.getByLabel('My Items Notebook Embed').getByLabel('More actions').click();
await page.getByRole('menuitem', { name: 'View Snapshot' }).click();
await expect(page.getByRole('dialog', { name: 'Modal Overlay' })).toBeVisible();
await expect(page.locator('#snapshotDescriptor')).toHaveText(
/SNAPSHOT \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/
);
// Open Annotation Editor with Painterro
await page.getByLabel('Annotate this snapshot').click();
await expect(page.locator('#snap-annotation-canvas')).toBeVisible();
// Clear the canvas
await page.getByRole('button', { name: 'Put text [T]' }).click();
// Click in the Painterro canvas to add a text annotation
await page.locator('.ptro-crp-el').click();
await page.locator('.ptro-text-tool-input').fill('...is there life on mars?');
// When working with Painterro, we need to check that the Apply button is hidden after clicking
await page.getByTitle('Apply').click();
await expect(page.getByTitle('Apply')).toBeHidden();
// Save and exit annotation window
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: 'Done' }).click();
// Open up annotation again
await page.getByRole('img', { name: 'My Items thumbnail' }).click();
await expect(page.getByLabel('Modal Overlay').getByRole('img')).toBeVisible();
});
test('A snapshot can be Annotated and saved as a JPG and PNG', async ({ page }) => {
//Open Snapshot Object View
await page.getByLabel('My Items Notebook Embed').getByLabel('More actions').click();
await page.getByRole('menuitem', { name: 'View Snapshot' }).click();
await expect(page.getByRole('dialog', { name: 'Modal Overlay' })).toBeVisible();
// Open Annotation Editor with Painterro
await page.getByLabel('Annotate this snapshot').click();
await expect(page.locator('#snap-annotation-canvas')).toBeVisible();
// Clear the canvas
await page.getByRole('button', { name: 'Put text [T]' }).click();
// Click in the Painterro canvas to add a text annotation
await page.locator('.ptro-crp-el').click();
await page.locator('.ptro-text-tool-input').fill('...is there life on mars?');
// When working with Painterro, we need to check that the Apply button is hidden after clicking
await page.getByTitle('Apply').click();
await expect(page.getByTitle('Apply')).toBeHidden();
// Save and exit annotation window
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: 'Done' }).click();
// Open up annotation again
await page.getByRole('img', { name: 'My Items thumbnail' }).click();
await expect(page.getByLabel('Modal Overlay').getByRole('img')).toBeVisible();
// Save as JPG
await Promise.all([
page.waitForEvent('download'), // Waits for the download event
page.getByLabel('Export as JPG').click() // Triggers the download
]);
// Save as PNG
await expect(page.getByLabel('Modal Overlay').getByRole('img')).toBeVisible();
await Promise.all([
page.waitForEvent('download'), // Waits for the download event
page.getByLabel('Export as PNG').click() // Triggers the download
]);
}); });
test.fixme(
'A snapshot can be Viewed, Annotated, display deleted, and saved from Container with 3 dot action menu',
async ({ page }) => {
await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More actions').click();
await page.getByRole('menuitem', { name: ' View Snapshot' }).click();
await expect(page.locator('.c-overlay__outer')).toBeVisible();
await page.getByTitle('Annotate').click();
await expect(page.locator('#snap-annotation-canvas')).toBeVisible();
await page.getByRole('button', { name: '' }).click();
// await expect(page.locator('#snap-annotation-canvas')).not.toBeVisible();
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: 'Done' }).click();
//await expect(await page.locator)
}
);
test.fixme('5 Snapshots can be added to a container', async ({ page }) => {}); test.fixme('5 Snapshots can be added to a container', async ({ page }) => {});
test.fixme( test.fixme(
'5 Snapshots can be added to a container and Deleted with Delete All action', '5 Snapshots can be added to a container and Deleted with Delete All action',
@ -116,10 +163,6 @@ test.describe('Snapshot Container tests', () => {
'A snapshot can be Deleted from Container with 3 dot action menu', 'A snapshot can be Deleted from Container with 3 dot action menu',
async ({ page }) => {} async ({ page }) => {}
); );
test.fixme(
'A snapshot can be Navigated To from Container with 3 dot action menu',
async ({ page }) => {}
);
test.fixme( test.fixme(
'A snapshot can be Navigated To Item in Time from Container with 3 dot action menu', 'A snapshot can be Navigated To Item in Time from Container with 3 dot action menu',
async ({ page }) => {} async ({ page }) => {}
@ -151,11 +194,4 @@ test.describe('Snapshot Container tests', () => {
//Snapshot removed from container? //Snapshot removed from container?
} }
); );
test.fixme(
'Verify Embedded options for PNG, JPG, and Annotate work correctly',
async ({ page }) => {
//Add snapshot to container
//Verify PNG, JPG, and Annotate buttons work correctly
}
);
}); });

View File

@ -37,7 +37,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
// Create Notebook // Create Notebook
testNotebook = await createDomainObjectWithDefaults(page, { type: '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 }) => { test('Inspect Notebook Entry Network Requests', async ({ page }) => {
@ -58,7 +58,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
page.click('[aria-label="Add Page"]') page.click('[aria-label="Add Page"]')
]); ]);
// Ensures that there are no other network requests // Ensures that there are no other network requests
await page.waitForLoadState('networkidle'); await page.waitForLoadState('domcontentloaded');
// Assert that only two requests are made // Assert that only two requests are made
// Network Requests are: // Network Requests are:
@ -77,7 +77,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
// 2) The shared worker event from 👆 POST request // 2) The shared worker event from 👆 POST request
notebookElementsRequests = []; notebookElementsRequests = [];
await nbUtils.enterTextEntry(page, 'First Entry'); await nbUtils.enterTextEntry(page, 'First Entry');
await page.waitForLoadState('networkidle'); await page.waitForLoadState('domcontentloaded');
expect(notebookElementsRequests.length).toBeLessThanOrEqual(2); expect(notebookElementsRequests.length).toBeLessThanOrEqual(2);
// Add some tags // Add some tags
@ -141,7 +141,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
// 4) The shared worker event from 👆 POST request // 4) The shared worker event from 👆 POST request
notebookElementsRequests = []; notebookElementsRequests = [];
await nbUtils.enterTextEntry(page, 'Fourth Entry'); await nbUtils.enterTextEntry(page, 'Fourth Entry');
page.waitForLoadState('networkidle'); page.waitForLoadState('domcontentloaded');
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4); 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 // 4) The shared worker event from 👆 POST request
notebookElementsRequests = []; notebookElementsRequests = [];
await nbUtils.enterTextEntry(page, 'Fifth Entry'); await nbUtils.enterTextEntry(page, 'Fifth Entry');
page.waitForLoadState('networkidle'); page.waitForLoadState('domcontentloaded');
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4); 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 // 4) The shared worker event from 👆 POST request
notebookElementsRequests = []; notebookElementsRequests = [];
await nbUtils.enterTextEntry(page, 'Sixth Entry'); await nbUtils.enterTextEntry(page, 'Sixth Entry');
page.waitForLoadState('networkidle'); page.waitForLoadState('domcontentloaded');
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4); expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4);
}); });
@ -227,7 +227,7 @@ async function addTagAndAwaitNetwork(page, tagName) {
page.locator(`[aria-label="Autocomplete Options"] >> text=${tagName}`).click(), page.locator(`[aria-label="Autocomplete Options"] >> text=${tagName}`).click(),
expect(page.locator(`[aria-label="Tag"]:has-text("${tagName}")`)).toBeVisible() 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 expect(page.locator(`[aria-label="Tag"]:has-text("${tagName}")`)).toBeHidden();
await page.waitForLoadState('networkidle'); await page.waitForLoadState('domcontentloaded');
} }

View File

@ -61,9 +61,12 @@ test.describe('Autoscale', () => {
await page.getByLabel('Edit Object').click(); await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'Config' }).click(); await page.getByRole('tab', { name: 'Config' }).click();
await turnOffAutoscale(page);
await setUserDefinedMinAndMax(page, '-2', '2'); // turn off autoscale
await page.getByRole('checkbox', { name: 'Auto scale' }).uncheck();
await page.getByLabel('Y Axis 1 Minimum value').fill('-2');
await page.getByLabel('Y Axis 1 Maximum value').fill('2');
// save // save
await page.click('button[title="Save"]'); await page.click('button[title="Save"]');
@ -127,26 +130,6 @@ test.describe('Autoscale', () => {
}); });
}); });
/**
* @param {import('@playwright/test').Page} page
*/
async function turnOffAutoscale(page) {
// uncheck autoscale
await page.getByRole('checkbox', { name: 'Auto scale' }).uncheck();
}
/**
* @param {import('@playwright/test').Page} page
* @param {string} min
* @param {string} max
*/
async function setUserDefinedMinAndMax(page, min, max) {
// set minimum value
await page.getByRole('spinbutton').first().fill(min);
// set maximum value
await page.getByRole('spinbutton').nth(1).fill(max);
}
/** /**
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */

View File

@ -24,58 +24,62 @@
Tests to verify log plot functionality when objects are missing Tests to verify log plot functionality when objects are missing
*/ */
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js'; import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Handle missing object for plots', () => { test.describe('Handle missing object for plots', () => {
test('Displays empty div for missing stacked plot item @unstable', async ({ test.beforeEach(async ({ page }) => {
page, await page.goto('./', { waitUntil: 'domcontentloaded' });
browserName, });
openmctConfig test('Displays empty div for missing stacked plot item', async ({ page, browserName }) => {
}) => {
// eslint-disable-next-line playwright/no-skipped-test // eslint-disable-next-line playwright/no-skipped-test
test.skip(browserName === 'firefox', 'Firefox failing due to console events being missed'); test.skip(browserName === 'firefox', 'Firefox failing due to console events being missed');
const { myItemsFolderName } = openmctConfig; let warningReceived = false;
const errorLogs = [];
page.on('console', (message) => { page.on('console', (message) => {
if (message.type() === 'warning' && message.text().includes('Missing domain object')) { if (message.type() === 'warning' && message.text().includes('Missing domain object')) {
errorLogs.push(message.text()); warningReceived = true;
} }
}); });
//Make stacked plot const stackedPlot = await createDomainObjectWithDefaults(page, {
await makeStackedPlot(page, myItemsFolderName); type: 'Stacked Plot'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: stackedPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: stackedPlot.uuid
});
//Gets local storage and deletes the last sine wave generator in the stacked plot //Gets local storage and deletes the last sine wave generator in the stacked plot
const localStorage = await page.evaluate(() => window.localStorage); const mct = await page.evaluate(() => window.localStorage.getItem('mct'));
const parsedData = JSON.parse(localStorage.mct); const parsedData = JSON.parse(mct);
const keys = Object.keys(parsedData); const key = Object.entries(parsedData).find(([, value]) => value.type === 'generator')?.[0];
const lastKey = keys[keys.length - 1];
delete parsedData[lastKey]; delete parsedData[key];
//Sets local storage with missing object //Sets local storage with missing object
await page.evaluate(`window.localStorage.setItem('mct', '${JSON.stringify(parsedData)}')`); const jsonData = JSON.stringify(parsedData);
await page.evaluate((data) => {
window.localStorage.setItem('mct', data);
}, jsonData);
//Reloads page and clicks on stacked plot //Reloads page and clicks on stacked plot
await Promise.all([page.reload(), page.waitForLoadState('networkidle')]); await Promise.all([page.reload(), page.waitForLoadState('domcontentloaded')]);
await page.reload({ waitUntil: 'domcontentloaded' });
await page.goto(stackedPlot.url);
//Verify Main section is there on load //Verify Main section is there on load
await expect await expect(page.locator('.l-browse-bar__object-name')).toContainText(stackedPlot.name);
.soft(page.locator('.l-browse-bar__object-name'))
.toContainText('Unnamed Stacked Plot');
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Stacked Plot').first().click()
]);
//Check that there is only one stacked item plot with a plot, the missing one will be empty //Check that there is only one stacked item plot with a plot, the missing one will be empty
await expect(page.locator('.c-plot--stacked-container:has(.gl-plot)')).toHaveCount(1); await expect(page.getByLabel('Stacked Plot Item')).toHaveCount(1);
//Verify that console.warn is thrown //Verify that console.warn was thrown
expect(errorLogs).toHaveLength(1); expect(warningReceived).toBe(true);
}); });
}); });
@ -92,7 +96,7 @@ async function makeStackedPlot(page, myItemsFolderName) {
await page.locator('li[role="menuitem"]:has-text("Stacked Plot")').click(); await page.locator('li[role="menuitem"]:has-text("Stacked Plot")').click();
await Promise.all([ await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle' }), page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
page.locator('button:has-text("OK")').click(), page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear //Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message') page.waitForSelector('.c-message-banner__message')
@ -153,7 +157,7 @@ async function createSineWaveGenerator(page) {
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click(); await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
await Promise.all([ await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle' }), page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
page.locator('button:has-text("OK")').click(), page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear //Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message') page.waitForSelector('.c-message-banner__message')

View File

@ -91,7 +91,7 @@ test.describe('Overlay Plot', () => {
// Assert that the legend is collapsed by default // Assert that the legend is collapsed by default
await expect(page.getByLabel('Plot Legend Collapsed')).toBeVisible(); await expect(page.getByLabel('Plot Legend Collapsed')).toBeVisible();
await expect(page.getByLabel('Plot Legend Expanded')).toBeHidden(); await expect(page.getByLabel('Plot Legend Expanded')).toBeHidden();
await expect(page.getByLabel('Expand by Default')).toHaveText('No'); await expect(page.getByLabel('Expand by Default')).toHaveText(/No/);
expect(await page.getByLabel('Plot Legend Item').count()).toBe(3); expect(await page.getByLabel('Plot Legend Item').count()).toBe(3);
@ -106,7 +106,7 @@ test.describe('Overlay Plot', () => {
await expect(page.getByRole('cell', { name: 'Name' })).toBeVisible(); await expect(page.getByRole('cell', { name: 'Name' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible(); await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Value' })).toBeVisible(); await expect(page.getByRole('cell', { name: 'Value' })).toBeVisible();
await expect(page.getByLabel('Expand by Default')).toHaveText('Yes'); await expect(page.getByLabel('Expand by Default')).toHaveText(/Yes/);
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(3); await expect(page.getByLabel('Plot Legend Item')).toHaveCount(3);
// Assert that the legend is expanded on page load // Assert that the legend is expanded on page load
@ -116,7 +116,7 @@ test.describe('Overlay Plot', () => {
await expect(page.getByRole('cell', { name: 'Name' })).toBeVisible(); await expect(page.getByRole('cell', { name: 'Name' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible(); await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Value' })).toBeVisible(); await expect(page.getByRole('cell', { name: 'Value' })).toBeVisible();
await expect(page.getByLabel('Expand by Default')).toHaveText('Yes'); await expect(page.getByLabel('Expand by Default')).toHaveText(/Yes/);
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(3); await expect(page.getByLabel('Plot Legend Item')).toHaveCount(3);
}); });
@ -144,15 +144,8 @@ test.describe('Overlay Plot', () => {
// Expand the "Sine Wave Generator" plot series options and enable limit lines // Expand the "Sine Wave Generator" plot series options and enable limit lines
await page.getByRole('tab', { name: 'Config' }).click(); await page.getByRole('tab', { name: 'Config' }).click();
await page await page.getByLabel('Expand Sine Wave Generator:').click();
.getByRole('list', { name: 'Plot Series Properties' }) await page.getByLabel('Limit lines').check();
.locator('span')
.first()
.click();
await page
.getByRole('list', { name: 'Plot Series Properties' })
.locator('[title="Display limit lines"]~div input')
.check();
await assertLimitLinesExistAndAreVisible(page); await assertLimitLinesExistAndAreVisible(page);
@ -189,6 +182,49 @@ test.describe('Overlay Plot', () => {
await assertLimitLinesExistAndAreVisible(page); await assertLimitLinesExistAndAreVisible(page);
}); });
test('Limit lines adjust when series is resized', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6987'
});
// Create an Overlay Plot with a default SWG
overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
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);
// Enter edit mode
await page.getByLabel('Edit Object').click();
// Expand the "Sine Wave Generator" plot series options and enable limit lines
await page.getByRole('tab', { name: 'Config' }).click();
await page.getByLabel('Expand Sine Wave Generator:').click();
await page.getByLabel('Limit lines').check();
await assertLimitLinesExistAndAreVisible(page);
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
const initialCoords = await assertLimitLinesExistAndAreVisible(page);
// Resize the chart container by showing the snapshot pane.
await page.getByLabel('Show Snapshots').click();
const newCoords = await assertLimitLinesExistAndAreVisible(page);
// We just need to know that the first limit line redrew somewhere lower than the initial y position.
expect(newCoords.y).toBeGreaterThan(initialCoords.y);
});
test('The elements pool supports dragging series into multiple y-axis buckets', async ({ test('The elements pool supports dragging series into multiple y-axis buckets', async ({
page page
}) => { }) => {
@ -273,38 +309,36 @@ test.describe('Overlay Plot', () => {
expect(yAxis3Group.getByRole('listitem').nth(0).getByText(swgB.name)).toBeTruthy(); expect(yAxis3Group.getByRole('listitem').nth(0).getByText(swgB.name)).toBeTruthy();
}); });
test.fixme( test('Clicking on an item in the elements pool brings up the plot preview with data points', async ({
'Clicking on an item in the elements pool brings up the plot preview with data points', page
async ({ page }) => { }) => {
test.info().annotations.push({ const swgA = await createDomainObjectWithDefaults(page, {
type: 'issue', type: 'Sine Wave Generator',
description: 'https://github.com/nasa/openmct/issues/7421' parent: overlayPlot.uuid
}); });
const swgA = await createDomainObjectWithDefaults(page, { await page.goto(overlayPlot.url);
type: 'Sine Wave Generator', // Wait for plot series data to load and be drawn
parent: overlayPlot.uuid await waitForPlotsToRender(page);
}); await page.getByLabel('Edit Object').click();
await page.goto(overlayPlot.url); await page.getByRole('tab', { name: 'Elements' }).click();
// 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.locator(`#inspector-elements-tree >> text=${swgA.name}`).click();
const plotPixels = await getCanvasPixels(page, '.js-overlay canvas');
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click(); const plotPixelSize = plotPixels.length;
const plotPixels = await getCanvasPixels(page, '.js-overlay canvas'); expect(plotPixelSize).toBeGreaterThan(0);
const plotPixelSize = plotPixels.length; });
expect(plotPixelSize).toBeGreaterThan(0);
}
);
test('Can remove an item via the elements pool action menu', async ({ page }) => { test('Can remove an item via the elements pool action menu', async ({ page }) => {
const swgA = await createDomainObjectWithDefaults(page, { const swgA = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator', type: 'Sine Wave Generator',
parent: overlayPlot.uuid parent: overlayPlot.uuid
}); });
const swgB = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url); await page.goto(overlayPlot.url);
// Wait for plot series data to load and be drawn // Wait for plot series data to load and be drawn
@ -319,6 +353,23 @@ test.describe('Overlay Plot', () => {
await page.getByRole('menuitem', { name: 'Remove' }).click(); 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 expect(swgAElementsPoolItem).toBeHidden();
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7530'
});
await test.step('Verify that the legend is correct after removing a series', async () => {
await page.getByLabel('Plot Canvas').hover();
await page.mouse.move(50, 0, {
steps: 10
});
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(1);
await expect(page.getByLabel(`Plot Legend Item for ${swgA.name}`)).toBeHidden();
await expect(page.getByLabel(`Plot Legend Item for ${swgB.name}`)).toBeVisible();
});
}); });
}); });
@ -337,4 +388,7 @@ async function assertLimitLinesExistAndAreVisible(page) {
for (let i = 0; i < limitLineCount; i++) { for (let i = 0; i < limitLineCount; i++) {
await expect(page.locator('.c-plot-limit-line').nth(i)).toBeVisible(); await expect(page.locator('.c-plot-limit-line').nth(i)).toBeVisible();
} }
const firstLimitLineCoords = await page.locator('.c-plot-limit-line').first().boundingBox();
return firstLimitLineCoords;
} }

View File

@ -0,0 +1,118 @@
/*****************************************************************************
* 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 testing the rendering and interaction of plots.
*
*/
import {
createDomainObjectWithDefaults,
getCanvasPixels,
setEndOffset,
setRealTimeMode,
setStartOffset
} from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Plot Controls', () => {
let overlayPlot;
test.beforeEach(async ({ page }) => {
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'domcontentloaded' });
overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
// Create an overlay plot with a sine wave generator
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await page.goto(`${overlayPlot.url}`);
});
test("Plots don't purge data when paused", async ({ page }) => {
// Set realtime mode with 2 second window
const startOffset = {
startMins: '00',
startSecs: '01'
};
const endOffset = {
endMins: '00',
endSecs: '01'
};
// Switch to real-time mode
await setRealTimeMode(page);
// Set start time offset
await setStartOffset(page, startOffset);
// Set end time offset
await setEndOffset(page, endOffset);
// Edit the overlay plot and turn off auto scale, setting the min and max to -1 and 1
// enter edit mode
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'Config' }).click();
// turn off autoscale
await page.getByRole('checkbox', { name: 'Auto scale' }).uncheck();
await page.getByLabel('Y Axis 1 Minimum value').fill('-1');
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' });
// hover over plot for plot controls
await page.getByLabel('Plot Canvas').hover();
// click on pause control
await page.getByTitle('Pause incoming real-time data').click();
// expect plot to be paused
await expect(page.getByTitle('Resume displaying real-time data')).toBeVisible();
// Wait for 2 seconds to stabilize plot data - future timestamp
// eslint-disable-next-line
await page.waitForTimeout(2000);
// Capture the # of plot points
const plotPixels = await getCanvasPixels(page, 'canvas');
const plotPixelSizeAtPause = plotPixels.length;
// Wait 2 seconds
// eslint-disable-next-line
await page.waitForTimeout(2000);
// Capture the # of plot points
const plotPixelsAfterWait = await getCanvasPixels(page, 'canvas');
const plotPixelSizeAfterWait = plotPixelsAfterWait.length;
// Expect before and after plot points to match
await expect(plotPixelSizeAtPause).toEqual(plotPixelSizeAfterWait);
});
});

View File

@ -25,7 +25,11 @@
* *
*/ */
import { createDomainObjectWithDefaults, getCanvasPixels } from '../../../../appActions.js'; import {
createDomainObjectWithDefaults,
getCanvasPixels,
setRealTimeMode
} from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js'; import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Plot Rendering', () => { test.describe('Plot Rendering', () => {
@ -50,13 +54,37 @@ test.describe('Plot Rendering', () => {
createMineFolderRequests.push(req); createMineFolderRequests.push(req);
}); });
expect(createMineFolderRequests.length).toEqual(0); expect(createMineFolderRequests.length).toEqual(0);
await page.getByLabel('Plot Canvas').hover();
}); });
test.fixme('Plot is rendered when infinity values exist', async ({ page }) => { test('Time conductor synchronizes with plot time range when that plot control is clicked', async ({
test.info().annotations.push({ page
type: 'issue', }) => {
description: 'https://github.com/nasa/openmct/issues/7421' // Navigate to Sine Wave Generator
}); await page.goto(sineWaveGeneratorObject.url);
// Switch to real-time mode
await setRealTimeMode(page);
// hover over plot for plot controls
await page.getByLabel('Plot Canvas').hover();
// click on pause control
await page.getByTitle('Pause incoming real-time data').click();
// expect plot to be paused
await expect(page.getByTitle('Resume displaying real-time data')).toBeVisible();
// hover over plot for plot controls
await page.getByLabel('Plot Canvas').hover();
// click on synchronize with time conductor
await page.getByTitle('Synchronize Time Conductor').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('Plot is rendered when infinity values exist', async ({ page }) => {
// Edit Plot // Edit Plot
await editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject); await editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject);

View File

@ -54,7 +54,9 @@ test.describe('Plots work in Previews', () => {
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// right click on the plot and select view large // right click on the plot and select view large
await page.getByLabel('Sine', { exact: true }).click({ button: 'right' }); await page.getByLabel(/Alpha-numeric telemetry value of.*/).click({
button: 'right'
});
await page.getByLabel('View Historical Data').click(); await page.getByLabel('View Historical Data').click();
await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible(); await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();
await page.getByRole('button', { name: 'Close' }).click(); await page.getByRole('button', { name: 'Close' }).click();

View File

@ -39,19 +39,23 @@ test.describe('Stacked Plot', () => {
await page.goto('./', { waitUntil: 'domcontentloaded' }); await page.goto('./', { waitUntil: 'domcontentloaded' });
stackedPlot = await createDomainObjectWithDefaults(page, { stackedPlot = await createDomainObjectWithDefaults(page, {
type: 'Stacked Plot' type: 'Stacked Plot',
name: 'Stacked Plot'
}); });
swgA = await createDomainObjectWithDefaults(page, { swgA = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator', type: 'Sine Wave Generator',
name: 'Sine Wave Generator A',
parent: stackedPlot.uuid parent: stackedPlot.uuid
}); });
swgB = await createDomainObjectWithDefaults(page, { swgB = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator', type: 'Sine Wave Generator',
name: 'Sine Wave Generator B',
parent: stackedPlot.uuid parent: stackedPlot.uuid
}); });
swgC = await createDomainObjectWithDefaults(page, { swgC = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator', type: 'Sine Wave Generator',
name: 'Sine Wave Generator C',
parent: stackedPlot.uuid parent: stackedPlot.uuid
}); });
}); });
@ -151,40 +155,80 @@ test.describe('Stacked Plot', () => {
await page.getByRole('tab', { name: 'Config' }).click(); await page.getByRole('tab', { name: 'Config' }).click();
// Click on the 1st plot // Click on the 1st plot
await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"] canvas`).nth(1).click(); await page
.getByLabel('Stacked Plot Item Sine Wave Generator A')
.getByLabel('Plot Canvas')
.click();
// Assert that the inspector shows the Y Axis properties for swgA // Assert that the inspector shows the Y Axis properties for swgA
await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText( await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();
'Plot Series'
);
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
await expect( await expect(
page.locator('[aria-label="Plot Series Properties"] .c-object-label') page.getByLabel('Inspector Views').getByText('Sine Wave Generator A', { exact: true })
).toContainText(swgA.name); ).toBeVisible();
// Click on the 2nd plot // Click on the 2nd plot
await page.locator(`[aria-label="Stacked Plot Item ${swgB.name}"] canvas`).nth(1).click(); await page
.getByLabel('Stacked Plot Item Sine Wave Generator B')
.getByLabel('Plot Canvas')
.click();
// Assert that the inspector shows the Y Axis properties for swgB // Assert that the inspector shows the Y Axis properties for swgB
await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText( await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();
'Plot Series'
);
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
await expect( await expect(
page.locator('[aria-label="Plot Series Properties"] .c-object-label') page.getByLabel('Inspector Views').getByText('Sine Wave Generator B', { exact: true })
).toContainText(swgB.name); ).toBeVisible();
// Click on the 3rd plot // Click on the 3rd plot
await page.locator(`[aria-label="Stacked Plot Item ${swgC.name}"] canvas`).nth(1).click(); await page
.getByLabel('Stacked Plot Item Sine Wave Generator C')
// Assert that the inspector shows the Y Axis properties for swgC .getByLabel('Plot Canvas')
await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText( .click();
'Plot Series' // Assert that the inspector shows the Y Axis properties for swgB
); await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
await expect( await expect(
page.locator('[aria-label="Plot Series Properties"] .c-object-label') page.getByLabel('Inspector Views').getByText('Sine Wave Generator C', { exact: true })
).toContainText(swgC.name); ).toBeVisible();
// Go into edit mode
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'Config' }).click();
// Click on the 1st plot
await page.getByLabel('Stacked Plot Item Sine Wave Generator A').click();
// Assert that the inspector shows the Y Axis properties for swgA
await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
await expect(
page.getByLabel('Inspector Views').getByText('Sine Wave Generator A', { exact: true })
).toBeVisible();
// Click on the 2nd plot
await page.getByLabel('Stacked Plot Item Sine Wave Generator B').click();
// Assert that the inspector shows the Y Axis properties for swgB
await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
await expect(
page.getByLabel('Inspector Views').getByText('Sine Wave Generator B', { exact: true })
).toBeVisible();
// Click on the 3rd plot
await page.getByLabel('Stacked Plot Item Sine Wave Generator C').click();
// Assert that the inspector shows the Y Axis properties for swgC
await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
await expect(
page.getByLabel('Inspector Views').getByText('Sine Wave Generator C', { exact: true })
).toBeVisible();
});
test('Changing properties of an immutable child plot are applied correctly', async ({ page }) => {
await page.goto(stackedPlot.url);
// Go into edit mode // Go into edit mode
await page.getByLabel('Edit Object').click(); await page.getByLabel('Edit Object').click();
@ -192,40 +236,35 @@ test.describe('Stacked Plot', () => {
await page.getByRole('tab', { name: 'Config' }).click(); await page.getByRole('tab', { name: 'Config' }).click();
// Click on canvas for the 1st plot // Click on canvas for the 1st plot
await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"]`).click(); await page.getByLabel(`Stacked Plot Item ${swgA.name}`).click();
// Assert that the inspector shows the Y Axis properties for swgA // Expand config for the series
await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText( await page.getByLabel('Expand Sine Wave Generator').click();
'Plot Series'
); // turn off alarm markers
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); await page.getByLabel('Alarm Markers').uncheck();
// save
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// reload page and waitForPlotsToRender
await page.reload();
await waitForPlotsToRender(page);
// Click on canvas for the 1st 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();
// Assert that alarm markers are still turned off
await expect( await expect(
page.locator('[aria-label="Plot Series Properties"] .c-object-label') page
).toContainText(swgA.name); .getByTitle('Display markers visually denoting points in alarm.')
.getByRole('cell', { name: 'Disabled' })
//Click on canvas for the 2nd plot ).toBeVisible();
await page.locator(`[aria-label="Stacked Plot Item ${swgB.name}"]`).click();
// Assert that the inspector shows the Y Axis properties for swgB
await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText(
'Plot Series'
);
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
await expect(
page.locator('[aria-label="Plot Series Properties"] .c-object-label')
).toContainText(swgB.name);
//Click on canvas for the 3rd plot
await page.locator(`[aria-label="Stacked Plot Item ${swgC.name}"]`).click();
// Assert that the inspector shows the Y Axis properties for swgC
await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText(
'Plot Series'
);
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
await expect(
page.locator('[aria-label="Plot Series Properties"] .c-object-label')
).toContainText(swgC.name);
}); });
test('the legend toggles between aggregate and per child', async ({ page }) => { test('the legend toggles between aggregate and per child', async ({ page }) => {

View File

@ -122,4 +122,14 @@ test.describe('Reload action', () => {
expect(fullReloadAlphaTelemetryValue).not.toEqual(afterReloadAlphaTelemetryValue); expect(fullReloadAlphaTelemetryValue).not.toEqual(afterReloadAlphaTelemetryValue);
expect(fullReloadBetaTelemetryValue).not.toEqual(afterReloadBetaTelemetryValue); expect(fullReloadBetaTelemetryValue).not.toEqual(afterReloadBetaTelemetryValue);
}); });
test('is disabled in Previews', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7638'
});
await page.getByLabel('Alpha Table Frame Controls').getByLabel('Large View').click();
await page.getByLabel('Modal Overlay').getByLabel('More actions').click();
await expect(page.getByLabel('Reload')).toBeHidden();
});
}); });

View File

@ -114,7 +114,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultFrameBorderColor), hexToRGB(defaultFrameBorderColor),
NO_STYLE_RGBA, NO_STYLE_RGBA,
hexToRGB(setTextColor), hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Check styles on StackedPlot2. Note: https://github.com/nasa/openmct/issues/7337 // Check styles on StackedPlot2. Note: https://github.com/nasa/openmct/issues/7337
@ -122,7 +124,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultFrameBorderColor), hexToRGB(defaultFrameBorderColor),
NO_STYLE_RGBA, NO_STYLE_RGBA,
hexToRGB(setTextColor), hexToRGB(setTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
}); });
@ -143,7 +147,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor), hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA, NO_STYLE_RGBA,
hexToRGB(defaultTextColor), hexToRGB(defaultTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Check styles on StackedPlot2 // Check styles on StackedPlot2
@ -151,7 +157,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor), hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA, NO_STYLE_RGBA,
hexToRGB(defaultTextColor), hexToRGB(defaultTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Set styles using setStyles function on StackedPlot1 but not StackedPlot2 // Set styles using setStyles function on StackedPlot1 but not StackedPlot2
@ -160,7 +168,7 @@ test.describe('Flexible Layout styling', () => {
setBorderColor, setBorderColor,
setBackgroundColor, setBackgroundColor,
setTextColor, setTextColor,
page.getByLabel('StackedPlot1 Frame') page.getByRole('group', { name: 'StackedPlot1 Frame' })
); );
// Check styles on StackedPlot1 // Check styles on StackedPlot1
@ -168,7 +176,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor), hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor), hexToRGB(setBackgroundColor),
hexToRGB(setTextColor), hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Check styles on StackedPlot2 // Check styles on StackedPlot2
@ -176,7 +186,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor), hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA, NO_STYLE_RGBA,
hexToRGB(defaultTextColor), hexToRGB(defaultTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Save Flexible Layout // Save Flexible Layout
@ -191,7 +203,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor), hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor), hexToRGB(setBackgroundColor),
hexToRGB(setTextColor), hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Check styles on StackedPlot2 // Check styles on StackedPlot2
@ -199,7 +213,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor), hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA, NO_STYLE_RGBA,
hexToRGB(defaultTextColor), hexToRGB(defaultTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
}); });
@ -241,7 +257,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor), hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor), hexToRGB(setBackgroundColor),
hexToRGB(setTextColor), hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Check styles on StackedPlot2 to verify they are the default // Check styles on StackedPlot2 to verify they are the default
@ -249,7 +267,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor), hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA, NO_STYLE_RGBA,
hexToRGB(defaultTextColor), hexToRGB(defaultTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Set styles using setStyles function on StackedPlot2 // Set styles using setStyles function on StackedPlot2
@ -258,7 +278,7 @@ test.describe('Flexible Layout styling', () => {
setBorderColor, setBorderColor,
setBackgroundColor, setBackgroundColor,
setTextColor, setTextColor,
page.getByLabel('StackedPlot2 Frame') page.getByRole('group', { name: 'StackedPlot2 Frame' })
); );
// Check styles on StackedPlot2 // Check styles on StackedPlot2
@ -266,7 +286,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor), hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor), hexToRGB(setBackgroundColor),
hexToRGB(setTextColor), hexToRGB(setTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Save Flexible Layout // Save Flexible Layout
@ -281,7 +303,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor), hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor), hexToRGB(setBackgroundColor),
hexToRGB(setTextColor), hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Check styles on StackedPlot2 // Check styles on StackedPlot2
@ -289,7 +313,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor), hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor), hexToRGB(setBackgroundColor),
hexToRGB(setTextColor), hexToRGB(setTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Directly navigate to the flexible layout // Directly navigate to the flexible layout
@ -326,7 +352,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor), hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor), hexToRGB(setBackgroundColor),
hexToRGB(setTextColor), hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Check styles on StackedPlot2 matches previous set colors // Check styles on StackedPlot2 matches previous set colors
@ -334,7 +362,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor), hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor), hexToRGB(setBackgroundColor),
hexToRGB(setTextColor), hexToRGB(setTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
}); });
@ -356,7 +386,7 @@ test.describe('Flexible Layout styling', () => {
setBorderColor, setBorderColor,
setBackgroundColor, setBackgroundColor,
setTextColor, setTextColor,
page.getByLabel('StackedPlot1 Frame') page.getByRole('group', { name: 'StackedPlot1 Frame' })
); );
// Check styles using checkStyles function // Check styles using checkStyles function
@ -364,7 +394,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor), hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor), hexToRGB(setBackgroundColor),
hexToRGB(setTextColor), hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Save Flexible Layout // Save Flexible Layout
@ -386,7 +418,7 @@ test.describe('Flexible Layout styling', () => {
'No Style', 'No Style',
'No Style', 'No Style',
'No Style', 'No Style',
page.getByLabel('StackedPlot1 Frame') page.getByRole('group', { name: 'StackedPlot1 Frame' })
); );
// Check styles using checkStyles function // Check styles using checkStyles function
@ -394,7 +426,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor), hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA, NO_STYLE_RGBA,
hexToRGB(inheritedColor), hexToRGB(inheritedColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Save Flexible Layout // Save Flexible Layout
await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('button', { name: 'Save' }).click();
@ -408,7 +442,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor), hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA, NO_STYLE_RGBA,
hexToRGB(inheritedColor), hexToRGB(inheritedColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
}); });

View File

@ -67,7 +67,7 @@ test.describe('Style Inspector Options', () => {
await expect(page.getByRole('tab', { name: 'Styles' })).toBeVisible(); await expect(page.getByRole('tab', { name: 'Styles' })).toBeVisible();
// Select Stacked Layout Column // Select Stacked Layout Column
await page.getByLabel('Stacked Plot Frame').click(); await page.getByRole('group', { name: 'Stacked Plot Frame' }).click();
// The overall Flex Layout or Stacked Plot itself MUST be style-able. // The overall Flex Layout or Stacked Plot itself MUST be style-able.
await expect(page.getByRole('tab', { name: 'Styles' })).toBeVisible(); await expect(page.getByRole('tab', { name: 'Styles' })).toBeVisible();

View File

@ -69,5 +69,9 @@ test.describe('Preview mode', () => {
await page.getByLabel('Overlay').getByLabel('More actions').click(); await page.getByLabel('Overlay').getByLabel('More actions').click();
await expect(page.getByLabel('Export Table Data')).toBeVisible(); await expect(page.getByLabel('Export Table Data')).toBeVisible();
await expect(page.getByLabel('Export Marked Rows')).toBeVisible(); await expect(page.getByLabel('Export Marked Rows')).toBeVisible();
await expect(page.getByLabel('Export Marked Rows')).toBeDisabled();
await page.getByLabel('Pause').click();
const tableWrapper = page.getByLabel('Preview Container').locator('div.c-table-wrapper');
await expect(tableWrapper).toHaveClass(/is-paused/);
}); });
}); });

View File

@ -20,10 +20,71 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import { createDomainObjectWithDefaults, setTimeConductorBounds } from '../../../../appActions.js'; import {
createDomainObjectWithDefaults,
navigateToObjectWithRealTime,
setTimeConductorBounds
} from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js'; import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Telemetry Table', () => { test.describe('Telemetry Table', () => {
let table;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
});
test('Limits to 50 rows by default', async ({ page }) => {
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: table.uuid
});
await navigateToObjectWithRealTime(page, table.url);
const rows = page.getByLabel('table content').getByLabel('Table Row');
await expect(rows).toHaveCount(50);
});
test('on load, auto scrolls to top for descending, and to bottom for ascending', async ({
page
}) => {
const sineWaveGenerator = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: table.uuid
});
// verify in telemetry table object view
await navigateToObjectWithRealTime(page, table.url);
expect(await getScrollPosition(page)).toBe(0);
// verify in telemetry table view
await page.goto(sineWaveGenerator.url);
await page.getByLabel('Open the View Switcher Menu').click();
await page.getByText('Telemetry Table', { exact: true }).click();
expect(await getScrollPosition(page)).toBe(0);
// navigate back to table
await page.goto(table.url);
// go into edit mode
await page.getByLabel('Edit Object').click();
// change sort direction
await page.locator('thead div').filter({ hasText: 'Time' }).click();
// save view
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// navigate away and back
await page.goto(sineWaveGenerator.url);
await page.goto(table.url);
// verify scroll position
expect(await getScrollPosition(page, false)).toBeLessThan(1);
});
test('unpauses and filters data when paused by button and user changes bounds', async ({ test('unpauses and filters data when paused by button and user changes bounds', async ({
page page
}) => { }) => {
@ -34,7 +95,6 @@ test.describe('Telemetry Table', () => {
await page.goto('./', { waitUntil: 'domcontentloaded' }); await page.goto('./', { waitUntil: 'domcontentloaded' });
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
await createDomainObjectWithDefaults(page, { await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator', type: 'Sine Wave Generator',
parent: table.uuid parent: table.uuid
@ -78,7 +138,6 @@ test.describe('Telemetry Table', () => {
test('Supports filtering telemetry by regular text search', async ({ page }) => { test('Supports filtering telemetry by regular text search', async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' }); await page.goto('./', { waitUntil: 'domcontentloaded' });
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
await createDomainObjectWithDefaults(page, { await createDomainObjectWithDefaults(page, {
type: 'Event Message Generator', type: 'Event Message Generator',
parent: table.uuid parent: table.uuid
@ -121,7 +180,6 @@ test.describe('Telemetry Table', () => {
test('Supports filtering using Regex', async ({ page }) => { test('Supports filtering using Regex', async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' }); await page.goto('./', { waitUntil: 'domcontentloaded' });
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
await createDomainObjectWithDefaults(page, { await createDomainObjectWithDefaults(page, {
type: 'Event Message Generator', type: 'Event Message Generator',
parent: table.uuid parent: table.uuid
@ -165,3 +223,42 @@ test.describe('Telemetry Table', () => {
await page.click('button[title="Pause"]'); await page.click('button[title="Pause"]');
}); });
}); });
async function getScrollPosition(page, top = true) {
const tableBody = page.locator('.c-table__body-w');
// Wait for the scrollbar to appear
await tableBody.evaluate((node) => {
return new Promise((resolve) => {
function checkScroll() {
if (node.scrollHeight > node.clientHeight) {
resolve();
} else {
setTimeout(checkScroll, 100);
}
}
checkScroll();
});
});
// make sure there are rows
const rows = page.getByLabel('table content').getByLabel('Table Row');
await rows.first().waitFor();
// Using this to allow for rows to come and go, so we can truly test the scroll position
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1000);
const { scrollTop, clientHeight, scrollHeight } = await tableBody.evaluate((node) => ({
scrollTop: node.scrollTop,
clientHeight: node.clientHeight,
scrollHeight: node.scrollHeight
}));
// eslint-disable-next-line playwright/no-conditional-in-test
if (top) {
return scrollTop;
} else {
return Math.abs(scrollHeight - (scrollTop + clientHeight));
}
}

View File

@ -0,0 +1,64 @@
/*****************************************************************************
* 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.
*****************************************************************************/
import {
createDomainObjectWithDefaults,
setIndependentTimeConductorBounds
} 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';
test.describe('Datepicker operations', () => {
test.beforeEach(async ({ page }) => {
await page.goto(FIXED_TIME);
});
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
await page.getByLabel('Global Time Conductor').locator('a').first().click();
await expect(page.getByRole('dialog')).toBeVisible();
// Click on the first cell
await page.getByText('27 239').click();
// 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, {
start: '2024-11-12 19:11:11.000Z',
end: '2024-11-12 20:11:11.000Z'
});
// Open ITC
await page.getByLabel('Start bounds').nth(0).click();
// Click on the datepicker icon
await page.locator('form a').first().click();
await expect(page.getByRole('dialog')).toBeVisible();
// Click on the first cell
await page.getByText('7 342').click();
// Expect datepicker to close and time conductor date setting to be changed
await expect(page.getByRole('dialog')).toHaveCount(0);
});
});

View File

@ -48,7 +48,7 @@ test.describe('Time conductor operations', () => {
await setTimeConductorBounds(page, startDate); await setTimeConductorBounds(page, startDate);
// Bring up the time conductor popup // Bring up the time conductor popup
const timeConductorMode = await page.locator('.c-compact-tc'); const timeConductorMode = page.locator('.c-compact-tc');
await timeConductorMode.click(); await timeConductorMode.click();
const startDateLocator = page.locator('input[type="text"]').first(); const startDateLocator = page.locator('input[type="text"]').first();
const endDateLocator = page.locator('input[type="text"]').nth(2); const endDateLocator = page.locator('input[type="text"]').nth(2);

View File

@ -66,7 +66,7 @@ test.describe('Timer', () => {
}); });
}); });
test.describe('Timer with target date', () => { test.describe('Timer with target date @clock', () => {
let timer; let timer;
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {

View File

@ -191,7 +191,7 @@ test.describe('Recent Objects', () => {
// Navigate to the clock and reveal it in the tree // Navigate to the clock and reveal it in the tree
await page.goto(clock.url); await page.goto(clock.url);
await page.getByTitle('Show selected item in tree').click(); await page.getByLabel('Show selected item in tree').click();
// Right click the clock and create an alias using the "link" context menu action // Right click the clock and create an alias using the "link" context menu action
const clockTreeItem = page const clockTreeItem = page

View File

@ -30,7 +30,7 @@ import { expect, test } from '../../baseFixtures.js';
test.describe('Renaming objects', () => { test.describe('Renaming objects', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
// Go to baseURL // 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 ({ test('When renaming objects, the browse bar and various components all update', async ({

View File

@ -0,0 +1,64 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, 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.
*****************************************************************************/
import { createDomainObjectWithDefaults, navigateToObjectWithRealTime } from '../../appActions.js';
import { expect, test } from '../../pluginFixtures.js';
test.describe('Staleness', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
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'
});
// edit properties and enable staleness updates
await page.getByLabel('More actions').click();
await page.getByLabel('Edit properties...').click();
await page.getByLabel('Provide Staleness Updates', { exact: true }).click();
await page.getByLabel('Save').click();
const folder = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Folder 1'
});
// Navigate to the stale object
await navigateToObjectWithRealTime(page, staleSWG.url);
// Assert that staleness is shown
await expect(page.locator(`${objectViewSelector} .${isStaleClass}`)).toBeAttached({
timeout: 30 * 1000 // Give 30 seconds for the staleness to be updated
});
// Immediately navigate to the folder
await page.goto(folder.url);
// Verify that staleness is not shown
await expect(page.locator(`${objectViewSelector} .${isStaleClass}`)).not.toBeAttached();
});
});

View File

@ -21,16 +21,7 @@
*****************************************************************************/ *****************************************************************************/
/* /*
This test suite is dedicated to tests which can quickly verify that any openmct installation is This suite is dedicated to tests which verify that tooltips are displayed correctly.
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.
*/ */
import { createDomainObjectWithDefaults, expandEntireTree } from '../../appActions.js'; import { createDomainObjectWithDefaults, expandEntireTree } from '../../appActions.js';
@ -48,7 +39,7 @@ test.describe('Verify tooltips', () => {
const swg2Path = 'My Items / Folder Foo / Folder Bar / SWG 2'; const swg2Path = 'My Items / Folder Foo / Folder Bar / SWG 2';
const swg3Path = 'My Items / Folder Foo / Folder Bar / Folder Baz / SWG 3'; 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' }); await page.goto('./', { waitUntil: 'domcontentloaded' });
folder1 = await createDomainObjectWithDefaults(page, { folder1 = await createDomainObjectWithDefaults(page, {
@ -89,7 +80,7 @@ test.describe('Verify tooltips', () => {
await expandEntireTree(page); 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 // Create LAD table
await createDomainObjectWithDefaults(page, { await createDomainObjectWithDefaults(page, {
type: 'LAD Table', type: 'LAD Table',
@ -98,25 +89,32 @@ test.describe('Verify tooltips', () => {
// Edit LAD table // Edit LAD table
await page.getByLabel('Edit Object').click(); await page.getByLabel('Edit Object').click();
// Add the Sine Wave Generator to the LAD table and save changes // Add the Sine Wave Generator to the LAD table and save changes.
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-lad-table-wrapper'); //TODO Follow up with https://github.com/nasa/openmct/issues/7773
await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.c-lad-table-wrapper'); await page.dragAndDrop(`text=${sineWaveObject1.name}`, '#lad-table-drop-area');
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-lad-table-wrapper'); await page.dragAndDrop(`text=${sineWaveObject2.name}`, '#lad-table-drop-area');
await page.locator('button[title="Save"]').click(); await page.dragAndDrop(`text=${sineWaveObject3.name}`, '#lad-table-drop-area');
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.keyboard.down('Control'); 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) { //Hover on something else
await page.locator('.c-create-button').hover(); await page.getByRole('button', { name: 'Create' }).hover();
await page.getByRole('cell', { name: object.name }).hover(); //Hover over second object
let tooltipText = await page.locator('.c-tooltip').textContent(); await page.getByLabel('lad name').getByText(sineWaveObject2.name).hover();
return tooltipText.replace('\n', '').trim(); await expect(page.getByRole('tooltip', { name: sineWaveObject2.path })).toBeVisible();
}
expect(await getToolTip(sineWaveObject1)).toBe(sineWaveObject1.path); //Hover on something else
expect(await getToolTip(sineWaveObject2)).toBe(sineWaveObject2.path); await page.getByRole('button', { name: 'Create' }).hover();
expect(await getToolTip(sineWaveObject3)).toBe(sineWaveObject3.path); //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 }) => { test('display correct paths for expanded and collapsed plot legend items', async ({ page }) => {
@ -128,66 +126,74 @@ test.describe('Verify tooltips', () => {
// Edit Overlay Plot // Edit Overlay Plot
await page.getByLabel('Edit Object').click(); await page.getByLabel('Edit Object').click();
// Add the Sine Wave Generator to the LAD table and save changes // Add the Sine Wave Generators to the and save changes
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.gl-plot'); await page
await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.gl-plot'); .getByLabel('Preview SWG 1 generator Object')
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.gl-plot'); .dragTo(page.getByLabel('Plot Container Style Target'));
await page.locator('button[title="Save"]').click(); 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(); 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'); await page.keyboard.down('Control');
//Hover over first object
async function getCollapsedLegendToolTip(object) { await page.getByText('SWG 1 Hz').hover();
await page.locator('.c-create-button').hover(); await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);
await page //Hover over another object to clear
.locator('.plot-series-name', { has: page.locator(`text="${object.name} Hz"`) }) await page.getByRole('button', { name: 'create' }).hover();
.hover(); //Hover over second object
let tooltipText = await page.locator('.c-tooltip').textContent(); await page.getByText('SWG 2 Hz').hover();
return tooltipText.replace('\n', '').trim(); await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject2.path);
} //Hover over another object to clear
await page.getByRole('button', { name: 'create' }).hover();
async function getExpandedLegendToolTip(object) { //Hover over third object
await page.locator('.c-create-button').hover(); await page.getByText('SWG 3 Hz').hover();
await page await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);
.locator('.plot-series-name', { has: page.locator(`text="${object.name}"`) }) //Release the Control Key
.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);
await page.keyboard.up('Control'); await page.keyboard.up('Control');
//Expand the legend
await page.locator('.gl-plot-legend__view-control.c-disclosure-triangle').click(); 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'); await page.keyboard.down('Control');
expect(await getExpandedLegendToolTip(sineWaveObject1)).toBe(sineWaveObject1.path); await page.getByLabel('Plot Legend Expanded').getByText('SWG 1').hover();
expect(await getExpandedLegendToolTip(sineWaveObject2)).toBe(sineWaveObject2.path); await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);
expect(await getExpandedLegendToolTip(sineWaveObject3)).toBe(sineWaveObject3.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 }) => { test('display correct paths when hovering over object labels', async ({ page }) => {
async function getObjectLabelTooltip(object) { //Navigate to SWG 1 in Tree
await page await page.getByLabel('Navigate to SWG 1 generator').click();
.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();
}
expect(await getObjectLabelTooltip(sineWaveObject1)).toBe(sineWaveObject1.path); //Expect tooltip to be the path of SWG 1
expect(await getObjectLabelTooltip(sineWaveObject3)).toBe(sineWaveObject3.path); 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 }) => { test('display correct paths when hovering over display layout pane headers', async ({ page }) => {
@ -198,8 +204,11 @@ test.describe('Verify tooltips', () => {
}); });
// Edit Overlay Plot // Edit Overlay Plot
await page.getByLabel('Edit Object').click(); 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(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Create Stacked Plot // Create Stacked Plot
@ -209,8 +218,9 @@ test.describe('Verify tooltips', () => {
}); });
// Edit Stacked Plot // Edit Stacked Plot
await page.getByLabel('Edit Object').click(); await page.getByLabel('Edit Object').click();
await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.c-plot--stacked.holder'); await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.c-plot--stacked.holder');
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.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Create Display Layout // Create Display Layout
@ -221,66 +231,77 @@ test.describe('Verify tooltips', () => {
// Edit Display Layout // Edit Display Layout
await page.getByLabel('Edit Object').click(); await page.getByLabel('Edit Object').click();
await page.dragAndDrop("text='Test Overlay Plot'", '.l-layout__grid-holder', { await page
targetPosition: { x: 0, y: 0 } .getByLabel('Preview Test Overlay Plot')
}); .dragTo(page.locator('#display-layout-drop-area'), {
await page.dragAndDrop("text='Test Stacked Plot'", '.l-layout__grid-holder', { targetPosition: { x: 0, y: 0 }
targetPosition: { x: 0, y: 250 } });
});
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.l-layout__grid-holder', { //Add Display Layout below Overlay Plot
targetPosition: { x: 500, y: 200 } await page
}); .getByLabel('Preview Test Stacked Plot')
await page.locator('button[title="Save"]').click(); .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(); 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.keyboard.down('Control');
await page.getByText('Test Overlay Plot').nth(2).hover(); //Hover Overlay Plot
let tooltipText = await page.locator('.c-tooltip').textContent(); await page.getByTitle('Test Overlay Plot').hover();
tooltipText = tooltipText.replace('\n', '').trim(); await expect(page.getByRole('tooltip')).toHaveText('My Items / Test Overlay Plot');
expect(tooltipText).toBe('My Items / Test Overlay Plot');
await page.keyboard.up('Control'); 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.keyboard.down('Control');
await page.locator('.plot-wrapper-expanded-legend .plot-series-name').first().hover(); await page.getByLabel('Plot Legend Item for Test').getByText('SWG').hover();
tooltipText = await page.locator('.c-tooltip').textContent(); await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject1.path);
await page.getByText('Test Stacked Plot').nth(2).hover(); //Hover over Stacked Plot Title
tooltipText = await page.locator('.c-tooltip').textContent(); await page.getByTitle('Test Stacked Plot').hover();
tooltipText = tooltipText.replace('\n', '').trim(); await expect(page.getByRole('tooltip')).toHaveText('My Items / Test Stacked Plot');
expect(tooltipText).toBe('My Items / Test Stacked Plot');
await page.getByText('SWG 3').nth(2).hover(); //Hover over SWG3 Object
tooltipText = await page.locator('.c-tooltip').textContent(); await page.getByLabel('Alpha-numeric telemetry name for SWG').hover();
tooltipText = tooltipText.replace('\n', '').trim(); await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);
expect(sineWaveObject3.path).toBe(tooltipText);
}); });
test('display correct paths when hovering over flexible object labels', async ({ page }) => { test('display correct paths when hovering over flexible object labels', async ({ page }) => {
//Create Flexible Layout
await createDomainObjectWithDefaults(page, { await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout', type: 'Flexible Layout',
name: 'Test Flexible Layout' name: 'Test Flexible Layout'
}); });
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-fl__container >> nth=0'); //Add SWG1 and SWG3 to Flexible Layout
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-fl__container >> nth=1'); 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(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
//Hover over SWG1 Object
await page.keyboard.down('Control'); await page.keyboard.down('Control');
await page.getByText('SWG 1').nth(2).hover(); await page.getByTitle('SWG 1').hover();
let tooltipText = await page.locator('.c-tooltip').textContent(); await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject1.path);
await page.getByText('SWG 3').nth(2).hover(); //Hover over SWG3 Object
tooltipText = await page.locator('.c-tooltip').textContent(); await page.getByTitle('SWG 3').hover();
tooltipText = tooltipText.replace('\n', '').trim(); await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);
expect(tooltipText).toBe(sineWaveObject3.path);
}); });
test('display correct paths when hovering over tab view labels', async ({ page }) => { test('display correct paths when hovering over tab view labels', async ({ page }) => {
@ -289,46 +310,40 @@ test.describe('Verify tooltips', () => {
name: 'Test Tabs View' name: 'Test Tabs View'
}); });
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-tabs-view__tabs-holder'); //Add SWG1 and SWG3 to Flexible Layout
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-tabs-view__tabs-holder'); 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.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.keyboard.down('Control'); await page.keyboard.down('Control');
await page.getByText('SWG 1').nth(2).hover(); await page.getByLabel('SWG 1 tab').getByText('SWG').hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject1.path);
await page.getByText('SWG 3').nth(2).hover(); await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);
tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim(); await page.getByLabel('SWG 3 tab').getByText('SWG').hover();
expect(tooltipText).toBe(sineWaveObject3.path); await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);
}); });
test('display correct paths when hovering tree items', async ({ page }) => { test('display correct paths when hovering tree items', async ({ page }) => {
await page.keyboard.down('Control'); await page.keyboard.down('Control');
await page.getByText('SWG 1').nth(0).hover(); await page.getByText('SWG 1').first().hover();
let tooltipText = await page.locator('.c-tooltip').textContent(); await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject1.path);
await page.getByText('SWG 3').nth(0).hover(); await page.getByText('SWG 3').first().hover();
tooltipText = await page.locator('.c-tooltip').textContent(); await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject3.path);
}); });
test('display correct paths when hovering search items', async ({ page }) => { test('display correct paths when hovering search items', async ({ page }) => {
await page.getByRole('searchbox', { name: 'Search Input' }).click(); 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.keyboard.down('Control');
await page.locator('.c-gsearch-result__title').hover(); await page.getByLabel('Object Results').getByText('SWG').hover();
let tooltipText = await page.locator('.c-tooltip').textContent(); await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject3.path);
}); });
test('display path for source telemetry when hovering over gauge', async ({ page }) => { test('display path for source telemetry when hovering over gauge', async ({ page }) => {
@ -336,13 +351,11 @@ test.describe('Verify tooltips', () => {
type: 'Gauge', type: 'Gauge',
name: 'Test 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'); await page.keyboard.down('Control');
// eslint-disable-next-line playwright/no-force-option await page.getByRole('meter').hover({ position: { x: 0, y: 0 } });
await page.locator('.c-gauge.c-dial').hover({ position: { x: 0, y: 0 }, force: true }); await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);
let tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject3.path);
}); });
test('display tooltip path for notebook embeds', async ({ page }) => { test('display tooltip path for notebook embeds', async ({ page }) => {
@ -351,27 +364,23 @@ test.describe('Verify tooltips', () => {
name: 'Test Notebook' 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.keyboard.down('Control');
await page.locator('.c-ne__embed').hover(); await page.getByLabel('SWG 3 Notebook Embed').hover();
let tooltipText = await page.locator('.c-tooltip').textContent(); await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject3.path);
}); });
test.fixme('display tooltip path for telemetry table names', async ({ page }) => { test('display tooltip path for telemetry table names', async ({ page }) => {
test.info().annotations.push({ // set endBound to 10 seconds after start bound to ensure that the telemetry doesn't change
type: 'issue', // const url = page.url();
description: 'https://github.com/nasa/openmct/issues/7421' // const parsedUrl = new URL(url.replace('#', '!'));
}); // const startBound = Number(parsedUrl.searchParams.get('tc.startBound'));
// set endBound to 10 seconds after start bound // const tenSecondsInMilliseconds = 10 * 1000;
const url = await page.url(); // const endBound = startBound + tenSecondsInMilliseconds;
const parsedUrl = new URL(url.replace('#', '!')); // parsedUrl.searchParams.set('tc.endBound', endBound);
const startBound = Number(parsedUrl.searchParams.get('tc.startBound')); // await page.goto(parsedUrl.href.replace('!', '#'));
const tenSecondsInMilliseconds = 10 * 1000;
const endBound = startBound + tenSecondsInMilliseconds;
parsedUrl.searchParams.set('tc.endBound', endBound);
await page.goto(parsedUrl.href.replace('!', '#'));
await createDomainObjectWithDefaults(page, { await createDomainObjectWithDefaults(page, {
type: 'Telemetry Table', type: 'Telemetry Table',
@ -381,48 +390,35 @@ test.describe('Verify tooltips', () => {
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-telemetry-table'); await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-telemetry-table');
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-telemetry-table'); await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-telemetry-table');
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.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.keyboard.down('Control'); await page.keyboard.down('Control');
//Hover over SWG3 in Telemetry Table
await page.locator('.noselect > [title="SWG 3"]').first().hover(); await page.locator('.noselect > [title="SWG 3"]').first().hover();
let tooltipText = await page.locator('.c-tooltip').textContent(); await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject3.path);
//Hover over SWG1 in Telemetry Table
await page.locator('.noselect > [title="SWG 1"]').first().hover(); await page.locator('.noselect > [title="SWG 1"]').first().hover();
tooltipText = await page.locator('.c-tooltip').textContent(); await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject1.path);
}); });
test('display tooltip path for recently viewed items', async ({ page }) => { test('display tooltip path for recently viewed items', async ({ page }) => {
// drag up Recently Viewed pane // drag up Recently Viewed pane
await page await page.getByLabel('Resize Recently Viewed Pane').hover();
.locator('.l-pane.l-pane--vertical-handle-before', {
hasText: 'Recently Viewed'
})
.locator('.l-pane__handle')
.hover();
await page.mouse.down(); await page.mouse.down();
await page.mouse.move(0, 300); await page.mouse.move(0, 300);
await page.mouse.up(); await page.mouse.up();
await page.keyboard.down('Control'); await page.keyboard.down('Control');
await page.getByLabel('Recent Objects').getByText(sineWaveObject3.name).hover(); await page.getByLabel('Recent Objects').getByText(sineWaveObject3.name).hover();
let tooltipText = await page.locator('.c-tooltip').textContent(); await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject3.path);
await page.getByLabel('Recent Objects').getByText(sineWaveObject2.name).hover(); await page.getByLabel('Recent Objects').getByText(sineWaveObject2.name).hover();
tooltipText = await page.locator('.c-tooltip').textContent(); await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject2.path);
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject2.path);
await page.getByLabel('Recent Objects').getByText(sineWaveObject1.name).hover(); await page.getByLabel('Recent Objects').getByText(sineWaveObject1.name).hover();
tooltipText = await page.locator('.c-tooltip').textContent(); await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject1.path);
}); });
test('display tooltip path for time strips', async ({ page }) => { test('display tooltip path for time strips', async ({ page }) => {
@ -445,23 +441,17 @@ test.describe('Verify tooltips', () => {
`text=${sineWaveObject3.name}`, `text=${sineWaveObject3.name}`,
'.c-object-view.is-object-type-time-strip' '.c-object-view.is-object-type-time-strip'
); );
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.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.keyboard.down('Control'); await page.keyboard.down('Control');
await page.getByText(sineWaveObject1.name).nth(2).hover(); await page.getByText(sineWaveObject1.name).nth(2).hover();
let tooltipText = await page.locator('.c-tooltip').textContent(); await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject1.path);
await page.getByText(sineWaveObject2.name).nth(2).hover(); await page.getByText(sineWaveObject2.name).nth(2).hover();
tooltipText = await page.locator('.c-tooltip').textContent(); await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject2.path);
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject2.path);
await page.getByText(sineWaveObject3.name).nth(2).hover(); await page.getByText(sineWaveObject3.name).nth(2).hover();
tooltipText = await page.locator('.c-tooltip').textContent(); await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject3.path);
}); });
}); });

Some files were not shown because too many files have changed in this diff Show More