Compare commits

...

107 Commits

Author SHA1 Message Date
7ccfc552f1 refactor: remove usage of deprecated Time API methods 2024-04-12 14:15:29 -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
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
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
dc9bd8bcb1 Remove all logic around auto centering and scrolling the timelist (#7474) 2024-02-22 17:56:54 +00:00
29d83e9c6d fix(#7456): allow children of Overlay Plots to be removed (#7516)
* fix(#7456): check if object is an identifier

* refactor: use v-show, remove unnecessary deep copy, set yAxes array to empty in a way that preserves reactivity

* refactor: use ESM exports

* refactor: use ESM imports

* test(e2e): add test for element item removal from overlay plot

* a11y: add accessible name for object labels

* refactor: move overlayPlot creation to beforeAll, use getByLabel
2024-02-21 16:07:48 -08:00
6393a77c19 [Mobile] Add Global Search to Mobile (#7477)
* Add status area back to mobile

* Make search results responsive to width

* Make clear search button always visible, regardless of hover

* Make clear search button visible, and fix weird margin in top left corner

* Fix input width, add logic to make close button work properly, fix margin on results so there is room for close button, fix various landscape mode issues

* update mobile testing

* Fix dropdown sizes, remove shadows and corners to make it look less like a popup and more full screen

* Add animation and persist search bar in mobile

* Fix linting issues

* Fix padding in Desktop

* fix padding in status area

* fix bad merge

* lint fixes

* fix bad merge... again

* again

* fixes to mobile

* update tests

* lint fix

* test fixes

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-02-21 15:29:38 -08:00
6bbabf9c45 chore(deps-dev): bump vue from 3.3.8 to 3.4.19 (#7511)
Bumps [vue](https://github.com/vuejs/core) from 3.3.8 to 3.4.19.
- [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.3.8...v3.4.19)

---
updated-dependencies:
- dependency-name: 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: Jesse Mazzella <ozyx@users.noreply.github.com>
2024-02-21 15:13:02 -08:00
f18d1d2a51 Add Collapse to Recent Objects (#7502)
* initial functional implementation

* removing vestigial code

* Fix styles for button bar

* Remove bound for :persist-position

* Revert "Remove bound for :persist-position"

This reverts commit ce2ea422d7.

* test(e2e): modify existing test to verify collapse/expand recent objects pane

---------

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2024-02-21 21:28:29 +00:00
5894c66df1 docs(release.yml): make plural (#7517) 2024-02-20 14:58:44 -08:00
6ff8c42041 docs: Update release.yml and docs (#7514)
Attempt one
2024-02-20 14:29:00 -08:00
9870a6bc9c Check role when receiving status updates in the Operator Status Indicator (#7509)
* check role when receiving status updates

* pass role to get status
2024-02-16 21:45:04 +00:00
317ea8c275 docs: Add openmct type hints (#7247)
* Add setAssetPath method to MCT type information

* Add TypeRegistry to OpenMCT typedef
2024-02-16 16:56:13 +00:00
bc36a93b9b chore: bump version to 4.0.0-next (#7506)
Bump to 4.0.0
2024-02-15 13:49:04 -08:00
847232d64b style: ensure legacy indicators align with Vue indicators (#7458)
* style: ensure legacy indicators align with Vue indicators

* fix: move logic to `vueWrapHtmlElement` and use `u-contents` class
2024-02-15 20:22:52 +01:00
4fbccd4c91 Ensure object specific actions load in previews (#7504)
* add actions after element has had chance to render

* add test

* remove test.only
2024-02-15 18:00:04 +00:00
cd560bceed Bypass cache/dirty when canceling transaction (#7503)
* force remote/non-cached when canceling transaction

* add test

* update unit test
2024-02-15 09:26:45 -08:00
e08633214e Misc memory leak fixes (#7224)
* fix(leak): remove font listeners

* fix(leak): release componentInstance

* fix(perf): remove unused emit

* fix(perf): only emit if tickWidth changes

* fix: warnings and undefined keys

* fix: remove unused bind

* fix: restore MctTicks component
2024-02-14 23:49:02 +00:00
a9ad0bf38a When plan view is used in a gantt chart, handle the domain object differently. (#7473)
Rename for readability. Track plan changes if object is a plan.
2024-02-14 22:32:50 +00:00
5f8d6899d2 Plot legends expand by default when enabled (#7453)
* expanded legend showing, but malformed

* fix legends

* add e2e test and aria labels for controls

* fix tests

* remove focused test

* make plot legend items dynamic

* expand legend immediately when changing default

* Ensure stacked plots show cumulative legend (#7481)

* simplify config loading logic

* wip

* fixed stacked plot legend issue

* fix legend

* remove console.debugs

* remove extraneous prop

* add test

* fix legend

* use props
2024-02-07 18:35:19 +00:00
cd6adbadde Upgrade prettier and eslint compatibility libraries to latest (#7478)
* chore: upgrade prettier and eslint libraries to latest

- upgrade prettier
- upgrade eslint
- upgrade eslint-plugin-prettier
- upgrade eslint-config-prettier

* chore: run lint:fix

* chore: add `prettier-eslint` devDependency

- The `prettier-eslint` vscode plugin sinc v6.0.0 no longer provides this package so we must install it as a devDependency so that autoformat works.

* chore: add recommended extensions file for vscode users

* Update extensions.json

* Revert "Update extensions.json"

This reverts commit 942f341a75.

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-02-07 00:16:22 +00:00
539b43325a test(e2e): add e2e and visual tests for Mission Status (plus a11y) (#7462)
* feat: enable mission status in example user

* test: add initial missionStatus suite

* test(WIP): mission status e2e suite

* test(e2e): add e2e and visual tests for mission status + a11y

* test(a11y): scan for a11y violations

* a11y: remove labels for non-interactive elements
2024-02-06 21:44:01 +00:00
eeb8e9704b Ensure previews work for plots (#7459)
* fix large view in tree

* remove existing view concept

* fix plots in overlays

* remove debug and actually remove overlays when dismissed

* add test

* improve tests

* move test
2024-02-06 08:16:31 +01:00
0aceb4b590 Filter values as a string not an object (#7448)
* Push the value of a property to the activity as a string if it is not undefined.

* Add documentation for sourceMap filterMetadata

* Allow . for filtering. Check for null values
2024-02-05 18:38:37 +00:00
82fa4c1597 feat: Mission Status for Situational Awareness (#7418)
* refactor: `UserIndicator` use vue component directly

* style(WIP): filler styles for user-indicator

* feat(WIP): working on mission status indicators

* feat: support mission statuses

* feat(WIP): can display mission statuses now

* feat(WIP): add composables and dynamically calculate popup position

* feat(WIP): dismissible popup, use moar compositionAPI

* Closes #7420
- Styling and markup for mission status control panel.
- Tweaks and additions to some common style elements.

* feat: set/unset mission status for role

* refactor: rename some functions

* feat: more renaming, get mission role statuses working

* refactor: more method renaming

* fix: remove dupe method

* feat: hook up event listeners

* refactor: convert to CompositionAPI and listen to events

* fix: add that back in, woops

* test: fix some existing tests

* lint: words for the word god

* refactor: rename

* fix: setting mission statuses

* style: fix go styling

* style: add mission status button

* refactor: rename `MissionRole` -> `MissionAction`

* test: fix most existing tests

* test: remove integration tests already covered by e2e

- These tests are going to be wonky since they depend on the View. Any unit tests depending on Vue / the View will become increasingly volatile over time as we migrate more of the app into the main Vue app. Since these LOC are already covered by e2e, I'm going to remove them. We will need to move towards a more component / Vue-friendly testing framework to stabilize all of this.

* docs: add documentation

* refactor: rename

* fix: a comma

* refactor: a word

* fix: emit parameter format

* fix: correct emit for `missionStatusActionChange`

---------

Co-authored-by: Charles Hacskaylo <charles.f.hacskaylo@nasa.gov>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-02-02 22:50:16 +00:00
ee5081f807 test(visual): add unclipped activity names visual tests + a11y fixes (#7454)
* test: add unclipped activity names visual tests + a11y fixes

* lint: add additional dictionaries
2024-02-02 19:47:52 +00:00
3cbaa7bf07 Have countdowns in the Timelist use a - symbol (#7452)
* fix issue

* add test

* remove debugger

* expanded legend showing, but malformed

* Revert "expanded legend showing, but malformed"

This reverts commit b954f00257.
2024-02-02 09:37:37 +01:00
18e4b9da65 Add Expanded view for Time List (#7378)
* Add activity states domain object and interceptor to auto create one

* Add activity state inspector option

* Only save status if we have a unique ids for activities

* Include the id in the activity properties

* Don't show activity state section in the inspector if multiple activities are selected

* Display activity properties when an activity row is selected in the timelist

* Add compact view for timelist

* Add inspector configuration for compact view

* Set colors based on time relation of activity

* Use activity id as key if it is available

* Ensure the correct option is selected for activity states

* Closes #7377
- Markup and CSS sanding and polishing.
- Still WIP!

* Closes #7377
- Markup and CSS sanding and polishing.
- Still WIP!

* Add status label

* Rename to Expanded view and isExpanded as properties. Add display style dropdown configuration in the inspector.

* Refactor activity selection. Display activity properties

* Closes #7377
- Label formatting Todo notes about states.
- Computed values and `v-ifs` added to control display for progress pie and countdown 'hero'.
- Still WIP!

* Closes #7377
- Add svg icons and some stubbed in logic.
- Still WIP!

* Remove activity states plugin. Move the activity states interceptor to the plan plugin.

* Change activity states interceptor parameters to options

* Rename constants

* Fix activity states test

* Addresses review comments making code more readable.

* Closes #7377
- Significant adds for large Time List element styling for activity states.
- `$color*` Time List-related theme constants remapped and significantly enhanced.
- Code cleanup and removal of stubbed-in SCSS vars.

* Closes #7377
- Unit testing and colors in Snow in progress.
- Fixed erroneous checkin in ExpandedViewItem.vue.

* Remove ExpandedView component and pull the ExpandedViewItem up to the top level.
Same for ListView, pulling the ListItem up one level.

* Fix sorting for compact view.
Hardcode options for switching compact/expanded views.

* Closes #7377
- Snow Time List colors finalized and smoke tested.
- New graphic SVG for skipped activity.
- Added aria labels to SVG graphics.

* Closes #7377
- Fixed div with no class.

* Add e2e test for activity states feature.

* Address review comments. Rename variables, documentation.

* No shallow copy

* Merge updates to activity-state

* Sync with activity states PR

* Draft of progress-pie

* - Add `s-selected` styling for Expanded Time List elements.

* Add 2 new date formats

* Look and feel enhancements for pie, zero duration events and start and end time formats

* Fix pie show/hide condition

* Final touches to the pie and labels

* Refactor label logic

* Closes #7377
- Added `sweep-hand` animation element to progress pie graphic SVG.

* Remove use of ListView - no point passing arrays around since we are already using sortedItems and itemProperties for expandedViewItems

* We addded a new column for duration and changed the previous duration to countdown. This required adjustment of the test

* Fix expanded view for timelist tests

* Closes #7377
- Fixed display logic for inferred execution states.

* Closes #7377
- Fixed a bug that threw console errors when a value was undefined.

* Optimize rendering of timelist activities

* Remove focused test

* Address review comments

* Remove reactive selection for plan activities

* destructure props into individual item properties for render performance benefits

* Use local variables and remove JSON utility methods

* Change cancelled to skipped

* Focus the activity tab when shown

* Fix label updates

* Add countup to cspell

* Remove progress pie due to licensing unknowns

---------

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Charles Hacskaylo <charles.f.hacskaylo@nasa.gov>
2024-01-30 16:30:57 -08:00
d42aa545bb Add support for multiple CouchDB databases, multiple namespaces, and readOnly configurations (#7413)
* two namespaces appear

* works with two databases

* try to batch requests

* fix indicators

* add option to omit root for myitems

* ready for review

* ready for review

* ready for review

* typo in README

* spelling

* spelling

* update readme

* fix tests

* Update src/plugins/persistence/couch/README.md

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

* remove omitRoot

* remove omitRoot

* fix my items to check for namespace when intercepting

* update readme

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2024-01-30 15:10:31 -08:00
69b81c00ca [e2e] Steps to reduce flakiness in test reporting (#7436)
* flakefinder gha

* Update e2e-flakefinder.yml

* driveby

* skip visual

* first attempt at sharding with circle

* Updated config.yml

* fixes

* missing quote

* re-enable old jobs and update to 7x

* max failures

* destructure the npm script

* missing quote

* revert

* uncomment and re-add 7 parallel

* add unit tests

* add p flag

* skip the flaky test
2024-01-30 14:21:52 -08:00
068ac4899d Fix constrast for accessibility (#7315)
* Closes #7304
- Change colors to increase contrast.
- New base level theme color var: `$colorBodyFgSubtle`.
- Minor CSS cleanups.
- WARNING: this appears to have added a regression in selects
that colors the arrow black in Espresso.

* Closes #7304
- Fix dropdown arrow colors, whew.
- Normalize font sizes in Status area.
- More color changes for contrast, including new theme constants.
- TODO: compare and sync Espresso with other themes.
- TODO: check for regressions!

* disable ruleset

* Closes #7304
- Normalize font sizes in multiple spots.
- More color changes for contrast, including more new theme constants.
- TODO: compare and sync Espresso with other themes.
- TODO: check for regressions!

* Closes #7304
- Reorganize CSS files for more uniformity.

* Closes #7304
- CSS normalized across all themes via Google Sheet at https://docs.google.com/spreadsheets/d/1SEEtuNSq6I7gvVHKpHW8_fp8Ltc-HOAWxrSAkUzS6Kw/edit?usp=sharing

* Closes #7304
- Color tweaks, normalization.

* Closes #7304
- Color tweaks, normalization.
- Search layout, contrast and font-size improvements.
- Added '+' icons to collapsed pane buttons.

* Closes #7304
- Shell head layout improvements.

* Update ColorKey for Take Snapshot Failures and Opacity labels. Also fix create menu

* Closes #7410
- CHERRY PICK FROM event-colors-7410.
- Event display approach modified to include background color.
- Theme colors modified and constrast verified via Wave a11y browser plugin.

* Closes #7304
- Set back to install Espresso theme by default.

* temporarily skip

* remove comment

* lint

* Update default colors

* update snapshot

* missed

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2024-01-29 21:30:55 -08:00
f8d936a834 Use different root for renderWhenVisible (#7415)
* attempt to fix

* reenable test

* going to revert most of this, but works

* slowly reverting changes

* further reversions to the mean

* reversion to the mean

* revert

* change to use openmct element

* reference issue

* reference issue

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-01-29 15:33:47 -08:00
5c21c34568 Support telemetry batching and move WebSocket handling to worker (#7391)
* Support subscription batching from API, Tables, and Plots

* Added batching worker

* Added configurable batch size and throttling rate

* Support batch size based throttling

* Default to latest strategy

* Don't hide original error

* Added copyright statement

* Renamed BatchingWebSocketProvider to BatchingWebSocket

* Adding docs

* renamed class. changed throttling strategy to be driven by the main thread

* Renamed classes

* Added more documentation

* Fixed broken tests

* Addressed review comments

* Clean up and reconnect on websocket close

* Better management of subscription strategies

* Add tests to catch edge cases where two subscribers request different strategies

* Ensure callbacks are invoked with telemetry in the requested format

* Remove console out. Oops

* Fix linting errors
2024-01-29 15:17:55 -08:00
0eea2e0bbc chore(deps-dev): bump marked from 11.1.0 to 11.2.0 (#7427)
Bumps [marked](https://github.com/markedjs/marked) from 11.1.0 to 11.2.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.1.0...v11.2.0)

---
updated-dependencies:
- dependency-name: marked
  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-01-29 12:32:06 -08:00
61acc91200 chore(deps-dev): bump sass-loader from 13.3.2 to 14.0.0 (#7428)
Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 13.3.2 to 14.0.0.
- [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/v13.3.2...v14.0.0)

---
updated-dependencies:
- dependency-name: sass-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-01-29 12:31:36 -08:00
a52982d2bf chore(deps-dev): bump moment from 2.29.4 to 2.30.1 (#7425)
Bumps [moment](https://github.com/moment/moment) from 2.29.4 to 2.30.1.
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.4...2.30.1)

---
updated-dependencies:
- dependency-name: moment
  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-01-29 12:27:25 -08:00
1d40b134b6 Make Tabs eager loading configurable but default to false (#7199)
* convert tabs plugin to use es6 import/export

* default of eager load is false but configurable

* change true/false select to toggleSwitch

* add and clean up unit tests

* Update test

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-01-29 12:08:38 -08:00
735c8236e5 Skip some tests, fix a mislabeled test, and add default condition for tabs (#7422)
* wrong format

* skip the flake for now

* Add Tabs test

* one more flake
2024-01-28 16:28:49 -08:00
dc5a3236b3 Activity state display for plans in Gantt and Time list views (#7370)
* Add activity states domain object and interceptor to auto create one

* Add activity state inspector option

* Only save status if we have a unique ids for activities

* Include the id in the activity properties

* Don't show activity state section in the inspector if multiple activities are selected

* Display activity properties when an activity row is selected in the timelist

* Use activity id as key if it is available

* Ensure the correct option is selected for activity states

* Add status label

* Refactor activity selection. Display activity properties

* Remove activity states plugin. Move the activity states interceptor to the plan plugin.

* Change activity states interceptor parameters to options

* Rename constants

* Fix activity states test

* Add e2e test for activity states feature.

* Address review comments. Rename variables, documentation.

* No shallow copy

* Suppress lint warning for conditionals

* Remove check for abort controller

* Move classes to components

* number primitive

* Closes #7369
- WIP tweaks to simplify the Inspector view.

* Ensure 'notStarted' is the default state for activities

* Remove extra quotes

* Closes #7369
- Mod to `s-selected` styling to allow selection visiblity on Time List rows.

* Use generated key for vue

* Fix e2e tests

* Fix timelist test

---------

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-01-28 15:46:10 -08:00
60e1eeba8e Add filtering by metadata (#7388)
* Add filtering by metadata. Add new sourceMap property to get a list of properties for metadata filtering.

* Change filter label names

* Add aria-labels

* Closes #7389
- Added a "No filters applied" message for both input areas.
- Added additional detail about how it works in the hint text visible while editing.

* Restore valid state if there is an error

* Fix linting error

* Tests for filtering by metadata

---------

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
2024-01-28 18:04:52 +00:00
1fc6056c51 Set disabled items to use disabled property (#7342)
* Closes #7322
- New CSS for `aria-disabled = true` property.
- Changed multiple items to use aria-disabled instead of .disabled, including:
  - Action menu
  - Super menu
  - Notebook drag area
- Tree item style modded to only italicize when is-navigated and is being edited.

* Closes #7322
- New CSS for `aria-disabled = true` property.
- Changed multiple items to use aria-disabled instead of .disabled, including:
  - Action menu
  - Super menu
  - Notebook drag area
- Tree item style modded to only italicize when is-navigated and is being edited.
- Create button sets itself to `disabled` when the editor is in use.

* Closes #7322
- Create button now _actually_ sets itself to `aria-disabled` when the editor is in use.
- CSS removes selector for `.is-editing`.

* fix conflict

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-01-26 14:55:13 -08:00
b9df97e2bc Table performance paging (#7399)
* dereactifying the row before passing it to the commponent

* debouncin

* i mean... throttle

* initial

* UI functionality, switching between modes, prevention of export in performance mode, respect size option in swgs

* added limit maintenance in table row collectins, autoscroll respecting sort order

* updating the logic to work correctly :)

* added handling for overflow rows, this way if an object is removed, we can go back to the most recent rows for all remaining items and repopulate the table if necessary

* removing debug row numbers

* Closes #7268
- Layout and style sanding and polishing.
- Added title to button.
- More direct button labeling.

* Closes #7268
Partially closes #7147
- Removed footer hover behavior: table footer now always visible.
- Tweaks to style, margin etc. to make footer more compact.

* moved row limiting out of table row collections and into telemetry collections, table row collections will only limit what they return in getRows, handling sorting when in different modes

* have swgs return enough data to fill the requested bounds

* support minmax in swgs

* using undefined for more clarity

* clearing up boolean typo

* Address lint fixes

* removing autoscroll for descending, it is not necessary

* update snapshots

* lint

---------

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-01-26 13:24:24 -08:00
b985619d16 Table CPU Performance Improvements (#7392)
* dereactifying the row before passing it to the commponent

* debouncin

* i mean... throttle
2024-01-26 10:40:16 -08:00
3e31bbef97 Get actions collection on Preview Container update (#7385)
* Get actions collection on Preview Container update

* Added fixme and link to initial ticket

* Stubbed out preview mode e2e test

* Lint fix

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2024-01-26 17:35:52 +00:00
3e5ada8f5f Fix actions menu on display layout alphanumerics (#7414)
* remove errant action

* add e2e test
2024-01-25 16:26:03 +01:00
2b2c74da9c New action to reload an individual view and all of its children (#7362)
* add reload action plugin

* checking for domain object before reloading

* check if objects are equal before refreshing

* add test

* lint

* change to label

* ensure object styles are initialized

* resubscribe to staleness too

* add better labels for tabels

* ensure tab uses exact for label now due to table aria changes

* fix table tests

* make tabs exact

* update conflicts

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2024-01-25 05:36:44 +00:00
450cab428f move performance tests to GHA (#7412)
* move performance tests to GHA

* no need for chrome beta

* Add baseline imagery test

* skip flaky app

* lint
2024-01-24 21:26:48 -08:00
0340fe18fa Change Imagery positional freshness label from 'POS' to 'ROV' (#7409)
Closes #7404
- Change 'POS' to 'ROV'.
2024-01-25 05:18:29 +00:00
401 changed files with 24844 additions and 3760 deletions

View File

@ -1,57 +1,31 @@
version: 2.1
orbs:
node: circleci/node@5.2.0
browser-tools: circleci/browser-tools@1.3.0
executors:
pw-focal-development:
docker:
- image: mcr.microsoft.com/playwright:v1.39.0-focal
- image: mcr.microsoft.com/playwright:v1.42.1-focal
environment:
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_LOGLEVEL: 'debug' # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742)
PERCY_PARALLEL_TOTAL: 2
ubuntu:
machine:
image: ubuntu-2204:current
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:
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:
node-version:
type: string
steps:
- checkout
- restore_cache_cmd:
node-version: << parameters.node-version >>
- node/install:
node-version: << parameters.node-version >>
- run: npm install --no-audit --progress=false
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
- node/install-packages
generate_and_store_version_and_filesystem_artifacts:
description: 'Track important packages and files'
steps:
@ -71,9 +45,6 @@ commands:
steps:
- run: npm run cov:e2e:report || true
- run: npm run cov:e2e:<<parameters.suite>>:publish
orbs:
node: circleci/node@5.1.0
browser-tools: circleci/browser-tools@1.3.0
jobs:
npm-audit:
parameters:
@ -105,10 +76,12 @@ jobs:
node-version: <<parameters.node-version>>
- browser-tools/install-chrome:
replace-existing: false
- run: npm run test
- run:
command: |
mkdir -p dist/reports/tests/
TESTFILES=$(circleci tests glob "src/**/*Spec.js")
echo "$TESTFILES" | circleci tests run --command="xargs npm run test" --verbose
- run: npm run cov:unit:publish
- save_cache_cmd:
node-version: <<parameters.node-version>>
- store_test_results:
path: dist/reports/tests/
- store_artifacts:
@ -123,7 +96,7 @@ jobs:
suite: #stable or full
type: string
executor: pw-focal-development
parallelism: 6
parallelism: 7
steps:
- build_and_install:
node-version: lts/hydrogen
@ -132,7 +105,11 @@ jobs:
equal: ['full', <<parameters.suite>>]
steps:
- run: npx playwright install chrome-beta
- run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
- run:
command: |
mkdir test-results
TESTFILES=$(circleci tests glob "e2e/**/*.spec.js")
echo "$TESTFILES" | circleci tests run --command="xargs npm run test:e2e:<<parameters.suite>>" --verbose --split-by=timings
- when:
condition:
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
@ -152,12 +129,37 @@ jobs:
equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_and_store_version_and_filesystem_artifacts
e2e-mobile:
executor: pw-focal-development
steps:
- build_and_install:
node-version: lts/hydrogen
- run: npm run test:e2e:mobile
- when:
condition:
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:
- generate_e2e_code_cov_report:
suite: full
- store_test_results:
path: test-results/results.xml
- store_artifacts:
path: test-results
- store_artifacts:
path: coverage
- store_artifacts:
path: html-test-results
- when:
condition:
equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_and_store_version_and_filesystem_artifacts
e2e-couchdb:
executor: ubuntu
steps:
- build_and_install:
node-version: lts/hydrogen
- run: npx playwright@1.39.0 install #Necessary for bare ubuntu machine
- run: npx playwright@1.42.1 install #Necessary for bare ubuntu machine
- run: |
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
@ -219,14 +221,15 @@ 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
steps:
- generate_and_store_version_and_filesystem_artifacts
visual-a11y-tests:
visual-a11y:
parameters:
suite:
type: string # ci or full
executor: pw-focal-development
parallelism: 2
steps:
- build_and_install:
node-version: lts/hydrogen
node-version: lts/iron
- run: npm run test:e2e:visual:<<parameters.suite>>
- store_test_results:
path: test-results/results.xml
@ -239,6 +242,7 @@ jobs:
equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_and_store_version_and_filesystem_artifacts
workflows:
overall-circleci-commit-status: #These jobs run on every commit
jobs:
@ -251,10 +255,9 @@ workflows:
- e2e-test:
name: e2e-stable
suite: stable
- mem-test
- perf-test
- visual-a11y-tests:
name: visual-test-ci
- e2e-mobile
- visual-a11y:
name: visual-a11y-ci
suite: ci
the-nightly: #These jobs do not run on PRs, but against master at night
@ -270,10 +273,11 @@ workflows:
- e2e-test:
name: e2e-full-nightly
suite: full
- mem-test
- e2e-mobile
- perf-test
- visual-a11y-tests:
name: visual-test-nightly
- mem-test
- visual-a11y:
name: visual-a11y-nightly
suite: full
- e2e-couchdb
triggers:

View File

@ -492,9 +492,14 @@
"gcov",
"WCAG",
"stackedplot",
"Andale"
"Andale",
"unnormalized",
"checksnapshots",
"specced",
"composables",
"countup"
],
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US"],
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US", "en-gb", "misc"],
"ignorePaths": [
"package.json",
"dist/**",

View File

@ -1,10 +1,13 @@
const LEGACY_FILES = ['example/**'];
module.exports = {
/** @type {import('eslint').Linter.Config} */
const config = {
env: {
browser: true,
es6: true,
es2024: true,
jasmine: true,
amd: true
node: true,
worker: true,
serviceworker: true
},
globals: {
_: 'readonly'
@ -23,10 +26,11 @@ module.exports = {
parser: '@babel/eslint-parser',
requireConfigFile: false,
allowImportExportEverywhere: true,
ecmaVersion: 2015,
ecmaVersion: 'latest',
ecmaFeatures: {
impliedStrict: true
}
},
sourceType: 'module'
},
rules: {
'simple-import-sort/imports': 'warn',
@ -152,7 +156,7 @@ module.exports = {
cases: {
pascalCase: true
},
ignore: ['^.*\\.js$']
ignore: ['^.*\\.(js|cjs|mjs)$']
}
],
'vue/first-attribute-linebreak': 'error',
@ -179,3 +183,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 checked to ensure there aren't other open [Pull Requests](https://github.com/nasa/openmct/pulls) for the same update/change?
* [ ] Is this change backwards compatible? For example, developers won't need to change how they are calling the API or how they've extended core plugins such as Tables or Plots.
* [ ] 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
@ -17,7 +17,6 @@ Closes <!--- Insert Issue Number(s) this PR addresses. Start by typing # will op
* [ ] 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 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?
### Reviewer Checklist

5
.github/release.yml vendored
View File

@ -1,5 +1,8 @@
changelog:
categories:
- title: 💥 Notable Changes
labels:
- notable_change
- title: 🏕 Features
labels:
- type:feature
@ -20,4 +23,4 @@ changelog:
- dependencies
- title: 🐛 Bug Fixes
labels:
- '*'
- "*"

View File

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

61
.github/workflows/e2e-flakefinder.yml vendored Normal file
View File

@ -0,0 +1,61 @@
name: 'pr:e2e:flakefinder'
on:
# push:
# branches: master
workflow_dispatch:
# pull_request:
# types:
# - labeled
# - opened
# schedule:
# - cron: '0 0 * * *'
jobs:
e2e-flakefinder:
if: contains(github.event.pull_request.labels.*.name, 'pr:e2e:flakefinder') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event.action == 'opened'
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 'lts/hydrogen'
- name: Cache NPM dependencies
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npx playwright@1.42.1 install
- run: npm ci --no-audit --progress=false
- name: Run E2E Tests (Repeated 10 Times)
run: npm run test:e2e:stable -- --retries=0 --repeat-each=10 --max-failures=50
- name: Archive test results
if: success() || failure()
uses: actions/upload-artifact@v3
with:
path: test-results
- name: Remove pr:e2e:flakefinder label (if present)
if: always()
uses: actions/github-script@v6
with:
script: |
const { owner, repo, number } = context.issue;
const labelToRemove = 'pr:e2e:flakefinder';
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: number,
name: labelToRemove
});
} catch (error) {
core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);
}

58
.github/workflows/e2e-perf.yml vendored Normal file
View File

@ -0,0 +1,58 @@
name: 'e2e-perf'
on:
push:
branches: master
workflow_dispatch:
pull_request:
types:
- labeled
- opened
schedule:
- cron: '0 0 * * *'
jobs:
e2e-full:
if: contains(github.event.pull_request.labels.*.name, 'pr:e2e:perf') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 'lts/hydrogen'
- name: Cache NPM dependencies
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npx playwright@1.42.1 install
- run: npm ci --no-audit --progress=false
- run: npm run test:perf:localhost
- run: npm run test:perf:contract
- run: npm run test:perf:memory
- name: Archive test results
if: success() || failure()
uses: actions/upload-artifact@v3
with:
path: test-results
- name: Remove pr:e2e:perf label (if present)
if: always()
uses: actions/github-script@v6
with:
script: |
const { owner, repo, number } = context.issue;
const labelToRemove = 'pr:e2e:perf';
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: number,
name: labelToRemove
});
} catch (error) {
core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);
}

View File

@ -33,9 +33,9 @@ jobs:
restore-keys: |
${{ runner.os }}-node-
- run: npx playwright@1.39.0 install
- run: npx playwright@1.42.1 install
- 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 cov:e2e:report || true
- shell: bash

View File

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

View File

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

View File

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

4
.gitignore vendored
View File

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

View File

@ -22,9 +22,3 @@
!index.html
!openmct.js
!SECURITY.md
# Add e2e tests to npm package
!/e2e/**/*
# ... 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
engine-strict=true
# Dont include lockfile
package-lock=false

13
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,13 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"rvest.vs-code-prettier-eslint"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": ["octref.vetur"]
}

View File

@ -1,8 +1,8 @@
/*
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.dev.js - the development configuration for OpenMCT
- webpack.coverage.js - imports webpack.dev.js and adds code coverage
- webpack.prod.mjs - the production configuration for OpenMCT (default)
- webpack.dev.mjs - the development configuration for OpenMCT
- 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`
will use the default production configuration.
*/
@ -15,6 +15,7 @@ import CopyWebpackPlugin from 'copy-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import { VueLoaderPlugin } from 'vue-loader';
import webpack from 'webpack';
import { merge } from 'webpack-merge';
let gitRevision = 'error-retrieving-revision';
let gitBranch = 'error-retrieving-branch';
@ -54,9 +55,11 @@ const config = {
globalObject: 'this',
filename: '[name].js',
path: path.resolve(projectRootDir, 'dist'),
library: 'openmct',
libraryExport: 'default',
libraryTarget: 'umd',
library: {
name: 'openmct',
type: 'umd',
export: 'default'
},
publicPath: '',
hashFunction: 'xxhash64',
clean: true

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
information to pull requests.
*/
import config from './webpack.dev.js';
// eslint-disable-next-line no-undef
const CI = process.env.CI === 'true';
config.devtool = CI ? false : undefined;
import config from './webpack.dev.mjs';
config.devtool = 'source-map';
config.devServer.hot = false;
config.module.rules.push({
@ -19,7 +16,6 @@ config.module.rules.push({
loader: 'babel-loader',
options: {
retainLines: true,
// eslint-disable-next-line no-undef
plugins: [
[
'babel-plugin-istanbul',

View File

@ -1,14 +1,15 @@
/*
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.
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 webpack from 'webpack';
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, {
mode: 'development',

View File

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

10
API.md
View File

@ -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
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.
The project is also monitored by [LGTM](https://lgtm.com/projects/g/nasa/openmct/) and is available to public.
### ESLint
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
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. [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`.
@ -91,12 +91,14 @@ There are a few reasons that your GitHub PR could be failing beyond simple faile
### Local=Pass and CI=Fail
Although rare, it is possible that your test can pass locally but fail in CI.
#### Busting Cache
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:
1. Navigate to the branch in Circle CI believed to have stale cache.
1. Click on the 'Trigger Pipeline' button.
1. Add Parameter -> Parameter Type = boolean , Name = BUST_CACHE ,Value = true
1. Click 'Trigger Pipeline'
### Reset your workspace
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:
```sh
nvm use
npm run clean
npm install
```
#### Run tests in the same container as CI

View File

@ -0,0 +1,30 @@
# Release of NASA Open MCT NPM Package
This document outlines the process and key considerations for releasing a new version of the NASA Open MCT project as an NPM (Node Package Manager) package.
## 1. Pre-requisites
Before releasing a new version of the NASA Open MCT NPM package, ensure all dependencies are updated, and comprehensive tests are performed. This ensures compatibility and performance of the Open MCT within the Node.js ecosystem.
## 2. Versioning
Versioning is a critical step for package release. The Open MCT team follows [Semantic Versioning (SemVer)](https://semver.org) that consists of three major components: MAJOR.MINOR.PATCH. These ensure a structured process for updating, bug fixes, backward compatibility, and software progress.
## 3. Changelog Maintenance
A comprehensive changelog file, `CHANGELOG.md`, documents any changes, adding a high level of transparencies for anyone desiring to look into the status of new and past progress. It includes the summation of any major new enhancements, changes, bug fixes, and the credits to the users responsible for each unique progress.
## 4. Notable Changes Labels on GitHub PRs
For the Open MCT package, we leverage GitHub's Pull Request (PR) mechanisms extensively, with three important PR labels dedicated to signifying 'notable_changes':
- **Breaking Change** Highlights the integration of changes that are suspected to break, or without a doubt will break, backward compatibility. These should signal to users the upgrade might be seamless only if dependency and integration factors are properly managed, if not, one should expect to manage atypical technical snags.
- **API change** Signifies when a contribution makes any complete or under layer changes to the communication or its supporting access processes. This label flags required see-through insight on how the web-based control panel sees and manipulates any value and or network logs.
- **Default Behavior Change:** In the incident an update either adjusts a form to or integrates a not previously kept setting or plugin. i.e. autoscale is enabled by default when working with plots.
## 6. Community & Contributions
A flat community and the rounded center are kept in continuous celebration, with the given station open for two open-specifying dialogues, research, and all-for development probing. State the ownership for a handed looped, a welcome for even structure-core and architectural draft and impend.
Thank you for your collaboration and commitment to moving the project onto a text big club.

7
e2e/.npmignore Normal file
View File

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

View File

@ -76,8 +76,9 @@ 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: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: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.
#### 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).
@ -89,15 +90,16 @@ At present, we are using percy with two configuration files: `./e2e/.percy.night
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
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
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
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
@ -109,7 +111,7 @@ For those interested in the mechanics of snapshot testing with Playwright, you c
// from our package.json or circleCI configuration file
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v{X.X.X}-focal /bin/bash
npm install
npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot
npm run test:e2e:checksnapshots
```
### Updating Snapshots
@ -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.
MacOS
```
npm run test:e2e:updatesnapshots
```
Linux/CI
```sh
// Replace {X.X.X} with the current Playwright version
// from our package.json or circleCI configuration file
@ -134,6 +128,12 @@ npm install
npm run test:e2e:updatesnapshots
```
Once that's done, you'll need to run the following to verify that the changes do not cause more problems:
```sh
npm run test:e2e:checksnapshots
```
## Automated Accessibility (a11y) Testing
Open MCT incorporates accessibility testing through two primary methods to ensure its compliance with accessibility standards:
@ -167,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:
`./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.
`./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.
`./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` - 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/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/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.
@ -223,7 +223,7 @@ Current list of test tags:
|Test Tag|Description|
|:-:|-|
|`@ipad` | Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no create button).|
|`@mobile` | Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no create button).|
|`@a11y` | Test case or test suite to execute playwright-axe accessibility checks and generate a11y reports.|
|`@gds` | Denotes a GDS Test Case used in the VIPER Mission.|
|`@addInit` | Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of `npm start`.|
@ -232,6 +232,7 @@ Current list of test tags:
|`@unstable` | A new test or test which is known to be flaky.|
|`@2p` | Indicates that multiple users are involved, or multiple tabs/pages are used. Useful for testing multi-user interactivity.|
|`@generatedata` | Indicates that a test is used to generate testdata or test the generated test data. Usually to be associated with localstorage, but this may grow over time.|
|`@clock` | A test which modifies the clock. These have expanded out of the visual tests and into the functional tests.
### Continuous Integration
@ -323,9 +324,17 @@ In terms of operating system testing, we're only limited by what the CI provider
#### **Mobile**
We have the Mission-need to support iPad. To run our iPad suite, please see our `playwright-*.config.js` with the 'iPad' project.
We have a Mission-need to support iPad and mobile devices. To run our test suites with mobile devices, please see our `playwright-mobile.config.js` projects.
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) and so this will likely turn into a separate 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.
For now, the mobile tests will exist in the /tests/mobile/ suites and be executed with the
```sh
npm run test:e2e:mobile
```
command.
#### **Skipping or executing tests based on browser, os, and/os browser version:**
@ -364,6 +373,7 @@ 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.
#### 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. 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.
@ -371,6 +381,7 @@ By adhering to this principle, we can create tests that are both robust and refl
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
1. Avoid app interaction when possible. The best way of doing this is to navigate directly by URL:
```js
@ -387,6 +398,7 @@ By adhering to this principle, we can create tests that are both robust and refl
This ensures that your changes will be picked up with large refactors.
##### 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. 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:
@ -407,7 +419,6 @@ By adhering to this principle, we can create tests that are both robust and refl
});
```
### How to write a great test
- Avoid using css locators to find elements to the page. Use modern web accessible locators like `getByRole`
@ -423,7 +434,7 @@ By adhering to this principle, we can create tests that are both robust and refl
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.
@ -432,22 +443,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.
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
- Employ the `createExampleTelemetryObject` appAction to source telemetry and specify a `name` to avoid autogenerated names.
- 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
- 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:
- `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:
```js
await percySnapshot(page, `Tree Pane w/ single level expanded (theme: ${theme})`, {
scope: treePane
});
```
- 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:
```js
//<Some interesting state>
await percySnapshot(page, `Before object expanded (theme: ${theme})`);
@ -497,11 +513,35 @@ test.describe('foo test suite', () => {
});
});
```
More info and options for `overrideClock` can be found in [baseFixtures.js](baseFixtures.js)
- Working with multiple pages
There are instances where multiple browser pages will needed to verify multi-page or multi-tab application behavior. Make sure to use the `@2p` annotation as well as name each page appropriately: i.e. `page1` and `page2` or `tab1` and `tab2` depending on the intended use case. Generally pages should be used unless testing `sharedWorker` code, specifically.
- 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
Test Reporting is done through official Playwright reporters and the CI Systems which execute them.
@ -577,6 +617,7 @@ A single e2e test in Open MCT is extended to run:
### Writing Tests
Playwright provides 3 supported methods of debugging and authoring tests:
- A 'watch mode' for running tests locally and debugging on the fly
- A 'debug mode' for debugging tests and writing assertions against tests
- A 'VSCode plugin' for debugging tests within the VSCode IDE.

View File

@ -392,6 +392,8 @@ async function setTimeConductorMode(page, isFixedTimespan = true) {
await page.getByRole('menuitem', { name: /Real-Time/ }).click();
await page.waitForURL(/tc\.mode=local/);
}
//dismiss the time conductor popup
await page.getByLabel('Discard changes and close time popup').click();
}
/**
@ -505,15 +507,14 @@ async function setTimeConductorBounds(page, startDate, endDate) {
* @param {string} startDate
* @param {string} endDate
*/
async function setIndependentTimeConductorBounds(page, startDate, endDate) {
// Activate Independent Time Conductor in Fixed Time Mode
await page.getByRole('switch').click();
async function setIndependentTimeConductorBounds(page, { start, end }) {
// Activate Independent Time Conductor
await page.getByLabel('Enable Independent Time Conductor').click();
// 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 setTimeBounds(page, startDate, endDate);
await setTimeBounds(page, start, end);
await page.keyboard.press('Enter');
}
@ -663,5 +664,6 @@ export {
setRealTimeMode,
setStartOffset,
setTimeConductorBounds,
setTimeConductorMode,
waitForPlotsToRender
};

View File

@ -36,32 +36,71 @@
import AxeBuilder from '@axe-core/playwright';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { expect, test } from './pluginFixtures.js';
// 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);
}
});
/**
* 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.
*
* @typedef {object} GenerateReportOptions
* @property {string} [reportName] - The name for the report file.
*
* @param {import('playwright').Page} page - The page object from Playwright.
* @param {string} testCaseName - The name of the test case.
* @param {GenerateReportOptions} [options={}] - The options for the report generation.
*
* @returns {Promise<object|null>} Returns the accessibility scan results if violations are found,
* otherwise returns null.
* @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.
*/
/* eslint-disable no-undef */
export async function scanForA11yViolations(page, testCaseName, options = {}) {
const builder = new AxeBuilder({ page });
builder.withTags(['wcag2aa']);
// https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md
builder.disableRules(['color-contrast']);
const accessibilityScanResults = await builder.analyze();
// Assert that no violations should be present
@ -94,4 +133,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
* 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}
* @private
* @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) {
const { url, lineNumber, columnNumber } = msg.location();
@ -61,14 +60,16 @@ function waitForAnimations(locator) {
);
}
/**
* This is part of our codecoverage shim.
const istanbulCLIOutput = fileURLToPath(new URL('.nyc_output', import.meta.url));
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}
*/
const istanbulCLIOutput = path.join(process.cwd(), '.nyc_output');
const extendedTest = test.extend({
coveragePath: [istanbulCLIOutput, { option: true }],
/**
* This allows the test to manipulate the browser clock. This is useful for Visual and Snapshot tests which need
* the Time Indicator Clock to be in a specific state.
@ -111,21 +112,55 @@ const extendedTest = test.extend({
scope: 'test'
}
],
/**
* Exposes a function to manually tick the clock. This is useful when overriding the clock to not
* tick (`shouldAdvanceTime: false`) for visual tests, as events such as re-renders and router params
* updates are clock-driven and must be manually ticked.
*
* Usage:
* ```js
* test.describe('Manual Clock Tick', () => {
* test.use({
* clockOptions: {
* now: MISSION_TIME, // Set to the desired time
* shouldAdvanceTime: false // Clock overridden to no longer tick
* }
* });
* test('Visual - Manual Clock Tick', async ({ page, tick }) => {
* // Tick the clock 2 seconds in the future
* await tick(2000);
* });
* });
* ```
*
* @param {Object} param0
* @param {import('@playwright/test').Page} param0.page
* @param {import('@playwright/test').Use} param0.use
*/
tick: async ({ page }, use) => {
// eslint-disable-next-line func-style
const tick = async (milliseconds) => {
await page.evaluate((_milliseconds) => {
window.__clock.tick(_milliseconds);
}, milliseconds);
};
await use(tick);
},
/**
* Extends the base context class to add codecoverage shim.
* @see {@link https://github.com/mxschmitt/playwright-test-coverage Github Project}
*/
context: async ({ context }, use) => {
context: async ({ context, coveragePath }, use) => {
await context.addInitScript(() =>
window.addEventListener('beforeunload', () =>
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) => {
if (coverageJSON) {
fs.writeFileSync(
path.join(istanbulCLIOutput, `playwright_coverage_${uuid()}.json`),
path.join(coveragePath, `playwright_coverage_${uuid()}.json`),
coverageJSON
);
}
@ -133,9 +168,9 @@ const extendedTest = test.extend({
await use(context);
for (const page of context.pages()) {
await page.evaluate(() =>
window.collectIstanbulCoverage(JSON.stringify(window.__coverage__))
);
await page.evaluate(() => {
window.collectIstanbulCoverage(JSON.stringify(window.__coverage__));
});
}
},
/**
@ -154,17 +189,13 @@ const extendedTest = test.extend({
// function in the generatorWorker context. This is necessary
// to ensure that example telemetry data is generated for the new clock time.
if (clockOptions?.now !== undefined) {
page.on(
'worker',
(worker) => {
page.on('worker', (worker) => {
if (worker.url().includes('generatorWorker')) {
worker.evaluate((time) => {
self.Date.now = () => time;
});
}, clockOptions.now);
}
},
clockOptions.now
);
});
}
// Capture any console errors during test execution

View File

@ -1,4 +1,3 @@
/* eslint-disable prettier/prettier */
/**
* Constants which may be used across all e2e tests.
*/
@ -8,12 +7,30 @@
* - 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)
// 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
* - This is the URL that the browser will be directed to when running visual tests. This URL
* - hides the tree and inspector to prevent visual noise
* - sets the time bounds to a fixed range
* These constants are used for initial navigation in visual tests, in either fixed or realtime mode.
* They navigate to the 'My Items' folder at MISSION_TIME.
* 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 { expect } from '../pluginFixtures.js';
/**
* @param {import('@playwright/test').Page} page
*/
async function navigateToFaultManagementWithExample(page) {
export async function navigateToFaultManagementWithExample(page) {
await page.addInitScript({
path: fileURLToPath(new URL('./addInitExampleFaultProvider.js', import.meta.url))
});
@ -35,7 +37,7 @@ async function navigateToFaultManagementWithExample(page) {
/**
* @param {import('@playwright/test').Page} page
*/
async function navigateToFaultManagementWithStaticExample(page) {
export async function navigateToFaultManagementWithStaticExample(page) {
await page.addInitScript({
path: fileURLToPath(new URL('./addInitExampleFaultProviderStatic.js', import.meta.url))
});
@ -46,7 +48,7 @@ async function navigateToFaultManagementWithStaticExample(page) {
/**
* @param {import('@playwright/test').Page} page
*/
async function navigateToFaultManagementWithoutExample(page) {
export async function navigateToFaultManagementWithoutExample(page) {
await page.addInitScript({
path: fileURLToPath(new URL('./addInitFaultManagementPlugin.js', import.meta.url))
});
@ -57,7 +59,7 @@ async function navigateToFaultManagementWithoutExample(page) {
/**
* @param {import('@playwright/test').Page} page
*/
async function navigateToFaultItemInTree(page) {
export async function navigateToFaultItemInTree(page) {
await page.goto('./', { waitUntil: 'networkidle' });
const faultManagementTreeItem = page
@ -75,88 +77,95 @@ async function navigateToFaultItemInTree(page) {
/**
* @param {import('@playwright/test').Page} page
*/
async function acknowledgeFault(page, rowNumber) {
export async function acknowledgeFault(page, rowNumber) {
await openFaultRowMenu(page, rowNumber);
await page.locator('.c-menu >> text="Acknowledge"').click();
// Click [aria-label="Save"]
await page.locator('[aria-label="Save"]').click();
await page.getByLabel('Acknowledge', { exact: true }).click();
await page.getByLabel('Save').click();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function shelveMultipleFaults(page, ...nums) {
export async function shelveMultipleFaults(page, ...nums) {
const selectRows = nums.map((num) => {
return selectFaultItem(page, num);
});
await Promise.all(selectRows);
await page.locator('button:has-text("Shelve")').click();
await page.locator('[aria-label="Save"]').click();
await page.getByLabel('Shelve selected faults').click();
await page.getByLabel('Save').click();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function acknowledgeMultipleFaults(page, ...nums) {
export async function acknowledgeMultipleFaults(page, ...nums) {
const selectRows = nums.map((num) => {
return selectFaultItem(page, num);
});
await Promise.all(selectRows);
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
*/
async function shelveFault(page, rowNumber) {
export async function shelveFault(page, rowNumber) {
await openFaultRowMenu(page, rowNumber);
await page.locator('.c-menu >> text="Shelve"').click();
// Click [aria-label="Save"]
await page.locator('[aria-label="Save"]').click();
await page.getByLabel('Save').click();
}
/**
* @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);
}
/**
* @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);
}
/**
* @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);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function clearSearch(page) {
export async function clearSearch(page) {
await enterSearchTerm(page, '');
}
/**
* @param {import('@playwright/test').Page} page
*/
async function selectFaultItem(page, rowNumber) {
await page.locator(`.c-fault-mgmt-item > input >> nth=${rowNumber - 1}`).check();
export async function selectFaultItem(page, rowNumber) {
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
*/
async function getHighestSeverity(page) {
export async function getHighestSeverity(page) {
const criticalCount = await page.locator('[title=CRITICAL]').count();
const warningCount = await page.locator('[title=WARNING]').count();
@ -172,7 +181,7 @@ async function getHighestSeverity(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 watchCount = await page.locator('[title=WATCH]').count();
@ -188,7 +197,7 @@ async function getLowestSeverity(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();
return count;
@ -197,7 +206,7 @@ async function getFaultResultCount(page) {
/**
* @param {import('@playwright/test').Page} page
*/
function getFault(page, rowNumber) {
export function getFault(page, rowNumber) {
const fault = page.locator(
`.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
*/
function getFaultByName(page, name) {
export function getFaultByName(page, name) {
const fault = page.locator(`.c-fault-mgmt__list-faultname:has-text("${name}")`);
return fault;
@ -217,7 +226,7 @@ function getFaultByName(page, name) {
/**
* @param {import('@playwright/test').Page} page
*/
async function getFaultName(page, rowNumber) {
export async function getFaultName(page, rowNumber) {
const faultName = await page
.locator(`.c-fault-mgmt__list-faultname >> nth=${rowNumber - 1}`)
.textContent();
@ -228,7 +237,7 @@ async function getFaultName(page, rowNumber) {
/**
* @param {import('@playwright/test').Page} page
*/
async function getFaultSeverity(page, rowNumber) {
export async function getFaultSeverity(page, rowNumber) {
const faultSeverity = await page
.locator(`.c-faults-list-view-item-body .c-fault-mgmt__list-severity >> nth=${rowNumber - 1}`)
.getAttribute('title');
@ -239,7 +248,7 @@ async function getFaultSeverity(page, rowNumber) {
/**
* @param {import('@playwright/test').Page} page
*/
async function getFaultNamespace(page, rowNumber) {
export async function getFaultNamespace(page, rowNumber) {
const faultNamespace = await page
.locator(`.c-fault-mgmt__list-path >> nth=${rowNumber - 1}`)
.textContent();
@ -250,7 +259,7 @@ async function getFaultNamespace(page, rowNumber) {
/**
* @param {import('@playwright/test').Page} page
*/
async function getFaultTriggerTime(page, rowNumber) {
export async function getFaultTriggerTime(page, rowNumber) {
const faultTriggerTime = await page
.locator(`.c-fault-mgmt__list-trigTime >> nth=${rowNumber - 1} >> .c-fault-mgmt-item__value`)
.textContent();
@ -261,35 +270,10 @@ async function getFaultTriggerTime(page, rowNumber) {
/**
* @param {import('@playwright/test').Page} page
*/
async function openFaultRowMenu(page, rowNumber) {
export async function openFaultRowMenu(page, rowNumber) {
// select
await page
.locator(`.c-fault-mgmt-item > .c-fault-mgmt__list-action-button >> nth=${rowNumber - 1}`)
.getByLabel('Disposition actions')
.nth(rowNumber - 1)
.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

@ -68,7 +68,6 @@ async function commitEntry(page) {
* @param {import('@playwright/test').Page} page
*/
async function startAndAddRestrictedNotebookObject(page) {
// eslint-disable-next-line no-undef
await page.addInitScript({
path: fileURLToPath(new URL('./addInitRestrictedNotebook.js', import.meta.url))
});

View File

@ -20,6 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../appActions.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
* start time as the start bound and the current activity's end time as the end bound.
* @param {import('@playwright/test').Page} page the page
* @param {object} plan The raw plan json to assert against
* @param {Object} plan The raw plan json to assert against
* @param {string} objectUrl The URL of the object to assert against (plan or gantt chart)
*/
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
* groups in the plan data.
* @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)
*/
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,
* and set the bounds to span all activities.
* @param {import('@playwright/test').Page} page
* @param {object} planJson
* @param {Object} planJson
* @param {string} 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}
*/
export function getEarliestStartTime(planJson) {
@ -134,7 +135,7 @@ export function getEarliestStartTime(planJson) {
/**
*
* @param {object} planJson
* @param {Object} planJson
* @returns {number}
*/
export function getLatestEndTime(planJson) {
@ -142,6 +143,18 @@ export function getLatestEndTime(planJson) {
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'.
* @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.
* @param {import('@playwright/test').Page} page
* @param {HTMLCanvasElement} canvas 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} xEnd a telemetry item with a plot
* @param {number} yEnd a telemetry item with a plot
* @returns {Promise}
*/
export async function createTags({ page, canvas, xEnd = 700, yEnd = 520 }) {

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

27
e2e/package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "openmct-e2e",
"version": "4.0.0-next",
"description": "The Open MCT e2e framework",
"type": "module",
"module": "index.js",
"exports": {
".": {
"import": "./index.js"
}
},
"scripts": {
"pretest:visual": "npm install",
"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.42.1",
"@axe-core/playwright": "4.8.5",
"sinon": "17.0.0"
},
"author": "NASA Ames Research Center",
"license": "Apache-2.0"
}

View File

@ -3,6 +3,7 @@
// eslint-disable-next-line no-unused-vars
import { devices } from '@playwright/test';
import { fileURLToPath } from 'url';
const MAX_FAILURES = 5;
const NUM_WORKERS = 2;
@ -10,10 +11,12 @@ const NUM_WORKERS = 2;
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
testDir: 'tests',
grepInvert: /@mobile/, //Ignore mobile tests
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
timeout: 60 * 1000,
webServer: {
command: 'npm run start:coverage',
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
url: 'http://localhost:8080/#',
timeout: 200 * 1000,
reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.
@ -26,7 +29,9 @@ const config = {
ignoreHTTPSErrors: true,
screenshot: 'only-on-failure',
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: [
{

View File

@ -1,17 +1,16 @@
// playwright.config.js
// @ts-check
// eslint-disable-next-line no-unused-vars
import { devices } from '@playwright/test';
import { fileURLToPath } from 'url';
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 0,
testDir: 'tests',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
testIgnore: '**/*.perf.spec.js',
timeout: 30 * 1000,
webServer: {
command: 'npm run start:coverage',
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
url: 'http://localhost:8080/#',
timeout: 120 * 1000,
reuseExistingServer: true
@ -35,7 +34,6 @@ const config = {
},
{
name: 'MMOC',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: {
browserName: 'chromium',
@ -47,8 +45,6 @@ const config = {
},
{
name: 'safari',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grep: /@ipad/, // only run ipad tests due to this bug https://github.com/microsoft/playwright/issues/8340
grepInvert: /@snapshot/,
use: {
browserName: 'webkit'
@ -56,7 +52,6 @@ const config = {
},
{
name: 'firefox',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: {
browserName: 'firefox'
@ -64,7 +59,6 @@ const config = {
},
{
name: 'canary',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: {
browserName: 'chromium',
@ -73,22 +67,11 @@ const config = {
},
{
name: 'chrome-beta',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: {
browserName: 'chromium',
channel: 'chrome-beta'
}
},
{
name: 'ipad',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grep: /@ipad/,
grepInvert: /@snapshot/,
use: {
browserName: 'webkit',
...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
}
}
],
reporter: [

View File

@ -0,0 +1,72 @@
// playwright.config.js
// @ts-check
import { devices } from '@playwright/test';
const MAX_FAILURES = 5;
import { fileURLToPath } from 'url';
/** @type {import('@playwright/test').PlaywrightTestConfig} */
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
testDir: 'tests',
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
timeout: 30 * 1000,
webServer: {
command: 'npm run start:coverage',
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
url: 'http://localhost:8080/#',
timeout: 200 * 1000,
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
workers: 1, //Limit to 1 due to resource constraints similar to https://github.com/percy/cli/discussions/1067
use: {
baseURL: 'http://localhost:8080/',
headless: true,
ignoreHTTPSErrors: true,
screenshot: 'only-on-failure',
trace: 'on-first-retry',
video: 'off',
// @ts-ignore - custom configuration option for nyc codecoverage output path
coveragePath: fileURLToPath(new URL('../.nyc_output', import.meta.url))
},
projects: [
{
name: 'ipad',
grep: /@mobile/,
use: {
storageState: fileURLToPath(
new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)
),
browserName: 'webkit',
...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
}
},
{
name: 'iphone',
grep: /@mobile/,
use: {
storageState: fileURLToPath(
new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)
),
browserName: 'webkit',
...devices['iPhone 14 Pro'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
}
}
],
reporter: [
['list'],
[
'html',
{
open: 'never',
outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
}
],
['junit', { outputFile: '../test-results/results.xml' }]
]
};
export default config;

View File

@ -1,6 +1,6 @@
// playwright.config.js
// @ts-check
import { fileURLToPath } from 'url';
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
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
webServer: {
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/#',
timeout: 200 * 1000,
reuseExistingServer: false

View File

@ -1,6 +1,6 @@
// playwright.config.js
// @ts-check
import { fileURLToPath } from 'url';
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
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
webServer: {
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/#',
timeout: 200 * 1000,
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
// @ts-check
import { fileURLToPath } from 'url';
/** @type {import('@playwright/test').PlaywrightTestConfig<{ theme: string }>} */
const config = {
retries: 0, // Visual tests should never retry due to snapshot comparison errors. Leaving as a shim
@ -11,6 +10,7 @@ const config = {
workers: 1, //Lower stress on Circle CI Agent for Visual tests https://github.com/percy/cli/discussions/1067
webServer: {
command: 'npm run start:coverage',
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
url: 'http://localhost:8080/#',
timeout: 200 * 1000,
reuseExistingServer: !process.env.CI

View File

@ -1,5 +1,7 @@
// playwright.config.js
// @ts-check
import { devices } from '@playwright/test';
import { fileURLToPath } from 'url';
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
@ -8,6 +10,7 @@ const config = {
timeout: 60 * 1000,
webServer: {
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/#',
timeout: 200 * 1000,
reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.
@ -28,6 +31,28 @@ const config = {
use: {
browserName: 'chromium'
}
},
{
name: 'ipad',
grep: /@mobile/,
use: {
storageState: fileURLToPath(
new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)
),
browserName: 'webkit',
...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
}
},
{
name: 'iphone',
grep: /@mobile/,
use: {
storageState: fileURLToPath(
new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)
),
browserName: 'webkit',
...devices['iPhone 14 Pro'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
}
}
],
reporter: [

View File

@ -1,4 +1,3 @@
/* eslint-disable no-undef */
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
@ -123,7 +122,6 @@ const extendedTest = test.extend({
theme: [theme, { option: true }],
// eslint-disable-next-line no-shadow
page: async ({ page, theme }, use, testInfo) => {
// eslint-disable-next-line playwright/no-conditional-in-test
if (theme === 'snow') {
//inject snow theme
await page.addInitScript({

File diff suppressed because one or more lines are too long

View File

@ -6,7 +6,8 @@
"end": 1660343797000,
"type": "Group 1",
"color": "orange",
"textColor": "white"
"textColor": "white",
"id": 1
},
{
"name": "Past event 2",
@ -14,7 +15,8 @@
"end": 1660429160000,
"type": "Group 1",
"color": "orange",
"textColor": "white"
"textColor": "white",
"id": 2
},
{
"name": "Past event 3",
@ -22,7 +24,8 @@
"end": 1660503981000,
"type": "Group 1",
"color": "orange",
"textColor": "white"
"textColor": "white",
"id": 3
},
{
"name": "Past event 4",
@ -30,7 +33,8 @@
"end": 1660624108000,
"type": "Group 1",
"color": "orange",
"textColor": "white"
"textColor": "white",
"id": 4
},
{
"name": "Past event 5",
@ -38,7 +42,8 @@
"end": 1660681529000,
"type": "Group 1",
"color": "orange",
"textColor": "white"
"textColor": "white",
"id": 5
}
]
}

View File

@ -6,7 +6,8 @@
"end": 1660343797000,
"type": "Group 1",
"color": "orange",
"textColor": "white"
"textColor": "white",
"id": 1
},
{
"name": "Time until supper",
@ -14,7 +15,8 @@
"end": 1650420410000,
"type": "Group 2",
"color": "blue",
"textColor": "white"
"textColor": "white",
"id": 2
}
],
"Group 2": [
@ -24,7 +26,8 @@
"end": 1650320102001,
"type": "Group 2",
"color": "green",
"textColor": "white"
"textColor": "white",
"id": 3
},
{
"name": "Time since last accident",
@ -32,7 +35,8 @@
"end": 1650320102002,
"type": "Group 1",
"color": "yellow",
"textColor": "white"
"textColor": "white",
"id": 4
}
]
}

View File

@ -174,6 +174,6 @@ test.describe('AppActions', () => {
type: 'Folder'
});
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)
*/
import { test } from '../../baseFixtures.js';
import { expect, test } from '../../baseFixtures.js';
import { MISSION_TIME } from '../../constants.js';
test.describe('baseFixtures tests', () => {
//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();
//Go to baseURL
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

@ -33,13 +33,18 @@
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 { expect, test } from '../../pluginFixtures.js';
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({
clockOptions: {
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 }) => {
// Create Display Layout
const parent = await createDomainObjectWithDefaults(page, {

View File

@ -31,8 +31,8 @@ import { createDomainObjectWithDefaults } from '../../appActions.js';
import { expect, test } from '../../pluginFixtures.js';
const TEST_FOLDER = 'test folder';
const jsonFilePath = 'e2e/test-data/ExampleLayouts.json';
const imageFilePath = 'e2e/test-data/rick.jpg';
const jsonFilePath = 'test-data/ExampleLayouts.json';
const imageFilePath = 'test-data/rick.jpg';
test.describe('Form Validation Behavior', () => {
test('Required Field indicators appear if title is empty and can be corrected', async ({

View File

@ -0,0 +1,127 @@
/*****************************************************************************
* 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 persistability checks
*/
import { fileURLToPath } from 'url';
import { expect, test } from '../../baseFixtures.js';
test.describe('Mission Status @addInit', () => {
const NO_GO = '0';
const GO = '1';
test.beforeEach(async ({ page }) => {
// FIXME: determine if plugins will be added to index.html or need to be injected
await page.addInitScript({
path: fileURLToPath(new URL('../../helper/addInitExampleUser.js', import.meta.url))
});
await page.goto('./', { waitUntil: 'domcontentloaded' });
await expect(page.getByText('Select Role')).toBeVisible();
// Description should be empty https://github.com/nasa/openmct/issues/6978
await expect(page.getByLabel('Dialog message')).toBeHidden();
// set role
await page.getByRole('button', { name: 'Select', exact: true }).click();
// dismiss role confirmation popup
await page.getByRole('button', { name: 'Dismiss' }).click();
});
test('Basic functionality', async ({ page }) => {
const imageryStatusSelect = page.getByRole('combobox', { name: 'Imagery' });
const commandingStatusSelect = page.getByRole('combobox', { name: 'Commanding' });
const drivingStatusSelect = page.getByRole('combobox', { name: 'Driving' });
const missionStatusPanel = page.getByRole('dialog', { name: 'User Control Panel' });
await test.step('Mission status panel shows/hides when toggled', async () => {
// Ensure that clicking the button toggles the dialog
await page.getByLabel('Toggle Mission Status Panel').click();
await expect(missionStatusPanel).toBeVisible();
await page.getByLabel('Toggle Mission Status Panel').click();
await expect(missionStatusPanel).toBeHidden();
await page.getByLabel('Toggle Mission Status Panel').click();
await expect(missionStatusPanel).toBeVisible();
// Ensure that clicking the close button closes the dialog
await page.getByLabel('Close Mission Status Panel').click();
await expect(missionStatusPanel).toBeHidden();
await page.getByLabel('Toggle Mission Status Panel').click();
await expect(missionStatusPanel).toBeVisible();
// Ensure clicking off the dialog also closes it
await page.getByLabel('My Items Grid View').click();
await expect(missionStatusPanel).toBeHidden();
await page.getByLabel('Toggle Mission Status Panel').click();
await expect(missionStatusPanel).toBeVisible();
});
await test.step('Mission action statuses have correct defaults and can be set', async () => {
await expect(imageryStatusSelect).toHaveValue(NO_GO);
await expect(commandingStatusSelect).toHaveValue(NO_GO);
await expect(drivingStatusSelect).toHaveValue(NO_GO);
await setMissionStatus(page, 'Imagery', GO);
await expect(imageryStatusSelect).toHaveValue(GO);
await expect(commandingStatusSelect).toHaveValue(NO_GO);
await expect(drivingStatusSelect).toHaveValue(NO_GO);
await setMissionStatus(page, 'Commanding', GO);
await expect(imageryStatusSelect).toHaveValue(GO);
await expect(commandingStatusSelect).toHaveValue(GO);
await expect(drivingStatusSelect).toHaveValue(NO_GO);
await setMissionStatus(page, 'Driving', GO);
await expect(imageryStatusSelect).toHaveValue(GO);
await expect(commandingStatusSelect).toHaveValue(GO);
await expect(drivingStatusSelect).toHaveValue(GO);
await setMissionStatus(page, 'Imagery', NO_GO);
await expect(imageryStatusSelect).toHaveValue(NO_GO);
await expect(commandingStatusSelect).toHaveValue(GO);
await expect(drivingStatusSelect).toHaveValue(GO);
await setMissionStatus(page, 'Commanding', NO_GO);
await expect(imageryStatusSelect).toHaveValue(NO_GO);
await expect(commandingStatusSelect).toHaveValue(NO_GO);
await expect(drivingStatusSelect).toHaveValue(GO);
await setMissionStatus(page, 'Driving', NO_GO);
await expect(imageryStatusSelect).toHaveValue(NO_GO);
await expect(commandingStatusSelect).toHaveValue(NO_GO);
await expect(drivingStatusSelect).toHaveValue(NO_GO);
});
});
});
/**
*
* @param {import('@playwright/test').Page} page
* @param {'Commanding'|'Imagery'|'Driving'} action
* @param {'0'|'1'} status
*/
async function setMissionStatus(page, action, status) {
await page.getByRole('combobox', { name: action }).selectOption(status);
await expect(
page.getByRole('alert').filter({ hasText: 'Successfully set mission status' })
).toBeVisible();
await page.getByLabel('Dismiss').click();
}

View File

@ -21,7 +21,6 @@
*****************************************************************************/
import fs from 'fs';
import { getPreciseDuration } from '../../../../src/utils/duration.js';
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../../appActions.js';
import {
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

@ -27,7 +27,7 @@ import {
assertPlanActivities,
assertPlanOrderedSwimLanes
} from '../../../helper/planningUtils.js';
import { test } from '../../../pluginFixtures.js';
import { expect, test } from '../../../pluginFixtures.js';
const testPlan1 = JSON.parse(
fs.readFileSync(
@ -63,4 +63,47 @@ test.describe('Plan', () => {
});
await assertPlanOrderedSwimLanes(page, testPlanWithOrderedLanes, planWithSwimLanes.url);
});
test('Allows setting the state of an activity when selected.', async ({ page }) => {
const groups = Object.keys(testPlan1);
const firstGroupKey = groups[0];
const firstGroupItems = testPlan1[firstGroupKey];
const firstActivity = firstGroupItems[0];
const lastActivity = firstGroupItems[firstGroupItems.length - 1];
const startBound = firstActivity.start;
// Set the endBound to the end time of the current activity
let endBound = lastActivity.end;
// eslint-disable-next-line playwright/no-conditional-in-test
if (endBound === startBound) {
// Prevent oddities with setting start and end bound equal
// via URL params
endBound += 1;
}
// Switch to fixed time mode with all plan events within the bounds
await page.goto(
`${plan.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=plan.view`
);
// select the first activity in the list
await page.getByText('Past event 1').click();
// Find the activity state section in the inspector
await page.getByRole('tab', { name: 'Activity' }).click();
// Check that activity state dropdown selection shows the `set status` option by default
await expect(page.getByLabel('Activity Status').locator("[aria-selected='true']")).toHaveText(
'Not started'
);
// Change the selection of the activity status
await page.getByRole('combobox').selectOption({ label: 'Aborted' });
// select a different activity and back to the previous one
await page.getByText('Past event 2').click();
await page.getByText('Past event 1').click();
// Check that activity state dropdown selection shows the previously selected option by default
await expect(page.getByLabel('Activity Status').locator("[aria-selected='true']")).toHaveText(
'Aborted'
);
});
});

View File

@ -22,71 +22,15 @@
import fs from 'fs';
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../../appActions.js';
import { getEarliestStartTime } from '../../../helper/planningUtils';
import { expect, test } from '../../../pluginFixtures.js';
const examplePlanSmall3 = JSON.parse(
const examplePlanSmall1 = JSON.parse(
fs.readFileSync(
new URL('../../../test-data/examplePlans/ExamplePlan_Small3.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 = 4;
const testPlan = {
TEST_GROUP: [
{
name: 'Past event 1',
start: 1660320408000,
end: 1660343797000,
type: 'TEST-GROUP',
color: 'orange',
textColor: 'white'
},
{
name: 'Past event 2',
start: 1660406808000,
end: 1660429160000,
type: 'TEST-GROUP',
color: 'orange',
textColor: 'white'
},
{
name: 'Past event 3',
start: 1660493208000,
end: 1660503981000,
type: 'TEST-GROUP',
color: 'orange',
textColor: 'white'
},
{
name: 'Past event 4',
start: 1660579608000,
end: 1660624108000,
type: 'TEST-GROUP',
color: 'orange',
textColor: 'white'
},
{
name: 'Past event 5',
start: 1660666008000,
end: 1660681529000,
type: 'TEST-GROUP',
color: 'orange',
textColor: 'white'
}
]
};
test.describe('Time List', () => {
test('Create a Time List, add a single Plan to it and verify all the activities are displayed with no milliseconds', 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
}) => {
// Goto baseURL
@ -103,12 +47,16 @@ test.describe('Time List', () => {
await test.step('Create a Plan and add it to the timelist', async () => {
await createPlanFromJSON(page, {
name: 'Test Plan',
json: testPlan,
json: examplePlanSmall1,
parent: timelist.uuid
});
const startBound = testPlan.TEST_GROUP[0].start;
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
const groups = Object.keys(examplePlanSmall1);
const firstGroupKey = groups[0];
const firstGroupItems = examplePlanSmall1[firstGroupKey];
const firstActivity = firstGroupItems[0];
const lastActivity = firstGroupItems[firstGroupItems.length - 1];
const startBound = firstActivity.start;
const endBound = lastActivity.end;
// Switch to fixed time mode with all plan events within the bounds
await page.goto(
@ -118,7 +66,7 @@ test.describe('Time List', () => {
// Verify all events are displayed
const eventCount = await page.getByRole('row').count();
// subtracting one for the header
await expect(eventCount - 1).toEqual(testPlan.TEST_GROUP.length);
await expect(eventCount - 1).toEqual(firstGroupItems.length);
});
await test.step('Does not show milliseconds in times', async () => {
@ -131,164 +79,86 @@ test.describe('Time List', () => {
await expect(row.locator('.--end')).not.toContainText('.');
await expect(row.locator('.--duration')).not.toContainText('.');
});
await test.step('Shows activity properties when a row is selected', async () => {
await page.getByRole('row').nth(2).click();
// Find the activity state section in the inspector
await page.getByRole('tab', { name: 'Activity' }).click();
// Check that activity state label is displayed in the inspector.
await expect(page.getByLabel('Activity Status').locator("[aria-selected='true']")).toHaveText(
'Not started'
);
});
});
});
/**
* 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} CountdownObject
* @property {string} sign - The sign of the countdown ('-' if the countdown is negative, otherwise undefined).
* @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 ({
test("View a timelist in expanded view, verify all the activities are displayed and selecting an activity shows it's properties", 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'
}) => {
// Goto baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
const timelist = await test.step('Create a Time List', async () => {
const createdTimeList = await createDomainObjectWithDefaults(page, { type: 'Time List' });
const objectName = await page.locator('.l-browse-bar__object-name').innerText();
expect(objectName).toBe(createdTimeList.name);
return createdTimeList;
});
// Create a Plan with events that count down and up.
// Add it as a child to the Time List.
await test.step('Create a Plan and add it to the timelist', async () => {
await createPlanFromJSON(page, {
json: examplePlanSmall3,
name: 'Test Plan',
json: examplePlanSmall1,
parent: timelist.uuid
});
// Navigate to the Time List in real-time mode
// Ensure that all activities are shown in the expanded view
const groups = Object.keys(examplePlanSmall1);
const firstGroupKey = groups[0];
const firstGroupItems = examplePlanSmall1[firstGroupKey];
const firstActivity = firstGroupItems[0];
const lastActivity = firstGroupItems[firstGroupItems.length - 1];
const startBound = firstActivity.start;
const endBound = lastActivity.end;
// Switch to fixed time mode with all plan events within the bounds
await page.goto(
`${timelist.url}?tc.mode=local&tc.startDelta=900000&tc.endDelta=1800000&tc.timeSystem=utc&view=grid`
`${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();
// Verify all events are displayed
const eventCount = await page.getByRole('row').count();
await expect(eventCount).toEqual(firstGroupItems.length);
});
await test.step('Shows activity properties when a row is selected in the expanded view', async () => {
await page.getByRole('row').nth(2).click();
// Find the activity state section in the inspector
await page.getByRole('tab', { name: 'Activity' }).click();
// Check that activity state label is displayed in the inspector.
await expect(page.getByLabel('Activity Status').locator("[aria-selected='true']")).toHaveText(
'Not started'
);
});
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 getAndAssertCountdownObject(page, i + 3);
// Wait until it changes
await expect(countdownCell).not.toHaveText(beforeCountdown.toString());
// Get the new countdown timestamp object
const afterCountdown = await getAndAssertCountdownObject(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 countdownCell = countUpCells[i];
// Get the initial count-up timestamp object
const beforeCountdown = await getAndAssertCountdownObject(page, i + 1);
// Wait until it changes
await expect(countdownCell).not.toHaveText(beforeCountdown.toString());
// Get the new count-up timestamp object
const afterCountdown = await getAndAssertCountdownObject(page, i + 1);
// Verify that the new count-up timestamp object is greater than the old one
expect(Number(afterCountdown.seconds)).toBeGreaterThan(Number(beforeCountdown.seconds));
});
}
await test.step("Verify absence of progress indication for an activity that's not in progress", async () => {
// When an activity is not in progress, the progress pie is not visible
const hidden = await page.getByRole('row').locator('path').nth(1).isHidden();
await expect(hidden).toBe(true);
});
});
/**
* 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 cell in the given row, assert that it matches the countdown
* regex, and return an object representing the countdown.
* @param {import('@playwright/test').Page} page
* @param {number} rowIndex the row index
* @returns {Promise<CountdownObject>} countdownObject
*/
async function getAndAssertCountdownObject(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 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);
});
@ -160,7 +163,10 @@ test.describe('Time Strip', () => {
const startBoundString = new Date(startBound).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
expect(await activityBounds.count()).toEqual(2);

View File

@ -35,7 +35,7 @@ import { expect, test } from '../../../../pluginFixtures.js';
let conditionSetUrl;
test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
test.describe.serial('Condition Set CRUD Operations on @localStorage @2p', () => {
test.beforeAll(async ({ browser }) => {
//TODO: This needs to be refactored
const context = await browser.newContext();
@ -68,9 +68,13 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
});
//Begin suite of tests again localStorage
test('Condition set object properties persist in main view and inspector @localStorage', async ({
page
}) => {
test.fixme(
'Condition set object properties persist in main view and inspector @localStorage',
async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7421'
});
//Navigate to baseURL with injected localStorage
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
@ -91,7 +95,8 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
.toContainText('Unnamed Condition Set');
//Assertions on loaded Condition Set in Inspector
expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
});
}
);
test('condition set object can be modified on @localStorage', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
@ -281,19 +286,29 @@ test.describe('Basic Condition Set Use', () => {
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 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.getByTitle('Show selected item in tree').click();
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();
@ -373,4 +388,90 @@ test.describe('Basic Condition Set Use', () => {
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.locator('[aria-label="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'
});
});
});

View File

@ -23,6 +23,7 @@ import { fileURLToPath } from 'url';
import {
createDomainObjectWithDefaults,
navigateToObjectWithFixedTimeBounds,
setFixedTimeMode,
setIndependentTimeConductorBounds,
setRealTimeMode,
@ -30,12 +31,120 @@ import {
} from '../../../../appActions.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)
);
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 =
'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', () => {
const PARENT_DISPLAY_LAYOUT_NAME = 'Parent Display Layout';
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();
});
test.use({
storageState: LOCALSTORAGE_PATH
storageState: CHILD_LAYOUT_STORAGE_STATE_PATH
});
test('can add/remove Text element to a single layout', async ({ page }) => {
@ -161,6 +270,13 @@ test.describe('Display Layout', () => {
const trimmedDisplayValue = displayLayoutValue.trim();
expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
// ensure we can right click on the alpha-numeric widget and view historical data
await page.getByLabel(/Alpha-numeric telemetry value of.*/).click({
button: 'right'
});
await page.getByLabel('View Historical Data').click();
await expect(page.getByLabel('Plot Container Style Target')).toBeVisible();
});
test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in fixed time', async ({
page
@ -329,7 +445,7 @@ test.describe('Display Layout', () => {
const startDate = '2021-12-30 01:01:00.000Z';
const endDate = '2021-12-30 01:11:00.000Z';
await setIndependentTimeConductorBounds(page, startDate, endDate);
await setIndependentTimeConductorBounds(page, { start: startDate, end: endDate });
// check image date
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();

View File

@ -20,25 +20,46 @@
* 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';
test.describe('The Fault Management Plugin using example faults', () => {
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 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
}) => {
await utils.selectFaultItem(page, 1);
await selectFaultItem(page, 1);
await page.getByRole('tab', { name: 'Config' }).click();
const selectedFaultName = await page
@ -48,22 +69,22 @@ test.describe('The Fault Management Plugin using example faults', () => {
.locator(`.c-inspector__properties >> :text("${selectedFaultName}")`)
.count();
await expect
.soft(page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').first())
.toHaveClass(/is-selected/);
expect.soft(inspectorFaultNameCount).toEqual(1);
await expect(
page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').first()
).toHaveClass(/is-selected/);
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
}) => {
await utils.selectFaultItem(page, 1);
await utils.selectFaultItem(page, 2);
await selectFaultItem(page, 1);
await selectFaultItem(page, 2);
const selectedRows = page.locator(
'.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();
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}")`)
.count();
expect.soft(firstNameInInspectorCount).toEqual(0);
expect.soft(secondNameInInspectorCount).toEqual(0);
expect(firstNameInInspectorCount).toEqual(0);
expect(secondNameInInspectorCount).toEqual(0);
});
test('Allows you to shelve a fault @unstable', async ({ page }) => {
const shelvedFaultName = await utils.getFaultName(page, 2);
const beforeShelvedFault = utils.getFaultByName(page, shelvedFaultName);
test('Allows you to shelve a fault', async ({ page }) => {
const shelvedFaultName = await getFaultName(page, 2);
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
const afterShelvedFault = utils.getFaultByName(page, shelvedFaultName);
expect.soft(await afterShelvedFault.count()).toBe(0);
const afterShelvedFault = getFaultByName(page, shelvedFaultName);
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 }) => {
const acknowledgedFaultName = await utils.getFaultName(page, 3);
test('Allows you to acknowledge a fault', async ({ page }) => {
const acknowledgedFaultName = await getFaultName(page, 3);
await utils.acknowledgeFault(page, 3);
await acknowledgeFault(page, 3);
const fault = utils.getFault(page, 3);
await expect.soft(fault).toHaveClass(/is-acknowledged/);
const fault = getFault(page, 3);
await expect(fault).toHaveClass(/is-acknowledged/);
await utils.changeViewTo(page, 'acknowledged');
await changeViewTo(page, 'acknowledged');
const acknowledgedViewFaultName = await utils.getFaultName(page, 1);
expect.soft(acknowledgedFaultName).toEqual(acknowledgedViewFaultName);
const acknowledgedViewFaultName = await getFaultName(page, 1);
expect(acknowledgedFaultName).toEqual(acknowledgedViewFaultName);
});
test('Allows you to shelve multiple faults @unstable', async ({ page }) => {
const shelvedFaultNameOne = await utils.getFaultName(page, 1);
const shelvedFaultNameFour = await utils.getFaultName(page, 4);
test('Allows you to shelve multiple faults', async ({ page }) => {
const shelvedFaultNameOne = await getFaultName(page, 1);
const shelvedFaultNameFour = await getFaultName(page, 4);
const beforeShelvedFaultOne = utils.getFaultByName(page, shelvedFaultNameOne);
const beforeShelvedFaultFour = utils.getFaultByName(page, shelvedFaultNameFour);
const beforeShelvedFaultOne = getFaultByName(page, shelvedFaultNameOne);
const beforeShelvedFaultFour = getFaultByName(page, shelvedFaultNameFour);
expect.soft(await beforeShelvedFaultOne.count()).toBe(1);
expect.soft(await beforeShelvedFaultFour.count()).toBe(1);
await expect(beforeShelvedFaultOne).toHaveCount(1);
await expect(beforeShelvedFaultFour).toHaveCount(1);
await utils.shelveMultipleFaults(page, 1, 4);
await shelveMultipleFaults(page, 1, 4);
// check it is removed from standard view
const afterShelvedFaultOne = utils.getFaultByName(page, shelvedFaultNameOne);
const afterShelvedFaultFour = utils.getFaultByName(page, shelvedFaultNameFour);
expect.soft(await afterShelvedFaultOne.count()).toBe(0);
expect.soft(await afterShelvedFaultFour.count()).toBe(0);
const afterShelvedFaultOne = getFaultByName(page, shelvedFaultNameOne);
const afterShelvedFaultFour = getFaultByName(page, shelvedFaultNameFour);
await expect(afterShelvedFaultOne).toHaveCount(0);
await expect(afterShelvedFaultFour).toHaveCount(0);
await utils.changeViewTo(page, 'shelved');
await changeViewTo(page, 'shelved');
const shelvedViewFaultOne = utils.getFaultByName(page, shelvedFaultNameOne);
const shelvedViewFaultFour = utils.getFaultByName(page, shelvedFaultNameFour);
const shelvedViewFaultOne = getFaultByName(page, shelvedFaultNameOne);
const shelvedViewFaultFour = getFaultByName(page, shelvedFaultNameFour);
expect.soft(await shelvedViewFaultOne.count()).toBe(1);
expect.soft(await shelvedViewFaultFour.count()).toBe(1);
await expect(shelvedViewFaultOne).toHaveCount(1);
await expect(shelvedViewFaultFour).toHaveCount(1);
});
test('Allows you to acknowledge multiple faults @unstable', async ({ page }) => {
const acknowledgedFaultNameTwo = await utils.getFaultName(page, 2);
const acknowledgedFaultNameFive = await utils.getFaultName(page, 5);
test('Allows you to acknowledge multiple faults', async ({ page }) => {
const acknowledgedFaultNameTwo = await getFaultName(page, 2);
const acknowledgedFaultNameFive = await getFaultName(page, 5);
await utils.acknowledgeMultipleFaults(page, 2, 5);
await acknowledgeMultipleFaults(page, 2, 5);
const faultTwo = utils.getFault(page, 2);
const faultFive = utils.getFault(page, 5);
const faultTwo = getFault(page, 2);
const faultFive = getFault(page, 5);
// check they have been acknowledged
await expect.soft(faultTwo).toHaveClass(/is-acknowledged/);
await expect.soft(faultFive).toHaveClass(/is-acknowledged/);
await expect(faultTwo).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 acknowledgedViewFaultFive = utils.getFaultByName(page, acknowledgedFaultNameFive);
const acknowledgedViewFaultTwo = getFaultByName(page, acknowledgedFaultNameTwo);
const acknowledgedViewFaultFive = getFaultByName(page, acknowledgedFaultNameFive);
expect.soft(await acknowledgedViewFaultTwo.count()).toBe(1);
expect.soft(await acknowledgedViewFaultFive.count()).toBe(1);
await expect(acknowledgedViewFaultTwo).toHaveCount(1);
await expect(acknowledgedViewFaultFive).toHaveCount(1);
});
test('Allows you to search faults @unstable', async ({ page }) => {
const faultThreeNamespace = await utils.getFaultNamespace(page, 3);
const faultTwoName = await utils.getFaultName(page, 2);
const faultFiveTriggerTime = await utils.getFaultTriggerTime(page, 5);
test('Allows you to search faults', async ({ page }) => {
const faultThreeNamespace = await getFaultNamespace(page, 3);
const faultTwoName = await getFaultName(page, 2);
const faultFiveTriggerTime = await getFaultTriggerTime(page, 5);
// should be all faults (5)
let faultResultCount = await utils.getFaultResultCount(page);
expect.soft(faultResultCount).toEqual(5);
let faultResultCount = await getFaultResultCount(page);
expect(faultResultCount).toEqual(5);
// search namespace
await utils.enterSearchTerm(page, faultThreeNamespace);
await enterSearchTerm(page, faultThreeNamespace);
faultResultCount = await utils.getFaultResultCount(page);
expect.soft(faultResultCount).toEqual(1);
expect.soft(await utils.getFaultNamespace(page, 1)).toEqual(faultThreeNamespace);
faultResultCount = await getFaultResultCount(page);
expect(faultResultCount).toEqual(1);
expect(await getFaultNamespace(page, 1)).toEqual(faultThreeNamespace);
// all faults
await utils.clearSearch(page);
faultResultCount = await utils.getFaultResultCount(page);
expect.soft(faultResultCount).toEqual(5);
await clearSearch(page);
faultResultCount = await getFaultResultCount(page);
expect(faultResultCount).toEqual(5);
// search name
await utils.enterSearchTerm(page, faultTwoName);
await enterSearchTerm(page, faultTwoName);
faultResultCount = await utils.getFaultResultCount(page);
expect.soft(faultResultCount).toEqual(1);
expect.soft(await utils.getFaultName(page, 1)).toEqual(faultTwoName);
faultResultCount = await getFaultResultCount(page);
expect(faultResultCount).toEqual(1);
expect(await getFaultName(page, 1)).toEqual(faultTwoName);
// all faults
await utils.clearSearch(page);
faultResultCount = await utils.getFaultResultCount(page);
expect.soft(faultResultCount).toEqual(5);
await clearSearch(page);
faultResultCount = await getFaultResultCount(page);
expect(faultResultCount).toEqual(5);
// search triggerTime
await utils.enterSearchTerm(page, faultFiveTriggerTime);
await enterSearchTerm(page, faultFiveTriggerTime);
faultResultCount = await utils.getFaultResultCount(page);
expect.soft(faultResultCount).toEqual(1);
expect.soft(await utils.getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime);
faultResultCount = await getFaultResultCount(page);
expect(faultResultCount).toEqual(1);
expect(await getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime);
});
test('Allows you to sort faults @unstable', async ({ page }) => {
const highestSeverity = await utils.getHighestSeverity(page);
const lowestSeverity = await utils.getLowestSeverity(page);
test('Allows you to sort faults', async ({ page }) => {
const highestSeverity = await getHighestSeverity(page);
const lowestSeverity = await getLowestSeverity(page);
const faultOneName = 'Example Fault 1';
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);
expect.soft(firstFaultName).toEqual(faultFiveName);
firstFaultName = await getFaultName(page, 1);
expect(firstFaultName).toEqual(faultFiveName);
await utils.sortFaultsBy(page, 'severity');
await sortFaultsBy(page, 'severity');
const sortedHighestSeverity = await utils.getFaultSeverity(page, 1);
const sortedLowestSeverity = await utils.getFaultSeverity(page, 5);
expect.soft(sortedHighestSeverity).toEqual(highestSeverity);
expect.soft(sortedLowestSeverity).toEqual(lowestSeverity);
const sortedHighestSeverity = await getFaultSeverity(page, 1);
const sortedLowestSeverity = await getFaultSeverity(page, 5);
expect(sortedHighestSeverity).toEqual(highestSeverity);
expect(sortedLowestSeverity).toEqual(lowestSeverity);
});
});
test.describe('The Fault Management Plugin without using example faults', () => {
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();
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();
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();
expect.soft(shelvedCount).toEqual(0);
expect(shelvedCount).toEqual(0);
});
test('Will return no faults when searching @unstable', async ({ page }) => {
await utils.enterSearchTerm(page, 'fault');
test('Will return no faults when searching', async ({ page }) => {
await enterSearchTerm(page, 'fault');
const faultCount = await page.locator('c-fault-mgmt__list').count();
expect.soft(faultCount).toEqual(0);
expect(faultCount).toEqual(0);
});
});

View File

@ -248,11 +248,10 @@ test.describe('Flexible Layout', () => {
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// flip on independent time conductor
await setIndependentTimeConductorBounds(
page,
'2021-12-30 01:01:00.000Z',
'2021-12-30 01:11:00.000Z'
);
await setIndependentTimeConductorBounds(page, {
start: '2021-12-30 01:01:00.000Z',
end: '2021-12-30 01:11:00.000Z'
});
// check image date
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();
expect(await containerHandles.count()).toEqual(3);
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?'
);
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);
await page.getByRole('group', { name: 'Child Layout 1' }).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?'
);
await page.getByRole('button', { name: 'OK', exact: true }).click();

View File

@ -136,7 +136,11 @@ test.describe('Gauge', () => {
// TODO: Verify changes in the UI
});
test('Gauge does not display NaN when data not available', async ({ page }) => {
test.fixme('Gauge does not display NaN when data not available', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7421'
});
// Create a Gauge
const gauge = await createDomainObjectWithDefaults(page, {
type: 'Gauge'
@ -171,13 +175,13 @@ test.describe('Gauge', () => {
});
// 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 expect(page.locator('[aria-label="Save"]')).toBeDisabled();
await page.getByLabel('Cancel').click();
// 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 expect(page.locator('[aria-label="Save"]')).toBeDisabled();
});

View File

@ -24,7 +24,7 @@
This test suite is dedicated to tests which verify the basic operations surrounding imagery,
but only assume that example imagery is present.
*/
/* globals process */
import { createDomainObjectWithDefaults, setRealTimeMode } from '../../../../appActions.js';
import { waitForAnimations } from '../../../../baseFixtures.js';
import { expect, test } from '../../../../pluginFixtures.js';
@ -363,7 +363,7 @@ test.describe('Example Imagery in Display Layout', () => {
await page.locator('li[title="View Large"]').click();
await expect(pausePlayButton).toHaveClass(/is-paused/);
await page.locator('[aria-label="Close"]').click();
await page.getByRole('button', { name: 'Close' }).click();
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
});
@ -386,7 +386,7 @@ test.describe('Example Imagery in Display Layout', () => {
await page.locator('li[title="View Large"]').click();
await expect(pausePlayButton).toHaveClass(/is-paused/);
await page.locator('[aria-label="Close"]').click();
await page.getByRole('button', { name: 'Close' }).click();
await expect.soft(pausePlayButton).toHaveClass(/is-paused/);
});
@ -509,7 +509,7 @@ test.describe('Example Imagery in Flexible layout', () => {
await page.getByRole('button', { name: 'Background Image', state: 'visible' });
// Close the large view
await page.getByLabel('Close').click();
await page.getByRole('button', { name: 'Close' }).click();
});
test.beforeEach(async ({ page }) => {
@ -773,7 +773,7 @@ async function dragContrastSliderAndAssertFilterValues(page) {
* Gets the filter:brightness value of the current background-image and
* asserts against an expected value
* @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) {
const backgroundImage = page.locator('.c-imagery__main-image__background-image');
@ -938,7 +938,7 @@ async function buttonZoomOnImageAndAssert(page) {
* Gets the filter:contrast value of the current background-image and
* asserts against an expected value
* @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) {
const backgroundImage = page.locator('.c-imagery__main-image__background-image');

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.beforeEach(async ({ page }) => {
// eslint-disable-next-line no-undef
await page.addInitScript({
path: fileURLToPath(
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 }) => {
const initStartBounds = await page.getByLabel('Start bounds').textContent();
const initEndBounds = await page.getByLabel('End bounds').textContent();
const exampleDataVisualizationSource = await createDomainObjectWithDefaults(page, {
type: 'Example Data Visualization Source'
});
@ -78,5 +79,9 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
await newPage.waitForLoadState();
// expect new tab title to contain '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

@ -277,7 +277,6 @@ test.describe('Notebook entry tests', () => {
// Create Notebook with URL Whitelist
let notebookObject;
test.beforeEach(async ({ page }) => {
// eslint-disable-next-line no-undef
await page.addInitScript({
path: fileURLToPath(new URL('../../../../helper/addInitNotebookWithUrls.js', import.meta.url))
});
@ -308,7 +307,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url);
// 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
.getByRole('treeitem', { name: overlayPlot.name })
@ -332,7 +331,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url);
// 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 page
@ -377,7 +376,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url);
// 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?`);
@ -404,7 +403,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url);
// 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?`);
@ -421,7 +420,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url);
// 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?`);
@ -438,7 +437,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url);
// 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?`);
@ -455,7 +454,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url);
// 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?`);
@ -483,7 +482,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url);
// 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,

View File

@ -68,7 +68,7 @@ test.describe('Snapshot image tests', () => {
// expect large image to be displayed
await expect(page.getByRole('dialog').getByText('favicon-96x96.png')).toBeVisible();
await page.getByLabel('Close').click();
await page.getByRole('button', { name: 'Close' }).click();
// drop another image onto the entry
await page.dispatchEvent('.c-snapshots', 'drop', { dataTransfer: dropTransfer });

View File

@ -71,42 +71,89 @@ test.describe('Snapshot Container tests', () => {
test.beforeEach(async ({ page }) => {
//Navigate to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
// 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.getByLabel('Open the Notebook Snapshot Menu').click();
await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();
await page.getByLabel('Show Snapshots').click();
});
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 expect(page.locator('.c-overlay__outer')).toBeVisible();
await expect(page.getByLabel('Modal Overlay')).toBeVisible();
await expect(page.getByLabel('Preview Container')).toBeVisible();
});
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();
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();
await page.getByRole('button', { name: '' }).click();
// await expect(page.locator('#snap-annotation-canvas')).not.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();
//await expect(await page.locator)
}
);
// 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('5 Snapshots can be added to a container', async ({ page }) => {});
test.fixme(
'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',
async ({ page }) => {}
);
test.fixme(
'A snapshot can be Navigated To from Container with 3 dot action menu',
async ({ page }) => {}
);
test.fixme(
'A snapshot can be Navigated To Item in Time from Container with 3 dot action menu',
async ({ page }) => {}
@ -151,11 +194,4 @@ test.describe('Snapshot Container tests', () => {
//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
}
);
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -24,138 +24,60 @@
Tests to verify log plot functionality when objects are missing
*/
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Handle missing object for plots', () => {
test('Displays empty div for missing stacked plot item @unstable', async ({
page,
browserName,
openmctConfig
}) => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Displays empty div for missing stacked plot item', async ({ page, browserName }) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(browserName === 'firefox', 'Firefox failing due to console events being missed');
const { myItemsFolderName } = openmctConfig;
const errorLogs = [];
let warningReceived = false;
page.on('console', (message) => {
if (message.type() === 'warning' && message.text().includes('Missing domain object')) {
errorLogs.push(message.text());
warningReceived = true;
}
});
//Make stacked plot
await makeStackedPlot(page, myItemsFolderName);
const stackedPlot = await createDomainObjectWithDefaults(page, {
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
const localStorage = await page.evaluate(() => window.localStorage);
const parsedData = JSON.parse(localStorage.mct);
const keys = Object.keys(parsedData);
const lastKey = keys[keys.length - 1];
const mct = await page.evaluate(() => window.localStorage.getItem('mct'));
const parsedData = JSON.parse(mct);
const key = Object.entries(parsedData).find(([, value]) => value.type === 'generator')?.[0];
delete parsedData[lastKey];
delete parsedData[key];
//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
await Promise.all([page.reload(), page.waitForLoadState('networkidle')]);
await page.reload({ waitUntil: 'domcontentloaded' });
await page.goto(stackedPlot.url);
//Verify Main section is there on load
await expect
.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()
]);
await expect(page.locator('.l-browse-bar__object-name')).toContainText(stackedPlot.name);
//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);
//Verify that console.warn is thrown
expect(errorLogs).toHaveLength(1);
await expect(page.getByLabel('Stacked Plot Item')).toHaveCount(1);
//Verify that console.warn was thrown
expect(warningReceived).toBe(true);
});
});
/**
* This is used the create a stacked plot object
* @private
*/
async function makeStackedPlot(page, myItemsFolderName) {
// fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
await page.goto('./', { waitUntil: 'domcontentloaded' });
// create stacked plot
await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Stacked Plot")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle' }),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
// save the stacked plot
await saveStackedPlot(page);
// create a sinewave generator
await createSineWaveGenerator(page);
// click on 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()
]);
// create a second sinewave generator
await createSineWaveGenerator(page);
// click on 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()
]);
}
/**
* This is used to save a stacked plot object
* @private
*/
async function saveStackedPlot(page) {
// save stacked plot
await page
.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button')
.nth(1)
.click();
await Promise.all([
page.locator('text=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' });
}
/**
* This is used to create a sine wave generator object
* @private
*/
async function createSineWaveGenerator(page) {
//Create sine wave generator
await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle' }),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
}

View File

@ -33,15 +33,15 @@ import {
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Overlay Plot', () => {
let overlayPlot;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
});
test('Plot legend color is in sync with plot series color', async ({ page }) => {
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
@ -63,6 +63,63 @@ test.describe('Overlay Plot', () => {
await expect(seriesColorSwatch).toHaveCSS('background-color', 'rgb(255, 166, 61)');
});
test('Plot legend expands by default', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7403'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url);
await page.getByRole('tab', { name: 'Config' }).click();
// Assert that the legend is collapsed by default
await expect(page.getByLabel('Plot Legend Collapsed')).toBeVisible();
await expect(page.getByLabel('Plot Legend Expanded')).toBeHidden();
await expect(page.getByLabel('Expand by Default')).toHaveText('No');
expect(await page.getByLabel('Plot Legend Item').count()).toBe(3);
// Change the legend to expand by default
await page.getByLabel('Edit Object').click();
await page.getByLabel('Expand By Default').check();
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Assert that the legend is now open
await expect(page.getByLabel('Plot Legend Collapsed')).toBeHidden();
await expect(page.getByLabel('Plot Legend Expanded')).toBeVisible();
await expect(page.getByRole('cell', { name: 'Name' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Value' })).toBeVisible();
await expect(page.getByLabel('Expand by Default')).toHaveText('Yes');
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(3);
// Assert that the legend is expanded on page load
await page.reload();
await expect(page.getByLabel('Plot Legend Collapsed')).toBeHidden();
await expect(page.getByLabel('Plot Legend Expanded')).toBeVisible();
await expect(page.getByRole('cell', { name: 'Name' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Value' })).toBeVisible();
await expect(page.getByLabel('Expand by Default')).toHaveText('Yes');
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(3);
});
test('Limit lines persist when series is moved to another Y Axis and on refresh', async ({
page
}) => {
@ -70,10 +127,6 @@ test.describe('Overlay Plot', () => {
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6338'
});
// Create an Overlay Plot with a default SWG
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
const swgA = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
@ -136,13 +189,60 @@ test.describe('Overlay Plot', () => {
await assertLimitLinesExistAndAreVisible(page);
});
test('The elements pool supports dragging series into multiple y-axis buckets', async ({
page
}) => {
const overlayPlot = await createDomainObjectWithDefaults(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
.getByRole('list', { name: 'Plot Series Properties' })
.locator('span')
.first()
.click();
await page
.getByRole('list', { name: 'Plot Series Properties' })
.getByRole('checkbox', { name: 'Limit lines' })
.check();
await assertLimitLinesExistAndAreVisible(page);
// Save (exit edit mode)
await page.locator('button[title="Save"]').click();
await page.locator('li[title="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 ({
page
}) => {
const swgA = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
@ -224,11 +324,12 @@ test.describe('Overlay Plot', () => {
expect(yAxis3Group.getByRole('listitem').nth(0).getByText(swgB.name)).toBeTruthy();
});
test('Clicking on an item in the elements pool brings up the plot preview with data points', async ({
page
}) => {
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
test.fixme(
'Clicking on an item in the elements pool brings up the plot preview with data points',
async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7421'
});
const swgA = await createDomainObjectWithDefaults(page, {
@ -244,10 +345,52 @@ test.describe('Overlay Plot', () => {
await page.getByRole('tab', { name: 'Elements' }).click();
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click();
const plotPixels = await getCanvasPixels(page, '.js-overlay canvas');
const plotPixelSize = plotPixels.length;
expect(plotPixelSize).toBeGreaterThan(0);
}
);
test('Can remove an item via the elements pool action menu', async ({ page }) => {
const swgA = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
const swgB = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url);
// Wait for plot series data to load and be drawn
await waitForPlotsToRender(page);
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'Elements' }).click();
const swgAElementsPoolItem = page.getByLabel(`Preview ${swgA.name}`);
await expect(swgAElementsPoolItem).toBeVisible();
await swgAElementsPoolItem.click({ button: 'right' });
await page.getByRole('menuitem', { name: 'Remove' }).click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
await 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();
});
});
});
@ -266,4 +409,7 @@ async function assertLimitLinesExistAndAreVisible(page) {
for (let i = 0; i < limitLineCount; i++) {
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

@ -52,7 +52,11 @@ test.describe('Plot Rendering', () => {
expect(createMineFolderRequests.length).toEqual(0);
});
test('Plot is rendered when infinity values exist', async ({ page }) => {
test.fixme('Plot is rendered when infinity values exist', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7421'
});
// Edit Plot
await editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject);

View File

@ -0,0 +1,90 @@
/*****************************************************************************
* 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('Plots work in Previews', () => {
test('We can preview plot in display layouts', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create a Sinewave Generator
const sineWaveObject = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator'
});
// Create a Display Layout
await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Test Display Layout'
});
// Edit Display Layout
await page.getByLabel('Edit Object').click();
// Expand the 'My Items' folder in the left tree
await page.getByLabel(`Expand ${myItemsFolderName} folder`).click();
// Add the Sine Wave Generator to the Display Layout and save changes
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
const layoutGridHolder = page.getByLabel('Test Display Layout Layout Grid');
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// right click on the plot and select view large
await page.getByLabel(/Alpha-numeric telemetry value of.*/).click({
button: 'right'
});
await page.getByLabel('View Historical Data').click();
await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();
await page.getByRole('button', { name: 'Close' }).click();
await page.getByLabel('Expand Test Display Layout layout').click();
// change to a plot and ensure embiggen works
await page.getByLabel('Edit Object').click();
await page.getByLabel('Move Sub-object Frame').click();
await page.getByText('View type').click();
await page.getByText('Overlay Plot').click();
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await expect(
page.getByLabel('Test Display Layout Layout', { exact: true }).getByLabel('Plot Canvas')
).toBeVisible();
await expect(page.getByLabel('Preview Container')).toBeHidden();
await page.getByLabel('Large View').click();
await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();
await page.getByRole('button', { name: 'Close' }).click();
// get last sinewave tree item (in the display layout)
await page
.getByRole('treeitem', { name: /Sine Wave Generator/ })
.locator('a')
.last()
.click({ button: 'right' });
await page.getByLabel('View', { exact: true }).click();
await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();
await page.getByRole('button', { name: 'Close' }).click();
});
});

View File

@ -257,6 +257,56 @@ test.describe('Stacked Plot', () => {
await assertAggregateLegendIsVisible(page);
});
test('can toggle between aggregate and per child legends', async ({ page }) => {
// make some an overlay plot
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
parent: stackedPlot.uuid
});
// make some SWGs for the overlay plot
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await page.goto(stackedPlot.url);
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'Config' }).click();
await page.getByLabel('Inspector Views').getByRole('checkbox').uncheck();
await page.getByLabel('Expand By Default').check();
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await expect(page.getByLabel('Plot Legend Expanded')).toHaveCount(1);
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(5);
// reload and ensure the legend is still expanded
await page.reload();
await expect(page.getByLabel('Plot Legend Expanded')).toHaveCount(1);
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(5);
// change to collapsed by default
await page.getByLabel('Edit Object').click();
await page.getByLabel('Expand By Default').uncheck();
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await expect(page.getByLabel('Plot Legend Collapsed')).toHaveCount(1);
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(5);
// change it to individual legends
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'Config' }).click();
await page.getByLabel('Show Legends For Children').check();
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await expect(page.getByLabel('Plot Legend Collapsed')).toHaveCount(4);
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(5);
});
});
/**

View File

@ -0,0 +1,135 @@
/*****************************************************************************
* 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, expandEntireTree } from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Reload action', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const displayLayout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout'
});
const alphaTable = await createDomainObjectWithDefaults(page, {
type: 'Telemetry Table',
name: 'Alpha Table'
});
const betaTable = await createDomainObjectWithDefaults(page, {
type: 'Telemetry Table',
name: 'Beta Table'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: alphaTable.uuid,
customParameters: {
'[aria-label="Data Rate (hz)"]': '0.001'
}
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: betaTable.uuid,
customParameters: {
'[aria-label="Data Rate (hz)"]': '0.001'
}
});
await page.goto(displayLayout.url);
// Expand all folders
await expandEntireTree(page);
await page.getByLabel('Edit Object', { exact: true }).click();
await page.dragAndDrop(`text='Alpha Table'`, '.l-layout__grid-holder', {
targetPosition: { x: 0, y: 0 }
});
await page.dragAndDrop(`text='Beta Table'`, '.l-layout__grid-holder', {
targetPosition: { x: 0, y: 250 }
});
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
});
test('can reload display layout and its children', async ({ page }) => {
const beforeReloadAlphaTelemetryValue = await page
.getByLabel('Alpha Table table content')
.getByLabel('wavelengths table cell')
.first()
.getAttribute('title');
const beforeReloadBetaTelemetryValue = await page
.getByLabel('Beta Table table content')
.getByLabel('wavelengths table cell')
.first()
.getAttribute('title');
// reload alpha
await page.getByTitle('View menu items').first().click();
await page.getByRole('menuitem', { name: /Reload/ }).click();
const afterReloadAlphaTelemetryValue = await page
.getByLabel('Alpha Table table content')
.getByLabel('wavelengths table cell')
.first()
.getAttribute('title');
const afterReloadBetaTelemetryValue = await page
.getByLabel('Beta Table table content')
.getByLabel('wavelengths table cell')
.first()
.getAttribute('title');
expect(beforeReloadAlphaTelemetryValue).not.toEqual(afterReloadAlphaTelemetryValue);
expect(beforeReloadBetaTelemetryValue).toEqual(afterReloadBetaTelemetryValue);
// now reload parent
await page.getByTitle('More actions').click();
await page.getByRole('menuitem', { name: /Reload/ }).click();
const fullReloadAlphaTelemetryValue = await page
.getByLabel('Alpha Table table content')
.getByLabel('wavelengths table cell')
.first()
.getAttribute('title');
const fullReloadBetaTelemetryValue = await page
.getByLabel('Beta Table table content')
.getByLabel('wavelengths table cell')
.first()
.getAttribute('title');
expect(fullReloadAlphaTelemetryValue).not.toEqual(afterReloadAlphaTelemetryValue);
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

@ -32,10 +32,10 @@ const setBorderColor = '#ff00ff';
const setBackgroundColor = '#5b0f00';
const setTextColor = '#e6b8af';
const defaultFrameBorderColor = '#e6b8af'; //default border color
const defaultBorderTargetColor = '#aaaaaa';
const defaultTextColor = '#aaaaaa'; // default text color
const inheritedColor = '#aaaaaa'; // inherited from the body style
const pukeGreen = '#6aa84f'; //Ugliest green known to man
const defaultBorderTargetColor = '#acacac';
const defaultTextColor = '#acacac'; // default text color
const inheritedColor = '#acacac'; // inherited from the body style
const pukeGreen = '#6aa84f'; //Ugliest green known to man 🤮
const NO_STYLE_RGBA = 'rgba(0, 0, 0, 0)'; //default background color value
test.describe('Flexible Layout styling', () => {
@ -114,7 +114,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultFrameBorderColor),
NO_STYLE_RGBA,
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
@ -122,7 +124,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultFrameBorderColor),
NO_STYLE_RGBA,
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),
NO_STYLE_RGBA,
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
@ -151,7 +157,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
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
@ -160,7 +168,7 @@ test.describe('Flexible Layout styling', () => {
setBorderColor,
setBackgroundColor,
setTextColor,
page.getByLabel('StackedPlot1 Frame')
page.getByRole('group', { name: 'StackedPlot1 Frame' })
);
// Check styles on StackedPlot1
@ -168,7 +176,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
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
@ -176,7 +186,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
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
@ -191,7 +203,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
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
@ -199,7 +213,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
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(setBackgroundColor),
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
@ -249,7 +267,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
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
@ -258,7 +278,7 @@ test.describe('Flexible Layout styling', () => {
setBorderColor,
setBackgroundColor,
setTextColor,
page.getByLabel('StackedPlot2 Frame')
page.getByRole('group', { name: 'StackedPlot2 Frame' })
);
// Check styles on StackedPlot2
@ -266,7 +286,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
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
@ -281,7 +303,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
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
@ -289,7 +313,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
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
@ -326,7 +352,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
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
@ -334,7 +362,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
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,
setBackgroundColor,
setTextColor,
page.getByLabel('StackedPlot1 Frame')
page.getByRole('group', { name: 'StackedPlot1 Frame' })
);
// Check styles using checkStyles function
@ -364,7 +394,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
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
@ -386,7 +418,7 @@ test.describe('Flexible Layout styling', () => {
'No Style',
'No Style',
'No Style',
page.getByLabel('StackedPlot1 Frame')
page.getByRole('group', { name: 'StackedPlot1 Frame' })
);
// Check styles using checkStyles function
@ -394,11 +426,13 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
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
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Reload page and verify that styles persist
await page.reload({ waitUntil: 'domcontentloaded' });
@ -408,7 +442,44 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
hexToRGB(inheritedColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
);
});
test('Styling, and then canceling reverts to previous style', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7233'
});
await page.goto(flexibleLayout.url);
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'Styles' }).click();
await setStyles(
page,
setBorderColor,
setBackgroundColor,
setTextColor,
page.getByLabel('Flexible Layout Column')
);
await page.getByLabel('Cancel Editing').click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
await checkStyles(
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
hexToRGB(inheritedColor),
page.getByLabel('Flexible Layout Column')
);
await page.reload();
await checkStyles(
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
hexToRGB(inheritedColor),
page.getByLabel('Flexible Layout Column')
);
});
});

View File

@ -36,9 +36,9 @@ import { test } from '../../../../pluginFixtures.js';
const setBorderColor = '#ff00ff';
const setBackgroundColor = '#5b0f00';
const setTextColor = '#e6b8af';
const defaultTextColor = '#aaaaaa'; // default text color
const defaultTextColor = '#acacac'; // default text color
const NO_STYLE_RGBA = 'rgba(0, 0, 0, 0)'; //default background color value
const DEFAULT_PLOT_VIEW_BORDER_COLOR = '#AAAAAA';
const DEFAULT_PLOT_VIEW_BORDER_COLOR = '#acacac';
const setFontSize = '72px';
const setFontWeight = '700'; //bold for monospace bold
const setFontFamily = '"Andale Mono", sans-serif';

View File

@ -67,7 +67,7 @@ test.describe('Style Inspector Options', () => {
await expect(page.getByRole('tab', { name: 'Styles' })).toBeVisible();
// 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.
await expect(page.getByRole('tab', { name: 'Styles' })).toBeVisible();

View File

@ -24,13 +24,18 @@ import { createDomainObjectWithDefaults } from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Tabs View', () => {
test('Renders tabbed elements', async ({ page }) => {
let tabsView;
let table;
let notebook;
let sineWaveGenerator;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const tabsView = await createDomainObjectWithDefaults(page, {
tabsView = await createDomainObjectWithDefaults(page, {
type: 'Tabs View'
});
const table = await createDomainObjectWithDefaults(page, {
table = await createDomainObjectWithDefaults(page, {
type: 'Telemetry Table',
parent: tabsView.uuid
});
@ -38,19 +43,21 @@ test.describe('Tabs View', () => {
type: 'Event Message Generator',
parent: table.uuid
});
const notebook = await createDomainObjectWithDefaults(page, {
notebook = await createDomainObjectWithDefaults(page, {
type: 'Notebook',
parent: tabsView.uuid
});
const sineWaveGenerator = await createDomainObjectWithDefaults(page, {
sineWaveGenerator = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: tabsView.uuid
});
});
page.goto(tabsView.url);
test('Renders tabbed elements', async ({ page }) => {
await page.goto(tabsView.url);
// select first tab
await page.getByLabel(`${table.name} tab`).click();
await page.getByLabel(`${table.name} tab`, { exact: true }).click();
// ensure table header visible
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
@ -58,7 +65,7 @@ test.describe('Tabs View', () => {
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
// select second tab
await page.getByLabel(`${notebook.name} tab`).click();
await page.getByLabel(`${notebook.name} tab`, { exact: true }).click();
// ensure notebook visible
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
@ -67,7 +74,7 @@ test.describe('Tabs View', () => {
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
// select third tab
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
await page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true }).click();
// expect sine wave generator visible
await expect(page.locator('.c-plot')).toBeVisible();
@ -78,7 +85,7 @@ test.describe('Tabs View', () => {
await expect(page.locator('canvas').nth(1)).toBeVisible();
// now try to select the first tab again
await page.getByLabel(`${table.name} tab`).click();
await page.getByLabel(`${table.name} tab`, { exact: true }).click();
// ensure table header visible
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
@ -86,3 +93,29 @@ test.describe('Tabs View', () => {
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
});
});
test.describe('Tabs View CRUD', () => {
let tabsView;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
tabsView = await createDomainObjectWithDefaults(page, {
type: 'Tabs View'
});
});
test('Eager Load Tabs is the default and then can be toggled off', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7198'
});
await page.goto(tabsView.url);
await page.getByLabel('Edit Object').click();
await page.getByLabel('More actions').click();
await page.getByLabel('Edit Properties...').click();
await expect(await page.getByLabel('Eager Load Tabs')).not.toBeChecked();
await page.getByLabel('Eager Load Tabs').setChecked(true);
await expect(await page.getByLabel('Eager Load Tabs')).toBeChecked();
});
});

View File

@ -0,0 +1,77 @@
/*****************************************************************************
* 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 preview plugin.
*/
import { createDomainObjectWithDefaults, expandEntireTree } from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Preview mode', () => {
test('all context menu items are available for a telemetry table', async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create a Display Layout
const displayLayout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout'
});
// Create a Telemetry Table
const telemetryTable = await createDomainObjectWithDefaults(page, {
type: 'Telemetry Table',
parent: displayLayout.uuid
});
// Create a Sinewave Generator
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: telemetryTable.uuid
});
await page.goto(displayLayout.url);
await page.getByLabel('View menu items').click();
await expect(page.getByLabel('Export Marked Rows')).toBeVisible();
await page.getByRole('menuitem', { name: 'Large View' }).click();
await page.getByLabel('Overlay').getByLabel('More actions').click();
await expect(page.getByLabel('Export Table Data')).toBeVisible();
await expect(page.getByLabel('Export Marked Rows')).toBeVisible();
await page.getByRole('menuitem', { name: 'Pause' }).click();
await page.getByRole('button', { name: 'Close' }).click();
await expandEntireTree(page);
await page.getByLabel('Edit Object').click();
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const telemetryTableTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(telemetryTable.name)
});
await telemetryTableTreeItem.locator('a').click();
await page.getByLabel('Overlay').getByLabel('More actions').click();
await expect(page.getByLabel('Export Table Data')).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,31 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { createDomainObjectWithDefaults, setTimeConductorBounds } from '../../../../appActions.js';
import {
createDomainObjectWithDefaults,
setTimeConductorBounds,
setTimeConductorMode
} from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
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 page.goto(table.url);
await setTimeConductorMode(page, false);
const rows = page.getByLabel('table content').getByLabel('Table Row');
await expect(rows).toHaveCount(50);
});
test('unpauses and filters data when paused by button and user changes bounds', async ({
page
}) => {
@ -34,7 +55,6 @@ test.describe('Telemetry Table', () => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: table.uuid
@ -64,10 +84,9 @@ test.describe('Telemetry Table', () => {
// Get the most recent telemetry date
const latestTelemetryDate = await page
.locator('table.c-telemetry-table__body > tbody > tr')
.getByLabel('table content')
.getByLabel('utc table cell')
.last()
.locator('td')
.nth(1)
.getAttribute('title');
// Verify that it is <= our new end bound
@ -79,7 +98,6 @@ test.describe('Telemetry Table', () => {
test('Supports filtering telemetry by regular text search', async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
await createDomainObjectWithDefaults(page, {
type: 'Event Message Generator',
parent: table.uuid
@ -91,7 +109,7 @@ test.describe('Telemetry Table', () => {
await page.getByRole('searchbox', { name: 'message filter input' }).click();
await page.getByRole('searchbox', { name: 'message filter input' }).fill('Roger');
let cells = await page.getByRole('cell', { name: /Roger/ }).all();
let cells = await page.getByRole('cell').getByText(/Roger/).all();
// ensure we've got more than one cell
expect(cells.length).toBeGreaterThan(1);
// ensure the text content of each cell contains the search term
@ -103,7 +121,10 @@ test.describe('Telemetry Table', () => {
await page.getByRole('searchbox', { name: 'message filter input' }).click();
await page.getByRole('searchbox', { name: 'message filter input' }).fill('Dodger');
cells = await page.getByRole('cell', { name: /Dodger/ }).all();
cells = await page
.getByRole('cell')
.getByText(/Dodger/)
.all();
// ensure we've got more than one cell
expect(cells.length).toBe(0);
// ensure the text content of each cell contains the search term
@ -119,7 +140,6 @@ test.describe('Telemetry Table', () => {
test('Supports filtering using Regex', async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
await createDomainObjectWithDefaults(page, {
type: 'Event Message Generator',
parent: table.uuid
@ -135,7 +155,7 @@ test.describe('Telemetry Table', () => {
await page.getByRole('searchbox', { name: 'message filter input' }).click();
await page.getByRole('searchbox', { name: 'message filter input' }).fill('/[Rr]oger/');
let cells = await page.getByRole('cell', { name: /Roger/ }).all();
let cells = await page.getByRole('cell').getByText(/Roger/).all();
// ensure we've got more than one cell
expect(cells.length).toBeGreaterThan(1);
// ensure the text content of each cell contains the search term
@ -147,7 +167,10 @@ test.describe('Telemetry Table', () => {
await page.getByRole('searchbox', { name: 'message filter input' }).click();
await page.getByRole('searchbox', { name: 'message filter input' }).fill('/[Dd]oger/');
cells = await page.getByRole('cell', { name: /Dodger/ }).all();
cells = await page
.getByRole('cell')
.getByText(/Dodger/)
.all();
// ensure we've got more than one cell
expect(cells.length).toBe(0);
// ensure the text content of each cell contains the search term

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);
// Bring up the time conductor popup
const timeConductorMode = await page.locator('.c-compact-tc');
const timeConductorMode = page.locator('.c-compact-tc');
await timeConductorMode.click();
const startDateLocator = page.locator('input[type="text"]').first();
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;
test.beforeEach(async ({ page }) => {

View File

@ -191,7 +191,7 @@ test.describe('Recent Objects', () => {
// Navigate to the clock and reveal it in the tree
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
const clockTreeItem = page
@ -298,7 +298,7 @@ test.describe('Recent Objects', () => {
// Assert that the list is empty
expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(0);
});
test('Ensure clear recent objects button is active or inactive', async ({ page }) => {
test('Verify functionality of "clear" and "collapse pane" buttons', async ({ page }) => {
// Assert that the list initially contains 3 objects (clock, folder, my items)
expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(3);
@ -331,6 +331,24 @@ test.describe('Recent Objects', () => {
expect(await page.getByRole('button', { name: 'Clear Recently Viewed' }).isEnabled()).toBe(
true
);
// Assert initial state of pane and collapse the Recent Objects panel
await expect(page.getByLabel('Expand Recently Viewed Pane')).toBeHidden();
await expect(page.getByLabel('Collapse Recently Viewed Pane')).toBeVisible();
await page.getByLabel('Collapse Recently Viewed Pane').click();
// Assert that the "Expand Recently Viewed Pane" button is visible
// and that the "Collapse Recently Viewed Pane" button is hidden
await expect(page.getByLabel('Expand Recently Viewed Pane')).toBeVisible();
await expect(page.getByLabel('Collapse Recently Viewed Pane')).toBeHidden();
// Expand the Recent Objects panel by clicking on the "Expand Recently Viewed Pane" button
await page.getByLabel('Expand Recently Viewed Pane').click();
// Assert that the "Expand Recently Viewed Pane" button is hidden
// and that the "Collapse Recently Viewed Pane" button is visible
await expect(page.getByLabel('Expand Recently Viewed Pane')).toBeHidden();
await expect(page.getByLabel('Collapse Recently Viewed Pane')).toBeVisible();
});
function assertInitialRecentObjectsListState() {

View File

@ -48,7 +48,7 @@ test('Verify that the create button appears and that the Folder Domain Object is
await expect(page.locator(':nth-match(:text("Folder"), 2)')).toBeEnabled();
});
test('Verify that My Items Tree appears @ipad', async ({ page, openmctConfig }) => {
test('Verify that My Items Tree appears', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
//Go to baseURL
await page.goto('./');

View File

@ -109,7 +109,7 @@ test.describe('Verify tooltips', () => {
async function getToolTip(object) {
await page.locator('.c-create-button').hover();
await page.getByRole('cell', { name: object.name }).hover();
await page.getByLabel('lad name').getByText(object.name).hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
return tooltipText.replace('\n', '').trim();
}
@ -359,7 +359,11 @@ test.describe('Verify tooltips', () => {
expect(tooltipText).toBe(sineWaveObject3.path);
});
test('display tooltip path for telemetry table names', async ({ page }) => {
test.fixme('display tooltip path for telemetry table names', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7421'
});
// set endBound to 10 seconds after start bound
const url = await page.url();
const parsedUrl = new URL(url.replace('#', '!'));

View File

@ -40,7 +40,7 @@ test.describe('Main Tree', () => {
type: 'Folder'
});
await page.getByTitle('Show selected item in tree').click();
await page.getByLabel('Show selected item in tree').click();
const clock = await createDomainObjectWithDefaults(page, {
type: 'Clock',

View File

@ -0,0 +1,75 @@
/*****************************************************************************
* 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 '../../../baseFixtures.js';
// We don't need cspell to check this. It doesn't know latin.
/* cSpell:disable */
const loremIpsum = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Molestie at elementum eu facilisis sed. Feugiat pretium nibh ipsum consequat. Amet consectetur adipiscing elit duis tristique sollicitudin nibh sit amet. Eget nullam non nisi est sit amet. A pellentesque sit amet porttitor eget dolor morbi non arcu. Ullamcorper sit amet risus nullam eget felis eget nunc. In tellus integer feugiat scelerisque varius morbi enim nunc. Ac feugiat sed lectus vestibulum mattis ullamcorper. Nulla facilisi morbi tempus iaculis urna id volutpat. Massa vitae tortor condimentum lacinia quis vel eros donec. Ornare quam viverra orci sagittis eu. Vestibulum sed arcu non odio. In egestas erat imperdiet sed euismod nisi porta lorem. Vitae auctor eu augue ut lectus arcu bibendum at. Donec adipiscing tristique risus nec feugiat in fermentum posuere urna. Velit euismod in pellentesque massa placerat duis ultricies. Nulla facilisi nullam vehicula ipsum a arcu cursus vitae. Aliquam malesuada bibendum arcu vitae elementum curabitur.
Vel eros donec ac odio tempor orci. Et netus et malesuada fames ac turpis egestas sed tempus. Turpis egestas pretium aenean pharetra magna ac placerat. Euismod elementum nisi quis eleifend. Vitae auctor eu augue ut lectus arcu. At imperdiet dui accumsan sit amet nulla facilisi. Est velit egestas dui id ornare arcu odio ut sem. Ornare arcu dui vivamus arcu felis. Luctus venenatis lectus magna fringilla. At elementum eu facilisis sed. Tristique et egestas quis ipsum suspendisse ultrices gravida dictum. Enim eu turpis egestas pretium aenean pharetra magna ac placerat. Lobortis scelerisque fermentum dui faucibus in. Tempor orci eu lobortis elementum nibh tellus molestie nunc non. Dignissim convallis aenean et tortor at risus. Enim tortor at auctor urna nunc id cursus. Libero volutpat sed cras ornare arcu dui vivamus. Scelerisque fermentum dui faucibus in ornare quam viverra.
Odio ut sem nulla pharetra. Neque vitae tempus quam pellentesque nec. A arcu cursus vitae congue mauris. Turpis nunc eget lorem dolor sed viverra ipsum nunc aliquet. Nibh tellus molestie nunc non blandit massa enim nec. Risus feugiat in ante metus dictum at tempor commodo ullamcorper. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Pulvinar elementum integer enim neque. Bibendum ut tristique et egestas. Nibh praesent tristique magna sit. Lectus magna fringilla urna porttitor. Eu non diam phasellus vestibulum lorem sed risus. Rhoncus mattis rhoncus urna neque. Rutrum tellus pellentesque eu tincidunt tortor aliquam. Pharetra convallis posuere morbi leo urna molestie at elementum. Quis commodo odio aenean sed adipiscing. Enim sit amet venenatis urna cursus eget nunc.
Enim nec dui nunc mattis. Cursus turpis massa tincidunt dui ut. Donec adipiscing tristique risus nec feugiat in. Eleifend mi in nulla posuere sollicitudin. Donec enim diam vulputate ut pharetra sit. Ultricies mi eget mauris pharetra et ultrices neque. Eros in cursus turpis massa tincidunt dui. Cursus risus at ultrices mi tempus imperdiet nulla malesuada. Morbi enim nunc faucibus a pellentesque sit. Porttitor rhoncus dolor purus non. Ac tortor vitae purus faucibus.
Proin libero nunc consequat interdum varius sit amet mattis vulputate. Metus dictum at tempor commodo ullamcorper a lacus vestibulum sed. Quisque non tellus orci ac auctor augue mauris. Id ornare arcu odio ut. Rhoncus est pellentesque elit ullamcorper dignissim. Senectus et netus et malesuada fames ac turpis egestas. Volutpat ac tincidunt vitae semper quis lectus nulla. Adipiscing elit duis tristique sollicitudin. Ipsum faucibus vitae aliquet nec ullamcorper sit. Gravida neque convallis a cras semper auctor neque vitae tempus. Porttitor leo a diam sollicitudin tempor id. Dictum non consectetur a erat nam at lectus. At volutpat diam ut venenatis tellus in. Morbi enim nunc faucibus a pellentesque sit amet. Cursus in hac habitasse platea. Sed augue lacus viverra vitae.
`;
test.describe('Inspector tests', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Content in inspector can be scrolled to vertically', async ({ page }) => {
const folderWithOverflowingTitle = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: loremIpsum
});
await page.goto(folderWithOverflowingTitle.url);
const inspectorPropertiesLocator = page
.getByRole('tabpanel', { name: 'Inspector Views' })
.getByLabel('Inspector Properties Details');
const inspectorPropertiesList = inspectorPropertiesLocator.getByRole('list');
const firstInspectorPropertyValue = inspectorPropertiesList
.getByRole('listitem')
.first()
.getByLabel('value', { exact: false });
const lastInspectorPropertyValue = inspectorPropertiesList
.getByRole('listitem')
.last()
.getByLabel('value', { exact: false });
// inspector content partially in viewport, but not all the way in viewport
await expect(inspectorPropertiesLocator).toBeInViewport();
await expect(inspectorPropertiesLocator).not.toBeInViewport({ ratio: 0.9 });
await expect(firstInspectorPropertyValue).toBeInViewport();
await expect(lastInspectorPropertyValue).not.toBeInViewport();
// using page.mouse.wheel to scroll the inspector content by the height of the content
// because click and scrollIntoView will scroll even if scrollbar not available
await inspectorPropertiesLocator.hover();
const offset = await inspectorPropertiesLocator.evaluate((el) => el.offsetHeight);
await page.mouse.wheel(0, offset);
await expect(lastInspectorPropertyValue).toBeInViewport();
});
});

View File

@ -0,0 +1,95 @@
/*****************************************************************************
* 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 can quickly verify that any openmct installation is
operable and that any type of testing can proceed.
Ideally, smoke tests should make zero assumptions about how and where they are run. This makes them
more resilient to change and therefor a better indicator of failure. Smoke tests will also run quickly
as they cover a very "thin surface" of functionality.
When deciding between authoring new smoke tests or functional tests, ask yourself "would I feel
comfortable running this test during a live mission?" Avoid creating or deleting Domain Objects.
Make no assumptions about the order that elements appear in the DOM.
*/
import { expect, test } from '../../pluginFixtures.js';
test.describe('Smoke tests for @mobile', () => {
test.beforeEach(async ({ page }) => {
//For now, this test is going to be hardcoded against './test-data/display_layout_with_child_layouts.json'
await page.goto('./');
});
test('Verify that My Items Tree appears @mobile', async ({ page }) => {
//My Items to be visible
await expect(page.getByRole('treeitem', { name: 'My Items' })).toBeVisible();
});
test('Verify that user can search @mobile', async ({ page }) => {
await page.getByRole('searchbox', { name: 'Search Input' }).click();
await page.getByRole('searchbox', { name: 'Search Input' }).fill('Parent Display Layout');
//Search Results appear in search modal
await expect(
page.getByLabel('Object Results').getByText('Parent Display Layout')
).toBeVisible();
//Clicking on the search result takes you to the object
await page.getByLabel('Object Results').getByText('Parent Display Layout').click();
await page.getByTitle('Collapse Browse Pane').click();
await expect(page.getByRole('main').getByText('Parent Display Layout')).toBeVisible();
});
test('Verify that user can change time conductor @mobile', async ({ page }) => {
//Collapse Browse Pane to get more Time Conductor space
await page.getByLabel('Collapse Browse Pane').click();
//Open Time Conductor and change to Real Time Mode and set offset hour by 1 hour
// Disabling line because we're intentionally obscuring the text
// eslint-disable-next-line playwright/no-force-option
await page.getByLabel('Time Conductor Mode').click({ force: true });
await page.getByLabel('Time Conductor Mode Menu').click();
await page.getByLabel('Real-Time').click();
await page.getByLabel('Start offset hours').fill('01');
await page.getByLabel('Submit time offsets').click();
await expect(page.getByLabel('Start offset: 01:30:00')).toBeVisible();
});
test('Remove Object and confirmation dialog @mobile', async ({ page }) => {
await page.getByRole('searchbox', { name: 'Search Input' }).click();
await page.getByRole('searchbox', { name: 'Search Input' }).fill('Parent Display Layout');
//Search Results appear in search modal
//Clicking on the search result takes you to the object
await page.getByLabel('Object Results').getByText('Parent Display Layout').click();
await page.getByTitle('Collapse Browse Pane').click();
await expect(page.getByRole('main').getByText('Parent Display Layout')).toBeVisible();
//Verify both objects are in view
await expect(await page.getByLabel('Child Layout 1 Layout')).toBeVisible();
await expect(await page.getByLabel('Child Layout 2 Layout')).toBeVisible();
//Remove First Object to bring up confirmation dialog
await page.getByLabel('View menu items').nth(1).click();
await page.getByLabel('Remove').click();
await page.getByRole('button', { name: 'OK' }).click();
//Verify that the object is removed
await expect(await page.getByLabel('Child Layout 1 Layout')).toBeVisible();
expect(await page.getByLabel('Child Layout 2 Layout').count()).toBe(0);
});
});

View File

@ -34,7 +34,7 @@ TODO:
import { expect, test } from '@playwright/test';
const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
const filePath = 'test-data/PerformanceDisplayLayout.json';
test.describe('Performance tests', () => {
test.beforeEach(async ({ page, browser }, testInfo) => {
@ -178,7 +178,7 @@ test.describe('Performance tests', () => {
console.log('jpgResourceTiming ' + JSON.stringify(jpgResourceTiming));
// Click Close Icon
await page.locator('[aria-label="Close"]').click();
await page.getByRole('button', { name: 'Close' }).click();
await page.evaluate(() => window.performance.mark('view-large-close-button'));
//await client.send('HeapProfiler.enable');

View File

@ -33,7 +33,7 @@ TODO:
import { expect, test } from '@playwright/test';
const notebookFilePath = 'e2e/test-data/PerformanceNotebook.json';
const notebookFilePath = 'test-data/PerformanceNotebook.json';
test.describe('Performance tests', () => {
test.beforeEach(async ({ page, browser }, testInfo) => {

View File

@ -299,7 +299,6 @@ test.describe('Navigation memory leak is not detected in', () => {
// for detecting memory leaks.
await page.evaluate(() => {
window.gcPromise = new Promise((resolve) => {
// eslint-disable-next-line no-undef
window.fr = new FinalizationRegistry(resolve);
window.fr.register(
window.openmct.layout.$refs.browseObject.$refs.objectViewWrapper.firstChild,

View File

@ -24,7 +24,7 @@ import { createDomainObjectWithDefaults, waitForPlotsToRender } from '../../appA
import { expect, test } from '../../pluginFixtures.js';
test.describe('Tabs View', () => {
test('Renders tabbed elements nicely', async ({ page }) => {
test('Renders tabbed elements only when visible', async ({ page }) => {
// Code to hook into the requestAnimationFrame function and log each call
let animationCalls = [];
await page.exposeFunction('logCall', (callCount) => {
@ -64,24 +64,24 @@ test.describe('Tabs View', () => {
page.goto(tabsView.url);
// select first tab
await page.getByLabel(`${table.name} tab`).click();
await page.getByLabel(`${table.name} tab`, { exact: true }).click();
// ensure table header visible
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
// select second tab
await page.getByLabel(`${notebook.name} tab`).click();
await page.getByLabel(`${notebook.name} tab`, { exact: true }).click();
// expect notebook visible
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
// select third tab
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
await page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true }).click();
// ensure sine wave generator visible
expect(await page.locator('.c-plot').isVisible()).toBe(true);
// now select notebook and clear animation calls
await page.getByLabel(`${notebook.name} tab`).click();
await page.getByLabel(`${notebook.name} tab`, { exact: true }).click();
animationCalls = [];
// expect notebook visible
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
@ -89,7 +89,7 @@ test.describe('Tabs View', () => {
// select sine wave generator and clear animation calls
animationCalls = [];
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
await page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true }).click();
// ensure sine wave generator visible
await waitForPlotsToRender(page);

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