Compare commits

...

91 Commits

Author SHA1 Message Date
Michael Rogers
7cf45ccce6 Time context get clock 2023-10-30 17:42:25 -05:00
Michael Rogers
6c46f7a87f Pass input-bounds 2023-10-30 16:56:30 -05:00
Michael Rogers
3a9cad6c19 Pass input-bounds 2023-10-30 16:48:42 -05:00
Michael Rogers
2424db4b06 WIP 2023-10-30 16:31:04 -05:00
Michael Rogers
d848cdcce6 WIP: log ITC 2023-10-30 14:43:25 -05:00
Scott Bell
13311b9fc8 Prevent infinite loop when updating a table row in place (#7154)
* bump index on update row in place

* add test

* Removing problematic test

* spelling
2023-10-23 09:22:13 -07:00
Jesse Mazzella
2daec448da chore: bump version to 3.2.0-next (#7117)
chore: bump version to 3.2.0-next

Co-authored-by: Scott Bell <scott@traclabs.com>
2023-10-23 12:23:34 +02:00
dependabot[bot]
5bd8d17592 chore(deps-dev): bump jasmine-core from 5.0.0 to 5.1.1 (#7008)
Bumps [jasmine-core](https://github.com/jasmine/jasmine) from 5.0.0 to 5.1.1.
- [Release notes](https://github.com/jasmine/jasmine/releases)
- [Changelog](https://github.com/jasmine/jasmine/blob/main/RELEASE.md)
- [Commits](https://github.com/jasmine/jasmine/compare/v5.0.0...v5.1.1)

---
updated-dependencies:
- dependency-name: jasmine-core
  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>
2023-10-20 23:20:32 +00:00
dependabot[bot]
954c72b100 chore(deps-dev): bump eslint-plugin-vue from 9.15.0 to 9.17.0 (#6907)
Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 9.15.0 to 9.17.0.
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v9.15.0...v9.17.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>
2023-10-20 19:01:12 +00:00
Jesse Mazzella
43338f3980 chore: remove vue/compat and complete Vue 3 migration (#7133)
* chore: remove custom `compatConfig` settings

* chore: remove `vue-compat` and adjust webpack config

* chore: explicitly define Vue feature flags

* fix: `_data` property moved to `_.data`

* fix(e2e): revert to original test procedures

* refactor: replace final instances of `$set`

* refactor: remove `Vue` imports from tests

* refactor: `Vue.ref()` -> `ref()`

* refactor: actually push the changes...

* test: replace unit test with e2e test

* test: remove test as it's already covered by e2e

* fix(test): use `$ref`s instead of `$children`

* test(fix): more `$refs`

* fix(test): more `$refs`

* fix(test): use `$refs` in InspectorStyles tests

* fix(SearchComponent): use `$attrs` correctly

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
2023-10-19 09:08:39 -07:00
dependabot[bot]
1414f54c17 chore(deps-dev): bump vue-eslint-parser from 9.3.1 to 9.3.2 (#7125)
Bumps [vue-eslint-parser](https://github.com/vuejs/vue-eslint-parser) from 9.3.1 to 9.3.2.
- [Release notes](https://github.com/vuejs/vue-eslint-parser/releases)
- [Commits](https://github.com/vuejs/vue-eslint-parser/compare/v9.3.1...v9.3.2)

---
updated-dependencies:
- dependency-name: vue-eslint-parser
  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>
2023-10-18 15:49:29 +00:00
Jesse Mazzella
9849e0398e fix(#7143): add eslint-plugin-no-sanitize and fix errors (#7144) 2023-10-16 09:26:12 -07:00
David Tsay
76889cf60d Rename all configuration inspector tabs to Config (#7140) 2023-10-12 13:48:13 -07:00
Scott Bell
26d3bd1e69 When dropping an unsupported file onto a notebook entry, tell the user it isnt supported (#7115)
* handle unknown files and deal with copy/paste

* add some nice errors if couch pukes

* added how to adjust couchdb limits

* throw error on anntotation change

* keep blockquotes

* add test for blockquotes

* allow multi-file drop of images too

* add test for big files

* spell check

* check for null

* need to ignore console errors

* reorder tests so we can ignore console errors

* when creating new entry, ready it for editing

* fix tests and empty embeds

* fix tests

* found similar issue from notebooks in plots
2023-10-12 07:34:32 +02:00
David Tsay
a16a1d35b6 do not store state in singleton action (#7121)
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2023-10-10 15:49:45 -07:00
David 'Epper' Marshall
97b2ebc0bb Fix remaining vue-compat warnings (#6966)
* PascalCase files

* emit warnings

* minor updates

* merge conflict resolve pt 1

* part 2

* update to eventbus

* eventbus spelling

* fix: import

* fix: eventBus injection

* fix: import

* fix(test): provide eventBus in overlay plot tests

* refactor: EventBus as composable

* chore: lint:fix

* chore: require vue event hyphenation

* fix: revert event renames

* refactor: use PascalCase name

* fix: ensure `$attrs` are properly bound

* fix: emit `click` event from SearchComponent

* chore: remove rules already included in `vue/vue3-recommended` ruleset

* fix: remove `Vue` import

* chore: remove unused files

* fix: fix lint scripts and make them cross-platform

* refactor: rename `DataImagery.vue` -> `ImageryInspectorView.vue`

* refactor: rename `NumericData.vue` -> `NumericDataInspectorView.vue`

* refactor: rename components

* refactor: rename `GeneralIndicators.vue` -> `StatusIndicators.vue`

* refactor: rename components

---------

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2023-10-10 13:29:01 -07:00
Khalid Adil
6b32c63039 [Staleness] Fix issue with object view staleness styles not being reset on clock change (#7122)
Add logic to un/re-subscribe when clock changes to object view
2023-10-06 15:26:05 -07:00
Scott Bell
084784a409 Handle negative height & width in image annotations (#7116)
* add fix

* remove debug code

* add test for unholy rectangles

* make test a bit more forgiving

* address pr review
2023-10-06 19:08:42 +02:00
Khalid Adil
734a8dd592 [Staleness] Fix staleness on clock change (#7088)
* Update staleness mixin
* Fix listeners and add guard
* Add check to make sure staleness only shows for correct clock
* Add guard for time api
* Cleanup the setting of isStale in ObjectView
* Cleanup use of combinedKey on LadTableSet
2023-10-04 13:39:20 -07:00
Jesse Mazzella
5eed5de3bb chore(cspell): use --quiet flag (#7110)
chore(cspell): use `--quiet` flag
2023-10-04 14:47:49 +02:00
Shefali Joshi
ce59c0f50a Check realtime mode in remote clock interceptor (#6985)
* Revert Date.now() purge change
Check that the request is using realtime before using the remote-clock start and end timestamps for telemetry requests
* docs: clarify comment

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-10-03 20:41:02 +00:00
David Tsay
d53d8d562e Eagerly lose WebGL context on DrawWebGL.destroy() (#7080)
* test default for preserveDrawingBuffer
* fix: delete WebGL resources on destroy
* fix: eagerly lose WebGL contexts on destroy
- Recommended by Mozilla's [WebGL best practices]-(https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#lose_contexts_eagerly).

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-10-03 19:33:51 +00:00
Jamie V
d973140906 [Documentation] Time API docs update new/deprecated functionality (#7107)
* update time api docs to reflect recent changes

* clarify what mode openmct starts in and update setMode arguments
2023-10-03 18:42:35 +00:00
dependabot[bot]
b1169ffd7d chore(deps-dev): bump eslint-plugin-compat from 4.1.4 to 4.2.0 (#7104)
Bumps [eslint-plugin-compat](https://github.com/amilajack/eslint-plugin-compat) from 4.1.4 to 4.2.0.
- [Release notes](https://github.com/amilajack/eslint-plugin-compat/releases)
- [Changelog](https://github.com/amilajack/eslint-plugin-compat/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amilajack/eslint-plugin-compat/compare/v4.1.4...v4.2.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-compat
  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>
2023-10-03 16:56:06 +00:00
Shefali Joshi
ede93d881c Ensure CouchDB changes for plans trigger updates in the view (#7099)
Listen to ALL changes for a plan since couchdb feed updates does not trigger a property only event. It triggers a catchall '*' event.

Co-authored-by: Scott Bell <scott@traclabs.com>
2023-10-02 23:26:13 +00:00
Scott Bell
6947c912a7 Add markdown to notebook entries (#7084)
* try marked out

* fix url validation

* now rendering blockquotes properly

* add abbrv, link titles, and strikethrough

* fix tests and lint

* Closes #6060
- CSS resets and styling for markdown-related HTML markup in Notebook entries.
- Better styling and cursor affordances for Notebook entry selection and editing interaction flow.

* add line breaks option

* Closes #6060
- Tab

* Closes #6060
- Conversion of contenteditable-div to textarea started.
- Stubbed in textarea with styles.

* have it markdown with a textarea and adjust size automatically

* Closes #6060
- Padding added back to text `div` area.

* Closes #6060
- Styles added to support Shift Log and hover behavior for entries on locked pages.
- Removed `--major` styling from Shift Log Commit Entries button
to reduce confusion with entry commit button.
- CSS code cleanups.

* two step focus/edit. also scroll into view for editing

* add markdown, strip all tags, and truncate

* lint

* remove unneeded code

* fix notebook entry, selected page may also be null

* fix existing notebook tests

* lint

* fix whitelist

* readd whitelist

* lint

* fix link tests

* fix tests

* fix tagging test

* add some markdown test

* get rid of pause

* add another sanitization step

---------

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-10-02 22:28:02 +00:00
David Tsay
2243381d52 Protect against prototype pollution in import action (#7094) 2023-10-02 14:50:53 -07:00
dependabot[bot]
3c7d3397d6 chore(deps-dev): bump cspell from 7.1.1 to 7.3.6 (#7067)
Bumps [cspell](https://github.com/streetsidesoftware/cspell) from 7.1.1 to 7.3.6.
- [Release notes](https://github.com/streetsidesoftware/cspell/releases)
- [Changelog](https://github.com/streetsidesoftware/cspell/blob/main/CHANGELOG.md)
- [Commits](https://github.com/streetsidesoftware/cspell/compare/v7.1.1...v7.3.6)

---
updated-dependencies:
- dependency-name: cspell
  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>
2023-10-02 17:38:42 +00:00
Shefali Joshi
9f8338124f Catchall for bug fix PRs and add performance category (#7096)
* Catchall for bug fix PRs and add performance category

* Move Bug fixes to the bottom

* Fix performance label
2023-09-29 17:17:14 +00:00
Scott Bell
ab0e2d2c96 Hide image controls when tagging, and hide compass HUD by default (#7028)
* make compass HUD configurable and hide image controls when tagging

* lint fixes

* address PR comments

* change prop to inject

---------

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2023-09-28 03:58:24 +00:00
Jesse Mazzella
c3b5e4e1e3 chore: add release.yml (#7090)
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2023-09-27 12:18:19 -07:00
Jesse Mazzella
8fc5aa6ed5 Revert "fix(templates): move to own folder" (#7092)
Revert "fix(templates): move to own folder (#7000)"

This reverts commit 5592d20ab1.
2023-09-26 16:02:48 -05:00
Even Stensberg
5592d20ab1 fix(templates): move to own folder (#7000)
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-09-26 13:37:55 -07:00
Scott Bell
ce2305455a Add script to delete annotations (#7069)
* add script

* add package.json and script to delete annotations

* amend help

* fix issues

* added explicit runs

* add design document and index creation to script

* update tests to wait for url to change

* i think we can remove this deprecated function now
2023-09-25 10:15:00 -07:00
Jesse Mazzella
ff2c8b35b0 🙅🚮␡ Removeopenmct.components🚮🙅 (#7075)
🙅🚮␡`openmct.components`␡🚮🙅

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2023-09-21 14:05:35 -07:00
Scott Bell
482f1f68dd Have annotations work with domain objects that have dots (#7065)
* migrating to new structure - wip

* notebooks work, now to plots and images

* resolve conflicts

* fix search

* add to readme

* spelling

* fix unit test

* add search by view for big search speedup

* spelling

* fix out of order search

* improve reliability of plot tagging tests
2023-09-21 13:50:08 -07:00
Andrew Henry
05c7a81630 Fix mojibake (#7073)
Add charset preamble to generated css
2023-09-21 01:01:30 +00:00
Shefali Joshi
b8949db767 Memory leak fixes for several views (#7057)
* Change the mount utility to use Vue's createApp and defineComponent methods

* Fix display layout memory leaks caused by `getSelectionContext`

* fix some display layout leaks due to use of slots

* Fix imagery memory leak (removed span tag). NOTE: CompassRose svg leaks memory - must test on firefox to see if this is a Chrome leak.

* Fix ActionsAPI action collection and applicable actions leak.

* Fix flexible layout memory leaks - remove listeners on unmount. NOTE: One type of overlay plot (Rover Yaw) is still leaking.

* pass in the el on mount

* e2e test config and spec changes

* Remove mounting of limit lines. Use components directly

* test: remove `.only()`

* Fix display layout memory leaks

* Enable passing tests

* e2e README and appActions should be what master has.

* lint: add word to cspell list

* lint: fixes

* lint:fix

* fix: revert `el` change

* fix: remove empty span

* fix: creating shapes in displayLayout

* fix: avoid `splice` as it loses reactivity

* test: reduce timeout time

* quick fixes

* add prod mode and convert the test config to select the correct mode

* Fix webpack prod config

* Add launch flag for exposing window.gc

* never worked

* explicit naming

* rename

* We don't need to destroy view providers

* test: increase timeout time

* test: unskip all mem tests

* fix(vue-loader): disable static hoisting

* chore: run `test:perf:memory`

* Don't destroy view providers

* Move context menu once listener to beforeUnmount instead.

* Disconnect all resize observers on unmount

* Delete Test vue component

* Use beforeUnmount and remove splice(0) in favor of [] for emptying arrays

* re-structure

* fix: unregister listener in pane.vue

* test: tweak timeouts

* chore: lint:fix

* test: unskip perf tests

* fix: unregister events properly

* fix: unregister listener

* fix: unregister listener

* fix: unregister listener

* fix: use `unmounted()`

* fix: unregister listeners

* fix: unregister listener properly

* chore: lint:fix

* test: fix imagery layer toggle test

* test: increase timeout

* Don't use anonymous functions for listeners

* Destroy objects and event listeners properly

* Delete config stores that are created by components

* Use the right unmount hook. Destroy mounted view on unmount.

* Use unmounted, not beforeUnmounted

* Lint fixes

* Fix time strip memory leak

* Undo unneeded change for memory leaks.

* chore: combine common webpack configs

---------

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-09-20 10:34:05 -07:00
dependabot[bot]
61e7050391 chore(deps-dev): bump typescript from 5.1.3 to 5.2.2 (#7007)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 5.1.3 to 5.2.2.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v5.1.3...v5.2.2)

---
updated-dependencies:
- dependency-name: typescript
  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>
2023-09-18 18:45:24 +00:00
dependabot[bot]
3f51516da9 chore(deps-dev): bump eslint-config-prettier from 8.8.0 to 9.0.0 (#6897)
Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 8.8.0 to 9.0.0.
- [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-config-prettier/compare/v8.8.0...v9.0.0)

---
updated-dependencies:
- dependency-name: eslint-config-prettier
  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>
2023-09-18 18:24:20 +00:00
Scott Bell
541a022f36 Embedding images in notebook entries (#7048)
* initial drag drop, wip

* images work as snapshots, but need to disable navigate to actions

* embed image name

* works now with images, need to be refactor so can duplicate code for entries too

* works dropping on entries too

* handle remote images too

* add e2e test

* spelling

* address most PR comments
2023-09-18 10:56:49 -07:00
David Tsay
c7b5ecbd68 Allow Data Visualization in inspector based on current selection (#7052)
* visualize data in inspector per selection

---------

Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-09-15 12:30:58 -07:00
David 'Epper' Marshall
6776cc308f PascalCase files (#6955)
* PascalCase files

* git mv for file name change

* renamed files

* merge changes from master

* fix: template name

* sort imports

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2023-09-14 09:55:44 -07:00
dependabot[bot]
95ac919ddf chore(deps): bump docker/login-action from 2 to 3 (#7053)
Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  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>
2023-09-12 16:17:23 -07:00
John Hill
2a1064cbab [CI] Stabilize visual tests, remove appAction, and update pane buttons (#7033)
* Add a VISUAL_URL constant and remove all vestiges of hide inspector and tree

* hide timer and add concurrency

* turn off concurrency

* factor out old appAction

* Add expand button to panes

* remove old slow annotations

* fix fault

* update domcontentloaded

* missed refactor

* driveby: setTimeBounds private

* add comments to the percyCSS

* suggest MISSION_TIME

* more notes

* regen

* clean up test

* driveby: clean up order

* restructure

* add new suite now that i'ts hidden

* use mission time everywhere possible

* driveby

* rerun generatedata

* comments

* lintfix
2023-09-11 23:33:46 +00:00
Scott Bell
8e917b2679 Remove large series models from reactive data in plots (#6961)
* remove series object from highlights

* remove series models from legend reactive data

* drawing all annotations at once is way faster

* fix multi annotations

* lots of reactive things depending on config

* make annotation lookup faster

* lint

* readd perf test

* address PR comments

* fix highlight typo

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2023-09-06 09:32:36 -07:00
Jesse Mazzella
0be106f29e Fix and re-enable disabled unit test suites (#6990)
* fix: register event listeners etc in `created()` hook

* fix: initialize `stalenessSubscription` before composition load and in `created()` hook

* refactor(test): make `openmct` const

* refactor: update overlayPlot spec to Vue 3 and fix tests

* refactor: fix eslint errors

* refactor: move initialization steps to `created()` hook

* test: re-enable and fix stackedPlot test suite

* fix: `hideTree=true` hides the tree again

* fix: add back in check on mount

* test: fix Layout tests

* fix: BarGraph test

* fix: plot inspector tests

* fix: reenable grand search tests

* fix: inspectorStyles test suite

* fix: re-enable most timeline tests

* fix: no need to hideTree in appactions

* fix: re-enable more tests

* test: re-enable more tests

* test: re-enable most plot tests

* chore: `lint:fix`

* test: re-enable timelist suite

* fix(#7016): timers count down or up to a target date

* test: add regression tests to cover disabled unit tests

* refactor: lint:fix

* refactor: no need for momentjs here

* fix: timerAction missed refactor

* fix: ensure timestamp is always UTC string

* test: use role selectors

* docs: add instructions for clock override in e2e

* docs: update

* Update readme

* lint

* spelling fixes

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-09-05 08:53:03 +00:00
Jesse Mazzella
9f7b3b9225 fix(#7022): remove ProgressBar artifacts from Notifications (#7024)
* fix: check for null or undefined

* refactor: `lint:fix`

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
2023-09-04 12:51:32 +00:00
Jesse Mazzella
e9b94c308b chore: pin vue package versions (#7032) 2023-09-03 15:14:54 +00:00
dependabot[bot]
64740e133f chore(deps-dev): bump eslint from 8.43.0 to 8.48.0 (#7010)
Bumps [eslint](https://github.com/eslint/eslint) from 8.43.0 to 8.48.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.43.0...v8.48.0)

---
updated-dependencies:
- dependency-name: eslint
  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>
2023-09-01 23:33:24 +00:00
Even Stensberg
c27ad469f6 feat(eslint): sort import rule (#6939)
* feat(eslint): sort import rule

* chore(deps): pin dep

* refactor: sort imports

---------

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2023-08-31 13:40:00 -07:00
Even Stensberg
e09a7aebae feat(linting): concurrent linting (#6969) 2023-08-30 23:12:06 +00:00
David 'Epper' Marshall
b87459dfd7 ProgressBar null not undefined (#6953)
* ProgressBar null not undefined

* notification banner null

* update progress dialog

* docs: update type

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2023-08-29 23:49:31 +00:00
Jesse Mazzella
ca06a6a047 docs: update staticRootPlugin README (#7014) 2023-08-29 23:09:28 +02:00
Even Stensberg
95e3ab25a4 fix(dialog): empty description (#6986)
* fix(dialog): empty description

* tests(e2e): adds roleview test

* tests(e2e): test in beforeEach

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-08-28 15:54:49 -07:00
Even Stensberg
f9db6433a1 docs(readme): indent correctly (#6996)
* fix(readme): indent correctly

* fix(readme): review

* fix(readme): review

* chore(docs): revise

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

* docs: tidy up a bit

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2023-08-28 19:59:25 +00:00
Even Stensberg
cbac2c1c82 docs(readme): note sections (#6997)
* fix(readme): note sections

* fix: notes

* fix: notes

* fix(readme): better markdown

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-08-28 19:50:20 +00:00
Shefali Joshi
a8678aa739 Imagery layer checkbox should match layer visiblity (#7003)
Set layer visibility to false if layer visibility cannot be persisted
2023-08-28 10:32:37 -07:00
Jamie V
244e3b7938 [Aborts] Abort Telemetry Collections requests on Navigation, Add abort functionality to getLimits (#6872)
* debug

* abort any pending requests on router "change:path" event, this should catch cases where the UI is bogged down and doesnt destroy the tc first

* english

* cant always be on

* adding abort to limits requests

* finally-ing off the promise

* need to just return the object not the property

* sticking with the try/catch structure we use elsewhere

* removing abort on nav, as views should be calling destroy

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2023-08-24 21:17:58 +00:00
John Hill
42b13c4dfb Fix couchdb setup and add a note on how to remove the container (#6915)
* discrete steps

* update script to remove couchdb

* address comments and add shellcheck linting

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-08-22 11:11:07 -07:00
Even Stensberg
351800b32a chore(npm): dont generate lockfile (#6970)
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-08-21 15:39:52 -07:00
Shefali Joshi
6db390a71a Add strategy latest and timeContext to auto flow tabular and gauge views (#6960) 2023-08-21 14:09:39 -07:00
Even Stensberg
9ece4e55dc chore(package.json): add fields (#6971)
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-08-21 20:32:07 +00:00
Even Stensberg
0a497483f2 fix(html): minor fixes from validation (#6962)
* fix(html): minor fixes from validation

* chore(html): Update index.html

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

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-08-21 09:58:06 -07:00
Even Stensberg
a52577e729 feat(tooling): adds nvm (#6938)
* feat(tooling): adds nvm

* fix(ci): nodev16 -> nodev18

* chore(node): dont modify ci config

* feat(nvm): add lts

* docs(readme): add section on nvm

* fix(docs): revise section

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-08-19 13:16:26 -07:00
David 'Epper' Marshall
a495e86231 fix(#6516): Progress Bar does not show progress percentage (#6952)
MCT 6516

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-08-18 22:54:58 +00:00
Shefali Joshi
bada228b8f Ensure that dynamically created vue components are destroyed. (#6948) 2023-08-17 23:24:02 +00:00
Khalid Adil
3f80b53ea6 [Tooltips] Fixes for dictionary objects and self-referential objects (#6916)
* Fix getTelemetryPath to handle cases where parent is the same as the child, handle yamcs aggregate telemetry, and fix how identifiers are passed in

* Cleanup getTelemetryPath

* Switch to filter instead of forEach

* Get path item names

* Remove tooltips on scroll of tree

* Remove handing for scroll

* Allow break-words

* Cleanup
2023-08-17 16:18:25 +00:00
Scott Bell
99a3e3fc32 Recent objects do not update when object names are changed (#6927)
* fix tree name issue

* add name to key, and name observers to recent objects

* no need to change key

* make more of app reactive to name changes

* fix browse bar and document title

* listen in properties for name changes

* add tests for renaming

* yeah spelling linter

* add semantic tags to forms and fixup tests

* change purpose

* actually delete the listener

* ensuring deletion

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-08-16 21:38:09 +00:00
Even Stensberg
2d92223e16 fix(package.json): add author (#6941)
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-08-16 14:10:24 -07:00
Jesse Mazzella
f21685e216 fix(e2e): Stabilize ITC tests (#6933)
* fix(ITC): initialize ITC in `created()` hook

* fix(e2e): stabilize ITC tests

* docs: add JSDocs

* refactor: lint:fix

* test(e2e): add comment and assertion

* refactor: lint:fix
2023-08-16 19:02:09 +00:00
Jesse Mazzella
6c92e31036 fix(#6942): Toggling FlexibleLayout toolbar options reflects immediately in the view (#6943)
* fix: restore reactivity of config settings

- move initialization steps to `created()` hook

- remove unnecessary `:key` binds

- fix comments

* refactor: clean up

* refactor: clean up

* refactor: lint:fix

* test(e2e): add regression test and cleanup suite

* refactor: consistency is key!

* test(fix): fix unit tests, further cleanup
2023-08-16 17:52:23 +00:00
Jesse Mazzella
82b1760b0e chore: bump version to 3.1.0-next and update docs (#6921)
* chore: bump version to `3.1.0-next`

* docs: update version.md

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-08-16 16:15:30 +00:00
Shefali Joshi
87feb0db34 Condition sets now provide the timeContext they're using when sending requests (#6929)
* Send in the timeContext for requests

* Fix failing test

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
2023-08-15 17:32:30 +00:00
Shefali Joshi
c53073b339 Fix remote clock subscription (#6919)
* If there are no listeners for this clock then don't bother subscribing after getting the domain object details

* Comment explaining the fix
2023-08-14 21:51:52 +00:00
Jesse Mazzella
57743e5918 fix: use loadDelay generator setting in subscriptions as well (#6918)
* refactor: use `getBounds()` instead of `bounds()`

* fix: use `loadDelay` in generator subscription

* refactor: fix up e2e test

* fix: remove `.only()`

* refactor: lint

* Start to fix up conditionSet test with comments

* test: edit conditionSet to add delay value

* test: tests the case where telemetry is available

* fix: remove `.only()`

* test: add comments, clarify assertion

* refactor: lint:fix

* test: fix conditionSet default condition name test

* test: add assertions to stabilize tags tests

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-08-14 19:04:01 +00:00
Jesse Mazzella
f3b819a786 chore: add vue3 to eslint, fix errors, and modify lint script (#6910) 2023-08-14 17:03:19 +00:00
John Hill
50694f600c Light refactor of visual tests (#5585) 2023-08-11 17:18:08 -07:00
Jesse Mazzella
10f3e13e4d chore: modify cspell config and fix all typos (#6908) 2023-08-10 16:20:16 +00:00
Scott Bell
9be9c5e28e Check for null in DuplicateAction (#6904)
check for null before checking before hasOwnProperty
2023-08-09 11:38:17 -07:00
Even Stensberg
58aeac94ac Feat(tooling): add cspell (#6892)
* feat(tooling): add cspell

* fix: pin dep

* ci(linting): add spelling
2023-08-09 08:34:45 -07:00
Jesse Mazzella
1e3097f54b chore: bump Playwright to 1.36.2 (#6901)
* chore: bump Playwright to `1.36.2`

* chore: remove `playwright/core` dependency

* chore: temporarily disable cacheing step

* chore: temp disable cacheing for e2e-couchdb run

* chore: restore cacheing step

* chore: remove `--prefer-offline` option
2023-08-08 10:44:16 -07:00
Shefali Joshi
6a9ff91d93 Dismiss the independent time conductor popup on unmount (#6859)
* Don't set conductor popup to null unless the view is being destroyed

* Replace beforeDestroy with beforeUnmount

* Propagate plot tick widths to timeline view

* Check if conductor popup exists before trying to remove it from the dom

* Fix imagery e2e test

* Revert accidental commit

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-08-07 23:19:13 +00:00
Charles Hacskaylo
accfbc96ab Fix Plan View duplicate scrollbars (#6865)
* Closes #6864
- CSS fixes to remove problematic duplicate overflow handling.

* fix(e2e): stabilize autoscale test

* fix(e2e): mark overlay plot tagging test as slow

* fix(e2e): stabilize ITC e2e test

* fix(e2e): don't use hard wait

* fix: remove .only 😳

---------

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-08-07 22:48:29 +00:00
Even Stensberg
9942bbbc0f chore: move indexTest to kebab-case (#6860) 2023-08-06 12:54:36 -07:00
John Hill
4287cd5413 [CI] Update docker login step to work across forks and non-nasa-users (#6891)
Continue on error
2023-08-05 15:05:40 -07:00
Scott Bell
ee6ca11558 Only load annotations in fixed time mode or frozen (#6866)
* fix annotations load to not happen on start

* remove range checking per annotated point

* back to local variable

* reduce tagging size to improve reliability of test

* remove .only

* remove .only

* reduce hz rate for functional test and keep high frequency test in performance

* remove console.debugs

* this test runs pretty fast now

* fix network request tests to match new behavior
2023-08-03 09:40:52 -07:00
Andrew Henry
676bb81eab Synchronize timers between multiple users (#6885)
* created a throttle util and using it in timer plugin to throttle refreshing the timer domain object

* Simplify timer logic

* Clarify code a little

* refactor: lint:fix

* Fix linting issue

---------

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-08-02 16:30:51 -07:00
Shefali Joshi
c6305697c0 Set the raw series limits so that we can get the raw series limits (#6877)
* Set the raw series limits so that we can get the raw series limits

* fix: `toRaw()` the other gets/sets/deletes

---------

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2023-08-02 09:50:03 -07:00
Jesse Mazzella
0421936874 fix: suppress deprecation warnings to once per unique args (#6875) 2023-08-02 09:11:41 -07:00
Jesse Mazzella
95e686038d fix: toggling markers, alarm markers, marker style + update Vue.extend() usage to Vue 3 (#6868)
* fix: update to `defineComponent` from `Vue.extend()`
* fix: unwrap Proxy arg before WeakMap.get()
* refactor: `defineComponent` not needed here
2023-08-01 14:07:59 -07:00
568 changed files with 10306 additions and 5343 deletions

View File

@@ -2,7 +2,7 @@ version: 2.1
executors:
pw-focal-development:
docker:
- image: mcr.microsoft.com/playwright:v1.32.3-focal
- image: mcr.microsoft.com/playwright:v1.36.2-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
@@ -162,7 +162,7 @@ jobs:
steps:
- build_and_install:
node-version: <<parameters.node-version>>
- run: npx playwright@1.32.3 install #Necessary for bare ubuntu machine
- run: npx playwright@1.36.2 install #Necessary for bare ubuntu machine
- run: |
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
@@ -197,7 +197,9 @@ jobs:
steps:
- build_and_install:
node-version: <<parameters.node-version>>
- run: npm run test:perf
- run: npm run test:perf:memory
- run: npm run test:perf:localhost
- run: npm run test:perf:contract
- store_test_results:
path: test-results/results.xml
- store_artifacts:
@@ -213,11 +215,13 @@ jobs:
parameters:
node-version:
type: string
suite:
type: string # ci or full
executor: pw-focal-development
steps:
- build_and_install:
node-version: <<parameters.node-version>>
- run: npm run test:e2e:visual
- run: npm run test:e2e:visual:<<parameters.suite>>
- store_test_results:
path: test-results/results.xml
- store_artifacts:
@@ -245,6 +249,8 @@ workflows:
- perf-test:
node-version: lts/hydrogen
- visual-test:
name: visual-test-ci
suite: ci
node-version: lts/hydrogen
the-nightly: #These jobs do not run on PRs, but against master at night
@@ -264,6 +270,8 @@ workflows:
- perf-test:
node-version: lts/hydrogen
- visual-test:
name: visual-test-nightly
suite: full
node-version: lts/hydrogen
- e2e-couchdb:
node-version: lts/hydrogen

503
.cspell.json Normal file
View File

@@ -0,0 +1,503 @@
{
"version": "0.2",
"language": "en,en-us",
"words": [
"gress",
"doctoc",
"minmax",
"openmct",
"datasources",
"recieved",
"evalute",
"Sinewave",
"deregistration",
"unregisters",
"configutation",
"configuation",
"codecov",
"carryforward",
"Chacon",
"Straub",
"OWASP",
"Testathon",
"exploratorily",
"Testathons",
"testathon",
"npmjs",
"publishj",
"treeitem",
"timespan",
"Timespan",
"spinbutton",
"popout",
"textbox",
"tablist",
"Telem",
"codecoverage",
"browserless",
"networkidle",
"nums",
"mgmt",
"faultname",
"gantt",
"sharded",
"perfromance",
"MMOC",
"deploysentinel",
"codegen",
"Unfortuantely",
"viewports",
"updatesnapshots",
"excercised",
"Circel",
"browsercontexts",
"miminum",
"testcase",
"testsuite",
"domcontentloaded",
"Tracefile",
"lcov",
"linecov",
"Browserless",
"webserver",
"yamcs",
"quickstart",
"subobject",
"autosize",
"Horz",
"vehicula",
"Praesent",
"pharetra",
"Duis",
"eget",
"arcu",
"elementum",
"mauris",
"Donec",
"nunc",
"quis",
"Proin",
"elit",
"Nunc",
"Aenean",
"mollis",
"hendrerit",
"Vestibulum",
"placerat",
"velit",
"augue",
"Quisque",
"mattis",
"lectus",
"rutrum",
"Fusce",
"tincidunt",
"nibh",
"blandit",
"urna",
"Nullam",
"congue",
"enim",
"Morbi",
"bibendum",
"Vivamus",
"imperdiet",
"Pellentesque",
"cursus",
"Aliquam",
"orci",
"Suspendisse",
"amet",
"justo",
"Etiam",
"vestibulum",
"ullamcorper",
"Cras",
"aliquet",
"Mauris",
"Nulla",
"scelerisque",
"viverra",
"metus",
"condimentum",
"varius",
"nulla",
"sapien",
"Curabitur",
"tristique",
"Nonsectetur",
"convallis",
"accumsan",
"lacus",
"posuere",
"turpis",
"egestas",
"feugiat",
"tortor",
"faucibus",
"euismod",
"pratices",
"pathing",
"pases",
"testcases",
"Noneditable",
"listitem",
"Gantt",
"timelist",
"timestrip",
"networkevents",
"fetchpriority",
"persistable",
"Persistable",
"persistability",
"Persistability",
"testdata",
"Testdata",
"metdata",
"timeconductor",
"contenteditable",
"autoscale",
"Autoscale",
"prepan",
"sinewave",
"cyanish",
"driv",
"searchbox",
"datetime",
"timeframe",
"recents",
"recentobjects",
"gsearch",
"Disp",
"Cloc",
"noselect",
"requestfailed",
"viewlarge",
"Imageurl",
"thumbstrip",
"checkmark",
"Unshelve",
"autosized",
"chacskaylo",
"numberfield",
"OPENMCT",
"Autoflow",
"Timelist",
"faultmanagement",
"GEOSPATIAL",
"geospatial",
"plotspatial",
"annnotation",
"keystrings",
"undelete",
"sometag",
"containee",
"composability",
"mutables",
"Mutables",
"composee",
"handleoutsideclick",
"Datetime",
"Perc",
"autodismiss",
"filetree",
"deeptailor",
"keystring",
"reindex",
"unlisten",
"symbolsfont",
"ellipsize",
"dismissable",
"TIMESYSTEM",
"Metadatas",
"stalenes",
"receieves",
"unsub",
"callbacktwo",
"unsubscribetwo",
"telem",
"Telemetery",
"unemitted",
"granually",
"timesystem",
"metadatas",
"iteratees",
"metadatum",
"printj",
"sprintf",
"unlisteners",
"amts",
"reregistered",
"hudsonfoo",
"onclone",
"autoflow",
"xdescribe",
"mockmct",
"Autoflowed",
"plotly",
"relayout",
"Plotly",
"Yaxis",
"showlegend",
"textposition",
"xaxis",
"automargin",
"fixedrange",
"yaxis",
"Axistype",
"showline",
"bglayer",
"autorange",
"hoverinfo",
"dotful",
"Dotful",
"cartesianlayer",
"scatterlayer",
"textfont",
"ampm",
"cdef",
"horz",
"STYLEABLE",
"styleable",
"afff",
"shdw",
"braintree",
"vals",
"Subobject",
"Shdw",
"Movebar",
"inspectable",
"Stringformatter",
"sclk",
"Objectpath",
"Keystring",
"duplicatable",
"composees",
"Composees",
"Composee",
"callthrough",
"objectpath",
"createable",
"noneditable",
"Classname",
"classname",
"selectedfaults",
"accum",
"newpersisted",
"Metadatum",
"MCWS",
"YAMCS",
"frameid",
"containerid",
"mmgis",
"PERC",
"curval",
"viewbox",
"mutablegauge",
"Flatbush",
"flatbush",
"Indicies",
"Marqueed",
"NSEW",
"nsew",
"vrover",
"gimbled",
"Pannable",
"unsynced",
"Unsynced",
"pannable",
"autoscroll",
"TIMESTRIP",
"TWENTYFOUR",
"FULLSIZE",
"intialize",
"Timestrip",
"spyon",
"Unlistener",
"multipane",
"DATESTRING",
"akhenry",
"Niklas",
"Hertzen",
"Kash",
"Nouroozi",
"Bostock",
"BOSTOCK",
"Arnout",
"Kazemier",
"Karolis",
"Narkevicius",
"Ashkenas",
"Madhavan",
"Iskren",
"Ivov",
"Chernev",
"Borshchov",
"painterro",
"sheetjs",
"Yuxi",
"ACITON",
"localstorage",
"Linkto",
"Painterro",
"Editability",
"filteredsnapshots",
"Fromimage",
"muliple",
"notebookstorage",
"Andpage",
"pixelize",
"Quickstart",
"indexhtml",
"youradminpassword",
"chttpd",
"sourcefiles",
"USERPASS",
"XPUT",
"adipiscing",
"eiusmod",
"tempor",
"incididunt",
"labore",
"dolore",
"aliqua",
"perspiciatis",
"iteree",
"submodels",
"symlog",
"Plottable",
"antisymlog",
"docstrings",
"webglcontextlost",
"gridlines",
"Xaxis",
"Crosshairs",
"telemetrylimit",
"xscale",
"yscale",
"untracks",
"swatched",
"NULLVALUE",
"unobserver",
"unsubscriber",
"drap",
"Averager",
"averager",
"movecolumnfromindex",
"callout",
"Konqueror",
"unmark",
"hitarea",
"Hitarea",
"Unmark",
"controlbar",
"reactified",
"perc",
"DHMS",
"timespans",
"timeframes",
"Timesystems",
"Hilite",
"datetimes",
"momentified",
"ucontents",
"TIMELIST",
"Timeframe",
"Guirk",
"resizeable",
"iframing",
"Btns",
"Ctrls",
"Chakra",
"Petch",
"propor",
"phoneandtablet",
"desktopandtablet",
"Imgs",
"UNICODES",
"datatable",
"csvg",
"cpath",
"cellipse",
"xlink",
"cstyle",
"bfill",
"ctitle",
"eicon",
"interactability",
"AFFORDANCES",
"affordance",
"scrollcontainer",
"Icomoon",
"icomoon",
"configurability",
"btns",
"AUTOFLOW",
"DATETIME",
"infobubble",
"thumbsbubble",
"codehilite",
"vscroll",
"bgsize",
"togglebutton",
"Hacskaylo",
"noie",
"fullscreen",
"horiz",
"menubutton",
"SNAPSHOTTING",
"snapshotting",
"PAINTERRO",
"ptro",
"PLOTLY",
"gridlayer",
"xtick",
"ytick",
"subobjects",
"Ucontents",
"Userand",
"Userbefore",
"brdr",
"pushs",
"ALPH",
"Recents",
"Qbert",
"Infobubble",
"haslink",
"VPID",
"vpid",
"updatedtest",
"KHTML",
"Chromezilla",
"Safarifox",
"deregistering",
"hundredtized",
"dhms",
"unthrottled",
"Codecov",
"dont",
"mediump",
"sinonjs",
"generatedata",
"grandsearch",
"websockets",
"swgs",
"memlab",
"devmode",
"blockquote",
"blockquotes",
"Blockquote",
"Blockquotes"
],
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US"],
"ignorePaths": [
"package.json",
"dist/**",
"package-lock.json",
"node_modules",
"coverage",
"*.log",
"html-test-results",
"test-results"
]
}

View File

@@ -9,13 +9,14 @@ module.exports = {
globals: {
_: 'readonly'
},
plugins: ['prettier'],
plugins: ['prettier', 'unicorn', 'simple-import-sort'],
extends: [
'eslint:recommended',
'plugin:compat/recommended',
'plugin:vue/recommended',
'plugin:vue/vue3-recommended',
'plugin:you-dont-need-lodash-underscore/compatible',
'plugin:prettier/recommended'
'plugin:prettier/recommended',
'plugin:no-unsanitized/DOM'
],
parser: 'vue-eslint-parser',
parserOptions: {
@@ -28,6 +29,10 @@ module.exports = {
}
},
rules: {
'simple-import-sort/imports': 'warn',
'simple-import-sort/exports': 'warn',
'vue/no-deprecated-dollar-listeners-api': 'warn',
'vue/no-deprecated-events-api': 'warn',
'vue/no-v-for-template-key': 'off',
'vue/no-v-for-template-key-on-child': 'error',
'prettier/prettier': 'error',
@@ -141,18 +146,26 @@ module.exports = {
'no-implicit-coercion': 'error',
//https://eslint.org/docs/rules/no-unneeded-ternary
'no-unneeded-ternary': 'error',
'unicorn/filename-case': [
'error',
{
cases: {
pascalCase: true
},
ignore: ['^.*\\.js$']
}
],
'vue/first-attribute-linebreak': 'error',
'vue/multiline-html-element-content-newline': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/multi-word-component-names': 'off', // TODO enable, align with conventions
'vue/no-mutating-props': 'off'
'vue/no-mutating-props': 'off' // TODO: Remove this rule and fix resulting errors
},
overrides: [
{
files: LEGACY_FILES,
rules: {
'no-unused-vars': [
'warn',
'error',
{
vars: 'all',
args: 'none',

23
.github/release.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
changelog:
categories:
- title: 🏕 Features
labels:
- type:feature
- title: 🎉 Enhancements
labels:
- type:enhancement
exclude:
labels:
- type:feature
- title: 🔧 Maintenance
labels:
- type:maintenance
- title: ⚡ Performance
labels:
- performance
- title: 👒 Dependencies
labels:
- dependencies
- title: 🐛 Bug Fixes
labels:
- '*'

View File

@@ -28,15 +28,16 @@ jobs:
restore-keys: |
${{ runner.os }}-node-
- run: npm install --cache ~/.npm --prefer-offline --no-audit --progress=false
- run: npm install --cache ~/.npm --no-audit --progress=false
- name: Login to DockerHub
uses: docker/login-action@v2
uses: docker/login-action@v3
continue-on-error: true
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- run: npx playwright@1.32.3 install
- run: npx playwright@1.36.2 install
- name: Start CouchDB Docker Container and Init with Setup Scripts
run: |

View File

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

View File

@@ -26,11 +26,11 @@ jobs:
- lts/hydrogen
architecture:
- x64
name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Setup node
uses: actions/setup-node@v3
with:
@@ -45,12 +45,12 @@ jobs:
restore-keys: |
${{ runner.os }}-${{ matrix.node_version }}-
- run: npm install --cache ~/.npm --prefer-offline --no-audit --progress=false
- run: npm install --cache ~/.npm --no-audit --progress=false
- run: npm test
- run: npm run lint -- --quiet
- name: Remove pr:platform label (if present)
if: always()
uses: actions/github-script@v6

5
.npmrc
View File

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

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
lts/*

View File

@@ -33,6 +33,16 @@ const projectRootDir = path.resolve(__dirname, '..');
/** @type {import('webpack').Configuration} */
const config = {
context: projectRootDir,
devServer: {
client: {
progress: true,
overlay: {
// Disable overlay for runtime errors.
// See: https://github.com/webpack/webpack-dev-server/issues/4771
runtimeErrors: false
}
}
},
entry: {
openmct: './openmct.js',
generatorWorker: './example/generator/generatorWorker.js',
@@ -67,8 +77,7 @@ const config = {
MCT: path.join(projectRootDir, 'src/MCT'),
testUtils: path.join(projectRootDir, 'src/utils/testUtils.js'),
objectUtils: path.join(projectRootDir, 'src/api/objects/object-utils.js'),
utils: path.join(projectRootDir, 'src/utils'),
vue: path.join(projectRootDir, 'node_modules/@vue/compat/dist/vue.esm-bundler.js'),
utils: path.join(projectRootDir, 'src/utils')
}
},
plugins: [
@@ -76,7 +85,9 @@ const config = {
__OPENMCT_VERSION__: `'${packageDefinition.version}'`,
__OPENMCT_BUILD_DATE__: `'${new Date()}'`,
__OPENMCT_REVISION__: `'${gitRevision}'`,
__OPENMCT_BUILD_BRANCH__: `'${gitBranch}'`
__OPENMCT_BUILD_BRANCH__: `'${gitBranch}'`,
__VUE_OPTIONS_API__: true, // enable/disable Options API support, default: true
__VUE_PROD_DEVTOOLS__: false // enable/disable devtools support in production, default: false
}),
new VueLoaderPlugin(),
new CopyWebpackPlugin({
@@ -100,6 +111,12 @@ const config = {
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[name].css'
}),
// Add a UTF-8 BOM to CSS output to avoid random mojibake
new webpack.BannerPlugin({
test: /.*Theme\.css$/,
raw: true,
banner: '@charset "UTF-8";'
})
],
module: {
@@ -125,10 +142,8 @@ const config = {
loader: 'vue-loader',
options: {
compilerOptions: {
whitespace: 'preserve',
compatConfig: {
MODE: 2
}
hoistStatic: false,
whitespace: 'preserve'
}
}
},

View File

@@ -45,14 +45,6 @@ module.exports = merge(common, {
directory: path.join(__dirname, '..', '/dist'),
publicPath: '/dist',
watch: false
},
client: {
progress: true,
overlay: {
// Disable overlay for runtime errors.
// See: https://github.com/webpack/webpack-dev-server/issues/4771
runtimeErrors: false
}
}
}
});

176
API.md
View File

@@ -44,8 +44,10 @@
- [Clocks](#clocks)
- [Defining and registering clocks](#defining-and-registering-clocks)
- [Getting and setting active clock](#getting-and-setting-active-clock)
- [Stopping an active clock](#stopping-an-active-clock)
- [⚠️ \[DEPRECATED\] Stopping an active clock](#-deprecated-stopping-an-active-clock)
- [Clock Offsets](#clock-offsets)
- [Time Modes](#time-modes)
- [Time Mode Helper Methods](#time-mode-helper-methods)
- [Time Events](#time-events)
- [List of Time Events](#list-of-time-events)
- [The Time Conductor](#the-time-conductor)
@@ -685,8 +687,9 @@ state of the application, and emits events to inform listeners when the state ch
Because the data displayed tends to be time domain data, Open MCT must always
have at least one time system installed and activated. When you download Open
MCT, it will be pre-configured to use the UTC time system, which is installed and activated, along with other default plugins, in `index.html`. Installing and activating a time system is simple, and is covered
[in the next section](#defining-and-registering-time-systems).
MCT, it will be pre-configured to use the UTC time system, which is installed and activated,
along with other default plugins, in `index.html`. Installing and activating a time system
is simple, and is covered [in the next section](#defining-and-registering-time-systems).
### Time Systems and Bounds
@@ -737,28 +740,38 @@ numbers in UTC terrestrial time.
#### Getting and Setting the Active Time System
Once registered, a time system can be activated by calling `timeSystem` with
the timeSystem `key` or an instance of the time system. If you are not using a
[clock](#clocks), you must also specify valid [bounds](#time-bounds) for the
timeSystem.
Once registered, a time system can be activated by calling `setTimeSystem` with
the timeSystem `key` or an instance of the time system. You can also specify
valid [bounds](#time-bounds) for the timeSystem.
```javascript
openmct.time.timeSystem('utc', bounds);
openmct.time.setTimeSystem('utc', bounds);
```
The current time system can be retrieved as well by calling `getTimeSystem`.
```javascript
openmct.time.getTimeSystem();
```
A time system can be immediately activated after registration:
```javascript
openmct.time.addTimeSystem(utcTimeSystem);
openmct.time.timeSystem(utcTimeSystem, bounds);
openmct.time.setTimeSystem(utcTimeSystem, bounds);
```
Setting the active time system will trigger a [`'timeSystem'`](#time-events)
event. If you supplied bounds, a [`'bounds'`](#time-events) event will be triggered afterwards with your newly supplied bounds.
Setting the active time system will trigger a [`'timeSystemChanged'`](#time-events)
event. If you supplied bounds, a [`'boundsChanged'`](#time-events) event will be triggered afterwards with your newly supplied bounds.
> ⚠️ **Deprecated**
> - The method `timeSystem()` is deprecated. Please use `getTimeSystem()` and `setTimeSystem()` as a replacement.
#### Time Bounds
The TimeAPI provides a getter/setter for querying and setting time bounds. Time
The TimeAPI provides a getter and setter for querying and setting time bounds. Time
bounds are simply an object with a `start` and an end `end` attribute.
- `start`: A `number` representing a moment in time in the active [Time System](#defining-and-registering-time-systems).
@@ -768,26 +781,34 @@ telemetry views.
This will be used as the end of the time period displayed by time-responsive
telemetry views.
If invoked with bounds, it will set the new time bounds system-wide. If invoked
without any parameters, it will return the current application-wide time bounds.
New bounds can be set system wide by calling `setBounds` with [bounds](#time-bounds).
``` javascript
const ONE_HOUR = 60 * 60 * 1000;
let now = Date.now();
openmct.time.bounds({start: now - ONE_HOUR, now);
openmct.time.setBounds({start: now - ONE_HOUR, now);
```
To respond to bounds change events, listen for the [`'bounds'`](#time-events)
Calling `getBounds` will return the current application-wide time bounds.
``` javascript
openmct.time.getBounds();
```
To respond to bounds change events, listen for the [`'boundsChanged'`](#time-events)
event.
> ⚠️ **Deprecated**
> - The method `bounds()` is deprecated and will be removed in a future release. Please use `getBounds()` and `setBounds()` as a replacement.
### Clocks
The Time API can be set to follow a clock source which will cause the bounds
to be updated automatically whenever the clock source "ticks". A clock is simply
an object that supports registration of listeners and periodically invokes its
listeners with a number. Open MCT supports registration of new clock sources that
tick on almost anything. A tick occurs when the clock invokes callback functions
registered by its listeners with a new time value.
The Time API requires a clock source which will cause the bounds to be updated
automatically whenever the clock source "ticks". A clock is simply an object that
supports registration of listeners and periodically invokes its listeners with a
number. Open MCT supports registration of new clock sources that tick on almost
anything. A tick occurs when the clock invokes callback functions registered by its
listeners with a new time value.
An example of a clock source is the [LocalClock](https://github.com/nasa/openmct/blob/master/src/plugins/utcTimeSystem/LocalClock.js)
which emits the current time in UTC every 100ms. Clocks can tick on anything. For
@@ -855,23 +876,29 @@ An example clock implementation is provided in the form of the [LocalClock](http
#### Getting and setting active clock
Once registered a clock can be activated by calling the `clock` function on the
Once registered a clock can be activated by calling the `setClock` function on the
Time API passing in the key or instance of a registered clock. Only one clock
may be active at once, so activating a clock will deactivate any currently
active clock. [`clockOffsets`](#clock-offsets) must be specified when changing a clock.
active clock and start the new clock. [`clockOffsets`](#clock-offsets) must be specified when changing a clock.
Setting the clock triggers a [`'clock'`](#time-events) event, followed by a [`'clockOffsets'`](#time-events) event, and then a [`'bounds'`](#time-events) event as the offsets are applied to the clock's currentValue().
Setting the clock triggers a [`'clockChanged'`](#time-events) event, followed by a [`'clockOffsetsChanged'`](#time-events) event, and then a [`'boundsChanged'`](#time-events) event as the offsets are applied to the clock's currentValue().
```
openmct.time.clock(someClock, clockOffsets);
openmct.time.setClock(someClock, clockOffsets);
```
Upon being activated, the time API will listen for tick events on the clock by calling `clock.on`.
The currently active clock (if any) can be retrieved by calling the same
function without any arguments.
The currently active clock can be retrieved by calling `getClock`.
#### Stopping an active clock
```
openmct.time.getClock();
```
> ⚠️ **Deprecated**
> - The method `clock()` is deprecated and will be removed in a future release. Please use `getClock()` and `setClock()` as a replacement.
#### ⚠️ [DEPRECATED] Stopping an active clock
_As of July 2023, this method will be deprecated. Open MCT will always have a ticking clock._
@@ -882,12 +909,14 @@ will stop the clock from ticking, and set the active clock to `undefined`.
openmct.time.stopClock();
```
> ⚠️ **Deprecated**
> - The method `stopClock()` is deprecated and will be removed in a future release.
#### Clock Offsets
When a clock is active, the time bounds of the application will be updated
automatically each time the clock "ticks". The bounds are calculated based on
the current value provided by the active clock (via its `tick` event, or its
`currentValue()` method).
When in Real-time [mode](#time-modes), the time bounds of the application will be updated automatically each time the
clock "ticks". The bounds are calculated based on the current value provided by
the active clock (via its `tick` event, or its `currentValue()` method).
Unlike bounds, which represent absolute time values, clock offsets represent
relative time spans. Offsets are defined as an object with two properties:
@@ -898,21 +927,77 @@ value provided by a clock's tick callback, or its `currentValue()` function.
- `end`: A `number` that must be >= 0 and which is used to calculate the end
bounds on each clock tick.
The `clockOffsets` function can be used to get or set clock offsets. For example,
The `setClockOffsets` function can be used to get or set clock offsets. For example,
to show the last fifteen minutes in a ms-based time system:
```javascript
var FIFTEEN_MINUTES = 15 * 60 * 1000;
openmct.time.clockOffsets({
openmct.time.setClockOffsets({
start: -FIFTEEN_MINUTES,
end: 0
})
```
The `getClockOffsets` method will return the currently set clock offsets.
```javascript
openmct.time.getClockOffsets()
```
**Note:** Setting the clock offsets will trigger an immediate bounds change, as
new bounds will be calculated based on the `currentValue()` of the active clock.
Clock offsets are only relevant when a clock source is active.
Clock offsets are only relevant when in Real-time [mode](#time-modes).
> ⚠️ **Deprecated**
> - The method `clockOffsets()` is deprecated and will be removed in a future release. Please use `getClockOffsets()` and `setClockOffsets()` as a replacement.
### Time Modes
There are two time modes in Open MCT, "Fixed" and "Real-time". In Real-time mode the
time bounds of the application will be updated automatically each time the clock "ticks".
The bounds are calculated based on the current value provided by the active clock. In
Fixed mode, the time bounds are set for a specified time range. When Open MCT is first
initialized, it will be in Real-time mode.
The `setMode` method can be used to set the current time mode. It accepts a mode argument,
`'realtime'` or `'fixed'` and it also accepts an optional [offsets](#clock-offsets)/[bounds](#time-bounds) argument dependent
on the current mode.
``` javascript
openmct.time.setMode('fixed');
openmct.time.setMode('fixed', bounds); // with optional bounds
```
or
``` javascript
openmct.time.setMode('realtime');
openmct.time.setMode('realtime', offsets); // with optional offsets
```
The `getMode` method will return the current time mode, either `'realtime'` or `'fixed'`.
``` javascript
openmct.time.getMode();
```
#### Time Mode Helper Methods
There are two methods available to determine the current time mode in Open MCT programmatically,
`isRealTime` and `isFixed`. Each one will return a boolean value based on the current mode.
``` javascript
if (openmct.time.isRealTime()) {
// do real-time stuff
}
```
``` javascript
if (openmct.time.isFixed()) {
// do fixed-time stuff
}
```
### Time Events
@@ -921,7 +1006,7 @@ The Time API is a standard event emitter; you can register callbacks for events
For example:
``` javascript
openmct.time.on('bounds', function callback (newBounds, tick) {
openmct.time.on('boundsChanged', function callback (newBounds, tick) {
// Do something with new bounds
});
```
@@ -930,7 +1015,7 @@ openmct.time.on('bounds', function callback (newBounds, tick) {
The events emitted by the Time API are:
- `bounds`: emitted whenever the bounds change. The callback will be invoked
- `boundsChanged`: emitted whenever the bounds change. The callback will be invoked
with two arguments:
- `bounds`: A [bounds](#getting-and-setting-bounds) bounds object
representing a new time period bound by the specified start and send times.
@@ -945,15 +1030,24 @@ The events emitted by the Time API are:
If `tick` is false,then the bounds change was not due to an automatic tick,
and a query for historical data may be necessary, depending on your data
caching strategy, and how significantly the start bound has changed.
- `timeSystem`: emitted whenever the active time system changes. The callback will be invoked with a single argument:
- `timeSystemChanged`: emitted whenever the active time system changes. The callback will be invoked with a single argument:
- `timeSystem`: The newly active [time system](#defining-and-registering-time-systems).
- `clock`: emitted whenever the clock changes. The callback will be invoked
- `clockChanged`: emitted whenever the clock changes. The callback will be invoked
with a single argument:
- `clock`: The newly active [clock](#clocks), or `undefined` if an active
clock has been deactivated.
- `clockOffsets`: emitted whenever the active clock offsets change. The
- `clockOffsetsChanged`: emitted whenever the active clock offsets change. The
callback will be invoked with a single argument:
- `clockOffsets`: The new [clock offsets](#clock-offsets).
- `modeChanged`: emitted whenever the time [mode](#time-modes) changed. The callback will
be invoked with one argument:
- `mode`: A string representation of the current time mode, either `'realtime'` or `'fixed'`.
> ⚠️ **Deprecated Events** (These will be removed in a future release):
> - `bounds` → `boundsChanged`
> - `timeSystem` → `timeSystemChanged`
> - `clock` → `clockChanged`
> - `clockOffsets` → `clockOffsetsChanged`
### The Time Conductor

View File

@@ -2,7 +2,9 @@
Open MCT (Open Mission Control Technologies) is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data.
Please visit our [Official Site](https://nasa.github.io/openmct/) and [Getting Started Guide](https://nasa.github.io/openmct/getting-started/)
> [!NOTE]
> Please visit our [Official Site](https://nasa.github.io/openmct/) and [Getting Started Guide](https://nasa.github.io/openmct/getting-started/)
Once you've created something amazing with Open MCT, showcase your work in our GitHub Discussions [Show and Tell](https://github.com/nasa/openmct/discussions/categories/show-and-tell) section. We love seeing unique and wonderful implementations of Open MCT!
@@ -14,19 +16,32 @@ Once you've created something amazing with Open MCT, showcase your work in our G
Building and running Open MCT in your local dev environment is very easy. Be sure you have [Git](https://git-scm.com/downloads) and [Node.js](https://nodejs.org/) installed, then follow the directions below. Need additional information? Check out the [Getting Started](https://nasa.github.io/openmct/getting-started/) page on our website.
(These instructions assume you are installing as a non-root user; developers have [reported issues](https://github.com/nasa/openmct/issues/1151) running these steps with root privileges.)
1. Clone the source code
1. Clone the source code:
`git clone https://github.com/nasa/openmct.git`
```
git clone https://github.com/nasa/openmct.git
```
2. Install development dependencies. Note: Check the package.json engine for our tested and supported node versions.
2. (Optional) Install the correct node version using [nvm](https://github.com/nvm-sh/nvm):
`npm install`
```
nvm install
```
3. Run a local development server
3. Install development dependencies (Note: Check the `package.json` engine for our tested and supported node versions):
`npm start`
```
npm install
```
Open MCT is now running, and can be accessed by pointing a web browser at [http://localhost:8080/](http://localhost:8080/)
4. Run a local development server:
```
npm start
```
> [!IMPORTANT]
> Open MCT is now running, and can be accessed by pointing a web browser at [http://localhost:8080/](http://localhost:8080/)
Open MCT is built using [`npm`](http://npmjs.com/) and [`webpack`](https://webpack.js.org/).
@@ -40,8 +55,12 @@ The clearest examples for developing Open MCT plugins are in the
[tutorials](https://github.com/nasa/openmct-tutorial) provided in
our documentation.
We want Open MCT to be as easy to use, install, run, and develop for as
possible, and your feedback will help us get there! Feedback can be provided via [GitHub issues](https://github.com/nasa/openmct/issues/new/choose), [Starting a GitHub Discussion](https://github.com/nasa/openmct/discussions), or by emailing us at [arc-dl-openmct@mail.nasa.gov](mailto:arc-dl-openmct@mail.nasa.gov).
> [!NOTE]
> We want Open MCT to be as easy to use, install, run, and develop for as
> possible, and your feedback will help us get there!
> Feedback can be provided via [GitHub issues](https://github.com/nasa/openmct/issues/new/choose),
> [Starting a GitHub Discussion](https://github.com/nasa/openmct/discussions),
> or by emailing us at [arc-dl-openmct@mail.nasa.gov](mailto:arc-dl-openmct@mail.nasa.gov).
## Developing Applications With Open MCT
@@ -51,6 +70,8 @@ For more on developing with Open MCT, see our documentation for a guide on [Deve
This is a fast moving project and we do our best to test and support the widest possible range of browsers, operating systems, and nodejs APIs. We have a published list of support available in our package.json's `browserslist` key.
The project uses `nvm` to ensure the node and npm version used, is coherent in all projects. Install nvm (non-windows), [here](https://github.com/nvm-sh/nvm) or the windows equivalent [here](https://github.com/coreybutler/nvm-windows)
If you encounter an issue with a particular browser, OS, or nodejs API, please file a [GitHub issue](https://github.com/nasa/openmct/issues/new/choose)
## Plugins
@@ -95,10 +116,10 @@ To run the performance tests:
`npm run test:perf`
The test suite is configured to all tests localed in `e2e/tests/` ending in `*.e2e.spec.js`. For more about the e2e test suite, please see the [README](./e2e/README.md)
The test suite is configured to all tests located in `e2e/tests/` ending in `*.e2e.spec.js`. For more about the e2e test suite, please see the [README](./e2e/README.md)
### Security Tests
Each commit is analyzed for known security vulnerabilities using [CodeQL](https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-javascript/). The list of CWE coverage items is avaiable in the [CodeQL docs](https://codeql.github.com/codeql-query-help/javascript-cwe/). The CodeQL workflow is specified in the [CodeQL analysis file](./.github/workflows/codeql-analysis.yml) and the custom [CodeQL config](./.github/codeql/codeql-config.yml).
Each commit is analyzed for known security vulnerabilities using [CodeQL](https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-javascript/). The list of CWE coverage items is available in the [CodeQL docs](https://codeql.github.com/codeql-query-help/javascript-cwe/). The CodeQL workflow is specified in the [CodeQL analysis file](./.github/workflows/codeql-analysis.yml) and the custom [CodeQL config](./.github/codeql/codeql-config.yml).
### Test Reporting and Code Coverage

View File

@@ -53,7 +53,7 @@ requirements.
Additionally, the following project-specific standards will be used:
* During development, a "-SNAPSHOT" suffix shall be appended to the
* During development, a "-next" suffix shall be appended to the
version number. The version number before the suffix shall reflect
the next expected version number for release.
* Prior to a 1.0.0 release, the _minor_ version will be incremented
@@ -93,7 +93,7 @@ numbers by the following process:
1. Update version number in `package.json`
1. Checkout branch created for the last sprint that has been successfully tested.
2. Remove a `-SNAPSHOT` suffix from the version in `package.json`.
2. Remove a `-next` suffix from the version in `package.json`.
3. Verify that resulting version number meets semantic versioning
requirements relative to previous stable version. Increment the
version number if necessary.
@@ -138,7 +138,7 @@ numbers by the following process:
1. Create a new branch off the `master` branch.
2. Remove any suffix from the version number,
or increment the _patch_ version if there is no suffix.
3. Append a `-SNAPSHOT` suffix.
3. Append a `-next` suffix.
4. Commit changes to `package.json` on the `master` branch.
The commit message should reference the sprint being opened,
preferably by a URL reference to the associated Milestone in
@@ -150,6 +150,6 @@ numbers by the following process:
Projects dependent on Open MCT being co-developed by the Open MCT
team should follow a similar process, except that they should
additionally update their dependency on Open MCT to point to the
latest archive when removing their `-SNAPSHOT` status, and
latest archive when removing their `-next` status, and
that they should be pointed back to the `master` branch after
this has completed.

24
e2e/.percy.ci.yml Normal file
View File

@@ -0,0 +1,24 @@
version: 2
snapshot:
widths: [1024]
min-height: 1440 # px
percyCSS: |
/* Clock indicator... your days are numbered */
.t-indicator-clock > .label {
opacity: 0 !important;
}
.c-input--datetime {
opacity: 0 !important;
}
/* Timer object text */
.c-ne__time-and-creator {
opacity: 0 !important;
}
/* Time Conductor ticks */
div.c-conductor-axis.c-conductor__ticks > svg {
opacity: 0 !important;
}
/* Embedded timestamp in notebooks */
.c-ne__embed__time{
opacity: 0 !important;
}

24
e2e/.percy.nightly.yml Normal file
View File

@@ -0,0 +1,24 @@
version: 2
snapshot:
widths: [1024, 2000]
min-height: 1440 # px
percyCSS: |
/* Clock indicator... your days are numbered */
.t-indicator-clock > .label {
opacity: 0 !important;
}
.c-input--datetime {
opacity: 0 !important;
}
/* Timer object text */
.c-ne__time-and-creator {
opacity: 0 !important;
}
/* Time Conductor ticks */
div.c-conductor-axis.c-conductor__ticks > svg {
opacity: 0 !important;
}
/* Embedded timestamp in notebooks */
.c-ne__embed__time{
opacity: 0 !important;
}

View File

@@ -1,6 +0,0 @@
version: 2
snapshot:
widths: [1024, 2000]
min-height: 1440 # px
discovery:
concurrency: 2 # https://github.com/percy/cli/discussions/1067

View File

@@ -72,19 +72,30 @@ Visual Testing is an essential part of our e2e strategy as it ensures that the a
For a better understanding of the visual issues which affect Open MCT, please see our bug tracker with the `label:visual` filter applied [here](https://github.com/nasa/openmct/issues?q=label%3Abug%3Avisual+)
To read about how to write a good visual test, please see [How to write a great Visual Test](#how-to-write-a-great-visual-test).
`npm run test:e2e:visual` will run all of the visual tests against a local instance of Open MCT. If no `PERCY_TOKEN` API key is found in the terminal or command line environment variables, no visual comparisons will be made.
`npm run test:e2e:visual` commands will run all of the visual tests against a local instance of Open MCT. If no `PERCY_TOKEN` API key is found in the terminal or command line environment variables, no visual comparisons will be made.
- `npm run test:e2e:visual:ci` will run against every commit and PR.
- `npm run test:e2e:visual: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)
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).
### (Advanced) Snapshot Testing
At present, we are using percy with two configuration files: `./e2e/.percy.nightly.yml` and `./e2e/.percy.ci.yml`. This is mainly to reduce the number of snapshots.
Snapshot testing is very similar to visual testing but allows us to be more precise in detecting change without relying on a 3rd party service. Unfortuantely, this precision requires advanced test setup and teardown and so we're using this pattern as a last resort.
### Advanced: Snapshot Testing (Not Recommended)
To give an example, if a _single_ visual test assertion for an Overlay plot is run through multiple DOM rendering engines at various viewports to see how the Plot looks. If that same test were run as a snapshot test, it could only be executed against a single browser, on a single platform (ubuntu docker container).
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.
Read more about [Playwright Snapshots](https://playwright.dev/docs/test-snapshots)
#### Open MCT's implementation
@@ -123,17 +134,17 @@ npm run test:e2e:updatesnapshots
## Performance Testing
The open source performance tests function mostly as a contract for the locator logic, functionality, and assumptions will work in our downstream, closed source test suites.
The open source performance tests function in three ways which match their naming and folder structure:
They're found under `./e2e/tests/performance` and are to be executed with the following npm script:
`npm run test:perf`
`./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.
These tests are expected to become blocking and gating with assertions as we extend the capabilities of Playwright.
## Test Architecture and CI
### Architecture (TODO)
### Architecture
### File Structure
@@ -147,8 +158,11 @@ Our file structure follows the type of type of testing being excercised at the e
|`./tests/functional/example/` | Tests which specifically verify the example plugins (e.g.: Sine Wave Generator).|
|`./tests/functional/plugins/` | Tests which loosely test each plugin. This folder is the most likely to change. Note: some `@snapshot` tests are still contained within this structure.|
|`./tests/framework/` | Tests which verify that our testing framework's functionality and assumptions will continue to work based on further refactoring or Playwright version changes (e.g.: verifying custom fixtures and appActions).|
|`./tests/performance/` | Performance tests.|
|`./tests/performance/` | Performance tests which should be run on every commit.|
|`./tests/performance/contract/` | A subset of performance tests which are designed to provide a contract between the open source tests which are run on every commit and the downstream tests which are run post merge and with other frameworks.|
|`./tests/performance/memory` | A subset of performance tests which are designed to test for memory leaks.|
|`./tests/visual/` | Visual tests.|
|`./tests/visual/component/` | Visual tests which are only run against a single component.|
|`./appActions.js` | Contains common methods which can be leveraged by test case authors to quickly move through the application when writing new tests.|
|`./baseFixture.js` | Contains base fixtures which only extend default `@playwright/test` functionality. The expectation is that these fixtures will be removed as the native Playwright API improves|
@@ -165,6 +179,7 @@ Open MCT is leveraging the [config file](https://playwright.dev/docs/test-config
|`./playwright-ci.config.js` | Used when running in CI or to debug CI issues locally|
|`./playwright-local.config.js` | Used when running locally|
|`./playwright-performance.config.js` | Used when running performance tests in CI or locally|
|`./playwright-performance-devmode.config.js` | Used when running performance tests in CI or locally|
|`./playwright-visual.config.js` | Used to run the visual tests in CI or locally|
#### Test Tags
@@ -182,6 +197,7 @@ Current list of test tags:
|`@snapshot` | Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of the playwright container.|
|`@unstable` | A new test or test which is known to be flaky.|
|`@2p` | Indicates that multiple users are involved, or multiple tabs/pages are used. Useful for testing multi-user interactivity.|
|`@generatedata` | Indicates that a test is used to generate testdata or test the generated test data. Usually to be associated with localstorage, but this may grow over time.|
### Continuous Integration
@@ -200,6 +216,7 @@ CircleCI
- Stable e2e tests against ubuntu and chrome
- Performance tests against ubuntu and chrome
- e2e tests are linted
- Visual tests are run in a single resolution on the default `espresso` theme
#### 2. Per-Merge Testing
@@ -207,18 +224,19 @@ Github Actions / Workflow
- Full suite against all browsers/projects. Triggered with Github Label Event 'pr:e2e'
- CouchDB Tests. Triggered on PR Create and again with Github Label Event 'pr:e2e:couchdb'
- Visual Tests. Triggered with Github Label Event 'pr:visual'
#### 3. Scheduled / Batch Testing
Nightly Testing in Circle CI
- Full e2e suite against ubuntu and chrome
- Full e2e suite against ubuntu and chrome, firefox, and an MMOC resolution profile
- Performance tests against ubuntu and chrome
- CouchDB suite
- Visual Tests are run in the full profile
Github Actions / Workflow
- Visual Test baseline generation.
- None at the moment
#### Parallelism and Fast Feedback
@@ -250,7 +268,7 @@ A testcase and testsuite are to be unmarked as @unstable when:
#### **What's supported:**
We are leveraging the `browserslist` project to declare our supported list of browsers.
We are leveraging the `browserslist` project to declare our supported list of browsers. We support macOS, Windows, and ubuntu 20+.
#### **Where it's tested:**
@@ -264,11 +282,17 @@ We also have the need to execute our e2e tests across this published list of bro
- A stable version of Chromium from the official chromium channels. This is always at least 1 version ahead of desktop chrome.
- `playwright-chrome`
- The stable channel of Chrome from the official chrome channels. This is always 2 versions behind chromium.
- `playwright-firefox`
- Firefox Latest Stable. Modified slightly by the playwright team to support a CDP Shim.
In terms of operating system testing, we're only limited by what the CI providers are able to support. The bulk of our testing is performed on the official playwright container which is based on ubuntu. Github Actions allows us to use `windows-latest` and `mac-latest` and is run as needed.
#### **Mobile**
We have the Mission-need to support iPad. To run our iPad suite, please see our `playwright-*.config.js` with the 'iPad' project.
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.
#### **Skipping or executing tests based on browser, os, and/os browser version:**
Conditionally skipping tests based on browser (**RECOMMENDED**):
@@ -295,14 +319,27 @@ Skipping based on browser version (Rarely used): <https://github.com/microsoft/p
## Test Design, Best Practices, and Tips & Tricks
### Test Design (TODO)
### Test Design
- How to make tests robust to function in other contexts (VISTA, VIPER, etc.)
- Leverage the use of `appActions.js` methods such as `createDomainObjectWithDefaults()`
- How to make tests faster and more resilient
- When possible, navigate directly by URL:
#### Test as the User
```javascript
In general, strive to test only through the UI as a user would. As stated in the [Playwright Best Practices](https://playwright.dev/docs/best-practices#test-user-visible-behavior):
> "Automated tests should verify that the application code works for the end users, and avoid relying on implementation details such as things which users will not typically use, see, or even know about such as the name of a function, whether something is an array, or the CSS class of some element. The end user will see or interact with what is rendered on the page, so your test should typically only see/interact with the same rendered output."
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.
1. Avoid creating locator aliases. This likely means that you're compensating for a bad locator. Improve the application instead.
1. Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });` instead of `{ waitUntil: 'networkidle' }`. Tests run against deployments with websockets often have issues with the networkidle detection.
#### How to make tests faster and more resilient
1. Avoid app interaction when possible. The best way of doing this is to navigate directly by URL:
```js
// You can capture the CreatedObjectInfo returned from this appAction:
const clock = await createDomainObjectWithDefaults(page, { type: 'Clock' });
@@ -310,12 +347,14 @@ Skipping based on browser version (Rarely used): <https://github.com/microsoft/p
await page.goto(clock.url);
```
- Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });`
1. Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });`
- Initial navigation should _almost_ always use the `{ waitUntil: 'domcontentloaded' }` option.
- Avoid repeated setup to test to test a single assertion. Write longer tests with multiple soft assertions.
1. Avoid repeated setup to test a single assertion. Write longer tests with multiple soft assertions.
This ensures that your changes will be picked up with large refactors.
### How to write a great test (WIP)
### How to write a great test
- Avoid using css locators to find elements to the page. Use modern web accessible locators like `getByRole`
- Use our [App Actions](./appActions.js) for performing common actions whenever applicable.
- Use `waitForPlotsToRender()` before asserting against anything that is dependent upon plot series data being loaded and drawn.
- If you create an object outside of using the `createDomainObjectWithDefaults` App Action, make sure to fill in the 'Notes' section of your object with `page.testNotes`:
@@ -328,7 +367,39 @@ Skipping based on browser version (Rarely used): <https://github.com/microsoft/p
await notesInput.fill(testNotes);
```
#### How to write a great visual test (TODO)
#### 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.
2. **Get the App into Interesting States**: Prioritize getting Open MCT into unusual layouts or behaviors before capturing a visual snapshot. For instance, you could open a dropdown menu.
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.
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/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})`);
//<Click on object>
await percySnapshot(page, `object expanded (theme: ${theme})`);
//Select from object
await percySnapshot(page, `object selected (theme: ${theme})`)
```
#### How to write a great network test
@@ -345,12 +416,35 @@ For now, our best practices exist as self-tested, living documentation in our [e
For best practices with regards to mocking network responses, see our [couchdb.e2e.spec.js](./tests/functional/couchdb.e2e.spec.js) file.
### Tips & Tricks (TODO)
### Tips & Tricks
The following contains a list of tips and tricks which don't exactly fit into a FAQ or Best Practices doc.
- (Advanced) Overriding the Browser's Clock
It is possible to override the browser's clock in order to control time-based elements. Since this can cause unwanted behavior (i.e. Tree not rendering), only use this sparingly. To do this, use the `overrideClock` fixture as such:
```js
const { test, expect } = require('../../pluginFixtures.js');
test.describe('foo test suite', () => {
// All subsequent tests in this suite will override the clock
test.use({
clockOptions: {
now: 1732413600000, // A timestamp given as milliseconds since the epoch
shouldAdvanceTime: true // Should the clock tick?
}
});
test('bar test', async ({ page }) => {
// ...
});
});
```
More info and options for `overrideClock` can be found in [baseFixtures.js](baseFixtures.js)
- Working with multiple pages
There are instances where multiple browser pages will need to be opened to verify multi-page or multi-tab application behavior.
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.
### Reporting

View File

@@ -35,6 +35,7 @@
* @property {string} type the type of domain object to create (e.g.: "Sine Wave Generator").
* @property {string} [name] the desired name of the created domain object.
* @property {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the Identifier or uuid of the parent object.
* @property {Object<string, string>} [customParameters] any additional parameters to be passed to the domain object's form. E.g. '[aria-label="Data Rate (hz)"]': {'0.1'}
*/
/**
@@ -65,7 +66,10 @@ const { expect } = require('@playwright/test');
* @param {CreateObjectOptions} options
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
*/
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) {
async function createDomainObjectWithDefaults(
page,
{ type, name, parent = 'mine', customParameters = {} }
) {
if (!name) {
name = `${type}:${genUuid()}`;
}
@@ -74,7 +78,7 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
// Navigate to the parent object. This is necessary to create the object
// in the correct location, such as a folder, layout, or plot.
await page.goto(`${parentUrl}?hideTree=true`);
await page.goto(`${parentUrl}`);
//Click the Create button
await page.click('button:has-text("Create")');
@@ -94,6 +98,13 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
await notesInput.fill(page.testNotes);
}
// If there are any further parameters, fill them in
for (const [key, value] of Object.entries(customParameters)) {
const input = page.locator(`form[name="mctForm"] ${key}`);
await input.fill('');
await input.fill(value);
}
// Click OK button and wait for Navigate event
await Promise.all([
page.waitForLoadState(),
@@ -168,7 +179,7 @@ async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
// Navigate to the parent object. This is necessary to create the object
// in the correct location, such as a folder, layout, or plot.
await page.goto(`${parentUrl}?hideTree=true`);
await page.goto(`${parentUrl}`);
// Click the Create button
await page.click('button:has-text("Create")');
@@ -177,7 +188,7 @@ async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
await page.click(`li:text("Plan")`);
// Modify the name input field of the domain object to accept 'name'
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
const nameInput = page.getByLabel('Title', { exact: true });
await nameInput.fill('');
await nameInput.fill(name);
@@ -208,6 +219,62 @@ async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
};
}
/**
* Create a standardized Telemetry Object (Sine Wave Generator) for use in visual tests
* and tests against plotting telemetry (e.g. logPlot tests).
* @param {import('@playwright/test').Page} page
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the uuid or identifier of the parent object. Defaults to 'mine'
* @returns {Promise<CreatedObjectInfo>} An object containing information about the telemetry object.
*/
async function createExampleTelemetryObject(page, parent = 'mine') {
const parentUrl = await getHashUrlToDomainObject(page, parent);
await page.goto(`${parentUrl}`);
await page.locator('button:has-text("Create")').click();
await page.locator('li:has-text("Sine Wave Generator")').click();
const name = 'VIPER Rover Heading';
await page.getByRole('dialog').locator('input[type="text"]').fill(name);
// Fill out the fields with default values
await page.getByRole('spinbutton', { name: 'Period' }).fill('10');
await page.getByRole('spinbutton', { name: 'Amplitude' }).fill('1');
await page.getByRole('spinbutton', { name: 'Offset' }).fill('0');
await page.getByRole('spinbutton', { name: 'Data Rate (hz)' }).fill('1');
await page.getByRole('spinbutton', { name: 'Phase (radians)' }).fill('0');
await page.getByRole('spinbutton', { name: 'Randomness' }).fill('0');
await page.getByRole('spinbutton', { name: 'Loading Delay (ms)' }).fill('0');
await page.getByRole('button', { name: 'Save' }).click();
// Wait until the URL is updated
await page.waitForURL(`**/${parent}/*`);
const uuid = await getFocusedObjectUuid(page);
const url = await getHashUrlToDomainObject(page, uuid);
return {
name,
uuid,
url
};
}
/**
* Navigates directly to a given object url, in fixed time mode, with the given start and end bounds.
* @param {import('@playwright/test').Page} page
* @param {string} url The url to the domainObject
* @param {string | number} start The starting time bound in milliseconds since epoch
* @param {string | number} end The ending time bound in milliseconds since epoch
*/
async function navigateToObjectWithFixedTimeBounds(page, url, start, end) {
await page.goto(
`${url}?tc.mode=fixed&tc.timeSystem=utc&tc.startBound=${start}&tc.endBound=${end}`
);
}
/**
* Open the given `domainObject`'s context menu from the object tree.
* Expands the path to the object and scrolls to it if necessary.
@@ -271,13 +338,13 @@ async function getFocusedObjectUuid(page) {
* URLs returned will be of the form `'./browse/#/mine/<uuid0>/<uuid1>/...'`
*
* @param {import('@playwright/test').Page} page
* @param {string} uuid the uuid of the object to get the url for
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} identifier the uuid or identifier of the object to get the url for
* @returns {Promise<string>} the url of the object
*/
async function getHashUrlToDomainObject(page, uuid) {
await page.waitForLoadState('load'); //Add some determinism
const hashUrl = await page.evaluate(async (objectUuid) => {
const path = await window.openmct.objects.getOriginalPath(objectUuid);
async function getHashUrlToDomainObject(page, identifier) {
await page.waitForLoadState('load');
const hashUrl = await page.evaluate(async (objectIdentifier) => {
const path = await window.openmct.objects.getOriginalPath(objectIdentifier);
let url =
'./#/browse/' +
[...path]
@@ -291,7 +358,7 @@ async function getHashUrlToDomainObject(page, uuid) {
}
return url;
}, uuid);
}, identifier);
return hashUrl;
}
@@ -300,6 +367,7 @@ async function getHashUrlToDomainObject(page, uuid) {
* Utilizes the OpenMCT API to detect if the UI is in Edit mode.
* @private
* @param {import('@playwright/test').Page} page
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} identifier
* @return {Promise<boolean>} true if the Open MCT is in Edit Mode
*/
async function _isInEditMode(page, identifier) {
@@ -316,11 +384,13 @@ async function setTimeConductorMode(page, isFixedTimespan = true) {
// Click 'mode' button
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
await page.getByRole('button', { name: 'Time Conductor Mode Menu' }).click();
// Switch time conductor mode
// Switch time conductor mode. Note, need to wait here for URL to update as the router is debounced.
if (isFixedTimespan) {
await page.getByRole('menuitem', { name: /Fixed Timespan/ }).click();
await page.waitForURL(/tc\.mode=fixed/);
} else {
await page.getByRole('menuitem', { name: /Real-Time/ }).click();
await page.waitForURL(/tc\.mode=local/);
}
}
@@ -410,8 +480,18 @@ async function setEndOffset(page, offset) {
await setTimeConductorOffset(page, offset);
}
/**
* Set the time conductor bounds in fixed time mode
*
* NOTE: Unless explicitly testing the Time Conductor itself, it is advised to instead
* navigate directly to the object with the desired time bounds using `navigateToObjectWithFixedTimeBounds()`.
* @param {import('@playwright/test').Page} page
* @param {string} startDate
* @param {string} endDate
*/
async function setTimeConductorBounds(page, startDate, endDate) {
// Bring up the time conductor popup
expect(await page.locator('.l-shell__time-conductor.c-compact-tc').count()).toBe(1);
await page.click('.l-shell__time-conductor.c-compact-tc');
await setTimeBounds(page, startDate, endDate);
@@ -419,20 +499,32 @@ async function setTimeConductorBounds(page, startDate, endDate) {
await page.keyboard.press('Enter');
}
/**
* Set the independent time conductor bounds in fixed time mode
* @param {import('@playwright/test').Page} page
* @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();
// Bring up the time conductor popup
await page.click('.c-conductor-holder--compact .c-compact-tc');
await expect(page.locator('.itc-popout')).toBeVisible();
await expect(page.locator('.itc-popout')).toBeInViewport();
await setTimeBounds(page, startDate, endDate);
await page.keyboard.press('Enter');
}
/**
* Set the bounds of the visible conductor in fixed time mode
* @private
* @param {import('@playwright/test').Page} page
* @param {string} startDate
* @param {string} endDate
*/
async function setTimeBounds(page, startDate, endDate) {
if (startDate) {
// Fill start time
@@ -453,18 +545,6 @@ async function setTimeBounds(page, startDate, endDate) {
}
}
/**
* Selects an inspector tab based on the provided tab name
*
* @param {import('@playwright/test').Page} page
* @param {String} name the name of the tab
*/
async function selectInspectorTab(page, name) {
const inspectorTabs = page.getByRole('tablist');
const inspectorTab = inspectorTabs.getByTitle(name);
await inspectorTab.click();
}
/**
* Waits and asserts that all plot series data on the page
* is loaded and drawn.
@@ -549,9 +629,25 @@ async function getCanvasPixels(page, canvasSelector) {
return getTelemValuePromise;
}
/**
* @param {import('@playwright/test').Page} page
* @param {string} myItemsFolderName
* @param {string} url
* @param {string} newName
*/
async function renameObjectFromContextMenu(page, url, newName) {
await openObjectTreeContextMenu(page, url);
await page.click('li:text("Edit Properties")');
const nameInput = page.getByLabel('Title', { exact: true });
await nameInput.fill('');
await nameInput.fill(newName);
await page.click('[aria-label="Save"]');
}
// eslint-disable-next-line no-undef
module.exports = {
createDomainObjectWithDefaults,
createExampleTelemetryObject,
createNotification,
createPlanFromJSON,
expandEntireTree,
@@ -559,6 +655,7 @@ module.exports = {
getCanvasPixels,
getHashUrlToDomainObject,
getFocusedObjectUuid,
navigateToObjectWithFixedTimeBounds,
openObjectTreeContextMenu,
setFixedTimeMode,
setRealTimeMode,
@@ -566,6 +663,6 @@ module.exports = {
setEndOffset,
setTimeConductorBounds,
setIndependentTimeConductorBounds,
selectInspectorTab,
waitForPlotsToRender
waitForPlotsToRender,
renameObjectFromContextMenu
};

View File

@@ -72,11 +72,16 @@ exports.test = base.test.extend({
/**
* This allows the test to manipulate the browser clock. This is useful for Visual and Snapshot tests which need
* the Time Indicator Clock to be in a specific state.
*
* Warning: Has many limitations and secondary side effects in Open MCT.
* 1. The tree component does not render.
* 2. page.WaitForNavigation does not trigger.
*
* Usage:
* ```
* ```js
* test.use({
* clockOptions: {
* now: 0,
* now: MISSION_TIME,
* shouldAdvanceTime: true
* ```
* If clockOptions are provided, will override the default clock with fake timers provided by SinonJS.
@@ -85,6 +90,7 @@ exports.test = base.test.extend({
*
* @see {@link https://github.com/microsoft/playwright/issues/6347 Github RFE}
* @see {@link https://github.com/sinonjs/fake-timers/#var-clock--faketimersinstallconfig SinonJS FakeTimers Config}
* @type {import('@types/sinonjs__fake-timers').FakeTimerInstallOpts}
*/
clockOptions: [undefined, { option: true }],
overrideClock: [
@@ -143,7 +149,24 @@ exports.test = base.test.extend({
* Extends the base page class to enable console log error detection.
* @see {@link https://github.com/microsoft/playwright/discussions/11690 Github Discussion}
*/
page: async ({ page, failOnConsoleError }, use) => {
page: async ({ page, failOnConsoleError, clockOptions }, use) => {
// If overriding the clock, we must also override the Date.now()
// function in the generatorWorker context. This is necessary
// to ensure that example telemetry data is generated for the new clock time.
if (clockOptions?.now !== undefined) {
page.on(
'worker',
(worker) => {
if (worker.url().includes('generatorWorker')) {
worker.evaluate((time) => {
self.Date.now = () => time;
});
}
},
clockOptions.now
);
}
// Capture any console errors during test execution
const messages = [];
page.on('console', (msg) => messages.push(msg));

18
e2e/constants.js Normal file
View File

@@ -0,0 +1,18 @@
/* eslint-disable prettier/prettier */
/**
* Constants which may be used across all e2e tests.
*/
/**
* Time Constants
* - 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)
/**
* 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
*/
export const VISUAL_URL = './#/browse/mine?tc.mode=fixed&tc.startBound=1693592063607&tc.endBound=1693593893607&tc.timeSystem=utc&view=grid&hideInspector=true&hideTree=true';

View File

@@ -23,6 +23,8 @@
const { createDomainObjectWithDefaults } = require('../appActions');
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
const CUSTOM_NAME = 'CUSTOM_NAME';
const path = require('path');
/**
* @param {import('@playwright/test').Page} page
@@ -32,7 +34,7 @@ async function enterTextEntry(page, text) {
await page.locator(NOTEBOOK_DROP_AREA).click();
// enter text
await page.locator('[aria-label="Notebook Entry"].is-selected div.c-ne__text').fill(text);
await page.getByLabel('Notebook Entry Input').last().fill(text);
await commitEntry(page);
}
@@ -62,8 +64,86 @@ async function commitEntry(page) {
await page.locator('.c-ne__save-button > button').click();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function startAndAddRestrictedNotebookObject(page) {
// eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, 'addInitRestrictedNotebook.js') });
await page.goto('./', { waitUntil: 'domcontentloaded' });
return createDomainObjectWithDefaults(page, {
type: CUSTOM_NAME,
name: 'Restricted Test Notebook'
});
}
/**
* @param {import('@playwright/test').Page} page
*/
async function lockPage(page) {
const commitButton = page.locator('button:has-text("Commit Entries")');
await commitButton.click();
//Wait until Lock Banner is visible
await page.locator('text=Lock Page').click();
}
/**
* Creates a notebook object and adds an entry.
* @param {import('@playwright/test').Page} - page to load
* @param {number} [iterations = 1] - the number of entries to create
*/
async function createNotebookAndEntry(page, iterations = 1) {
const notebook = createDomainObjectWithDefaults(page, { type: 'Notebook' });
for (let iteration = 0; iteration < iterations; iteration++) {
await enterTextEntry(page, `Entry ${iteration}`);
}
return notebook;
}
/**
* Creates a notebook object, adds an entry, and adds a tag.
* @param {import('@playwright/test').Page} page
* @param {number} [iterations = 1] - the number of entries (and tags) to create
*/
async function createNotebookEntryAndTags(page, iterations = 1) {
const notebook = await createNotebookAndEntry(page, iterations);
await page.getByRole('tab', { name: 'Annotations' }).click();
for (let iteration = 0; iteration < iterations; iteration++) {
// Hover and click "Add Tag" button
// Hover is needed here to "slow down" the actions while running in headless mode
await page.locator(`[aria-label="Notebook Entry"] >> nth = ${iteration}`).click();
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
// Click inside the tag search input
await page.locator('[placeholder="Type to select tag"]').click();
// Select the "Driving" tag
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
// Hover and click "Add Tag" button
// Hover is needed here to "slow down" the actions while running in headless mode
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
// Click inside the tag search input
await page.locator('[placeholder="Type to select tag"]').click();
// Select the "Science" tag
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
}
return notebook;
}
// eslint-disable-next-line no-undef
module.exports = {
enterTextEntry,
dragAndDropEmbed
dragAndDropEmbed,
startAndAddRestrictedNotebookObject,
lockPage,
createNotebookEntryAndTags,
createNotebookAndEntry
};

View File

@@ -99,3 +99,14 @@ export async function setBoundsToSpanAllActivities(page, planJson, planObjectUrl
`${planObjectUrl}?tc.mode=fixed&tc.startBound=${start}&tc.endBound=${end}&tc.timeSystem=utc&view=plan.view`
);
}
/**
* Uses the Open MCT API to set the status of a plan to 'draft'.
* @param {import('@playwright/test').Page} page
* @param {import('../../appActions').CreatedObjectInfo} plan
*/
export async function setDraftStatusForPlan(page, plan) {
await page.evaluate(async (planObject) => {
await window.openmct.status.set(planObject.uuid, 'draft');
}, plan);
}

View File

@@ -2,25 +2,24 @@
// playwright.config.js
// @ts-check
const CI = process.env.CI === 'true';
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 1, //Only for debugging purposes for trace: 'on-first-retry'
testDir: 'tests/performance/',
testMatch: '*.contract.perf.spec.js', //Run everything except contract tests which require marks in dev mode
timeout: 60 * 1000,
workers: 1, //Only run in serial with 1 worker
webServer: {
command: 'npm run start', //coverage not generated
command: 'npm run start', //need development mode for performance.marks and others
url: 'http://localhost:8080/#',
timeout: 200 * 1000,
reuseExistingServer: !CI
reuseExistingServer: false
},
use: {
browserName: 'chromium',
baseURL: 'http://localhost:8080/',
headless: CI, //Only if running locally
ignoreHTTPSErrors: true,
headless: true,
ignoreHTTPSErrors: false, //HTTP performance varies!
screenshot: 'off',
trace: 'on-first-retry',
video: 'off'
@@ -28,6 +27,7 @@ const config = {
projects: [
{
name: 'chrome',
testIgnore: '*.memory.perf.spec.js', //Do not run memory tests without proper flags. Shouldn't get here
use: {
browserName: 'chromium'
}

View File

@@ -0,0 +1,60 @@
/* eslint-disable no-undef */
// playwright.config.js
// @ts-check
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 0, //Only for debugging purposes for trace: 'on-first-retry'
testDir: 'tests/performance/',
testIgnore: '*.contract.perf.spec.js', //Run everything except contract tests which require marks in dev mode
timeout: 60 * 1000,
workers: 1, //Only run in serial with 1 worker
webServer: {
command: 'npm run start:prod', //Production mode
url: 'http://localhost:8080/#',
timeout: 200 * 1000,
reuseExistingServer: false //Must be run with this option to prevent dev mode
},
use: {
baseURL: 'http://localhost:8080/',
headless: true,
ignoreHTTPSErrors: false, //HTTP performance varies!
screenshot: 'off',
trace: 'on-first-retry',
video: 'off'
},
projects: [
{
name: 'chrome-memory',
testMatch: '*.memory.perf.spec.js', //Only run memory tests
use: {
browserName: 'chromium',
launchOptions: {
args: [
'--no-sandbox',
'--disable-notifications',
'--use-fake-ui-for-media-stream',
'--use-fake-device-for-media-stream',
'--js-flags=--no-move-object-start --expose-gc',
'--enable-precise-memory-info',
'--display=:100'
]
}
}
},
{
name: 'chrome',
testIgnore: '*.memory.perf.spec.js', //Do not run memory tests without proper flags
use: {
browserName: 'chromium'
}
}
],
reporter: [
['list'],
['junit', { outputFile: '../test-results/results.xml' }],
['json', { outputFile: '../test-results/results.json' }]
]
};
module.exports = config;

View File

@@ -45,8 +45,6 @@ const path = require('path');
// const createdObjects = new Map();
/**
* **NOTE: This feature is a work-in-progress and should not currently be used.**
*
* This action will create a domain object for the test to reference and return the uuid. If an object
* of a given name already exists, it will return the uuid of that object to the test instead of creating
* a new file. The intent is to move object creation out of test suites which are not explicitly worried
@@ -65,10 +63,7 @@ const path = require('path');
// await createDomainObjectWithDefaults(page, type, name);
// // Once object is created, get the uuid from the url
// const uuid = await page.evaluate(() => {
// return window.location.href.match(/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/)[0];
// });
// const uuid = getHashUrlToDomainObject(page);
// createdObjects.set(objectName, uuid);
@@ -146,6 +141,7 @@ exports.test = test.extend({
await use({ myItemsFolderName });
}
});
exports.expect = expect;
exports.request = request;

View File

@@ -1,22 +0,0 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:8080",
"localStorage": [
{
"name": "tcHistory",
"value": "{\"utc\":[{\"start\":1658617611983,\"end\":1658619411983}]}"
},
{
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"7fa5749b-8969-494c-9d85-c272516d333c\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1658619412848,\"modified\":1658619412848},\"7fa5749b-8969-494c-9d85-c272516d333c\":{\"identifier\":{\"key\":\"7fa5749b-8969-494c-9d85-c272516d333c\",\"namespace\":\"\"},\"name\":\"Unnamed Overlay Plot\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\",\"namespace\":\"\"}}]},\"modified\":1658619413566,\"location\":\"mine\",\"persisted\":1658619413567},\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\":{\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"identifier\":{\"key\":\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\"},\"modified\":1658619413552,\"location\":\"7fa5749b-8969-494c-9d85-c272516d333c\",\"persisted\":1658619413552}}"
},
{
"name": "mct-tree-expanded",
"value": "[\"/browse/mine\"]"
}
]
}
]
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,22 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:8080",
"localStorage": [
{
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"20e7d5fe-9cf8-4099-8957-9453a8954c67\",\"namespace\":\"\"},{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602540,\"created\":1732413600760,\"persisted\":1732413602540},\"20e7d5fe-9cf8-4099-8957-9453a8954c67\":{\"identifier\":{\"key\":\"20e7d5fe-9cf8-4099-8957-9453a8954c67\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with Telemetry Object\\nchrome\",\"modified\":1732413603960,\"location\":\"mine\",\"created\":1732413601820,\"persisted\":1732413603960},\"2db521a9-996d-4d04-a171-93f4c5c220af\":{\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"identifier\":{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"staleness\":false},\"modified\":1732413602540,\"location\":\"mine\",\"created\":1732413602540,\"persisted\":1732413602540}}"
},
{
"name": "mct-recent-objects",
"value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"},\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"staleness\":false},\"modified\":1732413602540,\"location\":\"mine\",\"created\":1732413602540,\"persisted\":1732413602540},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"20e7d5fe-9cf8-4099-8957-9453a8954c67\",\"namespace\":\"\"},{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602540,\"created\":1732413600760,\"persisted\":1732413602540},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/2db521a9-996d-4d04-a171-93f4c5c220af\",\"domainObject\":{\"identifier\":{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"},\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"staleness\":false},\"modified\":1732413602540,\"location\":\"mine\",\"created\":1732413602540,\"persisted\":1732413602540}},{\"objectPath\":[{\"identifier\":{\"key\":\"20e7d5fe-9cf8-4099-8957-9453a8954c67\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with Telemetry Object\\nchrome\",\"modified\":1732413603960,\"location\":\"mine\",\"created\":1732413601820,\"persisted\":1732413603960},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"20e7d5fe-9cf8-4099-8957-9453a8954c67\",\"namespace\":\"\"},{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602540,\"created\":1732413600760,\"persisted\":1732413602540},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/20e7d5fe-9cf8-4099-8957-9453a8954c67\",\"domainObject\":{\"identifier\":{\"key\":\"20e7d5fe-9cf8-4099-8957-9453a8954c67\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with Telemetry Object\\nchrome\",\"modified\":1732413603960,\"location\":\"mine\",\"created\":1732413601820,\"persisted\":1732413603960}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"20e7d5fe-9cf8-4099-8957-9453a8954c67\",\"namespace\":\"\"},{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602540,\"created\":1732413600760,\"persisted\":1732413602540},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine\",\"domainObject\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"20e7d5fe-9cf8-4099-8957-9453a8954c67\",\"namespace\":\"\"},{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602540,\"created\":1732413600760,\"persisted\":1732413602540}}]"
},
{
"name": "mct-tree-expanded",
"value": "[]"
}
]
}
]
}

View File

@@ -0,0 +1,18 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:8080",
"localStorage": [
{
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"98161570-a735-4a50-9c75-11b346ad3789\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413601340,\"created\":1732413600580,\"persisted\":1732413601340},\"98161570-a735-4a50-9c75-11b346ad3789\":{\"identifier\":{\"key\":\"98161570-a735-4a50-9c75-11b346ad3789\",\"namespace\":\"\"},\"name\":\"Overlay Plot with 5s Delay\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"477e60bb-4cba-4603-b4c9-2281ccf7e054\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"477e60bb-4cba-4603-b4c9-2281ccf7e054\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with 5s Delay\\nchrome\",\"modified\":1732413602660,\"location\":\"mine\",\"created\":1732413601340,\"persisted\":1732413602660},\"477e60bb-4cba-4603-b4c9-2281ccf7e054\":{\"identifier\":{\"key\":\"477e60bb-4cba-4603-b4c9-2281ccf7e054\",\"namespace\":\"\"},\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":5000,\"infinityValues\":false,\"staleness\":false},\"modified\":1732413602520,\"location\":\"98161570-a735-4a50-9c75-11b346ad3789\",\"created\":1732413602040,\"persisted\":1732413602520}}"
},
{
"name": "mct-tree-expanded",
"value": "[]"
}
]
}
]
}

File diff suppressed because one or more lines are too long

View File

@@ -117,6 +117,35 @@ test.describe('Renaming Timer Object', () => {
});
});
/**
* The next most important concept in our testing is working with telemetry objects. Telemetry is at the core of Open MCT
* and we have developed a great pattern for working with it.
*/
test.describe('Advanced: Working with telemetry objects', () => {
let displayLayout;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create a Display Layout with a meaningful name
displayLayout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Display Layout with Embedded SWG'
});
// Create Telemetry object within the parent object created above
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Telemetry',
parent: displayLayout.uuid //reference the display layout in the creation process
});
});
test('Can directly navigate to a Display Layout with embedded telemetry', async ({ page }) => {
//Now you can directly navigate to the displayLayout created in the beforeEach with the embedded telemetry
await page.goto(displayLayout.url);
//Expect the created Telemetry Object to be visible when directly navigating to the displayLayout
await expect(page.getByTitle('Sine')).toBeVisible();
});
});
/**
* Structure:
* Custom functions should be declared last.

View File

@@ -0,0 +1,259 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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.
*****************************************************************************/
/* global __dirname */
/**
* This test suite is dedicated to generating LocalStorage via Session Storage to be used
* in some visual test suites like controlledClock.visual.spec.js. This suite should run to completion
* and generate an artifact in ./e2e/test-data/<name>_storage.json . This will run
* on every commit to ensure that this object still loads into tests correctly and will retain the
* *.e2e.spec.js suffix.
*
* TODO: Provide additional validation of object properties as it grows.
* Verification of object properties happens in this file before the test-data is generated,
* and is additionally verified in the validation test suites below.
*/
const { test, expect } = require('../../pluginFixtures.js');
const {
createDomainObjectWithDefaults,
createExampleTelemetryObject
} = require('../../appActions.js');
const { MISSION_TIME } = require('../../constants.js');
const path = require('path');
const overlayPlotName = 'Overlay Plot with Telemetry Object';
test.describe('Generate Visual Test Data @localStorage @generatedata', () => {
test.use({
clockOptions: {
now: MISSION_TIME,
shouldAdvanceTime: true
}
});
test.beforeEach(async ({ page }) => {
// Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
// TODO: Visual test for the generated object here
// - Move to using appActions to create the overlay plot
// and embedded standard telemetry object
test('Generate Overlay Plot with Telemetry Object', async ({ page, context }) => {
// Create Overlay Plot
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
name: overlayPlotName
});
// Create Telemetry Object
const exampleTelemetry = await createExampleTelemetryObject(page);
// Make Link from Telemetry Object to Overlay Plot
await page.locator('button[title="More options"]').click();
// Select 'Create Link' from dropdown
await page.getByRole('menuitem', { name: ' Create Link' }).click();
// Search and Select for overlay Plot within Create Modal
await page.getByRole('dialog').getByRole('searchbox', { name: 'Search Input' }).click();
await page
.getByRole('dialog')
.getByRole('searchbox', { name: 'Search Input' })
.fill(overlayPlot.name);
await page
.getByRole('treeitem', { name: new RegExp(overlayPlot.name) })
.locator('a')
.click();
await page.getByRole('button', { name: 'Save' }).click();
await page.goto(overlayPlot.url);
// TODO: Flesh Out Assertions against created Objects
await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlotName);
await page.getByRole('tab', { name: 'Config' }).click();
await page
.getByRole('list', { name: 'Plot Series Properties' })
.locator('span')
.first()
.click();
// TODO: Modify the Overlay Plot to use fixed Scaling
// TODO: Verify Autoscaling.
// TODO: Fix accessibility of Plot Series Properties tables
// Assert that the Plot Series properties have the correct values
await expect(
page.locator('[role=cell]:has-text("Value")~[role=cell]:has-text("sin")')
).toBeVisible();
await expect(
page.locator(
'[role=cell]:has-text("Line Method")~[role=cell]:has-text("Linear interpolation")'
)
).toBeVisible();
await expect(
page.locator('[role=cell]:has-text("Markers")~[role=cell]:has-text("Point: 2px")')
).toBeVisible();
await expect(
page.locator('[role=cell]:has-text("Alarm Markers")~[role=cell]:has-text("Enabled")')
).toBeVisible();
await expect(
page.locator('[role=cell]:has-text("Limit Lines")~[role=cell]:has-text("Disabled")')
).toBeVisible();
await page.goto(exampleTelemetry.url);
await page.getByRole('tab', { name: 'Properties' }).click();
// TODO: assert Example Telemetry property values
// await page.goto(exampleTelemetry.url);
// Save localStorage for future test execution
await context.storageState({
path: path.join(__dirname, '../../../e2e/test-data/overlay_plot_storage.json')
});
});
// TODO: Merge this with previous test. Edit object created in previous test.
test('Generate Overlay Plot with 5s Delay', async ({ page, context }) => {
// add overlay plot with defaults
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
name: 'Overlay Plot with 5s Delay'
});
const swgWith5sDelay = await createExampleTelemetryObject(page, overlayPlot.uuid);
await page.goto(swgWith5sDelay.url);
await page.getByTitle('More options').click();
await page.getByRole('menuitem', { name: ' Edit Properties...' }).click();
//Edit Example Telemetry Object to include 5s loading Delay
await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000');
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
// focus the overlay plot
await page.goto(overlayPlot.url);
await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlot.name);
// Clear Recently Viewed
await page.getByRole('button', { name: 'Clear Recently Viewed' }).click();
await page.getByRole('button', { name: 'OK' }).click();
//Save localStorage for future test execution
await context.storageState({
path: path.join(__dirname, '../../../e2e/test-data/overlay_plot_with_delay_storage.json')
});
});
});
test.describe('Validate Overlay Plot with Telemetry Object @localStorage @generatedata', () => {
test.use({
storageState: path.join(__dirname, '../../../e2e/test-data/overlay_plot_storage.json')
});
test('Validate Overlay Plot with Telemetry Object', async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.locator('a').filter({ hasText: overlayPlotName }).click();
// TODO: Flesh Out Assertions against created Objects
await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlotName);
await page.getByRole('tab', { name: 'Config' }).click();
await page
.getByRole('list', { name: 'Plot Series Properties' })
.locator('span')
.first()
.click();
// TODO: Modify the Overlay Plot to use fixed Scaling
// TODO: Verify Autoscaling.
// TODO: Fix accessibility of Plot Series Properties tables
// Assert that the Plot Series properties have the correct values
await expect(
page.locator('[role=cell]:has-text("Value")~[role=cell]:has-text("sin")')
).toBeVisible();
await expect(
page.locator(
'[role=cell]:has-text("Line Method")~[role=cell]:has-text("Linear interpolation")'
)
).toBeVisible();
await expect(
page.locator('[role=cell]:has-text("Markers")~[role=cell]:has-text("Point: 2px")')
).toBeVisible();
await expect(
page.locator('[role=cell]:has-text("Alarm Markers")~[role=cell]:has-text("Enabled")')
).toBeVisible();
await expect(
page.locator('[role=cell]:has-text("Limit Lines")~[role=cell]:has-text("Disabled")')
).toBeVisible();
});
});
test.describe('Validate Overlay Plot with 5s Delay Telemetry Object @localStorage @generatedata', () => {
test.use({
storageState: path.join(
__dirname,
'../../../e2e/test-data/overlay_plot_with_delay_storage.json'
)
});
test('Validate Overlay Plot with Telemetry Object', async ({ page }) => {
const plotName = 'Overlay Plot with 5s Delay';
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.locator('a').filter({ hasText: plotName }).click();
// TODO: Flesh Out Assertions against created Objects
await expect(page.locator('.l-browse-bar__object-name')).toContainText(plotName);
await page.getByRole('tab', { name: 'Config' }).click();
await page
.getByRole('list', { name: 'Plot Series Properties' })
.locator('span')
.first()
.click();
// TODO: Modify the Overlay Plot to use fixed Scaling
// TODO: Verify Autoscaling.
// TODO: Fix accessibility of Plot Series Properties tables
// Assert that the Plot Series properties have the correct values
await expect(
page.locator('[role=cell]:has-text("Value")~[role=cell]:has-text("sin")')
).toBeVisible();
await expect(
page.locator(
'[role=cell]:has-text("Line Method")~[role=cell]:has-text("Linear interpolation")'
)
).toBeVisible();
await expect(
page.locator('[role=cell]:has-text("Markers")~[role=cell]:has-text("Point: 2px")')
).toBeVisible();
await expect(
page.locator('[role=cell]:has-text("Alarm Markers")~[role=cell]:has-text("Enabled")')
).toBeVisible();
await expect(
page.locator('[role=cell]:has-text("Limit Lines")~[role=cell]:has-text("Disabled")')
).toBeVisible();
});
});

View File

@@ -1,64 +0,0 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
This test suite is dedicated to generating LocalStorage via Session Storage to be used
in some visual test suites like controlledClock.visual.spec.js. This suite should run to completion
and generate an artifact named ./e2e/test-data/VisualTestData_storage.json . This will run
on every Commit to ensure that this object still loads into tests correctly and will retain the
.e2e.spec.js suffix.
TODO: Provide additional validation of object properties as it grows.
*/
const { createDomainObjectWithDefaults } = require('../../appActions.js');
const { test, expect } = require('../../pluginFixtures.js');
test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
//Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
const overlayPlot = await createDomainObjectWithDefaults(page, { type: 'Overlay Plot' });
// click create button
await page.locator('button:has-text("Create")').click();
// add sine wave generator with defaults
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
//Add a 5000 ms Delay
await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000');
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
// focus the overlay plot
await page.goto(overlayPlot.url);
await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlot.name);
//Save localStorage for future test execution
await context.storageState({ path: './e2e/test-data/VisualTestData_storage.json' });
});

View File

@@ -0,0 +1,59 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
Verify that the "Clear Data" menu action performs as expected for various object types.
*/
const { test, expect } = require('../../pluginFixtures.js');
const { createDomainObjectWithDefaults } = require('../../appActions.js');
const backgroundImageSelector = '.c-imagery__main-image__background-image';
test.describe('Clear Data Action', () => {
test.beforeEach(async ({ page }) => {
// Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create a default 'Example Imagery' object
const exampleImagery = await createDomainObjectWithDefaults(page, { type: 'Example Imagery' });
// Verify that the created object is focused
await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name);
await page.locator('.c-imagery__main-image__bg').hover({ trial: true });
await expect(page.locator(backgroundImageSelector)).toBeVisible();
});
test('works as expected with Example Imagery', async ({ page }) => {
await expect(await page.locator('.c-thumb__image').count()).toBeGreaterThan(0);
// Click the "Clear Data" menu action
await page.getByTitle('More options').click();
const clearDataMenuItem = page.getByRole('menuitem', {
name: 'Clear Data'
});
await expect(clearDataMenuItem).toBeEnabled();
await clearDataMenuItem.click();
// Verify that the background image is no longer visible
await expect(page.locator(backgroundImageSelector)).toBeHidden();
await expect(await page.locator('.c-thumb__image').count()).toBe(0);
});
});

View File

@@ -80,7 +80,7 @@ test.describe('CouchDB Status Indicator with mocked responses @couchdb', () => {
test.describe('CouchDB initialization with mocked responses @couchdb', () => {
test.use({ failOnConsoleError: false });
test("'My Items' folder is created if it doesn't exist", async ({ page }) => {
const mockedMissingObjectResponsefromCouchDB = {
const mockedMissingObjectResponseFromCouchDB = {
status: 404,
contentType: 'application/json',
body: JSON.stringify({})
@@ -92,7 +92,7 @@ test.describe('CouchDB initialization with mocked responses @couchdb', () => {
await page.route(
'**/mine',
(route) => {
route.fulfill(mockedMissingObjectResponsefromCouchDB);
route.fulfill(mockedMissingObjectResponseFromCouchDB);
},
{ times: 1 }
);

View File

@@ -46,7 +46,7 @@ test.describe('Example Event Generator CRUD Operations', () => {
});
});
test.describe('Example Event Generator Telemetry Event Verficiation', () => {
test.describe('Example Event Generator Telemetry Event Verification', () => {
test.fixme('telemetry is coming in for test event', async ({ page }) => {
// Go to object created in step one
// Verify the telemetry table is filled with > 1 row

View File

@@ -292,7 +292,7 @@ test.describe('Move & link item tests', () => {
});
test.fixme(
'Cannot move a previously created domain object to non-peristable object in Move Modal',
'Cannot move a previously created domain object to non-persistable object in Move Modal',
async ({ page }) => {
//Create a domain object
//Save Domain object

View File

@@ -20,11 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { test, expect } = require('../../../pluginFixtures');
const {
createPlanFromJSON,
createDomainObjectWithDefaults,
selectInspectorTab
} = require('../../../appActions');
const { createPlanFromJSON, createDomainObjectWithDefaults } = require('../../../appActions');
const testPlan1 = require('../../../test-data/examplePlans/ExamplePlan_Small1.json');
const testPlan2 = require('../../../test-data/examplePlans/ExamplePlan_Small2.json');
const {
@@ -80,7 +76,7 @@ test.describe('Gantt Chart', () => {
.locator('g')
.filter({ hasText: new RegExp(activity.name) })
.click();
await selectInspectorTab(page, 'Activity');
await page.getByRole('tab', { name: 'Activity' }).click();
const startDateTime = await page
.locator(

View File

@@ -73,7 +73,7 @@ const testPlan = {
};
test.describe('Time Strip', () => {
test('Create two Time Strips, add a single Plan to both, and verify they can have separate Indepdenent Time Contexts @unstable', async ({
test('Create two Time Strips, add a single Plan to both, and verify they can have separate Independent Time Contexts @unstable', async ({
page
}) => {
test.info().annotations.push({

View File

@@ -22,12 +22,15 @@
/*
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets. Note: this
suite is sharing state between tests which is considered an anti-pattern. Implimenting in this way to
suite is sharing state between tests which is considered an anti-pattern. Implementing in this way to
demonstrate some playwright for test developers. This pattern should not be re-used in other CRUD suites.
*/
const { test, expect } = require('../../../../pluginFixtures.js');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const {
createDomainObjectWithDefaults,
createExampleTelemetryObject
} = require('../../../../appActions');
let conditionSetUrl;
let getConditionSetIdentifierFromUrl;
@@ -127,7 +130,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
// Verify Inspector Details has updated Name property
expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
// Verify Tree reflects updated Name proprety
// Verify Tree reflects updated Name property
// Expand Tree
await page.locator(`text=Open MCT ${myItemsFolderName} >> span >> nth=3`).click();
// Verify Condition Set Object is renamed in Tree
@@ -150,7 +153,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
// Verify Inspector Details has updated Name property
expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
// Verify Tree reflects updated Name proprety
// Verify Tree reflects updated Name property
// Expand Tree
await page.locator(`text=Open MCT ${myItemsFolderName} >> span >> nth=3`).click();
// Verify Condition Set Object is renamed in Tree
@@ -205,23 +208,31 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
});
test.describe('Basic Condition Set Use', () => {
let conditionSet;
test.beforeEach(async ({ page }) => {
// Open a browser, navigate to the main page, and wait until all network events to resolve
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Can add a condition', async ({ page }) => {
// Create a new condition set
await createDomainObjectWithDefaults(page, {
conditionSet = await createDomainObjectWithDefaults(page, {
type: 'Condition Set',
name: 'Test Condition Set'
});
});
test('Creating a condition defaults the condition name to "Unnamed Condition"', async ({
page
}) => {
await page.goto(conditionSet.url);
// Change the object to edit mode
await page.locator('[title="Edit"]').click();
// Click Add Condition button
await page.locator('#addCondition').click();
// Check that the new Unnamed Condition section appears
const numOfUnnamedConditions = await page.locator('text=Unnamed Condition').count();
const numOfUnnamedConditions = await page
.locator('.c-condition__name', { hasText: 'Unnamed Condition' })
.count();
expect(numOfUnnamedConditions).toEqual(1);
});
test('ConditionSet should display appropriate view options', async ({ page }) => {
@@ -238,16 +249,13 @@ test.describe('Basic Condition Set Use', () => {
type: 'Sine Wave Generator',
name: 'Beta Sine Wave Generator'
});
const conditionSet1 = await createDomainObjectWithDefaults(page, {
type: 'Condition Set',
name: 'Test Condition Set'
});
await page.goto(conditionSet.url);
// Change the object to edit mode
await page.locator('[title="Edit"]').click();
// Expand the 'My Items' folder in the left tree
await page.goto(conditionSet1.url);
page.click('button[title="Show selected item in tree"]');
// Add the Alpha & Beta Sine Wave Generator to the Condition Set and save changes
const treePane = page.getByRole('tree', {
@@ -264,9 +272,9 @@ test.describe('Basic Condition Set Use', () => {
await alphaGeneratorTreeItem.dragTo(conditionCollection);
await betaGeneratorTreeItem.dragTo(conditionCollection);
const saveButtonLocator = page.locator('button[title="Save"]');
await saveButtonLocator.click();
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.click('button[title="Change the current view"]');
await expect(page.getByRole('menuitem', { name: /Lad Table/ })).toBeHidden();
@@ -274,95 +282,89 @@ test.describe('Basic Condition Set Use', () => {
await expect(page.getByRole('menuitem', { name: /Plot/ })).toBeVisible();
await expect(page.getByRole('menuitem', { name: /Telemetry Table/ })).toBeVisible();
});
test('ConditionSet should output blank instead of the default value', async ({ page }) => {
//Navigate to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
test('ConditionSet has correct outputs when telemetry is and is not available', async ({
page
}) => {
const exampleTelemetry = await createExampleTelemetryObject(page);
//Click the Create button
await page.click('button:has-text("Create")');
// Click the object specified by 'type'
await page.click(`li[role='menuitem']:text("Sine Wave Generator")`);
await page.getByRole('spinbutton', { name: 'Loading Delay (ms)' }).fill('8000');
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
await nameInput.fill('Delayed Sine Wave Generator');
// Click OK button and wait for Navigate event
await Promise.all([
page.waitForLoadState(),
page.click('[aria-label="Save"]'),
// Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
// Create a new condition set
await createDomainObjectWithDefaults(page, {
type: 'Condition Set',
name: 'Test Blank Output of Condition Set'
});
await page.getByTitle('Show selected item in tree').click();
await page.goto(conditionSet.url);
// Change the object to edit mode
await page.locator('[title="Edit"]').click();
// Click Add Condition button twice
// 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');
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
// Add the Sine Wave Generator to the Condition Set and save changes
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: 'Delayed Sine Wave Generator'
});
const conditionCollection = await page.locator('#conditionCollection');
// 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);
const firstCriterionTelemetry = await page.locator(
// Modify First Criterion
const firstCriterionTelemetry = page.locator(
'[aria-label="Criterion Telemetry Selection"] >> nth=0'
);
firstCriterionTelemetry.selectOption({ label: 'Delayed Sine Wave Generator' });
const secondCriterionTelemetry = await page.locator(
'[aria-label="Criterion Telemetry Selection"] >> nth=1'
);
secondCriterionTelemetry.selectOption({ label: 'Delayed Sine Wave Generator' });
const firstCriterionMetadata = await page.locator(
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');
const secondCriterionMetadata = await page.locator(
// Modify First Criterion
const secondCriterionTelemetry = page.locator(
'[aria-label="Criterion Telemetry Selection"] >> nth=1'
);
secondCriterionTelemetry.selectOption({ label: exampleTelemetry.name });
const secondCriterionMetadata = page.locator(
'[aria-label="Criterion Metadata Selection"] >> nth=1'
);
secondCriterionMetadata.selectOption({ label: 'Sine' });
const firstCriterionComparison = await page.locator(
'[aria-label="Criterion Comparison Selection"] >> nth=0'
);
firstCriterionComparison.selectOption({ label: 'is greater than or equal to' });
const secondCriterionComparison = await page.locator(
const secondCriterionComparison = page.locator(
'[aria-label="Criterion Comparison Selection"] >> nth=1'
);
secondCriterionComparison.selectOption({ label: 'is less than' });
const firstCriterionInput = await page.locator('[aria-label="Criterion Input"] >> nth=0');
await firstCriterionInput.fill('0');
const secondCriterionInput = await page.locator('[aria-label="Criterion Input"] >> nth=1');
const secondCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=1');
await secondCriterionInput.fill('0');
const saveButtonLocator = page.locator('button[title="Save"]');
await saveButtonLocator.click();
// Save ConditionSet
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
const outputValue = await page.locator('[aria-label="Current Output Value"]');
// Validate that the condition set is evaluating and outputting
// the correct value when the underlying telemetry subscription is active.
let outputValue = page.locator('[aria-label="Current Output Value"]');
await expect(outputValue).toHaveText('false');
await page.goto(exampleTelemetry.url);
// Edit SWG to add 8 second loading delay to simulate the case
// where telemetry is not available.
await page.getByTitle('More options').click();
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
await page.getByRole('spinbutton', { name: 'Loading Delay (ms)' }).fill('8000');
await page.getByLabel('Save').click();
// Expect that the output value is blank or '---' if the
// underlying telemetry subscription is not active.
await page.goto(conditionSet.url);
await expect(outputValue).toHaveText('---');
});
});

View File

@@ -260,9 +260,13 @@ test.describe('Display Layout', () => {
test('When multiple plots are contained in a layout, we only ask for annotations once @couchdb', async ({
page
}) => {
await setFixedTimeMode(page);
// Create another Sine Wave Generator
const anotherSineWaveObject = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator'
type: 'Sine Wave Generator',
customParameters: {
'[aria-label="Data Rate (hz)"]': '0.01'
}
});
// Create a Display Layout
await createDomainObjectWithDefaults(page, {
@@ -305,7 +309,8 @@ test.describe('Display Layout', () => {
// Time to inspect some network traffic
let networkRequests = [];
page.on('request', (request) => {
const searchRequest = request.url().endsWith('_find');
const searchRequest =
request.url().endsWith('_find') || request.url().includes('by_keystring');
const fetchRequest = request.resourceType() === 'fetch';
if (searchRequest && fetchRequest) {
networkRequests.push(request);
@@ -316,10 +321,21 @@ test.describe('Display Layout', () => {
// wait for annotations requests to be batched and requested
await page.waitForLoadState('networkidle');
// Network requests for the composite telemetry with multiple items should be:
// 1. a single batched request for annotations
expect(networkRequests.length).toBe(1);
await setRealTimeMode(page);
networkRequests = [];
await page.reload();
// wait for annotations to not load (if we have any, we've got a problem)
await page.waitForLoadState('networkidle');
// In real time mode, we don't fetch annotations at all
expect(networkRequests.length).toBe(0);
});
});

View File

@@ -22,7 +22,6 @@
const { test, expect } = require('../../../../pluginFixtures');
const utils = require('../../../../helper/faultUtils');
const { selectInspectorTab } = require('../../../../appActions');
test.describe('The Fault Management Plugin using example faults', () => {
test.beforeEach(async ({ page }) => {
@@ -41,7 +40,7 @@ test.describe('The Fault Management Plugin using example faults', () => {
}) => {
await utils.selectFaultItem(page, 1);
await selectInspectorTab(page, 'Fault Management Configuration');
await page.getByRole('tab', { name: 'Config' }).click();
const selectedFaultName = await page
.locator('.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname')
.textContent();
@@ -66,7 +65,7 @@ test.describe('The Fault Management Plugin using example faults', () => {
);
expect.soft(await selectedRows.count()).toEqual(2);
await selectInspectorTab(page, 'Fault Management Configuration');
await page.getByRole('tab', { name: 'Config' }).click();
const firstSelectedFaultName = await selectedRows.nth(0).textContent();
const secondSelectedFaultName = await selectedRows.nth(1).textContent();
const firstNameInInspectorCount = await page

View File

@@ -29,6 +29,10 @@ const {
test.describe('Flexible Layout', () => {
let sineWaveObject;
let clockObject;
let treePane;
let sineWaveGeneratorTreeItem;
let clockTreeItem;
let flexibleLayout;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
@@ -41,23 +45,27 @@ test.describe('Flexible Layout', () => {
clockObject = await createDomainObjectWithDefaults(page, {
type: 'Clock'
});
// Create a Flexible Layout
flexibleLayout = await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout'
});
// Define the Sine Wave Generator and Clock tree items
treePane = page.getByRole('tree', {
name: 'Main Tree'
});
sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
clockTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(clockObject.name)
});
});
test('panes have the appropriate draggable attribute while in Edit and Browse modes', async ({
page
}) => {
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
const clockTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(clockObject.name)
});
// Create a Flexible Layout
await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout'
});
await page.goto(flexibleLayout.url);
// Edit Flexible Layout
await page.locator('[title="Edit"]').click();
@@ -78,19 +86,79 @@ test.describe('Flexible Layout', () => {
dragWrapper = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
await expect(dragWrapper).toHaveAttribute('draggable', 'false');
});
test('changing toolbar settings in edit mode is immediately reflected and persists upon save', async ({
page
}) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6942'
});
await page.goto(flexibleLayout.url);
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
// Add the Sine Wave Generator and Clock to the Flexible Layout
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await clockTreeItem.dragTo(page.locator('.c-fl__container.is-empty'));
// Click on the first frame to select it
await page.locator('.c-fl-container__frame').first().click();
await expect(page.locator('.c-fl-container__frame > .c-frame').first()).toHaveAttribute(
's-selected',
''
);
// Assert the toolbar is visible
await expect(page.locator('.c-toolbar')).toBeInViewport();
// Assert the layout is in columns orientation
expect(await page.locator('.c-fl--rows').count()).toEqual(0);
// Change the layout to rows orientation
await page.getByTitle('Columns layout').click();
// Assert the layout is in rows orientation
expect(await page.locator('.c-fl--rows').count()).toBeGreaterThan(0);
// Assert the frame of the first item is visible
await expect(page.locator('.c-so-view').first()).not.toHaveClass(/c-so-view--no-frame/);
// Hide the frame of the first item
await page.getByTitle('Frame visible').click();
// Assert the frame is hidden
await expect(page.locator('.c-so-view').first()).toHaveClass(/c-so-view--no-frame/);
// Assert there are 2 containers
expect(await page.locator('.c-fl-container').count()).toEqual(2);
// Add a container
await page.getByTitle('Add Container').click();
// Assert there are 3 containers
expect(await page.locator('.c-fl-container').count()).toEqual(3);
// Save Flexible Layout
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Nav away and back
await page.goto(sineWaveObject.url);
await page.goto(flexibleLayout.url);
// Wait for the first frame to be visible so we know the layout has loaded
await expect(page.locator('.c-fl-container').nth(0)).toBeInViewport();
// Assert the settings have persisted
expect(await page.locator('.c-fl-container').count()).toEqual(3);
expect(await page.locator('.c-fl--rows').count()).toBeGreaterThan(0);
await expect(page.locator('.c-so-view').first()).toHaveClass(/c-so-view--no-frame/);
});
test('items in a flexible layout can be removed with object tree context menu when viewing the flexible layout', async ({
page
}) => {
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
// Create a Display Layout
await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout'
});
await page.goto(flexibleLayout.url);
// Edit Flexible Layout
await page.locator('[title="Edit"]').click();
@@ -121,17 +189,7 @@ test.describe('Flexible Layout', () => {
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/3117'
});
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
// Create a Flexible Layout
const flexibleLayout = await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout'
});
await page.goto(flexibleLayout.url);
// Edit Flexible Layout
await page.locator('[title="Edit"]').click();
@@ -167,19 +225,13 @@ test.describe('Flexible Layout', () => {
const exampleImageryObject = await createDomainObjectWithDefaults(page, {
type: 'Example Imagery'
});
// Create a Flexible Layout
await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout'
});
// Edit Display Layout
await page.goto(flexibleLayout.url);
// Edit Flexible Layout
await page.locator('[title="Edit"]').click();
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// Add the Sine Wave Generator to the Flexible Layout and save changes
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const exampleImageryTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(exampleImageryObject.name)
});

View File

@@ -57,6 +57,10 @@ test.describe('Example Imagery Object', () => {
await mouseZoomOnImageAndAssert(page, -2);
});
test('Compass HUD should be hidden by default', async ({ page }) => {
await expect(page.locator('.c-hud')).toBeHidden();
});
test('Can adjust image brightness/contrast by dragging the sliders', async ({
page,
browserName
@@ -79,25 +83,25 @@ test.describe('Example Imagery Object', () => {
// Test independent fixed time with global fixed time
// flip on independent time conductor
await page.getByRole('switch', { name: 'Enable Independent Time Conductor' }).click();
// Adding in delay to address flakiness of ITC test-- button event handlers not registering in time
await expect(page.locator('#independentTCToggle')).toBeChecked();
await expect(page.locator('.c-compact-tc').first()).toBeVisible();
await page.getByRole('button', { name: 'Independent Time Conductor Settings' }).click();
await page.getByRole('textbox', { name: 'Start date' }).fill('');
await page.getByRole('textbox', { name: 'Start date' }).fill('2021-12-30');
await page.keyboard.press('Tab');
await page.getByRole('textbox', { name: 'Start time' }).fill('');
await page.getByRole('textbox', { name: 'Start time' }).type('01:01:00');
await page.getByRole('textbox', { name: 'Start time' }).fill('01:01:00');
await page.keyboard.press('Tab');
await page.getByRole('textbox', { name: 'End date' }).fill('');
await page.getByRole('textbox', { name: 'End date' }).type('2021-12-30');
await page.getByRole('textbox', { name: 'End date' }).fill('2021-12-30');
await page.keyboard.press('Tab');
await page.getByRole('textbox', { name: 'End time' }).fill('');
await page.getByRole('textbox', { name: 'End time' }).type('01:11:00');
await page.getByRole('textbox', { name: 'End time' }).fill('01:11:00');
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');
// expect(await page.getByRole('button', { name: 'Submit time bounds' }).isEnabled()).toBe(true);
// await page.getByRole('button', { name: 'Submit time bounds' }).click();
// check image date
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
await expect(page.getByText('2021-12-30 01:01:00.000Z').first()).toBeVisible();
// flip it off
await page.getByRole('switch', { name: 'Disable Independent Time Conductor' }).click();
@@ -106,9 +110,12 @@ test.describe('Example Imagery Object', () => {
// Test independent fixed time with global realtime
await setRealTimeMode(page);
await expect(
page.getByRole('switch', { name: 'Enable Independent Time Conductor' })
).toBeEnabled();
await page.getByRole('switch', { name: 'Enable Independent Time Conductor' }).click();
// check image date to be in the past
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
await expect(page.getByText('2021-12-30 01:01:00.000Z').first()).toBeVisible();
// flip it off
await page.getByRole('switch', { name: 'Disable Independent Time Conductor' }).click();
// timestamp shouldn't be in the past anymore
@@ -195,23 +202,26 @@ test.describe('Example Imagery Object', () => {
expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);
});
test('Can use alt+shift+drag to create a tag', async ({ page }) => {
test('Can use alt+shift+drag to create a tag and ensure toolbars disappear', async ({ page }) => {
const canvas = page.locator('canvas');
await canvas.hover({ trial: true });
const canvasBoundingBox = await canvas.boundingBox();
const canvasCenterX = canvasBoundingBox.x + canvasBoundingBox.width / 2;
const canvasCenterY = canvasBoundingBox.y + canvasBoundingBox.height / 2;
await Promise.all(tagHotkey.map((x) => page.keyboard.down(x)));
await page.mouse.down();
// steps not working for me here
await page.mouse.move(canvasCenterX - 20, canvasCenterY - 20);
await page.mouse.move(canvasCenterX - 100, canvasCenterY - 100);
// toolbar should hide when we're creating annotations with a drag
await expect(page.locator('[role="toolbar"][aria-label="Image controls"]')).toBeHidden();
await page.mouse.up();
// toolbar should reappear when we're done creating annotations
await expect(page.locator('[role="toolbar"][aria-label="Image controls"]')).toBeVisible();
await Promise.all(tagHotkey.map((x) => page.keyboard.up(x)));
//Wait for canvas to stablize.
// Wait for canvas to stabilize.
await canvas.hover({ trial: true });
// add some tags
@@ -223,6 +233,20 @@ test.describe('Example Imagery Object', () => {
await page.getByRole('button', { name: /Add Tag/ }).click();
await page.getByPlaceholder('Type to select tag').click();
await page.getByText('Science').click();
// click on a separate part of the canvas to ensure no tags appear
await page.mouse.click(canvasCenterX + 10, canvasCenterY + 10);
await expect(page.getByText('Driving')).toBeHidden();
await expect(page.getByText('Science')).toBeHidden();
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7083'
});
// click on annotation again and expect tags to appear
await page.mouse.click(canvasCenterX - 50, canvasCenterY - 50);
await expect(page.getByText('Driving')).toBeVisible();
await expect(page.getByText('Science')).toBeVisible();
});
test('Can use + - buttons to zoom on the image @unstable', async ({ page }) => {
@@ -230,7 +254,6 @@ test.describe('Example Imagery Object', () => {
});
test('Can use the reset button to reset the image @unstable', async ({ page }, testInfo) => {
test.slow(testInfo.project === 'chrome-beta', 'This test is slow in chrome-beta');
// Get initial image dimensions
const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
@@ -391,7 +414,7 @@ test.describe('Example Imagery in Display Layout', () => {
/**
* Toggle layer visibility checkbox by clicking on checkbox label
* - should toggle checkbox and layer visibility for that image view
* - should NOT toggle checkbox and layer visibity for the first image view in display
* - should NOT toggle checkbox and layer visibility for the first image view in display
*/
test('Toggle layer visibility by clicking on label', async ({ page }) => {
test.info().annotations.push({

View File

@@ -48,12 +48,12 @@ test.describe('ExportAsJSON', () => {
test.fixme('Verify that a nested Object can be exported as JSON', async ({ page }) => {
// Create 2 objects with hierarchy
// Export as JSON
// Verify Hiearchy
// Verify Hierarchy
});
test.fixme(
'Verify that the ExportAsJSON dropdown does not appear for the item X',
async ({ page }) => {
// Other than non-persistible objects
// Other than non-persistable objects
}
);
});

View File

@@ -48,7 +48,7 @@ test.describe('ExportAsJSON', () => {
test.fixme(
'Verify that the ImportAsJSON dropdown does not appear for the item X',
async ({ page }) => {
// Other than non-persistible objects
// Other than non-persistable objects
}
);
});

View File

@@ -25,8 +25,7 @@ const {
createDomainObjectWithDefaults,
setStartOffset,
setFixedTimeMode,
setRealTimeMode,
selectInspectorTab
setRealTimeMode
} = require('../../../../appActions');
test.describe('Testing LAD table configuration', () => {
@@ -57,7 +56,7 @@ test.describe('Testing LAD table configuration', () => {
// // Add the Sine Wave Generator to the LAD table and save changes
// await page.dragAndDrop('role=treeitem[name=/Test Sine Wave Generator/]', '.c-lad-table-wrapper');
// select configuration tab in inspector
await selectInspectorTab(page, 'LAD Table Configuration');
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
// make sure headers are visible initially
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
@@ -87,7 +86,7 @@ test.describe('Testing LAD table configuration', () => {
// Edit LAD table
await page.locator('[title="Edit"]').click();
await selectInspectorTab(page, 'LAD Table Configuration');
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
// show timestamp column
await page.getByLabel('Timestamp').check();
@@ -105,7 +104,7 @@ test.describe('Testing LAD table configuration', () => {
// Edit LAD table
await page.locator('[title="Edit"]').click();
await selectInspectorTab(page, 'LAD Table Configuration');
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
// show units and type columns
await page.getByLabel('Units').check();

View File

@@ -39,7 +39,7 @@ test.describe('Notebook CRUD Operations', () => {
test.fixme('Can update a Notebook Object', async ({ page }) => {});
test.fixme('Can view a perviously created Notebook Object', async ({ page }) => {});
test.fixme('Can Delete a Notebook Object', async ({ page }) => {
// Other than non-persistible objects
// Other than non-persistable objects
});
});
@@ -279,8 +279,8 @@ test.describe('Notebook entry tests', () => {
// Click .c-notebook__drag-area
await page.locator('.c-notebook__drag-area').click();
await expect(page.locator('[aria-label="Notebook Entry Input"]')).toBeVisible();
await expect(page.locator('[aria-label="Notebook Entry"]')).toHaveClass(/is-selected/);
await expect(page.getByLabel('Notebook Entry Input')).toBeVisible();
await expect(page.getByLabel('Notebook Entry', { exact: true })).toHaveClass(/is-selected/);
});
test('When an object is dropped into a notebook, a new entry is created and it should be focused @unstable', async ({
page
@@ -369,6 +369,8 @@ test.describe('Notebook entry tests', () => {
const validLink = page.locator(`a[href="${TEST_LINK}"]`);
expect(await validLink.count()).toBe(1);
// Start waiting for popup before clicking. Note no await.
const popupPromise = page.waitForEvent('popup');
@@ -378,8 +380,6 @@ test.describe('Notebook entry tests', () => {
// Wait for the popup to load.
await popup.waitForLoadState();
expect.soft(popup.url()).toContain('www.google.com');
expect(await validLink.count()).toBe(1);
});
test('when an invalid link is entered into a notebook entry, it does not become clickable when viewing', async ({
page
@@ -447,6 +447,8 @@ test.describe('Notebook entry tests', () => {
const validLink = page.locator(`a[href="${TEST_LINK}"]`);
expect(await validLink.count()).toBe(1);
// Start waiting for popup before clicking. Note no await.
const popupPromise = page.waitForEvent('popup');
@@ -456,8 +458,6 @@ test.describe('Notebook entry tests', () => {
// Wait for the popup to load.
await popup.waitForLoadState();
expect.soft(popup.url()).toContain('www.google.com');
expect(await validLink.count()).toBe(1);
});
test('when a nefarious link is entered into a notebook entry, it is sanitized when viewing', async ({
page
@@ -482,4 +482,55 @@ test.describe('Notebook entry tests', () => {
expect.soft(await sanitizedLink.count()).toBe(1);
expect(await unsanitizedLink.count()).toBe(0);
});
test('Can add markdown to a notebook entry', async ({ page }) => {
await page.goto(notebookObject.url);
// Headers
const headerMarkdown = `# Big Header\n## Large Header\n### Medium Header\n#### Small Header`;
await nbUtils.enterTextEntry(page, headerMarkdown);
await expect(page.getByRole('heading', { name: 'Big Header' })).toBeVisible();
// Text markup
const markupText =
'**This is bold.** _This is italic_. `This is code`. ~This is strikethrough~';
await nbUtils.enterTextEntry(page, markupText);
await expect(page.locator('strong:has-text("This is bold.")')).toBeVisible();
// Tables
const tablesText = '|Col 1|Col 2|Col3|\n|-|-|-|\n |Value 1|Value 2|Value 3|\n';
await nbUtils.enterTextEntry(page, tablesText);
await expect(page.getByRole('cell', { name: 'Value 2' })).toBeVisible();
// Links
const linksText =
'Raw links https://www.google.com and Markdown links like [Google](https://www.google.com) work';
await nbUtils.enterTextEntry(page, linksText);
await expect(page.getByRole('link', { name: 'https://www.google.com' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Google', exact: true })).toBeVisible();
// Lists
const listsText = '- List item 1\n - Item 1A \n- List Item 2\n 1. Order 1\n 1. Order 2\n';
await nbUtils.enterTextEntry(page, listsText);
const childItem = page.locator('li:has-text("List Item 2") ol li:has-text("Order 2")');
await expect(childItem).toBeVisible();
// Code Blocks
const codeblockTest = '```javascript\nconst foo = "bar";\nconst bar = "foo";\n```';
await nbUtils.enterTextEntry(page, codeblockTest);
const codeBlock = page.locator('code.language-javascript:has-text("const foo = \\"bar\\";")');
await expect(codeBlock).toBeVisible();
// Blockquotes
const blockquoteTest =
'This is a quote by Mark Twain:\n> "The man with a new idea is a crank\n>until the idea succeeds."';
await nbUtils.enterTextEntry(page, blockquoteTest);
const firstLineOfBlockquoteText = page.locator(
'blockquote:has-text("The man with a new idea is a crank")'
);
await expect(firstLineOfBlockquoteText).toBeVisible();
const secondLineOfBlockquoteText = page.locator(
'blockquote:has-text("until the idea succeeds")'
);
await expect(secondLineOfBlockquoteText).toBeVisible();
});
});

View File

@@ -24,9 +24,11 @@
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks.
*/
const fs = require('fs').promises;
const { test, expect } = require('../../../../pluginFixtures');
// const { expandTreePaneItemByName, createDomainObjectWithDefaults } = require('../../../../appActions');
// const nbUtils = require('../../../../helper/notebookUtils');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const NOTEBOOK_NAME = 'Notebook';
test.describe('Snapshot Menu tests', () => {
test.fixme(
@@ -35,7 +37,7 @@ test.describe('Snapshot Menu tests', () => {
// There should be no default notebook
// Clear default notebook if exists using `localStorage.setItem('notebook-storage', null);`
// refresh page
// Click on 'Notebook Snaphot Menu'
// Click on 'Notebook Snapshot Menu'
// 'save to Notebook Snapshots' should be only option there
}
);
@@ -62,7 +64,7 @@ test.describe('Snapshot Menu tests', () => {
});
test.fixme('Snapshots adjust time conductor', async ({ page }) => {
// Create Telemetry object
// Set Telemetry object's timeconductor to Fixed time with Start and Endtimes are recorded
// Set Telemetry object's timeconductor to Fixed time with Start and End times are recorded
// Embed Telemetry object into notebook
// Set Time Conductor to Local clock
// Click into embedded telemetry object and verify object appears with same fixed time from record
@@ -161,3 +163,110 @@ test.describe('Snapshot Container tests', () => {
}
);
});
test.describe('Snapshot image tests', () => {
test.beforeEach(async ({ page }) => {
//Navigate to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create Notebook
await createDomainObjectWithDefaults(page, {
type: NOTEBOOK_NAME
});
});
test('Can drop an image onto a notebook and create a new entry', async ({ page }) => {
const imageData = await fs.readFile('src/images/favicons/favicon-96x96.png');
const imageArray = new Uint8Array(imageData);
const fileData = Array.from(imageArray);
const dropTransfer = await page.evaluateHandle((data) => {
const dataTransfer = new DataTransfer();
const file = new File([new Uint8Array(data)], 'favicon-96x96.png', { type: 'image/png' });
dataTransfer.items.add(file);
return dataTransfer;
}, fileData);
await page.dispatchEvent('.c-notebook__drag-area', 'drop', { dataTransfer: dropTransfer });
await page.locator('.c-ne__save-button > button').click();
// be sure that entry was created
await expect(page.getByText('favicon-96x96.png')).toBeVisible();
await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).click();
// expect large image to be displayed
await expect(page.getByRole('dialog').getByText('favicon-96x96.png')).toBeVisible();
await page.getByLabel('Close').click();
// drop another image onto the entry
await page.dispatchEvent('.c-snapshots', 'drop', { dataTransfer: dropTransfer });
// expect two embedded images now
expect(await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).count()).toBe(2);
await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More options').click();
await page.getByRole('menuitem', { name: /Remove This Embed/ }).click();
await page.getByRole('button', { name: 'Ok', exact: true }).click();
// expect one embedded image now as we deleted the other
expect(await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).count()).toBe(1);
});
});
test.describe('Snapshot image failure tests', () => {
test.use({ failOnConsoleError: false });
test.beforeEach(async ({ page }) => {
//Navigate to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create Notebook
await createDomainObjectWithDefaults(page, {
type: NOTEBOOK_NAME
});
});
test('Get an error notification when dropping unknown file onto notebook entry', async ({
page
}) => {
// fill Uint8Array array with some garbage data
const garbageData = new Uint8Array(100);
const fileData = Array.from(garbageData);
const dropTransfer = await page.evaluateHandle((data) => {
const dataTransfer = new DataTransfer();
const file = new File([new Uint8Array(data)], 'someGarbage.foo', { type: 'unknown/garbage' });
dataTransfer.items.add(file);
return dataTransfer;
}, fileData);
await page.dispatchEvent('.c-notebook__drag-area', 'drop', { dataTransfer: dropTransfer });
// should have gotten a notification from OpenMCT that we couldn't add it
await expect(page.getByText('Unknown object(s) dropped and cannot embed')).toBeVisible();
});
test('Get an error notification when dropping big files onto notebook entry', async ({
page
}) => {
const garbageSize = 15 * 1024 * 1024; // 15 megabytes
await page.addScriptTag({
// make the garbage client side
content: `window.bigGarbageData = new Uint8Array(${garbageSize})`
});
const bigDropTransfer = await page.evaluateHandle(() => {
const dataTransfer = new DataTransfer();
const file = new File([window.bigGarbageData], 'bigBoy.png', { type: 'image/png' });
dataTransfer.items.add(file);
return dataTransfer;
});
await page.dispatchEvent('.c-notebook__drag-area', 'drop', { dataTransfer: bigDropTransfer });
// should have gotten a notification from OpenMCT that we couldn't add it as it's too big
await expect(page.getByText('unable to embed')).toBeVisible();
});
});

View File

@@ -19,18 +19,18 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* global __dirname */
const { test, expect, streamToString } = require('../../../../pluginFixtures');
const { openObjectTreeContextMenu } = require('../../../../appActions');
const {
openObjectTreeContextMenu,
createDomainObjectWithDefaults
} = require('../../../../appActions');
const path = require('path');
const nbUtils = require('../../../../helper/notebookUtils');
lockPage,
dragAndDropEmbed,
enterTextEntry,
startAndAddRestrictedNotebookObject
} = require('../../../../helper/notebookUtils');
const TEST_TEXT = 'Testing text for entries.';
const TEST_TEXT_NAME = 'Test Page';
const CUSTOM_NAME = 'CUSTOM_NAME';
test.describe('Restricted Notebook', () => {
let notebook;
@@ -68,7 +68,7 @@ test.describe('Restricted Notebook', () => {
});
test('Can be locked if at least one page has one entry @addInit', async ({ page }) => {
await nbUtils.enterTextEntry(page, TEST_TEXT);
await enterTextEntry(page, TEST_TEXT);
const commitButton = page.locator('button:has-text("Commit Entries")');
expect(await commitButton.count()).toEqual(1);
@@ -79,7 +79,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
let notebook;
test.beforeEach(async ({ page }) => {
notebook = await startAndAddRestrictedNotebookObject(page);
await nbUtils.enterTextEntry(page, TEST_TEXT);
await enterTextEntry(page, TEST_TEXT);
await lockPage(page);
// open sidebar
@@ -125,7 +125,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
expect.soft(newPageCount).toEqual(1);
// enter test text
await nbUtils.enterTextEntry(page, TEST_TEXT);
await enterTextEntry(page, TEST_TEXT);
// expect new page to be lockable
const commitButton = page.getByRole('button', { name: ' Commit Entries' });
@@ -136,7 +136,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
// Delete the page
await page.getByRole('menuitem', { name: 'Delete Page' }).click();
// Click OK button
await page.getByRole('button', { name: 'Ok' }).click();
await page.getByRole('button', { name: 'Ok', exact: true }).click();
// deleted page, should no longer exist
const deletedPageElement = page.getByText(TEST_TEXT_NAME);
@@ -147,12 +147,12 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
test.describe('Restricted Notebook with a page locked and with an embed @addInit', () => {
test.beforeEach(async ({ page }) => {
const notebook = await startAndAddRestrictedNotebookObject(page);
await nbUtils.dragAndDropEmbed(page, notebook);
await dragAndDropEmbed(page, notebook);
});
test('Allows embeds to be deleted if page unlocked @addInit', async ({ page }) => {
// Click .c-ne__embed__name .c-popup-menu-button
await page.locator('.c-ne__embed__name .c-icon-button').click(); // embed popup menu
// Click embed popup menu
await page.locator('.c-ne__embed__name .c-icon-button').click();
const embedMenu = page.locator('body >> .c-menu');
await expect(embedMenu).toContainText('Remove This Embed');
@@ -160,8 +160,8 @@ test.describe('Restricted Notebook with a page locked and with an embed @addInit
test('Disallows embeds to be deleted if page locked @addInit', async ({ page }) => {
await lockPage(page);
// Click .c-ne__embed__name .c-popup-menu-button
await page.locator('.c-ne__embed__name .c-icon-button').click(); // embed popup menu
// Click embed popup menu
await page.locator('.c-ne__embed__name .c-icon-button').click();
const embedMenu = page.locator('body >> .c-menu');
await expect(embedMenu).not.toContainText('Remove This Embed');
@@ -174,7 +174,7 @@ test.describe('can export restricted notebook as text', () => {
});
test('basic functionality ', async ({ page }) => {
await nbUtils.enterTextEntry(page, `Foo bar entry`);
await enterTextEntry(page, `Foo bar entry`);
// Click on 3 Dot Menu
await page.locator('button[title="More options"]').click();
const downloadPromise = page.waitForEvent('download');
@@ -182,6 +182,8 @@ test.describe('can export restricted notebook as text', () => {
await page.getByRole('menuitem', { name: /Export Notebook as Text/ }).click();
await page.getByRole('button', { name: 'Save' }).click();
//Verify exported text as a stream of text instead of a file read from the filesystem
const download = await downloadPromise;
const readStream = await download.createReadStream();
const exportedText = await streamToString(readStream);
@@ -193,26 +195,3 @@ test.describe('can export restricted notebook as text', () => {
test.fixme('can export all notebook tags', async ({ page }) => {});
test.fixme('can export all notebook snapshots', async ({ page }) => {});
});
/**
* @param {import('@playwright/test').Page} page
*/
async function startAndAddRestrictedNotebookObject(page) {
await page.addInitScript({
path: path.join(__dirname, '../../../../helper/', 'addInitRestrictedNotebook.js')
});
await page.goto('./', { waitUntil: 'domcontentloaded' });
return createDomainObjectWithDefaults(page, { type: CUSTOM_NAME });
}
/**
* @param {import('@playwright/test').Page} page
*/
async function lockPage(page) {
const commitButton = page.locator('button:has-text("Commit Entries")');
await commitButton.click();
//Wait until Lock Banner is visible
await page.locator('text=Lock Page').click();
}

View File

@@ -25,57 +25,12 @@ This test suite is dedicated to tests which verify notebook tag functionality.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions');
const nbUtils = require('../../../../helper/notebookUtils');
/**
* Creates a notebook object and adds an entry.
* @param {import('@playwright/test').Page} - page to load
* @param {number} [iterations = 1] - the number of entries to create
*/
async function createNotebookAndEntry(page, iterations = 1) {
const notebook = createDomainObjectWithDefaults(page, { type: 'Notebook' });
for (let iteration = 0; iteration < iterations; iteration++) {
await nbUtils.enterTextEntry(page, `Entry ${iteration}`);
}
return notebook;
}
/**
* Creates a notebook object, adds an entry, and adds a tag.
* @param {import('@playwright/test').Page} page
* @param {number} [iterations = 1] - the number of entries (and tags) to create
*/
async function createNotebookEntryAndTags(page, iterations = 1) {
const notebook = await createNotebookAndEntry(page, iterations);
await selectInspectorTab(page, 'Annotations');
for (let iteration = 0; iteration < iterations; iteration++) {
// Hover and click "Add Tag" button
// Hover is needed here to "slow down" the actions while running in headless mode
await page.locator(`[aria-label="Notebook Entry"] >> nth = ${iteration}`).click();
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
// Click inside the tag search input
await page.locator('[placeholder="Type to select tag"]').click();
// Select the "Driving" tag
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
// Hover and click "Add Tag" button
// Hover is needed here to "slow down" the actions while running in headless mode
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
// Click inside the tag search input
await page.locator('[placeholder="Type to select tag"]').click();
// Select the "Science" tag
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
}
return notebook;
}
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const {
enterTextEntry,
createNotebookAndEntry,
createNotebookEntryAndTags
} = require('../../../../helper/notebookUtils');
test.describe('Tagging in Notebooks @addInit', () => {
test.beforeEach(async ({ page }) => {
@@ -85,7 +40,7 @@ test.describe('Tagging in Notebooks @addInit', () => {
test('Can load tags', async ({ page }) => {
await createNotebookAndEntry(page);
await selectInspectorTab(page, 'Annotations');
await page.getByRole('tab', { name: 'Annotations' }).click();
await page.locator('button:has-text("Add Tag")').click();
@@ -110,9 +65,9 @@ test.describe('Tagging in Notebooks @addInit', () => {
});
test('Can add tags with blank entry', async ({ page }) => {
await createDomainObjectWithDefaults(page, { type: 'Notebook' });
await selectInspectorTab(page, 'Annotations');
await page.getByRole('tab', { name: 'Annotations' }).click();
await nbUtils.enterTextEntry(page, '');
await enterTextEntry(page, '');
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
@@ -126,7 +81,7 @@ test.describe('Tagging in Notebooks @addInit', () => {
test('Can cancel adding tags', async ({ page }) => {
await createNotebookAndEntry(page);
await selectInspectorTab(page, 'Annotations');
await page.getByRole('tab', { name: 'Annotations' }).click();
// Test canceling adding a tag after we click "Type to select tag"
await page.locator('button:has-text("Add Tag")').click();
@@ -192,16 +147,13 @@ test.describe('Tagging in Notebooks @addInit', () => {
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5823'
});
await createNotebookEntryAndTags(page);
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = 1`;
await page.locator(entryLocator).click();
await page.locator(entryLocator).fill(`An entry without tags`);
await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').press('Enter');
await page.getByLabel('Notebook Entry Input').fill(`An entry without tags`);
await page.locator('.c-ne__save-button > button').click();
await page.hover('[aria-label="Notebook Entry Input"] >> nth=1');
await page.hover('[aria-label="Notebook Entry Display"] >> nth=1');
await page.locator('button[title="Delete this entry"]').last().click();
await expect(
page.locator('text=This action will permanently delete this entry. Do you wish to continue?')
@@ -255,7 +207,7 @@ test.describe('Tagging in Notebooks @addInit', () => {
test('Can cancel adding a tag', async ({ page }) => {
await createNotebookAndEntry(page);
await selectInspectorTab(page, 'Annotations');
await page.getByRole('tab', { name: 'Annotations' }).click();
// Click on the "Add Tag" button
await page.locator('button:has-text("Add Tag")').click();

View File

@@ -48,6 +48,8 @@ test.describe('Operator Status', () => {
});
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.locator('.c-message__action-text')).toBeHidden();
// set role
await page.getByRole('button', { name: 'Select' }).click();
// dismiss role confirmation popup
@@ -157,6 +159,6 @@ test.describe('Operator Status', () => {
});
test.fixme('iterate through all possible response values', async ({ page }) => {
// test all possible respone values for the poll
// test all possible response values for the poll
});
});

View File

@@ -24,7 +24,7 @@
Testsuite for plot autoscale.
*/
const { selectInspectorTab, setTimeConductorBounds } = require('../../../../appActions');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const { test, expect } = require('../../../../pluginFixtures');
test.use({
viewport: {
@@ -34,24 +34,33 @@ test.use({
});
test.describe('Autoscale', () => {
test('User can set autoscale with a valid range @snapshot', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
test('User can set autoscale with a valid range @snapshot', async ({ page }) => {
//This is necessary due to the size of the test suite.
test.slow();
await page.goto('./', { waitUntil: 'domcontentloaded' });
await setTimeRange(page);
const overlayPlot = await createDomainObjectWithDefaults(page, {
name: 'Test Overlay Plot',
type: 'Overlay Plot'
});
await createDomainObjectWithDefaults(page, {
name: 'Test Sine Wave Generator',
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await createSinewaveOverlayPlot(page, myItemsFolderName);
// Switch to fixed time, start: 2022-03-28 22:00:00.000 UTC, end: 2022-03-28 22:00:30.000 UTC
await page.goto(
`${overlayPlot.url}?tc.mode=fixed&tc.startBound=1648591200000&tc.endBound=1648591230000&tc.timeSystem=utc&view=plot-overlay`
);
await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']);
// enter edit mode
await page.click('button[title="Edit"]');
await selectInspectorTab(page, 'Config');
await page.getByRole('tab', { name: 'Config' }).click();
await turnOffAutoscale(page);
await setUserDefinedMinAndMax(page, '-2', '2');
@@ -67,7 +76,7 @@ test.describe('Autoscale', () => {
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
// Make sure that after turning off autoscale, the user entered range values are reflexted in the ticks.
// Make sure that after turning off autoscale, the user entered range values are reflected in the ticks.
await testYTicks(page, [
'-2.00',
'-1.50',
@@ -109,7 +118,7 @@ test.describe('Autoscale', () => {
// Ensure the drag worked.
await testYTicks(page, ['-0.50', '0.00', '0.50', '1.00', '1.50', '2.00', '2.50', '3.00']);
//Wait for canvas to stablize.
//Wait for canvas to stabilize.
await canvas.hover({ trial: true });
expect
@@ -118,72 +127,6 @@ test.describe('Autoscale', () => {
});
});
/**
* @param {import('@playwright/test').Page} page
* @param {string} start
* @param {string} end
*/
async function setTimeRange(
page,
start = '2022-03-29 22:00:00.000Z',
end = '2022-03-29 22:00:30.000Z'
) {
// Set a specific time range for consistency, otherwise it will change
// on every test to a range based on the current time.
await setTimeConductorBounds(page, start, end);
}
/**
* @param {import('@playwright/test').Page} page
* @param {string} myItemsFolderName
*/
async function createSinewaveOverlayPlot(page, myItemsFolderName) {
// click create button
await page.locator('button:has-text("Create")').click();
// add overlay plot with defaults
await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click();
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear1
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' });
// save (exit edit mode)
await page
.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button')
.nth(1)
.click();
await page.locator('text=Save and Finish Editing').click();
// click create button
await page.locator('button:has-text("Create")').click();
// add sine wave generator with defaults
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear1
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' });
// focus the overlay plot
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Overlay Plot').first().click()
]);
}
/**
* @param {import('@playwright/test').Page} page
*/

View File

@@ -26,7 +26,7 @@ necessarily be used for reference when writing new tests in this area.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { selectInspectorTab, setTimeConductorBounds } = require('../../../../appActions');
const { setTimeConductorBounds } = require('../../../../appActions');
test.describe('Log plot tests', () => {
test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({
@@ -34,14 +34,13 @@ test.describe('Log plot tests', () => {
openmctConfig
}) => {
const { myItemsFolderName } = openmctConfig;
//Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
//Test is slow and should be split in the future
test.slow();
await makeOverlayPlot(page, myItemsFolderName);
await testRegularTicks(page);
await enableEditMode(page);
await selectInspectorTab(page, 'Config');
await page.getByRole('tab', { name: 'Config' }).click();
await enableLogMode(page);
await testLogTicks(page);
await disableLogMode(page);

View File

@@ -29,7 +29,6 @@ const { test, expect } = require('../../../../pluginFixtures');
const {
createDomainObjectWithDefaults,
getCanvasPixels,
selectInspectorTab,
waitForPlotsToRender
} = require('../../../../appActions');
@@ -50,7 +49,7 @@ test.describe('Overlay Plot', () => {
await page.goto(overlayPlot.url);
await selectInspectorTab(page, 'Config');
await page.getByRole('tab', { name: 'Config' }).click();
// navigate to plot series color palette
await page.click('.l-browse-bar__actions__edit');
@@ -91,7 +90,7 @@ test.describe('Overlay Plot', () => {
await page.click('button[title="Edit"]');
// Expand the "Sine Wave Generator" plot series options and enable limit lines
await selectInspectorTab(page, 'Config');
await page.getByRole('tab', { name: 'Config' }).click();
await page
.getByRole('list', { name: 'Plot Series Properties' })
.locator('span')
@@ -117,7 +116,7 @@ test.describe('Overlay Plot', () => {
// Enter edit mode
await page.click('button[title="Edit"]');
await selectInspectorTab(page, 'Elements');
await page.getByRole('tab', { name: 'Elements' }).click();
// Drag Sine Wave Generator series from Y Axis 1 into Y Axis 2
await page
@@ -168,7 +167,7 @@ test.describe('Overlay Plot', () => {
await page.goto(overlayPlot.url);
await page.click('button[title="Edit"]');
await selectInspectorTab(page, 'Elements');
await page.getByRole('tab', { name: 'Elements' }).click();
// Drag swg a, c, e into Y Axis 2
await page
@@ -182,7 +181,7 @@ test.describe('Overlay Plot', () => {
.dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]'));
// Assert that Y Axis 1 and Y Axis 2 property groups are visible only
await selectInspectorTab(page, 'Config');
await page.getByRole('tab', { name: 'Config' }).click();
const yAxis1PropertyGroup = page.locator('[aria-label="Y Axis Properties"]');
const yAxis2PropertyGroup = page.locator('[aria-label="Y Axis 2 Properties"]');
@@ -196,7 +195,7 @@ test.describe('Overlay Plot', () => {
const yAxis2Group = page.getByLabel('Y Axis 2');
const yAxis3Group = page.getByLabel('Y Axis 3');
await selectInspectorTab(page, 'Elements');
await page.getByRole('tab', { name: 'Elements' }).click();
// Drag swg b into Y Axis 3
await page
@@ -204,14 +203,14 @@ test.describe('Overlay Plot', () => {
.dragTo(page.locator('[aria-label="Element Item Group Y Axis 3"]'));
// Assert that all Y Axis property groups are visible
await selectInspectorTab(page, 'Config');
await page.getByRole('tab', { name: 'Config' }).click();
await expect(yAxis1PropertyGroup).toBeVisible();
await expect(yAxis2PropertyGroup).toBeVisible();
await expect(yAxis3PropertyGroup).toBeVisible();
// Verify that the elements are in the correct buckets and in the correct order
await selectInspectorTab(page, 'Elements');
await page.getByRole('tab', { name: 'Elements' }).click();
expect(yAxis1Group.getByRole('listitem', { name: swgD.name })).toBeTruthy();
expect(yAxis1Group.getByRole('listitem').nth(0).getByText(swgD.name)).toBeTruthy();
@@ -242,7 +241,7 @@ test.describe('Overlay Plot', () => {
await waitForPlotsToRender(page);
await page.click('button[title="Edit"]');
await selectInspectorTab(page, 'Elements');
await page.getByRole('tab', { name: 'Elements' }).click();
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click();

View File

@@ -25,7 +25,7 @@
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const uuid = require('uuid').v4;
test.describe('Scatter Plot', () => {
@@ -54,7 +54,7 @@ test.describe('Scatter Plot', () => {
// the SWG appears in the elements pool
await page.goto(scatterPlot.url);
await editButton.click();
await selectInspectorTab(page, 'Elements');
await page.getByRole('tab', { name: 'Elements' }).click();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible();
await saveButton.click();
await page.locator('li[title="Save and Finish Editing"]').click();
@@ -82,7 +82,7 @@ test.describe('Scatter Plot', () => {
await editButton.click();
// Click the "Elements" tab
await selectInspectorTab(page, 'Elements');
await page.getByRole('tab', { name: 'Elements' }).click();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeHidden();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg2.name}`)).toBeVisible();
await saveButton.click();

View File

@@ -26,11 +26,7 @@ necessarily be used for reference when writing new tests in this area.
*/
const { test, expect } = require('../../../../pluginFixtures');
const {
createDomainObjectWithDefaults,
selectInspectorTab,
waitForPlotsToRender
} = require('../../../../appActions');
const { createDomainObjectWithDefaults, waitForPlotsToRender } = require('../../../../appActions');
test.describe('Stacked Plot', () => {
let stackedPlot;
@@ -75,7 +71,7 @@ test.describe('Stacked Plot', () => {
await page.click('button[title="Edit"]');
await selectInspectorTab(page, 'Elements');
await page.getByRole('tab', { name: 'Elements' }).click();
await swgBElementsPoolItem.click({ button: 'right' });
await page
@@ -107,7 +103,7 @@ test.describe('Stacked Plot', () => {
await page.click('button[title="Edit"]');
await selectInspectorTab(page, 'Elements');
await page.getByRole('tab', { name: 'Elements' }).click();
const stackedPlotItem1 = page.locator('.c-plot--stacked-container').nth(0);
const stackedPlotItem2 = page.locator('.c-plot--stacked-container').nth(1);
@@ -152,7 +148,7 @@ test.describe('Stacked Plot', () => {
}) => {
await page.goto(stackedPlot.url);
await selectInspectorTab(page, 'Config');
await page.getByRole('tab', { name: 'Config' }).click();
// Click on the 1st plot
await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"] canvas`).nth(1).click();
@@ -193,7 +189,7 @@ test.describe('Stacked Plot', () => {
// Go into edit mode
await page.click('button[title="Edit"]');
await selectInspectorTab(page, 'Config');
await page.getByRole('tab', { name: 'Config' }).click();
// Click on canvas for the 1st plot
await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"]`).click();
@@ -238,7 +234,7 @@ test.describe('Stacked Plot', () => {
// Go into edit mode
await page.click('button[title="Edit"]');
await selectInspectorTab(page, 'Config');
await page.getByRole('tab', { name: 'Config' }).click();
let legendProperties = await page.locator('[aria-label="Legend Properties"]');
await legendProperties.locator('[title="Display legends per sub plot."]~div input').uncheck();

View File

@@ -32,7 +32,7 @@ const {
waitForPlotsToRender
} = require('../../../../appActions');
test.describe.fixme('Plot Tagging', () => {
test.describe('Plot Tagging', () => {
/**
* Given a canvas and a set of points, tags the points on the canvas.
* @param {import('@playwright/test').Page} page
@@ -41,7 +41,7 @@ test.describe.fixme('Plot Tagging', () => {
* @param {Number} yEnd a telemetry item with a plot
* @returns {Promise}
*/
async function createTags({ page, canvas, xEnd, yEnd }) {
async function createTags({ page, canvas, xEnd = 700, yEnd = 520 }) {
await canvas.hover({ trial: true });
//Alt+Shift Drag Start to select some points to tag
@@ -63,7 +63,7 @@ test.describe.fixme('Plot Tagging', () => {
await page.keyboard.up('Alt');
await page.keyboard.up('Shift');
//Wait for canvas to stablize.
//Wait for canvas to stabilize.
await canvas.hover({ trial: true });
// add some tags
@@ -90,15 +90,17 @@ test.describe.fixme('Plot Tagging', () => {
await expect(page.getByText('No tags to display for this item')).toBeVisible();
const canvas = page.locator('canvas').nth(1);
//Wait for canvas to stabilize.
await waitForPlotsToRender(page);
//Wait for canvas to stablize.
await expect(canvas).toBeInViewport();
await canvas.hover({ trial: true });
// click on the tagged plot point
await canvas.click({
position: {
x: 325,
y: 377
x: 100,
y: 100
}
});
@@ -146,7 +148,10 @@ test.describe.fixme('Plot Tagging', () => {
// wait for plots to load
await waitForPlotsToRender(page);
await page.getByText('Annotations').click();
await expect(page.getByRole('tab', { name: 'Annotations' })).not.toHaveClass(/is-current/);
await page.getByRole('tab', { name: 'Annotations' }).click();
await expect(page.getByRole('tab', { name: 'Annotations' })).toHaveClass(/is-current/);
await expect(page.getByText('No tags to display for this item')).toBeVisible();
const canvas = page.locator('canvas').nth(1);
@@ -167,12 +172,11 @@ test.describe.fixme('Plot Tagging', () => {
});
test('Tags work with Overlay Plots', async ({ page }) => {
test.slow();
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6822'
});
//Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
test.slow();
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
@@ -181,13 +185,19 @@ test.describe.fixme('Plot Tagging', () => {
const alphaSineWave = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Alpha Sine Wave',
parent: overlayPlot.uuid
parent: overlayPlot.uuid,
customParameters: {
'[aria-label="Data Rate (hz)"]': '0.01'
}
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Beta Sine Wave',
parent: overlayPlot.uuid
parent: overlayPlot.uuid,
customParameters: {
'[aria-label="Data Rate (hz)"]': '0.02'
}
});
await page.goto(overlayPlot.url);
@@ -200,9 +210,7 @@ test.describe.fixme('Plot Tagging', () => {
await createTags({
page,
canvas,
xEnd: 700,
yEnd: 480
canvas
});
await setFixedTimeMode(page);
@@ -232,15 +240,15 @@ test.describe.fixme('Plot Tagging', () => {
test('Tags work with Plot View of telemetry items', async ({ page }) => {
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator'
type: 'Sine Wave Generator',
customParameters: {
'[aria-label="Data Rate (hz)"]': '0.01'
}
});
const canvas = page.locator('canvas').nth(1);
await createTags({
page,
canvas,
xEnd: 700,
yEnd: 480
canvas
});
await basicTagsTests(page);
});
@@ -253,13 +261,19 @@ test.describe.fixme('Plot Tagging', () => {
const alphaSineWave = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Alpha Sine Wave',
parent: stackedPlot.uuid
parent: stackedPlot.uuid,
customParameters: {
'[aria-label="Data Rate (hz)"]': '0.01'
}
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Beta Sine Wave',
parent: stackedPlot.uuid
parent: stackedPlot.uuid,
customParameters: {
'[aria-label="Data Rate (hz)"]': '0.02'
}
});
await page.goto(stackedPlot.url);
@@ -270,7 +284,7 @@ test.describe.fixme('Plot Tagging', () => {
page,
canvas,
xEnd: 700,
yEnd: 215
yEnd: 240
});
await basicTagsTests(page);
await testTelemetryItem(page, alphaSineWave);

View File

@@ -153,7 +153,6 @@ test.describe('Time conductor input fields real-time mode', () => {
await expect(page.locator('.c-compact-tc__setting-value.icon-plus')).toContainText('00:00:01');
// Verify url parameters persist after mode switch
await page.waitForNavigation({ waitUntil: 'networkidle' });
expect(page.url()).toContain(`startDelta=${startDelta}`);
expect(page.url()).toContain(`endDelta=${endDelta}`);
});

View File

@@ -25,12 +25,15 @@ const {
openObjectTreeContextMenu,
createDomainObjectWithDefaults
} = require('../../../../appActions');
import { MISSION_TIME } from '../../../../constants';
test.describe('Timer', () => {
let timer;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
await assertTimerElements(page, timer);
});
test('Can perform actions on the Timer', async ({ page }) => {
@@ -63,6 +66,70 @@ test.describe('Timer', () => {
});
});
test.describe('Timer with target date', () => {
let timer;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
await assertTimerElements(page, timer);
});
// Override clock
test.use({
clockOptions: {
now: MISSION_TIME,
shouldAdvanceTime: true
}
});
test('Can count down to a target date', async ({ page }) => {
// Set the target date to 2024-11-24 03:30:00
await page.getByTitle('More options').click();
await page.getByRole('menuitem', { name: /Edit Properties.../ }).click();
await page.getByPlaceholder('YYYY-MM-DD').fill('2024-11-24');
await page.locator('input[name="hour"]').fill('3');
await page.locator('input[name="min"]').fill('30');
await page.locator('input[name="sec"]').fill('00');
await page.getByLabel('Save').click();
// Get the current timer seconds value
const timerSecValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1);
await expect(page.locator('.c-timer__direction')).toHaveClass(/icon-minus/);
// Wait for the timer to count down and assert
await expect
.poll(async () => {
const newTimerValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1);
return Number(newTimerValue);
})
.toBeLessThan(Number(timerSecValue));
});
test('Can count up from a target date', async ({ page }) => {
// Set the target date to 2020-11-23 03:30:00
await page.getByTitle('More options').click();
await page.getByRole('menuitem', { name: /Edit Properties.../ }).click();
await page.getByPlaceholder('YYYY-MM-DD').fill('2020-11-23');
await page.locator('input[name="hour"]').fill('3');
await page.locator('input[name="min"]').fill('30');
await page.locator('input[name="sec"]').fill('00');
await page.getByLabel('Save').click();
// Get the current timer seconds value
const timerSecValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1);
await expect(page.locator('.c-timer__direction')).toHaveClass(/icon-plus/);
// Wait for the timer to count up and assert
await expect
.poll(async () => {
const newTimerValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1);
return Number(newTimerValue);
})
.toBeGreaterThan(Number(timerSecValue));
});
});
/**
* Actions that can be performed on a timer from context menus.
* @typedef {'Start' | 'Stop' | 'Pause' | 'Restart at 0'} TimerAction
@@ -141,14 +208,17 @@ function buttonTitleFromAction(action) {
* @param {TimerAction} action
*/
async function assertTimerStateAfterAction(page, action) {
const timerValue = page.locator('.c-timer__value');
let timerStateClass;
switch (action) {
case 'Start':
case 'Restart at 0':
timerStateClass = 'is-started';
expect(await timerValue.innerText()).toBe('0D 00:00:00');
break;
case 'Stop':
timerStateClass = 'is-stopped';
expect(await timerValue.innerText()).toBe('--:--:--');
break;
case 'Pause':
timerStateClass = 'is-paused';
@@ -157,3 +227,25 @@ async function assertTimerStateAfterAction(page, action) {
await expect.soft(page.locator('.c-timer')).toHaveClass(new RegExp(timerStateClass));
}
/**
* Assert that all the major components of a timer are present in the DOM.
* @param {import('@playwright/test').Page} page
* @param {import('../../../../appActions').CreatedObjectInfo} timer
*/
async function assertTimerElements(page, timer) {
const timerElement = page.locator('.c-timer');
const resetButton = page.getByRole('button', { name: 'Reset' });
const pausePlayButton = page
.getByRole('button', { name: 'Pause' })
.or(page.getByRole('button', { name: 'Start' }));
const timerDirectionIcon = page.locator('.c-timer__direction');
const timerValue = page.locator('.c-timer__value');
expect(await page.locator('.l-browse-bar__object-name').innerText()).toBe(timer.name);
expect(timerElement).toBeAttached();
expect(resetButton).toBeAttached();
expect(pausePlayButton).toBeAttached();
expect(timerDirectionIcon).toBeAttached();
expect(timerValue).toBeAttached();
}

View File

@@ -59,59 +59,57 @@ test.describe('Recent Objects', () => {
await page.mouse.move(0, 100);
await page.mouse.up();
});
test.fixme(
'Navigated objects show up in recents, object renames and deletions are reflected',
async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6818'
});
test('Navigated objects show up in recents, object renames and deletions are reflected', async ({
page
}) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6818'
});
// Verify that both created objects appear in the list and are in the correct order
await assertInitialRecentObjectsListState();
// Verify that both created objects appear in the list and are in the correct order
await assertInitialRecentObjectsListState();
// Navigate to the folder by clicking on the main object name in the recent objects list item
await page.getByRole('listitem', { name: folderA.name }).getByText(folderA.name).click();
await page.waitForURL(`**/${folderA.uuid}?*`);
expect(recentObjectsList.getByRole('listitem').nth(0).getByText(folderA.name)).toBeTruthy();
// Navigate to the folder by clicking on the main object name in the recent objects list item
await page.getByRole('listitem', { name: folderA.name }).getByText(folderA.name).click();
await page.waitForURL(`**/${folderA.uuid}?*`);
expect(recentObjectsList.getByRole('listitem').nth(0).getByText(folderA.name)).toBeTruthy();
// Rename
folderA.name = `${folderA.name}-NEW!`;
await page.locator('.l-browse-bar__object-name').fill('');
await page.locator('.l-browse-bar__object-name').fill(folderA.name);
await page.keyboard.press('Enter');
// Rename
folderA.name = `${folderA.name}-NEW!`;
await page.locator('.l-browse-bar__object-name').fill('');
await page.locator('.l-browse-bar__object-name').fill(folderA.name);
await page.keyboard.press('Enter');
// Verify rename has been applied in recent objects list item and objects paths
expect(
await page
.getByRole('navigation', {
name: clock.name
})
.locator('a')
.filter({
hasText: folderA.name
})
.count()
).toBeGreaterThan(0);
expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy();
// Delete
await page.click('button[title="Show selected item in tree"]');
// Delete the folder via the left tree pane treeitem context menu
// Verify rename has been applied in recent objects list item and objects paths
expect(
await page
.getByRole('treeitem', { name: new RegExp(folderA.name) })
.getByRole('navigation', {
name: clock.name
})
.locator('a')
.click({
button: 'right'
});
await page.getByRole('menuitem', { name: /Remove/ }).click();
await page.getByRole('button', { name: 'OK' }).click();
.filter({
hasText: folderA.name
})
.count()
).toBeGreaterThan(0);
expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy();
// Verify that the folder and clock are no longer in the recent objects list
await expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeHidden();
await expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeHidden();
}
);
await page.click('button[title="Show selected item in tree"]');
// Delete the folder via the left tree pane treeitem context menu
await page
.getByRole('treeitem', { name: new RegExp(folderA.name) })
.locator('a')
.click({
button: 'right'
});
await page.getByRole('menuitem', { name: /Remove/ }).click();
await page.getByRole('button', { name: 'OK' }).click();
// Verify that the folder and clock are no longer in the recent objects list
await expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeHidden();
await expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeHidden();
});
test('Clicking on an object in the path of a recent object navigates to the object', async ({
page,

View File

@@ -0,0 +1,78 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
This test suite is dedicated to tests for renaming objects, and their global application effects.
*/
const { test, expect } = require('../../baseFixtures.js');
const {
createDomainObjectWithDefaults,
renameObjectFromContextMenu
} = require('../../appActions.js');
test.describe('Renaming objects', () => {
test.beforeEach(async ({ page }) => {
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
});
test('When renaming objects, the browse bar and various components all update', async ({
page
}) => {
const folder = await createDomainObjectWithDefaults(page, {
type: 'Folder'
});
// Create a new 'Clock' object with default settings
const clock = await createDomainObjectWithDefaults(page, {
type: 'Clock',
parent: folder.uuid
});
// Rename
clock.name = `${clock.name}-NEW!`;
await renameObjectFromContextMenu(page, clock.url, clock.name);
// check inspector for new name
const titleValue = await page
.getByLabel('Title inspector properties')
.getByLabel('inspector property value')
.textContent();
expect(titleValue).toBe(clock.name);
// check browse bar for new name
await expect(page.locator(`.l-browse-bar >> text=${clock.name}`)).toBeVisible();
// check tree item for new name
await expect(
page.getByRole('listitem', {
name: clock.name
})
).toBeVisible();
// check recent objects for new name
await expect(
page.getByRole('navigation', {
name: clock.name
})
).toBeVisible();
// check title for new name
const title = await page.title();
expect(title).toBe(clock.name);
});
});

View File

@@ -24,7 +24,7 @@
*/
const { test, expect } = require('../../pluginFixtures');
const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../appActions');
const { createDomainObjectWithDefaults } = require('../../appActions');
const { v4: uuid } = require('uuid');
test.describe('Grand Search', () => {
@@ -61,7 +61,7 @@ test.describe('Grand Search', () => {
`Clock D ${myItemsFolderName} Red Folder Blue Folder`
);
// Click the Elements pool to dismiss the search menu
await selectInspectorTab(page, 'Elements');
await page.getByRole('tab', { name: 'Elements' }).click();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
@@ -77,11 +77,11 @@ test.describe('Grand Search', () => {
// Click [aria-label="OpenMCT Search"] a >> nth=0
await page.locator('[aria-label="Search Result"] >> nth=0').click();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeInViewport();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('foo');
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toBeInViewport();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
await page
@@ -175,7 +175,8 @@ test.describe('Grand Search', () => {
let networkRequests = [];
page.on('request', (request) => {
const searchRequest = request.url().endsWith('_find');
const searchRequest =
request.url().endsWith('_find') || request.url().includes('by_keystring');
const fetchRequest = request.resourceType() === 'fetch';
if (searchRequest && fetchRequest) {
networkRequests.push(request);

View File

@@ -50,8 +50,6 @@ test('Verify that the create button appears and that the Folder Domain Object is
test('Verify that My Items Tree appears @ipad', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
//Test.slow annotation is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
test.slow();
//Go to baseURL
await page.goto('./');

View File

@@ -23,7 +23,7 @@
const { test, expect } = require('../../pluginFixtures.js');
const {
createDomainObjectWithDefaults,
openObjectTreeContextMenu
renameObjectFromContextMenu
} = require('../../appActions.js');
test.describe('Main Tree', () => {
@@ -249,18 +249,3 @@ async function expandTreePaneItemByName(page, name) {
});
await treeItem.locator('.c-disclosure-triangle').click();
}
/**
* @param {import('@playwright/test').Page} page
* @param {string} myItemsFolderName
* @param {string} url
* @param {string} newName
*/
async function renameObjectFromContextMenu(page, url, newName) {
await openObjectTreeContextMenu(page, url);
await page.click('li:text("Edit Properties")');
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
await nameInput.fill('');
await nameInput.fill(newName);
await page.click('[aria-label="Save"]');
}

View File

@@ -131,8 +131,8 @@ test.describe('Performance tests', () => {
await page.evaluate(() => window.performance.mark('new-notebook-entry-created'));
// Enter Notebook Entry text
await page.locator('div.c-ne__text').last().fill('New Entry');
await page.keyboard.press('Enter');
await page.getByLabel('Notebook Entry Input').last().fill('New Entry');
await page.locator('.c-ne__save-button').click();
await page.evaluate(() => window.performance.mark('new-notebook-entry-filled'));
//Individual Notebook Entry Search

View File

@@ -1,121 +0,0 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
This test suite is an initial example for memory leak testing using performance. This configuration and execution must
be kept separate from the traditional performance measurements to avoid any "observer" effects associated with tracing
or profiling playwright and/or the browser.
Based on a pattern identified in https://github.com/trentmwillis/devtools-protocol-demos/blob/master/testing-demos/memory-leak-by-heap.js
and https://github.com/paulirish/automated-chrome-profiling/issues/3
Best path forward: https://github.com/cowchimp/headless-devtools/blob/master/src/Memory/example.js
*/
const { test, expect } = require('@playwright/test');
const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
// eslint-disable-next-line playwright/no-skipped-test
test.describe.skip('Memory Performance tests', () => {
test.beforeEach(async ({ page, browser }, testInfo) => {
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
// Click a:has-text("My Items")
await page.locator('a:has-text("My Items")').click({
button: 'right'
});
// Click text=Import from JSON
await page.locator('text=Import from JSON').click();
// Upload Performance Display Layout.json
await page.setInputFiles('#fileElem', filePath);
// Click text=OK
await page.locator('text=OK').click();
await expect(
page.locator('a:has-text("Performance Display Layout Display Layout")')
).toBeVisible();
});
test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => {
await page.goto('./', { waitUntil: 'networkidle' });
// To to Search Available after Launch
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill Search input
await page
.locator('[aria-label="OpenMCT Search"] input[type="search"]')
.fill('Performance Display Layout');
//Search Result Appears and is clicked
await Promise.all([
page.waitForNavigation(),
page.locator('a:has-text("Performance Display Layout")').first().click()
]);
//Time to Example Imagery Frame loads within Display Layout
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' });
//Time to Example Imagery object loads
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' });
const client = await page.context().newCDPSession(page);
await client.send('HeapProfiler.enable');
await client.send('HeapProfiler.startSampling');
// await client.send('HeapProfiler.collectGarbage');
await client.send('Performance.enable');
let performanceMetricsBefore = await client.send('Performance.getMetrics');
console.log(performanceMetricsBefore.metrics);
//await client.send('Performance.disable');
//Open Large view
await page.locator('button:has-text("Large View")').click();
await client.send('HeapProfiler.takeHeapSnapshot');
//Time to Imagery Rendered in Large Frame
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' });
//Time to Example Imagery object loads
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' });
// Click Close Icon
await page.locator('.c-click-icon').click();
//Time to Example Imagery Frame loads within Display Layout
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' });
//Time to Example Imagery object loads
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' });
await client.send('HeapProfiler.collectGarbage');
//await client.send('Performance.enable');
let performanceMetricsAfter = await client.send('Performance.getMetrics');
console.log(performanceMetricsAfter.metrics);
//await client.send('Performance.disable');
});
});

View File

@@ -0,0 +1,299 @@
/*****************************************************************************
* 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.
*****************************************************************************/
const { test, expect } = require('@playwright/test');
const memoryLeakFilePath = 'e2e/test-data/memory-leak-detection.json';
/**
* Executes tests to verify that views are not leaking memory on navigation away. This sort of
* memory leak is generally caused by a failure to clean up registered listeners.
*
* These tests are executed on a set of pre-built displays loaded from ../test-data/memory-leak-detection.json.
*
* In order to modify the test data set:
* 1. Run Open MCT locally (npm start)
* 2. Right click on a folder in the tree, and select "Import From JSON"
* 3. In the subsequent dialog, select the file ../test-data/memory-leak-detection.json
* 4. Click "OK"
* 5. Modify test objects as desired
* 6. Right click on the "Memory Leak Detection" folder, and select "Export to JSON"
* 7. Copy the exported file to ../test-data/memory-leak-detection.json
*
*/
const NAV_LEAK_TIMEOUT = 10 * 1000; // 10s
test.describe('Navigation memory leak is not detected in', () => {
test.beforeEach(async ({ page }) => {
// Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.locator('a:has-text("My Items")').click({
button: 'right'
});
await page.locator('text=Import from JSON').click();
// Upload memory-leak-detection.json
await page.setInputFiles('#fileElem', memoryLeakFilePath);
await page.locator('text=OK').click();
await expect(page.locator('a:has-text("Memory Leak Detection")')).toBeVisible();
});
test('plot view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'overlay-plot-single-1hz-swg', {
timeout: NAV_LEAK_TIMEOUT
});
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('stacked plot view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'stacked-plot-single-1hz-swg', {
timeout: NAV_LEAK_TIMEOUT
});
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('LAD table view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-single-1hz-swg', {
timeout: NAV_LEAK_TIMEOUT
});
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('LAD table set', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-set-single-1hz-swg', {
timeout: NAV_LEAK_TIMEOUT
});
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
//TODO: Figure out why using the `table-row` component inside the `table` component leaks TelemetryTableRow objects
test('telemetry table view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'telemetry-table-single-1hz-swg',
{
timeout: NAV_LEAK_TIMEOUT
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
//TODO: Figure out why using the `SideBar` component inside the leaks Notebook objects
test('notebook view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'notebook-memory-leak-detection-test',
{
timeout: NAV_LEAK_TIMEOUT
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('display layout of a single SWG alphanumeric', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'display-layout-single-1hz-swg',
{
timeout: NAV_LEAK_TIMEOUT
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('display layout of a single SWG plot', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'display-layout-single-overlay-plot',
{
timeout: NAV_LEAK_TIMEOUT
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
//TODO: Figure out why `svg` in the CompassRose component leaks imagery
test('example imagery view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'example-imagery-memory-leak-test',
{
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('display layout of example imagery views', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'display-layout-images-memory-leak-test',
{
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('display layout with plots of swgs, alphanumerics, and condition sets, ', async ({
page
}) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'display-layout-simple-telemetry',
{
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('flexible layout with plots of swgs', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'flexible-layout-plots-memory-leak-test',
{
timeout: NAV_LEAK_TIMEOUT
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('flexible layout of example imagery views', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'flexible-layout-images-memory-leak-test',
{
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('tabbed view of display layouts and time strips', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'tab-view-simple-memory-leak-test',
{
timeout: NAV_LEAK_TIMEOUT * 6 * 2 // 2 min
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('time strip view of telemetry', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'time-strip-telemetry-memory-leak-test',
{
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
/**
*
* @param {import('@playwright/test').Page} page
* @param {*} objectName
* @returns
*/
async function navigateToObjectAndDetectMemoryLeak(page, objectName) {
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill Search input
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill(objectName);
//Search Result Appears and is clicked
await Promise.all([
page.locator(`div.c-gsearch-result__title:has-text("${objectName}")`).first().click(),
page.waitForNavigation()
]);
// Register a finalization listener on the root node for the view. This tends to be the last thing to be
// garbage collected since it has either direct or indirect references to all resources used by the view. Therefore it's a pretty good proxy
// 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,
'navigatedObject',
window.openmct.layout.$refs.browseObject.$refs.objectViewWrapper.firstChild
);
});
});
// Nav back to folder
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
await page.waitForNavigation();
// This next code block blocks until the finalization listener is called and the gcPromise resolved. This means that the root node for the view has been garbage collected.
// In the event that the root node is not garbage collected, the gcPromise will never resolve and the test will time out.
await page.evaluate(() => {
const gcPromise = window.gcPromise;
window.gcPromise = null;
// Manually invoke the garbage collector once all references are removed.
window.gc();
return gcPromise;
});
// Clean up the finalization registry since we don't need it any more.
await page.evaluate(() => {
window.fr = null;
});
// If we get here without timing out, it means the garbage collection promise resolved and the test passed.
return true;
}
});

View File

@@ -0,0 +1,273 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
Tests to verify plot tagging performance.
*/
const { test, expect } = require('../../pluginFixtures');
const {
createDomainObjectWithDefaults,
setRealTimeMode,
setFixedTimeMode,
waitForPlotsToRender
} = require('../../appActions');
test.describe('Plot Tagging Performance', () => {
/**
* 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
* @returns {Promise}
*/
async function createTags({ page, canvas, xEnd = 700, yEnd = 520 }) {
await canvas.hover({ trial: true });
//Alt+Shift Drag Start to select some points to tag
await page.keyboard.down('Alt');
await page.keyboard.down('Shift');
await canvas.dragTo(canvas, {
sourcePosition: {
x: 1,
y: 1
},
targetPosition: {
x: xEnd,
y: yEnd
}
});
//Alt Drag End
await page.keyboard.up('Alt');
await page.keyboard.up('Shift');
//Wait for canvas to stabilize.
await canvas.hover({ trial: true });
// add some tags
await page.getByText('Annotations').click();
await page.getByRole('button', { name: /Add Tag/ }).click();
await page.getByPlaceholder('Type to select tag').click();
await page.getByText('Driving').click();
await page.getByRole('button', { name: /Add Tag/ }).click();
await page.getByPlaceholder('Type to select tag').click();
await page.getByText('Science').click();
}
/**
* Given a telemetry item (e.g., a Sine Wave Generator) with a plot, tests that the plot can be tagged.
* @param {import('@playwright/test').Page} page
* @param {import('../../../../appActions').CreatedObjectInfo} telemetryItem a telemetry item with a plot
* @returns {Promise}
*/
async function testTelemetryItem(page, telemetryItem) {
// Check that telemetry item also received the tag
await page.goto(telemetryItem.url);
await expect(page.getByText('No tags to display for this item')).toBeVisible();
const canvas = page.locator('canvas').nth(1);
//Wait for canvas to stabilize.
await canvas.hover({ trial: true });
// click on the tagged plot point
await canvas.click({
position: {
x: 100,
y: 100
}
});
await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Driving')).toBeHidden();
}
/**
* Given a page, tests that tags are searchable, deletable, and persist across reloads.
* @param {import('@playwright/test').Page} page
* @returns {Promise}
*/
async function basicTagsTests(page) {
// Search for Driving
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Clicking elsewhere should cause annotation selection to be cleared
await expect(page.getByText('No tags to display for this item')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('driv');
// click on the search result
await page
.getByRole('searchbox', { name: 'OpenMCT Search' })
.getByText(/Sine Wave/)
.first()
.click();
// Delete Driving
await page.hover('[aria-label="Tag"]:has-text("Driving")');
await page.locator('[aria-label="Remove tag Driving"]').click();
// Search for Science
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
await expect(page.locator('[aria-label="Search Result"]').nth(0)).toContainText('Science');
await expect(page.locator('[aria-label="Search Result"]').nth(0)).not.toContainText('Drilling');
// Search for Driving
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('driv');
await expect(page.getByText('No results found')).toBeVisible();
//Reload Page
await page.reload({ waitUntil: 'domcontentloaded' });
// wait for plots to load
await waitForPlotsToRender(page);
await page.getByText('Annotations').click();
await expect(page.getByText('No tags to display for this item')).toBeVisible();
const canvas = page.locator('canvas').nth(1);
// click on the tagged plot point
await canvas.click({
position: {
x: 100,
y: 100
}
});
await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Driving')).toBeHidden();
}
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Tags work with Overlay Plots', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6822'
});
//Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
test.slow();
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
const alphaSineWave = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Alpha Sine Wave',
parent: overlayPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Beta Sine Wave',
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url);
let canvas = page.locator('canvas').nth(1);
// Switch to real-time mode
// Adding tags should pause the plot
await setRealTimeMode(page);
await createTags({
page,
canvas
});
await setFixedTimeMode(page);
await basicTagsTests(page);
await testTelemetryItem(page, alphaSineWave);
// set to real time mode
await setRealTimeMode(page);
// Search for Science
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
// click on the search result
await page
.getByRole('searchbox', { name: 'OpenMCT Search' })
.getByText('Alpha Sine Wave')
.first()
.click();
// wait for plots to load
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
// expect plot to be paused
await expect(page.locator('[title="Resume displaying real-time data"]')).toBeVisible();
await setFixedTimeMode(page);
});
test('Tags work with Plot View of telemetry items', async ({ page }) => {
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator'
});
const canvas = page.locator('canvas').nth(1);
await createTags({
page,
canvas
});
await basicTagsTests(page);
});
test('Tags work with Stacked Plots', async ({ page }) => {
const stackedPlot = await createDomainObjectWithDefaults(page, {
type: 'Stacked Plot'
});
const alphaSineWave = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Alpha Sine Wave',
parent: stackedPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Beta Sine Wave',
parent: stackedPlot.uuid
});
await page.goto(stackedPlot.url);
const canvas = page.locator('canvas').nth(1);
await createTags({
page,
canvas,
xEnd: 700,
yEnd: 240
});
await basicTagsTests(page);
await testTelemetryItem(page, alphaSineWave);
});
});

View File

@@ -1,63 +0,0 @@
/* eslint-disable no-undef */
/*****************************************************************************
* 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.
*****************************************************************************/
/* global __dirname */
/*
Collection of Visual Tests set to run with modified init scripts to inject plugins not otherwise available in the default contexts.
These should only use functional expect statements to verify assumptions about the state
in a test and not for functional verification of correctness. Visual tests are not supposed
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
*/
// eslint-disable-next-line no-unused-vars
const { test, expect } = require('../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../appActions');
const percySnapshot = require('@percy/playwright');
const path = require('path');
const CUSTOM_NAME = 'CUSTOM_NAME';
test.describe('Visual - addInit', () => {
test.use({
clockOptions: {
now: 0, //Set browser clock to UNIX Epoch
shouldAdvanceTime: false //Don't advance the clock
}
});
test('Restricted Notebook is visually correct @addInit @unstable', async ({ page, theme }) => {
await page.addInitScript({
path: path.join(__dirname, '../../helper', './addInitRestrictedNotebook.js')
});
//Go to baseURL
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
await page.getByTitle('Collapse Browse Pane').click();
await createDomainObjectWithDefaults(page, { type: CUSTOM_NAME });
// Take a snapshot of the newly created CUSTOM_NAME notebook
await percySnapshot(page, `Restricted Notebook with CUSTOM_NAME (theme: '${theme}')`);
});
});

View File

@@ -0,0 +1,53 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
Tests the branding associated with the default deployment. At least the about modal for now
*/
const { test, expect } = require('../../../pluginFixtures');
const percySnapshot = require('@percy/playwright');
const VISUAL_URL = require('../../../constants').VISUAL_URL;
test.describe('Visual - Branding', () => {
test.beforeEach(async ({ page }) => {
//Go to baseURL and Hide Tree
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
});
test('Visual - About Modal', async ({ page, theme }) => {
// Click About button
await page.click('.l-shell__app-logo');
// Modify the Build information in 'about' to be consistent run-over-run
const versionInformationLocator = page.locator('ul.t-info.l-info.s-info').first();
await expect(versionInformationLocator).toBeEnabled();
await versionInformationLocator.evaluate(
(node) =>
(node.innerHTML =
'<li>Version: visual-snapshot</li> <li>Build Date: Mon Nov 15 2021 08:07:51 GMT-0800 (Pacific Standard Time)</li> <li>Revision: 93049cdbc6c047697ca204893db9603b864b8c9f</li> <li>Branch: master</li>')
);
// Take a snapshot of the About modal
await percySnapshot(page, `About (theme: '${theme}')`);
});
});

View File

@@ -20,16 +20,37 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
* This test suite template is to be used when verifying Test Data files found in /e2e/test-data/
*/
const { test } = require('../../../pluginFixtures.js');
const { VISUAL_URL, MISSION_TIME } = require('../../../constants.js');
const percySnapshot = require('@percy/playwright');
const { test } = require('../../baseFixtures');
//Declare the scope of the visual test
const inspectorPane = '.l-shell__pane-inspector';
test.describe('recycled_local_storage @localStorage', () => {
//We may want to do some additional level of verification of this file. For now, we just verify that it exists and can be used in a test suite.
test.use({ storageState: './e2e/test-data/recycled_local_storage.json' });
test('Can use recycled_local_storage file', async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
test.describe('Visual - Controlled Clock', () => {
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
});
test.use({
storageState: './e2e/test-data/overlay_plot_with_delay_storage.json',
clockOptions: {
now: MISSION_TIME,
shouldAdvanceTime: true
}
});
test('Inspector from overlay_plot_with_delay_storage @localStorage', async ({ page, theme }) => {
//Expand the Inspector Pane
await page.getByRole('button', { name: 'Inspect' }).click();
await percySnapshot(page, `Inspector view of overlayPlot (theme: ${theme})`, {
scope: inspectorPane
});
//Open Annotations Tab
await page.getByRole('tab', { name: 'Annotations' }).click();
await percySnapshot(page, `Inspector view of Annotations Tab (theme: ${theme})`, {
scope: inspectorPane
});
});
});

View File

@@ -21,15 +21,25 @@
*****************************************************************************/
const { test } = require('../../../pluginFixtures.js');
const { createDomainObjectWithDefaults } = require('../../../appActions.js');
const {
expandTreePaneItemByName,
createDomainObjectWithDefaults
} = require('../../../appActions.js');
const VISUAL_URL = require('../../../constants.js').VISUAL_URL;
const percySnapshot = require('@percy/playwright');
test.describe('Visual - Tree Pane', () => {
test('Tree pane in various states @unstable', async ({ page, theme, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
//Declare the scope of the visual test
const treePane = "[role=tree][aria-label='Main Tree']";
test.describe('Visual - Tree Pane', () => {
test('Tree pane in various states', async ({ page, theme, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
//Open Tree
await page.getByRole('button', { name: 'Browse' }).click();
//Create a Folder Structure
const foo = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Foo Folder'
@@ -57,8 +67,6 @@ test.describe('Visual - Tree Pane', () => {
name: 'Z Clock'
});
const treePane = "[role=tree][aria-label='Main Tree']";
await percySnapshot(page, `Tree Pane w/ collapsed tree (theme: ${theme})`, {
scope: treePane
});
@@ -88,14 +96,3 @@ test.describe('Visual - Tree Pane', () => {
});
});
});
/**
* @param {import('@playwright/test').Page} page
* @param {string} name
*/
async function expandTreePaneItemByName(page, name) {
const treePane = page.getByTestId('tree-pane');
const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
const expandTriangle = treeItem.locator('.c-disclosure-triangle');
await expandTriangle.click();
}

View File

@@ -21,34 +21,35 @@
*****************************************************************************/
/*
Collection of Visual Tests set to run in a default context. The tests within this suite
are only meant to run against openmct started by `npm start` within the
`./e2e/playwright-visual.config.js` file.
Collection of Visual Tests set to run with browser clock manipulate made possible with the
clockOptions plugin fixture.
*/
const { VISUAL_URL, MISSION_TIME } = require('../../constants');
const { test, expect } = require('../../pluginFixtures');
const percySnapshot = require('@percy/playwright');
test.describe('Visual - Controlled Clock @localStorage', () => {
test.describe('Visual - Controlled Clock', () => {
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
});
test.use({
storageState: './e2e/test-data/VisualTestData_storage.json',
storageState: './e2e/test-data/overlay_plot_with_delay_storage.json',
clockOptions: {
now: 0, //Set browser clock to UNIX Epoch
now: MISSION_TIME,
shouldAdvanceTime: false //Don't advance the clock
}
});
test('Overlay Plot Loading Indicator @localStorage', async ({ page, theme }) => {
// Go to baseURL
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
await page.getByTitle('Collapse Browse Pane').click();
await page.locator('a:has-text("Unnamed Overlay Plot Overlay Plot")').click();
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
await page.locator('a').filter({ hasText: 'Overlay Plot with 5s Delay' }).click();
//Ensure that we're on the Unnamed Overlay Plot object
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
await expect(page.locator('.l-browse-bar__object-name')).toContainText(
'Overlay Plot with 5s Delay'
);
//Wait for canvas to be rendered and stop animating
//Wait for canvas to be rendered and stop animating, but plot should not be loaded. Cannot use waitForPlotsToRender
await page.locator('canvas >> nth=1').hover({ trial: true });
//Take snapshot of Sine Wave Generator within Overlay Plot

View File

@@ -1,173 +0,0 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
Collection of Visual Tests set to run in a default context. The tests within this suite
are only meant to run against openmct started by `npm start` within the
`./e2e/playwright-visual.config.js` file.
These should only use functional expect statements to verify assumptions about the state
in a test and not for functional verification of correctness. Visual tests are not supposed
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
*/
const { test, expect } = require('../../pluginFixtures');
const percySnapshot = require('@percy/playwright');
const { createDomainObjectWithDefaults } = require('../../appActions');
test.describe('Visual - Default', () => {
test.beforeEach(async ({ page }) => {
//Go to baseURL and Hide Tree
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
await page.getByTitle('Collapse Browse Pane').click();
});
test.use({
clockOptions: {
now: 0, //Set browser clock to UNIX Epoch
shouldAdvanceTime: false //Don't advance the clock
}
});
test('Visual - Root and About', async ({ page, theme }) => {
// Verify that Create button is actionable
await expect(page.locator('button:has-text("Create")')).toBeEnabled();
// Take a snapshot of the Dashboard
await percySnapshot(page, `Root (theme: '${theme}')`);
// Click About button
await page.click('.l-shell__app-logo');
// Modify the Build information in 'about' to be consistent run-over-run
const versionInformationLocator = page.locator('ul.t-info.l-info.s-info').first();
await expect(versionInformationLocator).toBeEnabled();
await versionInformationLocator.evaluate(
(node) =>
(node.innerHTML =
'<li>Version: visual-snapshot</li> <li>Build Date: Mon Nov 15 2021 08:07:51 GMT-0800 (Pacific Standard Time)</li> <li>Revision: 93049cdbc6c047697ca204893db9603b864b8c9f</li> <li>Branch: master</li>')
);
// Take a snapshot of the About modal
await percySnapshot(page, `About (theme: '${theme}')`);
});
test('Visual - Default Condition Set @unstable', async ({ page, theme }) => {
await createDomainObjectWithDefaults(page, { type: 'Condition Set' });
// Take a snapshot of the newly created Condition Set object
await percySnapshot(page, `Default Condition Set (theme: '${theme}')`);
});
test('Visual - Default Condition Widget @unstable', async ({ page, theme }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5349'
});
await createDomainObjectWithDefaults(page, { type: 'Condition Widget' });
// Take a snapshot of the newly created Condition Widget object
await percySnapshot(page, `Default Condition Widget (theme: '${theme}')`);
});
test('Visual - Time Conductor start time is less than end time', async ({ page, theme }) => {
const year = new Date().getFullYear();
let startDate = 'xxxx-01-01 01:00:00.000Z';
startDate = year + startDate.substring(4);
let endDate = 'xxxx-01-01 02:00:00.000Z';
endDate = year + endDate.substring(4);
await page.getByRole('button', { name: 'Time Conductor Settings' }).click();
await page.locator('input[type="text"]').nth(1).fill(endDate.toString());
await page.locator('input[type="text"]').first().fill(startDate.toString());
// verify no error msg
await percySnapshot(page, `Default Time conductor (theme: '${theme}')`);
startDate = year + 1 + startDate.substring(4);
await page.locator('input[type="text"]').first().fill(startDate.toString());
await page.locator('input[type="text"]').nth(1).click();
// verify error msg for start time (unable to capture snapshot of popup)
await percySnapshot(page, `Start time error (theme: '${theme}')`);
startDate = year - 1 + startDate.substring(4);
await page.locator('input[type="text"]').first().fill(startDate.toString());
endDate = year - 2 + endDate.substring(4);
await page.locator('input[type="text"]').nth(1).fill(endDate.toString());
await page.locator('input[type="text"]').first().click();
// verify error msg for end time (unable to capture snapshot of popup)
await percySnapshot(page, `End time error (theme: '${theme}')`);
});
test('Visual - Sine Wave Generator Form', async ({ page, theme }) => {
//Click the Create button
await page.click('button:has-text("Create")');
// Click text=Sine Wave Generator
await page.click('text=Sine Wave Generator');
await percySnapshot(page, `Default Sine Wave Generator Form (theme: '${theme}')`);
await page.locator('.field.control.l-input-sm input').first().click();
await page.locator('.field.control.l-input-sm input').first().fill('');
// Validate red x mark
await percySnapshot(page, `removed amplitude property value (theme: '${theme}')`);
});
test('Visual - Save Successful Banner @unstable', async ({ page, theme }) => {
await createDomainObjectWithDefaults(page, { type: 'Timer' });
await page.locator('.c-message-banner__message').hover({ trial: true });
await percySnapshot(page, `Banner message shown (theme: '${theme}')`);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
await percySnapshot(page, `Banner message gone (theme: '${theme}')`);
});
test('Visual - Display Layout Icon is correct', async ({ page, theme }) => {
//Click the Create button
await page.click('button:has-text("Create")');
//Hover on Display Layout option.
await page.locator('text=Display Layout').hover();
await percySnapshot(page, `Display Layout Create Menu (theme: '${theme}')`);
});
test('Visual - Default Gauge is correct @unstable', async ({ page, theme }) => {
await createDomainObjectWithDefaults(page, { type: 'Gauge' });
// Take a snapshot of the newly created Gauge object
await percySnapshot(page, `Default Gauge (theme: '${theme}')`);
});
});

View File

@@ -0,0 +1,101 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
Collection of Visual Tests set to run in a default context with default Plugins. The tests within this suite
are only meant to run against openmct's app.js started by `npm run start` within the
`./e2e/playwright-visual.config.js` file.
*/
const { test, expect } = require('../../pluginFixtures');
const percySnapshot = require('@percy/playwright');
const { createDomainObjectWithDefaults } = require('../../appActions');
const { VISUAL_URL } = require('../../constants');
test.describe('Visual - Default', () => {
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
});
test('Visual - Default Dashboard', async ({ page, theme }) => {
// Verify that Create button is actionable
await expect(page.locator('button:has-text("Create")')).toBeEnabled();
// Take a snapshot of the Dashboard
await percySnapshot(page, `Default Dashboard (theme: '${theme}')`);
});
test('Visual - Default Condition Set', async ({ page, theme }) => {
await createDomainObjectWithDefaults(page, {
type: 'Condition Set',
name: 'Default Condition Set'
});
// Take a snapshot of the newly created Condition Set object
await percySnapshot(page, `Default Condition Set (theme: '${theme}')`);
});
test('Visual - Default Condition Widget', async ({ page, theme }) => {
await createDomainObjectWithDefaults(page, {
type: 'Condition Widget',
name: 'Default Condition Widget'
});
// Take a snapshot of the newly created Condition Widget object
await percySnapshot(page, `Default Condition Widget (theme: '${theme}')`);
});
test('Visual - Sine Wave Generator Form', async ({ page, theme }) => {
//Click the Create button
await page.click('button:has-text("Create")');
// Click text=Sine Wave Generator
await page.click('text=Sine Wave Generator');
await percySnapshot(page, `Default Sine Wave Generator Form (theme: '${theme}')`);
await page.locator('.field.control.l-input-sm input').first().click();
await page.locator('.field.control.l-input-sm input').first().fill('');
// Validate red x mark
await percySnapshot(page, `removed amplitude property value (theme: '${theme}')`);
});
test('Visual - Display Layout Icon is correct in Create Menu', async ({ page, theme }) => {
// Click the Create button
await page.click('button:has-text("Create")');
// Hover on Display Layout option.
await page.locator('text=Display Layout').hover();
await percySnapshot(page, `Display Layout Create Menu (theme: '${theme}')`);
});
test('Visual - Default Gauge', async ({ page, theme }) => {
await createDomainObjectWithDefaults(page, {
type: 'Gauge',
name: 'Default Gauge'
});
// Take a snapshot of the newly created Gauge object
await percySnapshot(page, `Default Gauge (theme: '${theme}')`);
});
});

View File

@@ -26,12 +26,12 @@ const percySnapshot = require('@percy/playwright');
const utils = require('../../helper/faultUtils');
test.describe('The Fault Management Plugin Visual Test', () => {
test.describe('Fault Management Visual Tests', () => {
test('icon test', async ({ page, theme }) => {
await page.addInitScript({
path: path.join(__dirname, '../../helper/', 'addInitFaultManagementPlugin.js')
});
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
await percySnapshot(page, `Fault Management icon appears in tree (theme: '${theme}')`);
});
@@ -46,7 +46,7 @@ test.describe('The Fault Management Plugin Visual Test', () => {
await percySnapshot(
page,
`Acknowledged faults, have a checkmark on the fault icon and appear in the acknowldeged view (theme: '${theme}')`
`Acknowledged faults, have a checkmark on the fault icon and appear in the acknowledged view (theme: '${theme}')`
);
});

View File

@@ -23,13 +23,15 @@
const { expect, test } = require('../../pluginFixtures');
const percySnapshot = require('@percy/playwright');
const { createDomainObjectWithDefaults } = require('../../appActions');
const VISUAL_URL = require('../../constants').VISUAL_URL;
test.describe('Visual - LAD Table', () => {
/** @type {import('@playwright/test').Locator} */
let ladTable;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
// Create LAD Table
ladTable = await createDomainObjectWithDefaults(page, {
type: 'LAD Table',
@@ -55,9 +57,6 @@ test.describe('Visual - LAD Table', () => {
});
test('Toggled column widths behave accordingly', async ({ page, theme }) => {
await page.goto(ladTable.url);
//Close panes for visual consistency
await page.getByTitle('Collapse Inspect Pane').click();
await page.getByTitle('Collapse Browse Pane').click();
await expect(page.locator('button[title="Expand Columns"]')).toBeVisible();

View File

@@ -23,28 +23,78 @@
const { test } = require('../../pluginFixtures');
const percySnapshot = require('@percy/playwright');
const { expandTreePaneItemByName, createDomainObjectWithDefaults } = require('../../appActions');
const {
startAndAddRestrictedNotebookObject,
enterTextEntry
} = require('../../helper/notebookUtils');
const { VISUAL_URL } = require('../../constants');
test.describe('Visual - Restricted Notebook', () => {
test.beforeEach(async ({ page }) => {
await startAndAddRestrictedNotebookObject(page);
});
test('Restricted Notebook is visually correct @addInit', async ({ page, theme }) => {
// Take a snapshot of the newly created CUSTOM_NAME notebook
await percySnapshot(page, `Restricted Notebook with CUSTOM_NAME (theme: '${theme}')`);
});
});
test.describe('Visual - Notebook', () => {
test('Accepts dropped objects as embeds @unstable', async ({ page, theme, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
// Create Notebook
const notebook = await createDomainObjectWithDefaults(page, {
let notebook;
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
notebook = await createDomainObjectWithDefaults(page, {
type: 'Notebook',
name: 'Embed Test Notebook'
name: 'Test Notebook'
});
});
test('Accepts dropped objects as embeds', async ({ page, theme, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
// Create Overlay Plot
await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
name: 'Dropped Overlay Plot'
});
//Open Tree
await page.getByRole('button', { name: 'Browse' }).click();
await expandTreePaneItemByName(page, myItemsFolderName);
await page.goto(notebook.url);
await page.dragAndDrop('role=treeitem[name=/Dropped Overlay Plot/]', '.c-notebook__drag-area');
await percySnapshot(page, `Notebook w/ dropped embed (theme: ${theme})`);
});
test("Blur 'Add tag' on Notebook", async ({ page, theme }) => {
await enterTextEntry(page, 'Entry 0');
await percySnapshot(page, `Notebook Entry (theme: '${theme}')`);
// Open the Inspector
await page.getByRole('button', { name: 'Inspect' }).click();
// Open the Annotations tab
await page.getByRole('tab', { name: 'Annotations' }).click();
// Take snapshot of the notebook with the Annotations tab opened
await percySnapshot(page, `Notebook Annotation (theme: '${theme}')`);
// Add annotation
await page.locator('button:has-text("Add Tag")').click();
// Take snapshot of the notebook with the AutoComplete field visible
await percySnapshot(page, `Notebook Add Tag (theme: '${theme}')`);
// Click inside the AutoComplete field
await page.locator('[placeholder="Type to select tag"]').click();
// Click on the "Tags" header (simulating a click outside the autocomplete field)
await page.locator('div.c-inspect-properties__header:has-text("Tags")').click();
// Take snapshot of the notebook with the AutoComplete field hidden and with the "Add Tag" button visible
await percySnapshot(page, `Notebook Annotation de-select blur (theme: '${theme}')`);
});
});

View File

@@ -27,11 +27,11 @@
const { test, expect } = require('../../pluginFixtures');
const percySnapshot = require('@percy/playwright');
const { createDomainObjectWithDefaults } = require('../../appActions');
const VISUAL_URL = require('../../constants').VISUAL_URL;
test.describe("Visual - Check Notification Info Banner of 'Save successful'", () => {
test.beforeEach(async ({ page }) => {
// Go to baseURL and Hide Tree
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
});
test("Create a clock, click on 'Save successful' banner and dismiss it", async ({
@@ -39,25 +39,24 @@ test.describe("Visual - Check Notification Info Banner of 'Save successful'", ()
theme
}) => {
// Create a clock domain object
await createDomainObjectWithDefaults(page, { type: 'Clock' });
// Verify there is a button with aria-label="Review 1 Notification"
expect(await page.locator('button[aria-label="Review 1 Notification"]').isVisible()).toBe(true);
// Verify there is a button with aria-label="Clear all notifications"
expect(await page.locator('button[aria-label="Clear all notifications"]').isVisible()).toBe(
true
);
await createDomainObjectWithDefaults(page, {
type: 'Clock',
name: 'Default Clock'
});
// Click on the div with role="alert" that has "Save successful" text
await page.locator('div[role="alert"]:has-text("Save successful")').click();
// Verify there is a div with role="dialog"
expect(await page.locator('div[role="dialog"]').isVisible()).toBe(true);
// Verify the div with role="dialog" contains text "Save successful"
expect(await page.locator('div[role="dialog"]').innerText()).toContain('Save successful');
await percySnapshot(page, `Notification banner - ${theme}`);
await percySnapshot(page, `Notification banner shows Save successful (theme: '${theme}')`);
// Verify there is a button with text "Dismiss"
expect(await page.locator('button:has-text("Dismiss")').isVisible()).toBe(true);
await percySnapshot(page, `Notification banner shows Dismiss (theme: '${theme}')`);
// Click on button with text "Dismiss"
await page.locator('button:has-text("Dismiss")').click();
// Verify there is no div with role="dialog"
expect(await page.locator('div[role="dialog"]').isVisible()).toBe(false);
await percySnapshot(page, `Notification banner dismissed (theme: '${theme}')`);
});
});

View File

@@ -21,16 +21,20 @@
*****************************************************************************/
const { test } = require('../../pluginFixtures');
const { setBoundsToSpanAllActivities } = require('../../helper/planningUtils');
const {
setBoundsToSpanAllActivities,
setDraftStatusForPlan
} = require('../../helper/planningUtils');
const { createDomainObjectWithDefaults, createPlanFromJSON } = require('../../appActions');
const percySnapshot = require('@percy/playwright');
const VISUAL_URL = require('../../constants').VISUAL_URL;
const examplePlanSmall = require('../../test-data/examplePlans/ExamplePlan_Small2.json');
const snapshotScope = '.l-shell__pane-main .l-pane__contents';
test.describe('Visual - Planning', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
});
test('Plan View', async ({ page, theme }) => {
@@ -50,8 +54,7 @@ test.describe('Visual - Planning', () => {
name: 'Plan Visual Test (Draft)',
json: examplePlanSmall
});
await page.goto('./#/browse/mine');
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
await setDraftStatusForPlan(page, plan);
await setBoundsToSpanAllActivities(page, examplePlanSmall, plan.url);
@@ -87,7 +90,7 @@ test.describe('Visual - Planning', () => {
await setDraftStatusForPlan(page, plan);
await page.goto('./#/browse/mine');
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
await setBoundsToSpanAllActivities(page, examplePlanSmall, ganttChart.url);
await percySnapshot(page, `Gantt Chart View w/ draft status (theme: ${theme})`, {
@@ -95,14 +98,3 @@ test.describe('Visual - Planning', () => {
});
});
});
/**
* Uses the Open MCT API to set the status of a plan to 'draft'.
* @param {import('@playwright/test').Page} page
* @param {import('../../appActions').CreatedObjectInfo} plan
*/
async function setDraftStatusForPlan(page, plan) {
await page.evaluate(async (planObject) => {
await window.openmct.status.set(planObject.uuid, 'draft');
}, plan);
}

View File

@@ -26,64 +26,76 @@ This test suite is dedicated to tests which verify search functionality.
const { test, expect } = require('../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../appActions');
const { VISUAL_URL } = require('../../constants');
const percySnapshot = require('@percy/playwright');
test.describe('Grand Search', () => {
test.beforeEach(async ({ page, theme }) => {
//Go to baseURL and Hide Tree
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
await page.getByTitle('Collapse Browse Pane').click();
let clock;
let displayLayout;
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
displayLayout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Visual Test Display Layout'
});
clock = await createDomainObjectWithDefaults(page, {
type: 'Clock',
name: 'Visual Test Clock',
parent: displayLayout.uuid
});
});
test.use({
clockOptions: {
now: 0, //Set browser clock to UNIX Epoch
shouldAdvanceTime: false //Don't advance the clock
}
});
//This needs to be rewritten to use a non clock or non display layout object
test('Can search for objects, and subsequent search dropdown behaves properly @unstable', async ({
test('Can search for folder object, and subsequent search dropdown behaves properly', async ({
page,
theme
}) => {
// await createDomainObjectWithDefaults(page, 'Display Layout');
// await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
// await page.locator('text=Save and Finish Editing').click();
const folder1 = 'Folder1';
await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: folder1
});
const searchInput = page.getByRole('searchbox', { name: 'Search Input' });
const searchResults = page.getByRole('searchbox', { name: 'OpenMCT Search' });
// Navigate to display layout
await page.goto(displayLayout.url);
// Click [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill(folder1);
await expect(page.locator('[aria-label="Search Result"]')).toContainText(folder1);
await percySnapshot(page, 'Searching for Folder Object');
// Search for the clock object
await searchInput.click();
await searchInput.fill(clock.name);
await expect(searchResults.getByText('Visual Test Clock')).toBeVisible();
//Searching for an object returns that object in the grandsearch
await percySnapshot(page, `Searching for Clock Object (theme: '${theme}')`);
// Enter Edit mode on the Display Layout
await page.getByRole('button', { name: 'Edit' }).click();
// Navigate to the clock object while in edit mode on the display layout
await searchInput.click();
await searchResults.getByText('Visual Test Clock').click();
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
await page.locator('[aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock').click();
await percySnapshot(
page,
'Preview for clock should display when editing enabled and search item clicked'
`Preview for clock should display when editing enabled and search item clicked (theme: '${theme}')`
);
await page.locator('[aria-label="Close"]').click();
await percySnapshot(page, 'Search should still be showing after preview closed');
// Close the preview
await page.getByRole('button', { name: 'Close' }).click();
await percySnapshot(
page,
`Search should still be showing after preview closed (theme: '${theme}')`
);
await page
.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button')
.nth(1)
.click();
// Save and finish editing the Display Layout
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.locator('text=Save and Finish Editing').click();
// Search for the clock object
await searchInput.click();
await searchInput.fill(clock.name);
await expect(searchResults.getByText('Visual Test Clock')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
// Navigate to the clock object while not in edit mode on the display layout
await searchResults.getByText('Visual Test Clock').click();
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl');
await Promise.all([page.waitForNavigation(), page.locator('text=Unnamed Clock').click()]);
await percySnapshot(
page,
`Clicking on search results should navigate to them if not editing (theme: '${theme}')`

View File

@@ -19,8 +19,8 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventTelmetryProvider from './EventTelemetryProvider';
import EventMetadataProvider from './EventMetadataProvider';
import EventTelemetryProvider from './EventTelemetryProvider';
export default function EventGeneratorPlugin(options) {
return function install(openmct) {
@@ -36,7 +36,7 @@ export default function EventGeneratorPlugin(options) {
};
}
});
openmct.telemetry.addProvider(new EventTelmetryProvider());
openmct.telemetry.addProvider(new EventTelemetryProvider());
openmct.telemetry.addProvider(new EventMetadataProvider());
};
}

View File

@@ -19,8 +19,8 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventMessageGeneratorPlugin from './plugin.js';
import { createOpenMct, resetApplicationState } from '../../src/utils/testing';
import EventMessageGeneratorPlugin from './plugin.js';
describe('the plugin', () => {
let openmct;

View File

@@ -22,6 +22,7 @@
import EventEmitter from 'EventEmitter';
import { v4 as uuid } from 'uuid';
import createExampleUser from './exampleUserCreator';
const STATUSES = [

View File

@@ -88,10 +88,10 @@ define(['uuid'], function ({ v4: uuid }) {
};
WorkerInterface.prototype.subscribe = function (request, cb) {
const id = request.id;
const { id, loadDelay } = request;
const messageId = this.dispatch('subscribe', request, (message) => {
if (!this.staleTelemetryIds[id]) {
cb(message.data);
setTimeout(() => cb(message.data), Math.max(loadDelay, 0));
}
});

View File

@@ -20,11 +20,11 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import GeneratorMetadataProvider from './GeneratorMetadataProvider';
import GeneratorProvider from './GeneratorProvider';
import SinewaveLimitProvider from './SinewaveLimitProvider';
import SinewaveStalenessProvider from './SinewaveStalenessProvider';
import StateGeneratorProvider from './StateGeneratorProvider';
import GeneratorMetadataProvider from './GeneratorMetadataProvider';
export default function (openmct) {
openmct.types.addType('example.state-generator', {

View File

@@ -40,8 +40,8 @@ const DEFAULT_IMAGE_SAMPLES = [
'https://www.hq.nasa.gov/alsj/a16/AS16-117-18747.jpg',
'https://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg'
];
const DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS = 20000;
const MIN_IMAGE_LOAD_DELAY_IN_MILISECONDS = 5000;
const DEFAULT_IMAGE_LOAD_DELAY_IN_MILLISECONDS = 20000;
const MIN_IMAGE_LOAD_DELAY_IN_MILLISECONDS = 5000;
let openmctInstance;
@@ -58,7 +58,7 @@ export default function () {
initialize: (object) => {
object.configuration = {
imageLocation: '',
imageLoadDelayInMilliSeconds: DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS,
imageLoadDelayInMilliSeconds: DEFAULT_IMAGE_LOAD_DELAY_IN_MILLISECONDS,
imageSamples: [],
layers: []
};
@@ -188,20 +188,20 @@ function getImageLoadDelay(domainObject) {
openmctInstance.objects.mutate(
domainObject,
'configuration.imageLoadDelayInMilliSeconds',
DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS
DEFAULT_IMAGE_LOAD_DELAY_IN_MILLISECONDS
);
return DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS;
return DEFAULT_IMAGE_LOAD_DELAY_IN_MILLISECONDS;
}
if (imageLoadDelay < MIN_IMAGE_LOAD_DELAY_IN_MILISECONDS) {
if (imageLoadDelay < MIN_IMAGE_LOAD_DELAY_IN_MILLISECONDS) {
openmctInstance.objects.mutate(
domainObject,
'configuration.imageLoadDelayInMilliSeconds',
MIN_IMAGE_LOAD_DELAY_IN_MILISECONDS
MIN_IMAGE_LOAD_DELAY_IN_MILLISECONDS
);
return MIN_IMAGE_LOAD_DELAY_IN_MILISECONDS;
return MIN_IMAGE_LOAD_DELAY_IN_MILLISECONDS;
}
return imageLoadDelay;

View File

@@ -1,19 +0,0 @@
<template>
<div class="example">{{ msg }}</div>
</template>
<script>
export default {
data() {
return {
msg: 'Hello world!'
};
}
};
</script>
<style>
.example {
color: red;
}
</style>

View File

@@ -1,35 +0,0 @@
import Vue from 'vue';
import HelloWorld from './HelloWorld.vue';
function SimpleVuePlugin() {
return function install(openmct) {
openmct.types.addType('hello-world', {
name: 'Hello World',
description: 'An introduction object',
creatable: true
});
openmct.objectViews.addProvider({
name: 'demo-provider',
key: 'hello-world',
cssClass: 'icon-packet',
canView: function (d) {
return d.type === 'hello-world';
},
view: function (domainObject) {
var vm;
return {
show: function (container) {
vm = new Vue(HelloWorld);
container.appendChild(vm.$mount().$el);
},
destroy: function (container) {
//vm.$destroy();
}
};
}
});
};
}
export default SimpleVuePlugin;

View File

@@ -28,7 +28,7 @@
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, shrink-to-fit=no"
/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<title></title>
<title>Open MCT</title>
<script src="dist/openmct.js"></script>
<link
rel="icon"
@@ -51,7 +51,7 @@
sizes="16x16"
type="image/x-icon"
/>
<style type="text/css">
<style>
@keyframes splash-spinner {
0% {
transform: translate(-50%, -50%) rotate(0deg);

View File

@@ -45,7 +45,7 @@ module.exports = (config) => {
basePath: '',
frameworks: ['jasmine', 'webpack'],
files: [
'indexTest.js',
'index-test.js',
// included means: should the files be included in the browser using <script> tag?
// We don't want them as a <script> because the shared worker source
// needs loaded remotely by the shared worker process.
@@ -102,7 +102,7 @@ module.exports = (config) => {
failFast: false
},
preprocessors: {
'indexTest.js': ['webpack', 'sourcemap']
'index-test.js': ['webpack', 'sourcemap']
},
webpack: webpackConfig,
webpackMiddleware: {

View File

@@ -1,6 +1,6 @@
{
"name": "openmct",
"version": "3.0.0-SNAPSHOT",
"version": "3.2.0-next",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.22.5",
@@ -8,27 +8,30 @@
"@deploysentinel/playwright": "0.3.4",
"@percy/cli": "1.26.0",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.32.3",
"@playwright/test": "1.36.2",
"@types/eventemitter3": "1.2.0",
"@types/jasmine": "4.3.4",
"@types/lodash": "4.14.192",
"@vue/compat": "^3.1.0",
"@vue/compiler-sfc": "^3.1.0",
"@vue/compiler-sfc": "3.3.4",
"babel-loader": "9.1.0",
"babel-plugin-istanbul": "6.1.1",
"codecov": "3.8.3",
"comma-separated-values": "3.6.4",
"copy-webpack-plugin": "11.0.0",
"cspell": "7.3.6",
"css-loader": "6.8.1",
"d3-axis": "3.0.0",
"d3-scale": "3.3.0",
"d3-selection": "3.0.0",
"eslint": "8.43.0",
"eslint-config-prettier": "8.8.0",
"eslint-plugin-compat": "4.1.4",
"eslint": "8.48.0",
"eslint-config-prettier": "9.0.0",
"eslint-plugin-compat": "4.2.0",
"eslint-plugin-no-unsanitized": "4.0.2",
"eslint-plugin-playwright": "0.12.0",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-vue": "9.15.0",
"eslint-plugin-simple-import-sort": "10.0.0",
"eslint-plugin-unicorn": "44.0.2",
"eslint-plugin-vue": "9.17.0",
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
"eventemitter3": "1.2.0",
"file-saver": "2.0.5",
@@ -36,7 +39,7 @@
"git-rev-sync": "3.0.2",
"html2canvas": "1.4.1",
"imports-loader": "4.0.1",
"jasmine-core": "5.0.0",
"jasmine-core": "5.1.1",
"karma": "6.4.2",
"karma-chrome-launcher": "3.2.0",
"karma-cli": "2.0.0",
@@ -49,13 +52,14 @@
"karma-webpack": "5.0.0",
"location-bar": "3.0.1",
"lodash": "4.17.21",
"marked": "9.0.3",
"mini-css-extract-plugin": "2.7.6",
"moment": "2.29.4",
"moment-duration-format": "2.3.2",
"moment-timezone": "0.5.41",
"npm-run-all2": "6.0.6",
"nyc": "15.1.0",
"painterro": "1.2.78",
"playwright-core": "1.32.3",
"plotly.js-basic-dist": "2.20.0",
"plotly.js-gl2d-dist": "2.20.0",
"prettier": "2.8.7",
@@ -66,22 +70,26 @@
"sass-loader": "13.3.2",
"sinon": "15.1.0",
"style-loader": "3.3.3",
"typescript": "5.1.3",
"tiny-emitter": "2.1.0",
"typescript": "5.2.2",
"uuid": "9.0.0",
"vue": "^3.1.0",
"vue-eslint-parser": "9.3.1",
"vue": "3.3.4",
"vue-eslint-parser": "9.3.2",
"vue-loader": "16.8.3",
"webpack": "5.88.0",
"vue-loader": "^16.0.0",
"webpack-cli": "5.1.1",
"webpack-dev-server": "4.15.1",
"webpack-merge": "5.9.0"
},
"scripts": {
"clean": "rm -rf ./dist ./node_modules ./package-lock.json ./coverage ./html-test-results ./test-results ./.nyc_output ",
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
"start": "npx webpack serve --config ./.webpack/webpack.dev.js",
"start:prod": "npx webpack serve --config ./.webpack/webpack.prod.js",
"start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.js",
"lint": "eslint example src e2e --ext .js,.vue openmct.js --max-warnings=0",
"lint:js": "eslint \"example/**/*.js\" \"src/**/*.js\" \"e2e/**/*.js\" \"openmct.js\" --max-warnings=0",
"lint:vue": "eslint \"src/**/*.vue\"",
"lint:spelling": "cspell \"**/*.{js,md,vue}\" --show-context --gitignore --quiet",
"lint": "run-p \"lint:js -- {1}\" \"lint:vue -- {1}\" \"lint:spelling -- {1}\" --",
"lint:fix": "eslint example src e2e --ext .js,.vue openmct.js --fix",
"build:prod": "webpack --config ./.webpack/webpack.prod.js",
"build:dev": "webpack --config ./.webpack/webpack.dev.js",
@@ -92,14 +100,18 @@
"test:debug": "KARMA_DEBUG=true karma start",
"test:e2e": "npx playwright test",
"test:e2e:couchdb": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @couchdb --workers=1",
"test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert \"@unstable|@couchdb\"",
"test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert \"@unstable|@couchdb|@generatedata\"",
"test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable",
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
"test:e2e:generatedata": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @generatedata",
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots",
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js --grep-invert @unstable",
"test:e2e:visual:ci": "percy exec --config ./e2e/.percy.ci.yml --partial -- npx playwright test --config=e2e/playwright-visual.config.js --project=chrome --grep-invert @unstable",
"test:e2e:visual:full": "percy exec --config ./e2e/.percy.nightly.yml -- npx playwright test --config=e2e/playwright-visual.config.js --grep-invert @unstable",
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js --grep-invert @couchdb",
"test:e2e:watch": "npx playwright test --ui --config=e2e/playwright-ci.config.js",
"test:perf": "npx playwright test --config=e2e/playwright-performance.config.js",
"test:perf:contract": "npx playwright test --config=e2e/playwright-performance-dev.config.js",
"test:perf:localhost": "npx playwright test --config=e2e/playwright-performance-prod.config.js --project=chrome",
"test:perf:memory": "npx playwright test --config=e2e/playwright-performance-prod.config.js --project=chrome-memory",
"update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2023/gm' ./src/ui/layout/AboutDialog.vue",
"update-copyright-date": "npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\s\\(c\\)\\s20\\d\\d\\-20\\d\\d/Copyright \\(c\\)\\ 2014\\-2023/gm'",
"cov:e2e:report": "nyc report --reporter=lcovonly --report-dir=./coverage/e2e",
@@ -108,6 +120,7 @@
"cov:unit:publish": "codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit",
"prepare": "npm run build:prod && npx tsc"
},
"homepage": "https://nasa.github.io/openmct",
"repository": {
"type": "git",
"url": "https://github.com/nasa/openmct.git"
@@ -123,6 +136,12 @@
"ios_saf >= 16",
"Safari >= 16"
],
"author": "",
"license": "Apache-2.0"
"author": {
"name": "National Aeronautics and Space Administration",
"url": "https://www.nasa.gov"
},
"license": "Apache-2.0",
"keywords": [
"nasa"
]
}

View File

@@ -33,7 +33,7 @@ define([
'./ui/registries/ToolbarRegistry',
'./ui/router/ApplicationRouter',
'./ui/router/Browse',
'./ui/layout/Layout.vue',
'./ui/layout/AppLayout.vue',
'./ui/preview/plugin',
'./api/Branding',
'./plugins/licenses/plugin',
@@ -43,7 +43,6 @@ define([
'./plugins/duplicate/plugin',
'./plugins/importFromJSONAction/plugin',
'./plugins/exportAsJSONAction/plugin',
'./ui/components/components',
'vue'
], function (
EventEmitter,
@@ -68,7 +67,6 @@ define([
DuplicateActionPlugin,
ImportFromJSONAction,
ExportAsJSONAction,
components,
Vue
) {
/**
@@ -430,7 +428,6 @@ define([
};
MCT.prototype.plugins = plugins;
MCT.prototype.components = components.default;
return MCT;
});

View File

@@ -20,8 +20,8 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ActionCollection from './ActionCollection';
import { createOpenMct, resetApplicationState } from '../../utils/testing';
import ActionCollection from './ActionCollection';
describe('The ActionCollection', () => {
let openmct;

View File

@@ -20,9 +20,10 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import ActionCollection from './ActionCollection';
import _ from 'lodash';
import ActionCollection from './ActionCollection';
class ActionsAPI extends EventEmitter {
constructor(openmct) {
super();
@@ -92,7 +93,7 @@ class ActionsAPI extends EventEmitter {
if (this._actionCollections.has(key)) {
let actionCollection = this._actionCollections.get(key);
actionCollection.off('destroy', this._updateCachedActionCollections);
delete actionCollection.applicableActions;
this._actionCollections.delete(key);
}
}

View File

@@ -20,9 +20,9 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ActionsAPI from './ActionsAPI';
import { createOpenMct, resetApplicationState } from '../../utils/testing';
import ActionCollection from './ActionCollection';
import ActionsAPI from './ActionsAPI';
describe('The Actions API', () => {
let openmct;

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