Compare commits

...

237 Commits

Author SHA1 Message Date
98fd496c1f initial WIP 2023-03-03 14:46:58 -08:00
0dd12bce85 Freeze plot automatically when adding tags (#6377)
* freeze if annotation is selected

* add test

* add stacked plot test

* actually test ticket issue

* wait for canvas to stabalize

* wait for canvas to stabalize

* address PR comments
2023-03-02 18:42:25 +01:00
9c9e0442f1 chore(deps-dev): bump sanitize-html from 2.8.1 to 2.10.0 (#6360)
Bumps [sanitize-html](https://github.com/apostrophecms/sanitize-html) from 2.8.1 to 2.10.0.
- [Release notes](https://github.com/apostrophecms/sanitize-html/releases)
- [Changelog](https://github.com/apostrophecms/sanitize-html/blob/main/CHANGELOG.md)
- [Commits](https://github.com/apostrophecms/sanitize-html/compare/2.8.1...2.10.0)

---
updated-dependencies:
- dependency-name: sanitize-html
  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-02-28 09:32:14 +01:00
d49f057698 [Tags] Fix jumping of tags on hover (#6358) 2023-02-27 18:02:58 -08:00
c74ad1279c chore(deps-dev): bump moment-timezone from 0.5.40 to 0.5.41 (#6375)
Bumps [moment-timezone](https://github.com/moment/moment-timezone) from 0.5.40 to 0.5.41.
- [Release notes](https://github.com/moment/moment-timezone/releases)
- [Changelog](https://github.com/moment/moment-timezone/blob/develop/changelog.md)
- [Commits](https://github.com/moment/moment-timezone/compare/0.5.40...0.5.41)

---
updated-dependencies:
- dependency-name: moment-timezone
  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-02-27 23:44:34 +00:00
470a451956 chore(deps-dev): bump eslint from 8.34.0 to 8.35.0 (#6374)
Bumps [eslint](https://github.com/eslint/eslint) from 8.34.0 to 8.35.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.34.0...v8.35.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>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-02-27 15:20:37 -08:00
fa6cbb6f4d fix(#6158): StackedPlotItem reorders are reflected on the plot in real time (#6346)
* fix: ensure Vue can react to stacked plot item reorders

- Use Vue.$set instead of assignment so Vue can pick up StackedPlot composition reorders immediately

* test(e2e): add test for stacked plot item reorder

* docs: specify plot item order in comments

* fix: correct page.goto() url
2023-02-27 14:35:02 -08:00
52c00cfaef Ignore focus mouseover event when composed in layout (#6373)
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2023-02-27 21:57:15 +00:00
96d723a424 Changed opacity from 0.5 to 0.7 (#6194) 2023-02-27 13:45:23 -08:00
fb4b80862e Supress LAD Table View from ConditionSet (#6372)
* add test and fix

* remove premature test

* PR comments
2023-02-27 21:37:42 +00:00
bb2c8cfa63 fix(#6330): Clicking Add Tag and then removing an existing tag causes the Add Tag button to disappear (#6371) 2023-02-27 13:14:43 -08:00
ceffee9f22 docs: Remove LGTM badge from README (#6072)
Update README.md

remove LGTM-Badge - Sadly, LGTM.com is no longer available
2023-02-22 23:09:31 +00:00
a08ccd80dc chore: bump version to 2.2.0-SNAPSHOT (#6341)
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-02-15 21:17:21 +00:00
3509eacdec Debounce search results (#6259)
* throttle search results to one a second

* changed to use custom debounce due to async

* attempt to fix flakey test

* attempt to fix flakey test

* revert test changes

* reduce debounce time and add e2e test

* allow canceling of timeout

* removed debug

* make search result e2e tests more stable

* make drop down selector a constant

* address pr comments
2023-02-15 21:50:03 +01:00
d4496cba41 [Notebook] Links, Restricted Notebook Links, Search links (#6344)
* big simplification and enhancement to highlight component and added formatting for locked notebooks as well

* fix typo, fix logic
2023-02-15 11:05:28 -08:00
64f300d466 Emit indices of out of order telemetry collection items (#6342)
* Emit the indices of items added by the telemetry collections
* Handle added item indices for imagery
2023-02-14 15:25:23 -08:00
8de24a109a Have clicking on annotation search result use the preview action if in edit mode (#6331)
* fix preview issue

* reenabled test suite after testing 30x in parallel

* add e2e test and disable unit tests for search

* change to const
2023-02-14 11:29:18 -08:00
6d62e0e73c Decouple removal of independent time context for a view from refreshing context for other views (#6334)
* Decouple removing the context for a view from refreshing the context of views to get their upstream contexts

* Add test for clicking on an item in the elements pool for a preview

* Add test to ensure independent time context for items with no parents works as expected
2023-02-13 21:19:26 +00:00
5da1c9c0d7 Compass rose fix (#6318)
* won't mount if cameraAngleOfView undefined

* fix non-gimbling camera azimuth

correct pan to azimuth

* reorganize shared props passing

* enable hud for non-gimbling cameras

* fix unit tests

* rotate function needs to work with numbers

* avoid -0

* fix: don't delete imagery size, update related telemetry on focusedImage change

* refactor: remove unused prop

* fix: ensure thumbnail key is unique

* fix: watch `focusedImage`, not `focusedImageIndex`

- Corrects a false assumption that if the `focusedImageIndex` changes, the `focusedImage` has changed. This was causing us to mistakenly reset a lot of display props that control whether or not the Compass shows.

- For example, if an image falls out of bounds, the `focusedImageIndex` will change as the old image is removed from the array.

---------

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2023-02-13 19:28:00 +00:00
4fa9a9697b chore(deps-dev): bump eslint from 8.32.0 to 8.34.0 (#6325)
Bumps [eslint](https://github.com/eslint/eslint) from 8.32.0 to 8.34.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.32.0...v8.34.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-02-13 18:52:09 +00:00
bf48a6e306 chore(deps-dev): bump eslint-plugin-compat from 4.0.2 to 4.1.1 (#6311)
Bumps [eslint-plugin-compat](https://github.com/amilajack/eslint-plugin-compat) from 4.0.2 to 4.1.1.
- [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.0.2...v4.1.1)

---
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>
2023-02-13 10:34:36 -08:00
00ad452930 chore(deps-dev): bump typescript from 4.9.4 to 4.9.5 (#6233)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.9.4 to 4.9.5.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.9.4...v4.9.5)

---
updated-dependencies:
- dependency-name: typescript
  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-02-13 10:16:33 -08:00
8df1f6406b docs: fix docker command and formatting (#6329)
- Fixes an inconsistency in the docs with regards to the docker command for spinning up a Playwright container

- Closes an unclosed markdown code block
2023-02-13 18:09:55 +01:00
a50960d66c Remove console warn (#6320) 2023-02-10 13:47:46 -08:00
e3a69c8856 [Notebook] Fix link formatting on load, remove duplicate snapshot indicator (#6317)
* adding urlWhitelist to data so when/if it is updated it triggers link formatting

* removing dupe notebook and restricted notebook checks and just moving it to base notebook functionality install checks

* nullthing to see here
2023-02-10 12:08:58 -08:00
672cb7e621 Prevent tabbing into Notebook entries (#6315)
Closes #6312
- Set entry inputs to `tabindex="-1"` to prevent tabbing into entry inputs.

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2023-02-09 23:51:38 +00:00
7dcccee1ae [TimeAPI] Fix independent time context check (#6308)
* Fix independent time context to check first object in path (self) for upstream content instead of last object in path

* Revert changes that don't allow unregistering independent time context

---------

Co-authored-by: Shefali <simplyrender@gmail.com>
2023-02-09 15:34:17 -08:00
302dbe7359 fix(#6268): show correct yAxis options upon series drag to another yAxis (#6301)
* fix: show yAxis properties when series is moved

- Shows the correct yAxis properties immediately when a series is moved to another yAxis

* test: check for correct yAxis properties

- refactor test to use unique autogenerated object names

- update Elements pool pane handle selector

* test: fix goto and move to beforeEach

* test: ☠️ disable GrandSearch and ApplicationRouterSpec ☠️

---------

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2023-02-09 11:58:03 -08:00
b4df01965e Fix router test flakes (#6309)
* resolve conflicts
* disable flaky tests
2023-02-09 11:34:34 -08:00
5a8f1d542e Allow tags files to define namespace to save annotations (#6274)
* allow tags files to define namespace to save annotations

* add tests

* typo in test name

* lint

* change param to objects object and remove debug
2023-02-08 14:48:03 +00:00
10decda94e When auto scale is turned off, handle user specified range correctly (#6258)
* Fix typo when saving the user specified range
* Ensure range is specified when autoscale is turned off
* Don't redraw unless absolutely necessary
* Add 'stats' to the handled attributes for redrawing plots
* Handle x axis displayRange, marker shape and size to redraw
* If there are is no closest data point for a plot, skip annotation gathering
* Ensure that min and max user defined ranges are valid when autoscale is disabled. Otherwise, enable autoscale to true.
* Fix autoscale e2e test
* updated snapshot
* Update e2e/README.md
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-02-07 15:19:50 -08:00
5b1f8d0eac Add cancel adding tag mechanism and fix persistent options visible (#6273)
* Add cancel adding tag mechanism and fix persistent options visible

* Add functional test for cancel adding a tag

* Create addtag.visual.spec.js and move createNotebookAndEntry to appActions.js

* Remove createDomainObjectWithDefaults function from tags.e2e.spec.js

* Integrate Percy snapshots and remove assertions

* Move createNotebookAndEntry function back to tags.e2e.spec.js

* Update locator of Annotations tab, split helper function and add test.beforeEach
2023-02-07 17:24:37 +00:00
2f6e1b703a [Staleness] Handle Overlay Plots in Stacked Plots and removing LAD Tables from LAD Table Sets (#6281)
* add handling for composition items (ex overlay plot) in stacked plots, fix swg staleness provider isStale method response

* typo

* removing staleness listeners when ladtable is remove

* addressing pr comments for this component

* address changes requested for lad table sets

* had to update is-stale for the row since we used combined keys in lad table sets now
2023-02-06 14:08:14 -08:00
5384022a59 fix: DisplayLayout shapes can be selected and manipulated again (#6289)
fix: handle case where parentObject is undefined

- Fixes manipulation of parentless display layout objects such as shapes and lines
2023-02-06 12:49:50 -08:00
b57974b462 [ObjectAPI] Cleanup code and remove possible memory leak (#6269)
* Destroy mutable after refresh
* Check if domainObject supports mutation before getting mutable
2023-02-06 18:53:06 +00:00
3c36ba9a71 Fix keys duplication error (#6243)
* Update key value of notification-message

* Add 'Notifications can be dismissed individually' test
2023-02-06 17:59:26 +00:00
2ac463de90 test(e2e): Add tests for Recent Objects (#6270)
* test(e2e): add test for recent objects target

* test(e2e): Add RecentObjects tests

- Test for 'target button' scroll and animation

- Test for persistence on refresh

- Test for displaying objects and aliases uniquely

* test(e2e): add test for recent objects limit

* refactor: compress to a single line

* test(e2e): recents max limit test nests objects

- Do deep nesting of objects instead of flat objects

- Collapse the tree completely and then test the "target" button for the most deeply nested item

* test(e2e): update locator to not use `nth(i)`
2023-02-04 00:15:42 +00:00
be38c3e654 Fix stacked plot child selection (#6275)
* Fix selections for different scenarios

* Ensure plot selection in stacked plots works when there are no selected or found annotations

* Adds e2e test for stacked plot selection and fixes the old e2e test which was testing overlay plots instead.

* Fix selection of plots while in Edit mode

* Improve tests for stacked plots

* refactor: remove unnecessary `await`s

* a11y: move aria-label to StackedPlotItem

* refactor(e2e): combine like tests, unique object names

- Use unique object names in `text=` selectors

- Combine like tests to reduce execution time

- Use `getByRole` selectors where able

* docs(e2e): add comments to test

* fix: add class back for unit test selector

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2023-02-03 23:56:50 +00:00
0f312a88bb [Notebook] Sanitize entries before save for extra protection (#6255)
* Sanitizing before save as well to be be doubly safe

---------

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2023-02-03 02:16:45 +00:00
422b7f3e09 Compass rose rotation fixes (#6260) 2023-02-02 17:18:41 -08:00
800062d37e fix: remove 1px padding and re-enable autoscale snapshot test (#6271)
* style: remove 1px padding from plot legend item

* test: re-enable autoscale snapshot test
2023-02-02 15:50:37 -08:00
c1e8c7915c [Staleness] Fix removed object error and clean up (#6241)
* fixing error from plots when removing swg and making methods and props private for swg staleness provider

* removing unsubscribes from destroy hooks if the item has been removed already and reverting an unneccesary change

* checking for undefined staleness response

* removed un-neccesary code
2023-02-01 14:06:54 -08:00
c1c1d87953 Fix multiple y axis issues (#6204)
* Ensure enabling log mode does not reset series that don't belong to that yaxis.
propagate both left and right y axes widths so that plots can adjust accordingly

* Revert code
Handle second axis resizing

* Fixes issue where logMode was getting initialized incorrectly for multiple y axes

* Get the yAxisId of the series from the model.

* Address review comments - rename params for readability

* Fix number of log ticks expected and the tick values since we reduced the number of secondary ticks

* Fix log plot test

* Add guard code during destroy

* Add missing remove callback
2023-02-01 21:46:15 +00:00
0382d22f7f [Notebook] Entry links tests (#6190)
* removing dupe nb install, adding whitelist nb init script, testing whitelist urls

* updating from copy

* addressing PR comments for cleaner tests

* removing .only

* added a secure url test and a subdomain url test and simplified some code

* not messin with protocols atm

* update variable name
2023-02-01 11:55:08 -08:00
f570424357 Fix stacked plots legend (#6199)
* Add listeners to remove stacked plot series and make keys unique

* don't add overlay plots to stacked plot legends

* Ensure series colors are drawn correctly in the plot legend

* Remove legend from mct plot. Remove series reactivity from stackd plot and add them to the legend instead.

* Clean up stacked plots so that the plot legend needs fewer props
Also make sure that plot selection inside a stacked plot works - this had regressed due to plot annotations

* Fix console error in plot elements pool and plot legend - reset arrays to empty
* Ensure color in the y axis swatch updates correctly

* Fix small issues with removing objects from STacked plots

* Fix selection for annotations and also select stacked plot child items

* fix notebook tagging

* remove unused annotation editor and change selection to single object

* remove reference to deleted css

* fix e2e tests

* Fix small typos into the selection context for Notebooks.

* Add a typ that identifies that an annotation selection is coming from a search result

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
2023-02-01 10:14:02 -08:00
393c801426 Closes #6215 (#6222)
- Markup cleanups, CSS placement improvements for tags.
- Better approach to tag layout.
- CSS prop migrated to _constants.scss.
- Style and layout improvements for `.c-autocomplete*` input.

Co-authored-by: Scott Bell <scott@traclabs.com>
2023-02-01 11:29:51 +01:00
6d63339b23 fix: Navigating from Recent Objects breadcrumb correctly updates the URL hash (#6234)
* fix: provide hashUrl for ObjectPath breadcrumbs

* a11y: add `navigation` role and aria-label to breadcrumb

* test(e2e): add regression test for breadcrumb nav

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
2023-02-01 11:11:44 +01:00
66d7c626e1 Replace structuredClone with JSON.parse (#6237) 2023-01-31 15:10:13 -08:00
2246f33023 Color fix for Recently Viewed items (#6227)
- Normalized color styles.

Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
2023-01-31 19:42:15 +00:00
871362d469 Fix plot composition (#6206)
* Fixed composition

* Remove unnecessary guard code

* Removing deprecated code

* Use valid key for stacked plot v-for

* Fixed object API specs to expect old values as well as new values in mutation callbacks

* Fixed existing tests

* Added E2E test

* Fixed linting error

---------

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2023-01-31 06:01:00 +00:00
cc1bf47f5a fix(recents): fix vue warnings after target animation (#6203) 2023-01-31 01:45:08 +00:00
9c784398b3 fix(e2e): temp fix for appAction test flake (#6226) 2023-01-30 18:55:24 +00:00
21ce013df2 path change (#6224) 2023-01-29 13:49:00 -08:00
d20c2a3e3c fix: ensure MoveAction always saves transaction (#6196)
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2023-01-26 22:18:46 +00:00
8d1a2e6716 Make tree items more actionable and add AppAction for expanding the object tree (#5997)
* style: add `visibility` to tree expand triangles

- The purpose of this is so that Playwright can perform actionability checks on the tree items. This will make operations involving expanding tree items much easier to perform in e2e.

* feat(e2e): Add AppAction to expand the entire tree

* fix: wait for loading indicator

* test: add test for `expandEntireTree`

* test: update `expandEntireTree` and tree selectors

- Use dynamic aria-label for different tree implementations

- Get rid of CSS ids which are only for testing

- Update percy tree scope selector

* chore(lint): remove unused variable

* refactor(e2e): update tree locators

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-01-26 17:25:15 +00:00
01f724959d Ensure limit lines for both the old and new y axes are redrawn when a series moves from one y axis to another (#6181)
Optimize initialization of Plot configuration
Ensure the the y axis form correctly saves any changes to the configuration
Fix excluded limits test
2023-01-26 17:11:13 +00:00
3ae6290ec3 Visual tweaks to Recently Viewed items (#6183)
- Reduced size of icon.
- Tightened spacing.
2023-01-25 14:15:50 -08:00
ba5ed27e74 fix: skip if no yAxisId exists on persistedConfig (#6188) 2023-01-25 19:18:26 +00:00
ca737d8afa fix(elementItemGroup): 🚫👶📜📊 (#6171)
- translation: remove the baby scroll bars from element item groups
2023-01-24 23:48:49 +00:00
33a275e8bc fix(multiYAxis): get yRange for yAxis of the series (#6170)
* fix: get yRange for the yAxis of the series

* refactor: use collection methods, define as vars
2023-01-24 14:22:48 -08:00
60e808689c Mct6157 creating annotations for plots on multiple yaxes (#6161)
* First pass

* Get bounding box min and max values based on the y axis that a series belongs to.
Handle removal of telemetry from an overlay plot
Handle addition of telemetry after annotations have been saved to an overlay plot

* Fix showing the rectangle for a given target's bounding box.

* remove invalid comment

Co-authored-by: Scott Bell <scott@traclabs.com>
2023-01-24 11:00:45 +00:00
8847c862fa [Staleness] API and Component Functionality (#6108)
* initial telemetry api updates for staleness support

* modifying staleness to a subsription style

* fixing variable name

* debuggin

* put the subscribe in the wrong place

* stale class for object views

* temp cyan border for testing

* added staleness to swg, working on stacked plot staleness

* WIP: stacked plot staleness

* reverting, going a different route

* staleness on stacked plots

* plot legend staleness

* remove debug code

* staleness for alphanumerics

* lad table and table set staleness

* overlay plot staleness

* remove debug code

* hardened lad staleness functionality fixed plots without composition bug

* adding staleness to gauges

* renaming telemetry age check functionality so it does not conflict with new staleness functionality

* couple one-off fixes here and there, and WIP for condition sets, moving to telemetry tables to facilitate styling of completed components

* small fix on lad tables, added staleness functionality to tables

* finishing up condition sets

* some cleaning up

* adding border to condition sets when an item is stale

* fixing dub sub

* addressing PR comment, moving repeated code to a function

* robustified the SWG stalenes provider, little fixes here and there as far as cleaning up listeners and... whatnot

* removing debug code

* typo fixes

* cleanin up debug code

* created a simple stalenes mixin for more basic usage in components

* more robustification, if a new staleness subscription happens, will now send the current staleness value if we have it, beefed up example stalenes swg provider

* beefed up staleness mixin a bit to give it more use

* copyright

* cleanin up ladtable code

* cleanin up ladtable code

* cleaning up lad table sets

* some minor updates

* Closes #6109
- New staleness glyph and font CSS added.

* Closes #6109
- Normalized staleness colors as theme constants.
- New mixins for staleness application to view elements.
- Applied staleness styling to all relevant view elements.
- TODO: smoke-test in Show theme.

* adding staleness utils helper, mixin and isStale functionalirty for telemtry api

* Closes #6109
- Refined style for Snow theme.

* need to have one domainObject per stalenes utility

* making sure we handle domains correctly while dealing with staleness

* couple fixes

* moving abort controller logic to a spot where it makes more sense

* added some more info for the StalenesProvider interface docs

* returning undefinded for ifStale requests with no provider

* debuggin

* debuggin

* missed "isStale" call in condtioncollections

* removing debug code and using mixin unsubscribe in gauge

* fixing tests

* more targeted tree item click

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Scott Bell <scott@traclabs.com>
2023-01-23 10:27:04 -08:00
1b71a3bf33 Multiple Y-Axes for Overlay Plots (#6153)
Support multiple y-axes in overlay plots

Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
Co-authored-by: Rukmini Bose <rukmini.bose15@gmail.com>
2023-01-23 15:34:26 +00:00
9980aab18f 5834 stacked plot removing objects from a stacked plot will not remove them from the legend (#6022)
* Add listeners to remove stacked plot series and make keys unique

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2023-01-22 10:38:05 -08:00
5e530aa625 feat: Support thumbnails in ImageryView and ImageryTimeView (#6132)
* fix: get image thumbnail formatter

* refactor: ImageryView cleanup

* docs: add comment

* feat: Support thumbnails in ImageryView

- Prefer an image's thumbnail URL if its available

* feat: Support thumbnails in ImageryTimeView

* refactor: rename variable

* test(WIP): add thumbnail unit test, not working yet

* test: temp disable test

* feat: imagery thumbnail urls for example imagery

* test: add unit test for imagery thumbnails

* test(e2e): check for thumbnail urls

- Update imagery view tests to check for use of thumbnail urls
2023-01-21 11:25:35 -08:00
986c596d90 Imagery compass rose enhancements (#6140)
* Fixes 6139
- Markup changes and improvements in CompassRose.vue.
- Improved sun and edge gradients.
- Related CSS styles updated.
- Changed compass key color from cyan to white to avoid conflict with staleness color.

* change var def to avoid collision

* compass rose should size itself based on image

* allow heading or camera pan for fixed cameras

* suppress HUD if no camera pan

* allow image to display compass rose for other cams

* update example imagery to accept transformations

* remove comments

Co-authored-by: David Tsay <david.e.tsay@nasa.gov>
Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
2023-01-20 23:21:57 -08:00
4d84b16d8b [Notebook] Convert full links in entries, into clickable links (#6090)
* Automatically promote urls to hyperlinks if matches whitelist
* Disable v-html lint warning for notebook entries
* Check whether domain endswith given partial

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2023-01-20 18:27:19 -08:00
20c7b23a4f [Notebook] Embed enhancements (#5809)
* added new menu and actions to notebook embed as well as new information on embed

* fix method name case

* Add action messages. Fix margins

* Added bg icons. Change sizing of icons and thumbnails. Add scrolling to overflow embeds

* Rename embed wrapper

* adding dynamce class for scrolling the embeds wrapper based on need

* Add styling to embed scrolling container

* Change tag margin for better spacing between rows. Class rename. Minor styling changes to embed container. Change supermenu icon size

* Change action menu size

* Fix inner shadows. Revert tag code change. Create new theme constants. Make embed container constant

* Fix scroll and snow theme colors

* Fix overflow bug in entries and embed container. Refactor code so that containers optimize space of each entry

* Fix lint error

* Fix so embed container goes full width

* Fix input container to extend full width. Fix margin between notebook elements

* Addressed PR review comments.

* Address PR changes. Fix text overflow for long words.

* address pr review comments

* fixing tests

* first pass

* i've wasted too much time on this

Co-authored-by: Rukmini Bose <rukmini.bose15@gmail.com>
Co-authored-by: rukmini-bose <48999852+rukmini-bose@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-01-21 00:53:37 +00:00
d1c7d133fc 5853 plot annotations prototype (#6000)
* Implement new search and tagging for notebooks
* Add inspector and plot annotations
* Clean up inspector for plots and other views
* Bump webpack defaults for windows
* Notebook annotations are shown in inspector now
* Only allow annotations if plot is paused or in fixed time. also do not mutate if immutable
* Key off local events instead of remote (for now)
2023-01-20 14:34:12 -08:00
edbbebe329 [CLA on File] style: added padding to object & icon button labels (#6036) 2023-01-20 11:07:56 -08:00
f98a2cdd6b feat: Recent Objects (#6103)
* clicking recent objects selects that object
* clicking target navigates to but does not select that object
* max 20 recent objects
2023-01-20 10:27:09 -08:00
22621aaaf8 6098 operator status indicator v11 improvements (#6112)
* Added clear poll button to clear all statuses
* Clear current poll question
* Added table for operator status

Co-authored-by: Michael Rogers <contact@mhrogers.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2023-01-19 18:56:46 -08:00
e0ca6200bb Handle pausing of imagery from viewLargeAction - 3647 (#5901)
* get imagery view context and externally set pause and thumbnail index

* Test pause/play state in realtime mode

* Created an onPreviewMode change handler to be invoked from view large

* Add optional chaining to method invocation

* Change onItemClicked to invoke to resolve repeat large view action error
2023-01-19 18:45:40 -08:00
70074c52c8 Fix Notifications Overlay that opens automatically (#6133)
* Show NotificationIndicator also if NotificationsList is shown

* Create Notification Overlay Regression Test

* Move notification regression test under notification.e2e.spec.js

* Update selector of Notification Banner

* Rename test to "Notification Overlay"
2023-01-18 22:20:47 +00:00
d5adaf6e8c Bump eslint-plugin-playwright from 0.11.2 to 0.12.0 (#6125)
Bumps [eslint-plugin-playwright](https://github.com/playwright-community/eslint-plugin-playwright) from 0.11.2 to 0.12.0.
- [Release notes](https://github.com/playwright-community/eslint-plugin-playwright/releases)
- [Commits](https://github.com/playwright-community/eslint-plugin-playwright/compare/v0.11.2...v0.12.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-17 22:00:58 +00:00
8125632728 Allow form file input to accept other MIME types (#6089)
* allow non json raw files upload

* add e2e test

* compress image
2023-01-17 14:25:18 -06:00
14c9dd0a32 Bump plotly.js-gl2d-dist from 2.14.0 to 2.17.1 (#6104)
Bumps [plotly.js-gl2d-dist](https://github.com/plotly/plotly.js) from 2.14.0 to 2.17.1.
- [Release notes](https://github.com/plotly/plotly.js/releases)
- [Changelog](https://github.com/plotly/plotly.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/plotly/plotly.js/compare/v2.14.0...v2.17.1)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2023-01-17 10:32:14 -08:00
9ae58f8441 tooling(webpack): base paths of rootfolder (#6123) 2023-01-17 08:05:34 -08:00
4889284335 Bump eslint from 8.31.0 to 8.32.0 (#6124)
Bumps [eslint](https://github.com/eslint/eslint) from 8.31.0 to 8.32.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.31.0...v8.32.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-16 15:25:19 -08:00
c2183d4de2 Bump @percy/cli from 1.16.0 to 1.17.0 (#6110)
Bumps [@percy/cli](https://github.com/percy/cli/tree/HEAD/packages/cli) from 1.16.0 to 1.17.0.
- [Release notes](https://github.com/percy/cli/releases)
- [Commits](https://github.com/percy/cli/commits/v1.17.0/packages/cli)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-13 22:39:46 -08:00
902d80c214 [CLA Approved] Remove notification independently (#6079)
* Add closeOverlay and notifications-count attributes to notification-message

* Add "Dismiss notification" button to NotificationMessage

* Add aria-labels to Alert Banner

* Add aria-modal and role dialog to OverlayComponent

* Add ARIA roles to NotificationMessage and NotificationsList

* Add ARIA role alert to NotificationBanner

* Create Notification E2E Test for dismissing the 'Save successful' dialog

* refactor: fix up types for NotificationAPI

* test: Add `createNotification` appAction

* test: add basic test for `createNotification`

* test: add stub for notification functional test

* Create clock using createDomainObjectWithDefaults

* Replace text-selection with button-selection

* Uninstall @types/eventemitter3

* Revert "Uninstall @types/eventemitter3"

This reverts commit 37e4df9a75.

* fix: remove duplicate dependency

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-01-14 02:12:08 +00:00
22ce817443 Bump eslint-plugin-vue from 9.8.0 to 9.9.0 (#6117)
Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 9.8.0 to 9.9.0.
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v9.8.0...v9.9.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>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-13 16:36:47 -08:00
cdb202d8ba tooling(webpack): move webpack to its own folder (#6076) 2023-01-12 11:46:35 -08:00
905373f294 Bump sass-loader from 13.0.2 to 13.2.0 (#5968)
Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 13.0.2 to 13.2.0.
- [Release notes](https://github.com/webpack-contrib/sass-loader/releases)
- [Changelog](https://github.com/webpack-contrib/sass-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/sass-loader/compare/v13.0.2...v13.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-09 16:57:54 -08:00
60c07ab506 Bump sass from 1.56.1 to 1.57.1 (#6068)
Bumps [sass](https://github.com/sass/dart-sass) from 1.56.1 to 1.57.1.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.56.1...1.57.1)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rukmini Bose (Ruki) <48999852+rukmini-bose@users.noreply.github.com>
2023-01-09 16:45:50 -08:00
7336abc111 Bump css-loader from 6.7.1 to 6.7.3 (#6056)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 6.7.1 to 6.7.3.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v6.7.1...v6.7.3)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-09 16:34:24 -08:00
8fe9da89a3 Bump eslint from 8.30.0 to 8.31.0 (#6091)
Bumps [eslint](https://github.com/eslint/eslint) from 8.30.0 to 8.31.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.30.0...v8.31.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-06 08:56:14 -08:00
e6bdaa957a Bump plotly.js-basic-dist from 2.14.0 to 2.17.0 (#6078)
Bumps [plotly.js-basic-dist](https://github.com/plotly/plotly.js) from 2.14.0 to 2.17.0.
- [Release notes](https://github.com/plotly/plotly.js/releases)
- [Changelog](https://github.com/plotly/plotly.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/plotly/plotly.js/compare/v2.14.0...v2.17.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-03 16:27:54 -08:00
93b5519c4b Bump karma-spec-reporter from 0.0.34 to 0.0.36 (#6058)
Bumps [karma-spec-reporter](https://github.com/tmcgee123/karma-spec-reporter) from 0.0.34 to 0.0.36.
- [Release notes](https://github.com/tmcgee123/karma-spec-reporter/releases)
- [Commits](https://github.com/tmcgee123/karma-spec-reporter/compare/v0.0.34...v0.0.36)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-03 13:32:19 -08:00
04ef4b369c chore: bump version to 2.1.6-SNAPSHOT (#6092) 2023-01-03 09:50:28 -08:00
5424a62db5 [Notebook] Handle conflicts properly (#6067)
* making a revert on failed save more clear

* only notify conflicts for non sync items in object api, spruce up notebook with better transaction tracking and observing and unobserving during transactions, structuredClone backup in monkeypatch

* WIP

* WIP debuggin

* fresh start

* dont observe in transaction objects, small changes to notebook vue to indicate saving/prevent spamming, added forceRemote flag to objects.get

* updating readability of code as well as fix issue of stuck transaction for same value entry edits

* once entry is created, click out to blur

* quick revert
;

* click outside of entry to blur and commit

* switched to enter... as suggested :)

* removing unused variable

* initializing transaction to null as we are using that now for no transaction

* fix: ensure EventSource is closed so it recovers

- Make sure to close the CouchDB EventSource as well, so that it can recover in the case where two tabs or windows are on Open MCT and one refreshes. The check on line 81 was preventing recovery since the EventSource was not closed properly.

* enhance, enhance, enhance readability

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2022-12-29 14:11:08 -08:00
9ed9e62202 Use the current clock's timestamp to show the now line in the timestrip (#6082) 2022-12-28 22:18:47 +00:00
327fc826c1 fix(imagery): Unblock 'latest' strategy requests for Related Telemetry in realtime mode (#6080)
* fix: use ephemeral timeContext for thumbnail metadata requests

* fix(TEMP): use `eval-source-map`

- **!!! REVERT THIS CHANGE BEFORE MERGE !!!**

* fix: only mutate if object supports mutation

* fix: pass identifier instead of whole domainObject

* fix: add start and end bounds to request

* Revert "fix(TEMP): use `eval-source-map`"

This reverts commit 7972d8c33a.

* docs: add comments
2022-12-28 19:12:00 +00:00
a9e3eca35c chore: bump Playwright to v1.29 (#6004)
* chore: bump Playwright to 1.28.0

* chore: bump playwright to v1.29.0

* fix: remove `|| true` shim for codecov

* Revert "fix: remove `|| true` shim for codecov"

This reverts commit ca3766fb5a.

* docs: add instructions for upgrading Playwright
2022-12-27 14:46:19 -08:00
cbecd79f71 Do not register time system listener until we have resolve remote clock object (#6063) 2022-12-20 14:01:47 -08:00
3deb2e3dc2 Bump moment-timezone from 0.5.38 to 0.5.40 (#6050)
Bumps [moment-timezone](https://github.com/moment/moment-timezone) from 0.5.38 to 0.5.40.
- [Release notes](https://github.com/moment/moment-timezone/releases)
- [Changelog](https://github.com/moment/moment-timezone/blob/develop/changelog.md)
- [Commits](https://github.com/moment/moment-timezone/compare/0.5.38...0.5.40)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2022-12-20 13:50:57 -08:00
d6e80447ab Mutables for the Tree 🎄 + clean up TreeItem observers and mutables properly (#6032)
* fix: refresh object after conflict error

* fix: recover from error thrown during create

- Ensure that the "Saving" modal dialog is closed

- Notify user of the error, and also print to console to catch in e2e

* fix: default selector tree item to 'mine' folder

- If create fails due to a conflict or otherwise, and the user immediately tries to "Create" again, default the selector tree's selected item to the "mine" folder (which we know exists).

* fix: don't listen to composition if Selector Tree

* refactor: remove dead code

* fix: use MutableDomainObjects in the tree

- Only use mutables and observers if NOT a SelectorTree

- Properly clean up observers and mutables when a parent item is removed from the tree

* test: verify conflicts don't break object creation

* test: verify dialog closes and object is created

* refactor(e2e): update test

- Error notification on 'My Items' folder missing was removed, so don't check for it

* test: increase timeout

* refactor(e2e): use Promise.any()

* refactor(e2e): use Promise instead of polling

* test: add 2p annotation

* test: use `waitForRequest` instead of promise

- tidy up test, add comments describing our pattern

* docs(e2e): add best practices for network tests

* refactor(e2e): avoid using Promise.any

* fix: de-reactify observer and mutable maps

* fix: destroy by path on treeItem close

* fix: don't refresh for synchronized objects

* docs: fix a typo 🔥

* fix: remove existing mutable before adding

* fix: fail fast if these aren't functions

- Remove check for typeof 'function' to not hide any potential coding errors

* fix: walk up navigationPath if item not found

* chore: fix lint errors

* fix: parse conflicted object name correctly

* fix: re-throw conflict error

* fix: Cancel edit mode on conflict
2022-12-20 13:27:51 -08:00
1a4bd0fb55 Bump eslint from 8.29.0 to 8.30.0 (#6066)
Bumps [eslint](https://github.com/eslint/eslint) from 8.29.0 to 8.30.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.29.0...v8.30.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-20 00:42:32 +00:00
80f89c7609 fix: no deleted objects in locator search (#6038)
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2022-12-19 15:21:24 -08:00
b82649772f Bump sinon from 14.0.1 to 15.0.1 (#6057)
Bumps [sinon](https://github.com/sinonjs/sinon) from 14.0.1 to 15.0.1.
- [Release notes](https://github.com/sinonjs/sinon/releases)
- [Changelog](https://github.com/sinonjs/sinon/blob/main/docs/changelog.md)
- [Commits](https://github.com/sinonjs/sinon/compare/v14.0.1...v15.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-12-19 11:21:27 -08:00
7f2ed27106 [CLA Approved] Add rows refractor (#5284)
* addRows Refractor

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-12-19 18:13:53 +00:00
57e02db6b5 Prevent scrolling the window area on new image thumb telemetry - 5867 (#5961)
* Setup a scroll handler to avoid using scrollIntoView when in a layout

* Implement a separate scroll to action when in layouts

* Simplified scroll reset event and logic

* Adjust test to capture new scroll handler

* Remove done invocation after converting to async fn

* Prevent default for arrow keys to avoid scrolling layoyut

* await scrollToFocused

* Logical or to nullish coalescing

* Removed set in favor of using isNavigatedObject api

* Apply animation style after image history has length

* Lint fixes

Co-authored-by: Scott Bell <scott@traclabs.com>
2022-12-16 10:06:16 +01:00
d54335d21c Bump version to 2.1.5-SNAPSHOT (#6052) 2022-12-12 15:57:03 -08:00
e0ed0bb6e2 [Plots] Ignore Infinity when autoscaling y-axis (#5907)
* Change approach to filter positive and negative infinity values when updating stats

* Change filter when there are no plot stats and a positive/negative infinity value occurs

* Add check for negative infinity

* Name the unplottable values array and move it to the constructor

* Add option to render infinity values

* Add e2e test to render plot with infinity values

* Add accessibility labels to help locate items in tests
Refactor tests

* refactor(e2e): stabilize plotRendering test

Co-authored-by: Shefali <simplyrender@gmail.com>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2022-12-12 11:51:57 -08:00
ed3fd8f965 Bump babel-loader from 9.0.0 to 9.1.0 (#5947)
Bumps [babel-loader](https://github.com/babel/babel-loader) from 9.0.0 to 9.1.0.
- [Release notes](https://github.com/babel/babel-loader/releases)
- [Changelog](https://github.com/babel/babel-loader/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel-loader/compare/v9.0.0...v9.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-08 22:45:25 +00:00
e6d59c61d1 Bump typescript from 4.9.3 to 4.9.4 (#6046)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.9.3 to 4.9.4.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.9.3...v4.9.4)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-08 14:33:49 -08:00
b74b27c464 docs: fixed punctuation & grammar in summary section (#6037) 2022-12-06 23:59:54 +00:00
d35e161701 Bump @types/jasmine from 4.3.0 to 4.3.1 (#6040)
Bumps [@types/jasmine](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jasmine) from 4.3.0 to 4.3.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jasmine)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-06 18:08:24 +00:00
653cb62f9c Bump mini-css-extract-plugin from 2.6.1 to 2.7.2 (#6043)
Bumps [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) from 2.6.1 to 2.7.2.
- [Release notes](https://github.com/webpack-contrib/mini-css-extract-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/mini-css-extract-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/mini-css-extract-plugin/compare/v2.6.1...v2.7.2)

---
updated-dependencies:
- dependency-name: mini-css-extract-plugin
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-06 11:00:34 +01:00
19b3232fa0 Bump eslint from 8.28.0 to 8.29.0 (#6041)
Bumps [eslint](https://github.com/eslint/eslint) from 8.28.0 to 8.29.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.28.0...v8.29.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-05 16:56:58 -08:00
19892aab53 [MMGIS] Modify Independent time contexts, open in new tab action, and expose overlay plots to support MMGIS pivoting (#6025)
* Expose overlay plot so that it can be imported by an external plugin

* If the current object has an independentContext, ignore any upstream independentContext

* Accept any custom url param in open in new tab action
2022-12-02 17:03:43 -06:00
a168ce25cf Bump eslint-plugin-vue from 9.7.0 to 9.8.0 (#6012)
Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 9.7.0 to 9.8.0.
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v9.7.0...v9.8.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>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-02 21:44:49 +00:00
189c58f952 Remove Go To Original when in edit mode (#5966)
Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
2022-12-02 21:16:36 +00:00
0dfc028e1b Bump @types/lodash from 4.14.190 to 4.14.191 (#6027)
Bumps [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) from 4.14.190 to 4.14.191.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-01 13:32:36 -08:00
77e93f1aee Bump eslint from 8.27.0 to 8.28.0 (#6002)
Bumps [eslint](https://github.com/eslint/eslint) from 8.27.0 to 8.28.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.27.0...v8.28.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

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>
2022-11-30 22:29:47 +00:00
394fbbe61b Bump @types/lodash from 4.14.189 to 4.14.190 (#6010)
Bumps [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) from 4.14.189 to 4.14.190.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-30 18:18:36 +00:00
40afb04f0c Merge release/2.1.3 into master (#6015)
* Bump version to `2.1.3` (#5973)
* Preserve Gauge configuration changes on create/edit (#5986)
* fix(#5985): deep merge on create/edit properties
- Perform a deep merge of old and new properties on Create/Edit properties actions
* refactor(e2e): improve selector in appActions
* test(e2e): add tests for gauges
- test creating a non-default gauge (checks only for console errors)
- test updating a gauge (checks only for console errors)
* fix(e2e): use pluginFixtures for gauge tests
* fix(e2e): prevent fail if testNotes is undefined
* Make the tree key unique (#5989)

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2022-11-29 17:51:43 -08:00
be73b0158a Remove link to retired live demo (#6008)
* Remove link to retired live demo
Closes https://github.com/nasa/openmct/issues/6006
Remove link to retired live demo
* Added screenshot
2022-11-28 17:06:55 +00:00
625205f24b do not use dev source maps in prod (#6007) 2022-11-28 12:00:18 -05:00
a706a8b73e Bump @percy/cli from 1.13.0 to 1.16.0 (#6011) 2022-11-24 13:02:02 -08:00
1ddf5e5137 feat(imagery): show viewable area when zoomed (#5877)
* feat: viewable area

* chore: add test

* fix: get image ref when real-time

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-11-21 13:06:12 -08:00
a79646a915 Bump typescript from 4.8.4 to 4.9.3 (#5988)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.8.4 to 4.9.3.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-19 00:04:16 +00:00
d5266e7ac7 Bump webpack-cli from 4.10.0 to 5.0.0 (#5996)
Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 4.10.0 to 5.0.0.
- [Release notes](https://github.com/webpack/webpack-cli/releases)
- [Changelog](https://github.com/webpack/webpack-cli/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-cli/compare/webpack-cli@4.10.0...webpack-cli@5.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-18 19:25:36 +00:00
05de7ee2e0 Bump @braintree/sanitize-url from 6.0.0 to 6.0.2 (#5970)
Bumps [@braintree/sanitize-url](https://github.com/braintree/sanitize-url) from 6.0.0 to 6.0.2.
- [Release notes](https://github.com/braintree/sanitize-url/releases)
- [Changelog](https://github.com/braintree/sanitize-url/blob/main/CHANGELOG.md)
- [Commits](https://github.com/braintree/sanitize-url/compare/v6.0.0...v6.0.2)

---
updated-dependencies:
- dependency-name: "@braintree/sanitize-url"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-17 09:48:23 -08:00
dad88112c4 Bump @types/lodash from 4.14.188 to 4.14.189 (#5987) 2022-11-15 13:42:48 -08:00
202d6d8c5d Bump version to 2.1.4-SNAPSHOT (#5974) 2022-11-14 10:48:36 -08:00
e70bcc414c revert persisted time to last actual persisted time on save error (#5971) 2022-11-10 15:25:35 -08:00
7bb4a136d7 Use Composition API to add/remove from composition (#5941)
* Use composition API in RemoveAction

* refactor: ScatterPlotView to use composition API

* fix: initialize transaction to null and reset

* fix: remove seriesKey and correct found condition

* refactor: Gauge to use composition API

* refactor: DisplayLayout to use composition API

* test: RemoveAction starts and ends transactions

* test: add ScatterPlot add/remove telemetry test

* test: fix e2e test and add annotation

* test: remove unnecessary awaits

* test: make some displayLayout tests stable

* test{displayLayout}: navigate to objects via url

* test(gauge): add test for add/remove telemetry

* fix(#3117): init layoutItems within transaction

* refactor: add clearSelection() method

* test: remove unstable tag

* fix(#3117): init frames and use transactions

- fixes #3117 for flexible layouts by syncing frames and composition

- also uses transactions now to avoid race condition

* test(flexibleLayout): removing items via context menu

- add test for removing items via context menu while focusing the layout

- add test for removing items via context menu while not focusing the layout

* fix(e2e): use pluginFixtures

* refactor(e2e): improve selectors

* refactor: use async/await for saving transactions

* docs(e2e): fix comments

* test: use soft expects

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2022-11-10 20:06:13 +00:00
8af3b4309f Add 'View type' label to Display Layout toolbar (#5909)
Addresses #5480

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-11-10 11:38:05 -08:00
bed3d83fd7 Bump moment-timezone from 0.5.37 to 0.5.38 (#5881)
Bumps [moment-timezone](https://github.com/moment/moment-timezone) from 0.5.37 to 0.5.38.
- [Release notes](https://github.com/moment/moment-timezone/releases)
- [Changelog](https://github.com/moment/moment-timezone/blob/develop/changelog.md)
- [Commits](https://github.com/moment/moment-timezone/compare/0.5.37...0.5.38)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-10 17:06:52 +00:00
efda42cf6d Make package public - master (#5967)
* Update package.json

* Update npm-prerelease.yml
2022-11-10 08:45:55 -08:00
e8ee5b3fc9 fix test flake: calculate persisted time last (#5923)
* fix: calculate persisted time last
2022-11-10 08:40:09 -08:00
393cb9767f Bump sass from 1.55.0 to 1.56.1 (#5963) 2022-11-10 16:26:19 +00:00
8b5daad65c feat: sort interceptors by priority, ensure myItemsInterceptor runs first (#5965)
* feat: sort interceptors by priority

* fix(#5914): high priority for MyItemsInterceptor

* fix: create myItems if object is falsy

* test(e2e): update snapshots

Co-authored-by: Scott Bell <scott@traclabs.com>
2022-11-10 08:00:29 -08:00
fabfecdb3e Check for null plot wrapper on plot resize (#5960)
* check for null plot wrapper first

* make code clearer with short circuit up front
2022-11-08 16:11:46 -08:00
a2d8b13204 Bump eslint from 8.26.0 to 8.27.0 (#5955)
Bumps [eslint](https://github.com/eslint/eslint) from 8.26.0 to 8.27.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.26.0...v8.27.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-08 18:14:08 +00:00
4b14d2d6d2 [e2e][couchdb] Add test coverage for mct5616 (#5913)
* [e2e][couchdb] Add test for mct5616

- Add test to verify that modifying object properties generates a single persistence operation with CouchDB

* [e2e] Poll for 1 sec to allow time for request

* [e2e] ignore expected console error

* add comments

* add `aria-label`s to form select fields

* add `aria-label` for clock format select

* [e2e] Improve selector

* make property name less ambiguous

* refactor(e2e): use default object name
2022-11-07 16:04:02 -08:00
d545124942 test(e2e): unique names for created objects by default (#5945)
* feat(e2e): default unique names for new objects

* refactor(e2e): reference generated object names

- Fixes the tests that were locating "Unnamed <object_type>" to use the generated unique names

* feat(e2e): add testInfo into domainObject notes

- adds info about the currently running test and its project to notes

* fix(e2e): fix selector for notes section

* feat: ARIA: menu role for menus and SuperMenus

- Implements the [ARIA: menu role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/roles/menu_role) for Menus and SuperMenus,

* refactor(e2e): use role selectors for menu items

* refactor(e2e): better selectors for "OK" button

* refactor(e2e): better selectors for menu items

* refactor(e2e): improve selector

* refactor(e2e): update test to use appActions

* refactor(e2e): update test to use object name

* refactor(e2e): improve selectors for menu items

* test(e2e): fix search test

* refactor(e2e): update more plain 'text=' selectors

* fix: resolve codeQL error

- remove superfluous argument

* refactor(e2e): move testNotes to `pluginFixtures` and update imports

* refactor(e2e): remove unused fixture from test

* refactor: add dynamic id to form textareas

* refactor(e2e): improve notes textarea selector

* refactor(e2e): remove unused fixture
2022-11-07 23:50:33 +00:00
6abdbfdff0 skip one mo (#5958) 2022-11-07 23:39:01 +00:00
500e655476 Bump jasmine-core from 4.4.0 to 4.5.0 (#5934) 2022-11-04 17:14:07 +00:00
5e1f026db2 Bump @types/lodash from 4.14.186 to 4.14.188 (#5950) 2022-11-04 10:07:49 -07:00
d9efae98c8 Bump @percy/cli from 1.11.0 to 1.13.0 (#5942) 2022-11-04 16:50:42 +00:00
091f6406a8 Merge release/2.1.2 into master (#5946)
* Bump version to `2.1.2`

* Ensure properties stay in sync and are committed only once (#5717)

* Ensure form properties stay in sync
* Separate out overlay based forms and custom forms
* Use a transaction to save properties
* Fix GaugeController to not depend on event emitted from FormsAPI
* refactor showForms to call showCustomForm

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>

* Fix persistence timestamp (#5916)

* Calculate persisted timestamp last
* Added regression tests
* Correct transaction handling code
* Code cleanup

* Fix typo for publish (#5936)

* Add e2e tests to npm package (#5930)

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-11-03 13:49:03 -07:00
42a0e503cc Bump eslint-plugin-vue from 9.6.0 to 9.7.0 (#5932)
Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 9.6.0 to 9.7.0.
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v9.6.0...v9.7.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>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-01 16:41:03 +00:00
4697352f60 Fix now marker for time system axis in timestrips and plans (#5911)
* Ensure the now marker spans the height of the plan.
* Set height for timestrip for the now marker
* Fix linting issues
* Fix failing test
2022-10-31 23:56:52 +00:00
015c764ab3 Update dependabot.yml (#5935) 2022-10-31 21:58:12 +00:00
8fe465d9fc Bump eslint from 8.25.0 to 8.26.0 (#5908)
Bumps [eslint](https://github.com/eslint/eslint) from 8.25.0 to 8.26.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.25.0...v8.26.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-28 21:57:31 +00:00
9c1368885a Bump babel-loader from 8.2.5 to 9.0.0 (#5920) 2022-10-27 14:51:01 -07:00
391c0b2e7c Bump version to 2.1.3-SNAPSHOT (#5904) 2022-10-27 17:29:41 +00:00
2ae061dbcd Bump sinon from 14.0.0 to 14.0.1 (#5831)
Bumps [sinon](https://github.com/sinonjs/sinon) from 14.0.0 to 14.0.1.
- [Release notes](https://github.com/sinonjs/sinon/releases)
- [Changelog](https://github.com/sinonjs/sinon/blob/main/docs/changelog.md)
- [Commits](https://github.com/sinonjs/sinon/compare/v14.0.0...v14.0.1)
Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-10-25 11:42:08 -07:00
41fc502564 Generate type declarations for CompositionAPI and publish with OpenMCT (#5838)
* add typescript

* update tsconfig

* convert to es6 class

* Convert more stuff to es6 class

* skip checking libs, test files

* more es6 classes!

* Fix some jsdocs

* Rename file

* Improve jsdoc types

* Rename references as well

* more types

* types for CompositionAPI

* Types for CompositionCollection

* Types for CompositionProvider

* type

* types for api

* nvm

* cleanup MCT

* Fix API type definition

* Generate types before publish

* fix openmct 👀

* rename PublicAPI -> OpenMCT and document methods

* try and fix visual test ?

* Make private methods private

* more private methods!!

* import all es6 api's so we get more types for free

* convert Selection to es6 class

* remove redundant docs

* fix Branding types

* fix openmct.start() types

* Remove useless `@memberof`

* Add parameter name

* [docs] Add a section on Types

* markdownlint

* word

* Add section on limitations / contibuting types

* Let these methods be private

* make private members private, fix a type

* fix another type

* Make method private

* Update docs for `skipMutate` and related methods

* Rename file and fix references

* `DefaultCompositionProvider` extends `CompositionProvider`

* Make private members private

* Type for `AbortSignal`

* `domainObject` must be accessible for perf tests

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-10-21 17:29:52 -07:00
b4554d2fc1 User attribution (#5827)
* initial changes adding modified and created by fields to domain objects and updating properties inspector
* adding created date to object creation
* added a test for created timestamp
* updating remove action to hold the transaction and disregard edit state when handling transactions, also updated object api to return transaction when starting and ignore edit state when determining if transaction is active
* updating docs for object api "startTransaction"
* updating incorrect use of edit and transaction in our appActions for testing

Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Shefali <simplyrender@gmail.com>
2022-10-21 17:05:59 -07:00
feba5f6d3b use full identifier instead of key (#5891) 2022-10-21 16:45:52 -07:00
4357d35f4a Centre and wrap Condition Widget label (#5886)
* [ConditionWidget] Center label text

Addresses #5799

* [ConditionWidget] Wrap label text

Addresses #5799

* [ConditionWidget] Add padding to label

Addresses #5799

* [ConditionWidget] Use interiorMargin value for padding

Addresses #5799
2022-10-21 16:35:59 -07:00
5041f80e5b Update version to 2.1.2-SNAPSHOT (#5897) 2022-10-21 12:16:46 -07:00
9e23f79bc8 Add time context to telemetry requests (#5887)
* add time context to telemetry requests

* change to empty array

* refactor telemetry api to use time context

* removed unused function

* add tests

* add test, rename function

* make function public
2022-10-21 20:25:24 +02:00
bd1e869f6a fix timing issue (#5896) 2022-10-21 20:19:41 +02:00
e4a36532e7 Bump @types/lodash from 4.14.178 to 4.14.186 (#5892) 2022-10-19 20:40:30 +00:00
2bc2316613 Bump @types/jasmine from 4.0.1 to 4.3.0 (#5893) 2022-10-19 13:34:44 -07:00
2fa36b2176 Delete lighthouse.yml (#5885) 2022-10-18 16:10:09 -07:00
efa38d779e Remove two types from package and pin others (#5883)
* Update package.json

* Update package.json

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-10-18 21:17:30 +00:00
951cc6ec0d Bump eslint-plugin-vue from 9.3.0 to 9.6.0 (#5837)
Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 9.3.0 to 9.6.0.
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v9.3.0...v9.6.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>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-18 16:31:01 +00:00
ef4b8a9934 Bump @percy/cli from 1.10.3 to 1.11.0 (#5846) 2022-10-17 17:52:11 -07:00
c14b48917e Plan documentation (#5871)
* Plan documentation
2022-10-13 20:04:51 -07:00
26165d0a99 Bump eslint from 8.24.0 to 8.25.0 (#5861)
Bumps [eslint](https://github.com/eslint/eslint) from 8.24.0 to 8.25.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.24.0...v8.25.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

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>
2022-10-13 16:53:05 +00:00
f7cf3f72c2 Add playwright-core to dependabot ignore list (#5863) 2022-10-10 15:35:40 -07:00
cb8e09c9f9 Master 2.1.1 (#5858)
* Update version

* Don't delete annotations if there aren't any (#5829)

* don't delete annotations if there aren't any

* add test and align playwright-test

* align core with test

* added annotation describing test

* Add `aria-label` for time conductor history button (#5830)

* [Overlay Plot] Inspector series and legend sync fix (#5835)

* fixed overlay plots to react to series removals correctly, added alias visual to elements pool aliased items

* Keep transaction open on failed editor save (#5840)

* do not end a transaction on a failed editor save
* add unit tests for successful editor save and unsuccessful editor save

* If no matching tags, do not attempt tag search (#5839)

* do not attempt search if no matching tags

* fix timing on test

* commit again in hopes that github will run checks

* add back null tag check

* add some better documentation to tests

Co-authored-by: Andrew Henry <akhenry@gmail.com>

* Update version for  master

Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-10-08 09:04:38 -07:00
026eb86f5f 4417 fix codeql issues (#5793)
* Add release to codeql and queries to match lgtm

* Add lgtm config file

* Custom codeQL config to ignore app.js

* Custom config for lgtm

* Remove query filter for lgtm

* Updated the security test docs

* Remove lgtm.yml and delete app.js references

* Update codeql-config.yml

Co-authored-by: Alize Nguyen <alizenguyen@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-10-07 16:30:09 +00:00
866859a937 [CouchDB] Re-establish feed connection if EventSource is closed due to error (#5845)
* Re-establish feed connection if EventSource is closed due to error

* Use keepAlive timer

* Get rid of magic numbers and add comment
2022-10-06 21:41:38 +02:00
afc54f41f6 Publish example layouts json (#5842) 2022-10-05 14:17:36 -07:00
72c980f991 Have webpack-dev-server write webpack assets to disk (#5836)
* create the files not webpacked and watch css

* removing trailing slash
2022-10-04 10:53:44 -07:00
9bf39a9cd4 Bump eslint from 8.23.1 to 8.24.0 (#5807)
Bumps [eslint](https://github.com/eslint/eslint) from 8.23.1 to 8.24.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.23.1...v8.24.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-10-03 11:02:01 -07:00
33fd95cb2b Revert "[User Attribution] "createdBy" and "modifiedBy" fields for domainObjects (#5741)" (#5826)
This reverts commit 8c92178895.
2022-09-30 16:53:49 -07:00
8c92178895 [User Attribution] "createdBy" and "modifiedBy" fields for domainObjects (#5741)
* Implementation of user attribution of object changes
* Adds created date to object creation
* Updating remove action to wait for save before navigationg

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-09-30 13:47:10 -07:00
35bbebbbc7 Fix plot resize handling (#5637)
* Imagery thumbnail regression fixes - 5327 (#5591)

* Add an active class to thumbnail to indicate current focused image

* Differentiate bg color between real-time and fixed

* scrollIntoView inline: center

* Added watcher for bounds change to trigger thumbnail scroll

* Resolve merge conflict with requestHistory change to telemetry collection

* Split thumbnail into sub component

* Monitor isFixed value to unpause playback status

Co-authored-by: Khalid Adil <khalidadil29@gmail.com>

* [e2e] Improve appActions (#5592)

* update selectors to use aria labels

* Update appActions

- Create new function `getHashUrlToDomainObject` to get the browse url to a given object given its uuid

- Create new function `getFocusedObjectUuid`... self explanatory :)

- Update `createDomainObjectWIthDefaults` to make use of the new url generation

- Update `createDomainObject...`'s arguments to be more organized, and accept a parent object

- Update some docs, still need to clarify some

* Update appActions e2e tests

- Refactor for organization

- Test our new appActions in one go

* Update existing usages of `createDomainObject...` to match the new API

* fix accidental renamed export

* Fix jsdoc return types

* refactor telemetryTable test to use appActions

* Improve selectors

* Refactor test

* improve selector

* add clock mode appActions

* lint

* Fix jsdoc

* Code review comments

* mark failing visual tests as fixme temporarily

* Update package.json (#5601)

* Fix menu style in Snow theme (#5557)

* Include the plan source map when generating the time list/plan hybrid object (#5604)

* Search should indicate in progress and no results states, filter orphaned results (#5599)

* no matching result implemented

* now filtering annotations that are orphaned

* filter object results without valid paths

* add progress bar

* added e2e tests

* removed extraneous click

* fix typos

* fix unit tests

* lint

* address pr comments

* fix tests

* fix tests, centralize logic to object api, check for root instead

* remove debug statement

* lint

* fix documentation

* lint

* fix doc

* made some optimizations after talking with akhenry

* fix test

* update docs

* fix docs

* Have in-memory search indexer use composition API (#5578)

* need to remove tags and objects on composition removal
* had to separate out emits from load as it was causing memory indexer to loop upon itself

* Add parsing for areIdsEqual util to consistently remove folders (#5589)

* Add parsing util to identifier for ID comparison

* Moved firstIdentifier to top of function

* Lint fix

Co-authored-by: Andrew Henry <akhenry@gmail.com>

* Revert "Have in-memory search indexer use composition API (#5578)" (#5609)

This reverts commit 7cf11e177c.

* [e2e] Tests for Display Layout and LAD Tables and telemetry (#5607)

* Check for circular references in originalPath - 5615 (#5619)

* check for circular references

* add test

* fix test

* address PR comments by making comments better

* fix docs...again

* Don't request data if there is a mouse click with no action
Don't request data if width gets smaller or if the change is less than the threshold of 50.

* Update version number

* Prevent cyclic references in link & move actions (#5635)

* do not create circular refs

* add negative validation test

* move to plugin

* add link test too

* fix docs

* refactored per john request

* fix path

* use appAction lib

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

* [Fault Management] New Example Provider, Unit and e2e tests (#5579)

* added unit tests for fault management plugin

* modified the example fault provider to work out of the box

* updating for new e2e folder structure

* part of the e2e tests

* WIP

* Imagery thumbnail regression fixes - 5327 (#5569)

* Add an active class to thumbnail to indicate current focused image

* Differentiate bg color between real-time and fixed

* scrollIntoView inline: center

* Added watcher for bounds change to trigger thumbnail scroll

* Resolve merge conflict with requestHistory change to telemetry collection

* Split thumbnail into sub component

* Monitor isFixed value to unpause playback status

* updated search to include name, namespace and description added some more e2e tests

* added rest of e2e tests

* fixed my init script, had to disable lint for no-force because it was not working without it, saw online this may be a pw bug

* fix: removing maelstrom theme from application (#5600)

* added some tests for no faults

* visual tests

* added visual tests for fault management

* created utils file for shared functionality between function and visual tests

* updating to 2.0.8

* tryin to remove imagery changes from master

* trying to trigger a refresh

* tryin to refresh

* updated search to include name, namespace and description added some more e2e tests

* added rest of e2e tests

* fix: removing maelstrom theme from application (#5600)

* fixed my init script, had to disable lint for no-force because it was not working without it, saw online this may be a pw bug

* added some tests for no faults

* visual tests

* added visual tests for fault management

* created utils file for shared functionality between function and visual tests

* updating to 2.0.8

* no clue

* still no clue

* removing imports and chaning to requires

* updating utils file to work with require

* fixing paths

* fixing a test I had messed up when adding static exmaple faults

* ONE LAST PATH FIX... hopefully

* typo in files fix

* fix folder typo

* thought I got this one, but apparently not, well I did now! who is laughing now!?

Co-authored-by: Michael Rogers <contact@mhrogers.com>
Co-authored-by: Vitor Henckel <vitor@henckel.com.br>

* Sort tree items locally on rename (#5643)

* fix typo

* Sort the tree items locally on object rename

* Use the navigationPath as a key

- This ensures that objects AND linked objects will be sorted

* add 'tree' and 'treeitem' roles to mct-tree

* WIP tree item reordering test

* Select the first object that matches

* Test that all object links are also reordered

* Get the final uuid before queryParams as notebook sections have uuids

* Make `openObjectTreeContextMenu` more deterministic and update usage

* Add `expandPathToTreeItem` and `expandTreeItemByName` appActions

* add `#tree-pane` id for the tree view

* Add tree visual component test suite and bump percy-cli

* Remove tree appActions

* Better variable name

Co-authored-by: Scott Bell <scott@traclabs.com>

* Mct5549 fix indexer composition error (#5610)

* [Display Layout] Composition and configuration sync (#5669)

LGTM

* [e2e] Stabilize notebook tag tests (#5681)

* Use more deterministic selector

* Hover first to "slow down" e2e actions while in headless mode

* Moves condition set fix into 2.0.8 (#5673)

* Remove flag that determines if data should be reloaded on interactions.
Separate logic to clear history and reload data.

* Rename method to clarify intention

* Set Focused Image index after a imagery is selected from a timestrip - 5632 (#5664)

* Set focused image when timestamp prop is passed in

* Unused var

* Create timestrip with imagery child

* Add equality check for hovered image and view large image url

* Cleanup

* Time List 5534 for release/2.0.8 (#5678)

* Changes to Time List view. Closes #5534.
- Compacted table row spacing.
- Set all timeframes to display by default when creating a new Time List.
- Removed 'Upload plan' file button from properties.

* Changes to Time List view. Closes #5534.
- Better hint text for editing Timeframe Inspector section.

Co-authored-by: Andrew Henry <akhenry@gmail.com>

* [CI] Enable couchdb e2e testing in open source (#5655)

* Reduce threshold to 10px - Chrome resizes to about 7 pixes and Firefox to 0.

* boilerplate for coverage

* add stubs

* Update version

* Remove debugging code

* [Flexible Layout] Fix draggable status for layout items while in browse mode (#5750)

* Modify flexible layout pages to make them not draggable in browse mode and add e2e test

* Don't destroy mutable if the domain object is not ready yet (#5695)

* Check if the domain object is set (mounted is done) before trying to destroy the mutable

* Use optional chaining. Add mutable promise check to prevent memory leaks

* Don't request data if there is a mouse click with no action
Don't request data if width gets smaller or if the change is less than the threshold of 50.

* Remove flag that determines if data should be reloaded on interactions.
Separate logic to clear history and reload data.

* Rename method to clarify intention

* Reduce threshold to 10px - Chrome resizes to about 7 pixes and Firefox to 0.

* boilerplate for coverage

* add stubs

* Remove debugging code

* Remove unused import

* Request priority (#5737)

* Set priority of couch requests to high
* Set priority of image requests to low
* Add e2e test for low-priority images

* Add test for re-requests on clicking a plot

* Adding new tests for testing plot requests for historical data

* Clean up e2e test for plot requesting historical data

* Write tests to ensure resizing the plot makes requests for data appropriately

* Remove fdescribe

* Fix resizing plot tests

Co-authored-by: Michael Rogers <contact@mhrogers.com>
Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: Alize Nguyen <alizenguyen@gmail.com>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Vitor Henckel <vitor@henckel.com.br>
2022-09-30 20:04:19 +00:00
ce463babff 5734 synchronization for new tags on notebook entries (#5763)
* trying this again

* wip

* wip

* wip

* one annotation per tag

* fixed too many events firing

* syncing works mostly

* syncing properly across existing annotations

* search with multiple tags

* resolve conflicts between different tag editors

* resolve conflicts

* fix annotation tests

* combine search results

* modify tests

* prevent infinite loop creating annotation

* add modified and deleted

* revert index checkin

* change to standard couch deleted flag

* revert throwing of error

* resolve conflict issues

* work in progress, but load annotations once from notebook

* works to add

* attempt 1

* wip

* last changes

* listening works, though still getting conflicts

* rename to annotationLastCreated

* use local mutable again

* works with new tags syncing

* listeners wont fire if modification is null

* clean up code

* fixed local search

* cleaned up log messages

* remove on more log

* add e2e test for network traffic

* lint

* change to use good old for each

* add some local variables for clarity

* Update src/api/objects/ObjectAPI.js

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

* Update src/api/objects/ObjectAPI.js

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

* Update src/plugins/notebook/components/Notebook.vue

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

* press enter for last entry

* add test explanation of numbers

* fix spread typo

* add some nice jsdoc

* throw some errors

* use really small integer instead

* remove unneeded binding

* make method public and jsdoc it

* use mutables

* clean up tests

* clean up tests

* use aria labels for tests

* add some proper tsdoc to annotation api

* add undelete test

Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-09-30 10:32:11 -07:00
27c30132d2 4386 - In time conductor history, show them on hover if only milliseconds have changed - tooltip (#5783)
* 4386 - In time conductor history, show them on hover if only milliseconds have changed

* [e2e] Add quick test

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2022-09-30 10:16:35 -07:00
2bdac56505 Replace app.js with webpack-dev-server (#5797) 2022-09-30 08:17:02 -07:00
35c42ba43d Reviewer to smoke test PRs before merge (#5771)
* Reviewer to smoke test PRs before merge

* Update PULL_REQUEST_TEMPLATE.md

* Update PULL_REQUEST_TEMPLATE.md

* Update PULL_REQUEST_TEMPLATE.md

Changes based on feedback

* Update PULL_REQUEST_TEMPLATE.md

Tweaking the language slightly for clarity.

* Update PULL_REQUEST_TEMPLATE.md

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-09-28 17:13:41 -07:00
6bdb8c9e1c Bump eslint-plugin-playwright from 0.11.1 to 0.11.2 (#5804)
* Bump eslint-plugin-playwright from 0.11.1 to 0.11.2

Bumps [eslint-plugin-playwright](https://github.com/playwright-community/eslint-plugin-playwright) from 0.11.1 to 0.11.2.
- [Release notes](https://github.com/playwright-community/eslint-plugin-playwright/releases)
- [Commits](https://github.com/playwright-community/eslint-plugin-playwright/compare/v0.11.1...v0.11.2)

---
updated-dependencies:
- dependency-name: eslint-plugin-playwright
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix new lint error

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 <jesse.d.mazzella@nasa.gov>
2022-09-26 10:00:51 -07:00
4a5467aba7 Bump sass from 1.54.9 to 1.55.0 (#5800) 2022-09-23 10:50:22 -07:00
b85238d7d0 Update version for sprint 2.1.1 (#5796)
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-09-21 00:11:23 +00:00
410b3d6036 Misc UI 5640 (#5762)
* Closes #5640
- Layout frame edit handle visually tweaked, no longer resizes frame contents on hover when editing.
- Gauge now centers in its container instead of left flush.
- Added large bg-icon for aggregate telemetry.
- Removed 'no class' single wrapper div in mct-tree.vue.
- Refinements to appearance of locator in save/edit overlay dialog.
- Better styling for search in progress and 'no results' messaging.
- Revised art and glyph for `icon-tabular-scrolling`.
- Change TelemetryTableType to use icon-tabular-scrolling icon and bg-icon.
- Fix bad/erroneous descriptions for Telemetry Table, LAD Table and LAD Table Set.
- Fix descriptions for multiple view types.
- Removed unused .bg-icon class.
- Fixed bad value check that was causing panes not to collapse when dragged
(by design) to a very narrow width.
- Visual style for grippy adjusted for better clarity on some monitors.
- Normalize size of alphanumeric's labels and values in Display Layouts.
- Fix margin strategy for object label in main view.
- Fixed Locator to dynamically size itself to use available vertical space.
- Added new `l-overlay-dialog` size parameter.
- Better approach to overlay sizing to make Properties dialogs larger vertically
and fullscreen overlays more apparent to the user.
- Normalized approach to numeric inputs to fix cross-browser
inconsistency with input spinners.
- Fixed layout problem in `Datetime.vue` component due to
numeric input spinners changes.
- Added object types for Display Layout drawing objects.
- Code modded in `ObjectName.vue` to grab layout object cssClass from
layoutItem.type.
- Big thanks to @jvigliotta for the assist with this!
- update snapshots

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-09-20 23:40:16 +00:00
9a727cac2e [Notebook] Dropping item into "drop area" in notebook not working (#5791)
* notebook embed visual test initial

* waiting for createNewEmbed util function to finish before adding embed

* removing debug code

* adding unstable tag to visual test

* added e2e embed tests for existing and new entriesupdated some test utils as well

* removing test artifacts

* removing more test artifacts

* adding test data back in

* removing debug code

* adding back in test artifacts

* updated test I broke

* removing focused test

* removing unsed variable

* fix lint issues

* adding unstable tag for new e2e tests
2022-09-20 17:43:48 -05:00
be5472ebdb Ignore patch of eslint-plugin-vue (#5786) 2022-09-19 10:41:43 -07:00
f39419bc84 2.1.0 backmerge into master (#5784)
* Update version

* [Flexible Layout] Fix draggable status for layout items while in browse mode (#5750)

* Modify flexible layout pages to make them not draggable in browse mode and add e2e test

* Don't destroy mutable if the domain object is not ready yet (#5695)

* Check if the domain object is set (mounted is done) before trying to destroy the mutable

* Use optional chaining. Add mutable promise check to prevent memory leaks

* Request priority (#5737)

* Set priority of couch requests to high
* Set priority of image requests to low
* Add e2e test for low-priority images

* Timelist views support fixed time - 5629 (#5726)

* Initialize full view for fixed time

* Clean up

* Add back in ticker after merge

* Check for undefined clock instead of timestamp

* Cleanup

* Initialize full view for fixed time

* Clean up

* Add back in ticker after merge

* Check for undefined clock instead of timestamp

* Cleanup

* Update timestamp method and remove from beforeDestroy

* Shorten ternary to optional chaining

* Cleanup unused var

* Moved duplicated logic to method

* Reorder methods

* Update Timelist.vue

Set timestamp to clock start when in fixed time

* Added blank line

* Lint fix

* Update pluginSpec.js

* Invoke currentValue method properly

Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Michael Rogers <contact@mhrogers.com>
2022-09-19 10:18:20 -07:00
07bf85a623 Bump @percy/cli from 1.7.2 to 1.10.3 (#5780) 2022-09-16 16:22:42 -07:00
425e662d6e Show version information for Plans if available (#5703)
* Show document version if it is available
* Add test for plan version display in the inspector
2022-09-16 11:36:04 -07:00
2a689b896f [Build] Add @babel/eslint-parser to ignore list (#5759)
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-09-14 21:29:03 +00:00
ffe6fd1941 Update our Browser Support Matrix documentation (#5756)
* docs for chrome

* Tidy up formatting, spelling/grammar, update snippet

* more readme fixes

* Update example test suite

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-09-14 14:16:20 -07:00
cae579f5b3 Add baseUrl, outDir, and exclude options (#5757)
- Fixes erroneous TS language server warnings showing up in the VSCode terminal

- Ensures TypeScript language server features (such as refactor) are able to work properly by excluding build files
2022-09-14 10:05:41 -07:00
a073649e64 Bump sass from 1.54.4 to 1.54.9 (#5739)
Bumps [sass](https://github.com/sass/dart-sass) from 1.54.4 to 1.54.9.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.54.4...1.54.9)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-13 22:55:27 +00:00
f40e14cb2c Bump webpack-hot-middleware from 2.25.1 to 2.25.2 (#5648)
Bumps [webpack-hot-middleware](https://github.com/webpack-contrib/webpack-hot-middleware) from 2.25.1 to 2.25.2.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-09-13 22:28:42 +00:00
4629fbf115 Bump webpack from 5.68.0 to 5.74.0 (#5561)
Bumps [webpack](https://github.com/webpack/webpack) from 5.68.0 to 5.74.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.68.0...v5.74.0)

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-09-13 22:24:36 +00:00
500e3bc583 Fix legend color to be in sync with plot color on change (#5371)
* Update name and color for legend when series changes

Co-authored-by: Joshi <simplyrender@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-09-13 22:10:21 +00:00
a65757d197 A couple of fixes for percy flakiness (#5752)
* deflake

* disable percy temporarily

* lower concurrency

* doc some changes

* Revert "disable percy temporarily"

This reverts commit 46f2bcf9c4.
2022-09-13 11:18:12 -07:00
f20bb4de10 Bump eslint from 8.23.0 to 8.23.1 (#5748)
Bumps [eslint](https://github.com/eslint/eslint) from 8.23.0 to 8.23.1.
- [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.23.0...v8.23.1)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-13 10:33:41 +00:00
1c762f506f Bump vue-eslint-parser from 9.0.2 to 9.1.0 (#5744)
Bumps [vue-eslint-parser](https://github.com/vuejs/vue-eslint-parser) from 9.0.2 to 9.1.0.
- [Release notes](https://github.com/vuejs/vue-eslint-parser/releases)
- [Commits](https://github.com/vuejs/vue-eslint-parser/compare/v9.0.2...v9.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-13 12:28:28 +02:00
7d900a80b5 Bump style-loader from 1.3.0 to 3.3.1 (#5016) 2022-09-12 20:14:56 -07:00
8a06dedf9d Fix typo in CONTRIBUTING.md (#5719)
inheritence -> inheritance

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-09-12 14:14:38 -07:00
15ab0dae50 [e2e] Clean up example imagery tests, fix display layout test (#5685)
* Clean up imagery tests, fix display layout test

(cherry picked from commit 7d6699fcecb34cf98bac4aa8f9a1b2996736325c)

* Code review comments, more cleanup

* Add Tabs View suite, extract common actions to functions

* Add missing await

* remove unnecessary wait

* lint

* [e2e] playwright v1.25.0 -->v1.25.2

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-09-12 09:19:24 -07:00
a7ea5afa59 Fix Independent Time Conductor for Plans within Time Strips (#5671)
* Update `IndependentTimeContext` only if its `objectPath` differs

* Ensure independent time conductor, fixed and realtime inputs have the right objectPath

* [e2e] Add Plan creation test

* [e2e] add Create TimeStrip test

* mark new faultManagement suite with @unstable

* Upgrade to @playwright/test v1.25.0

* Extract `createPlanFromJSON` to appActions

* [e2e] Add TimeStrip test for Plans, Independent Time Contexts

* [e2e] Move test annotation to the top

* [e2e] fix timestrip test

* Update docker image so the tests run

* update playwright on GHA as well

* [e2e] Fix menu test

* Error if no objectPath provided

Co-authored-by: Shefali <simplyrender@gmail.com>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2022-09-09 17:31:03 -07:00
c231c2d7cb [e2e] Fix intermittent visual test timeouts (#5743)
* try fix visual test failure

* disable actionTimeout

* increase percy loglevel

* comment

* Remove `@percy/cli` dependency as it may be conflicting

* Upgrade `percy/cli` to 1.10.2

* drop percy/cli version down to 1.7.2

* enable timeouts for actions again
2022-09-09 16:29:03 -04:00
47fb81ff1c Bump uuid from 8.3.2 to 9.0.0 (#5732) 2022-09-06 10:42:36 -07:00
0efc6987a5 Bump jasmine-core from 4.3.0 to 4.4.0 (#5733) 2022-09-06 10:29:26 -07:00
79d1df39b7 Add check to hide any old popup items when showing a new one (#5680)
* Add check to hide any old popup items when showing a new one

* Use more deterministic selector

* Hover first to "slow down" e2e actions while in headless mode

* Add notebook tests

* Combine the section tests

* Set focused image when timestamp prop is passed in

* Unused var

* Create timestrip with imagery child

* Add equality check for hovered image and view large image url

* Cleanup

* add annotation and checks

* [e2e] Minor locator improvements

* Update button title

* skipping with context

Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2022-08-31 17:11:09 +00:00
0c9ea26888 Swallow abort errors in search (#5650)
* Imagery thumbnail regression fixes - 5327 (#5591)

* Add an active class to thumbnail to indicate current focused image

* Differentiate bg color between real-time and fixed

* scrollIntoView inline: center

* Added watcher for bounds change to trigger thumbnail scroll

* Resolve merge conflict with requestHistory change to telemetry collection

* Split thumbnail into sub component

* Monitor isFixed value to unpause playback status

Co-authored-by: Khalid Adil <khalidadil29@gmail.com>

* [e2e] Improve appActions (#5592)

* update selectors to use aria labels

* Update appActions

- Create new function `getHashUrlToDomainObject` to get the browse url to a given object given its uuid

- Create new function `getFocusedObjectUuid`... self explanatory :)

- Update `createDomainObjectWIthDefaults` to make use of the new url generation

- Update `createDomainObject...`'s arguments to be more organized, and accept a parent object

- Update some docs, still need to clarify some

* Update appActions e2e tests

- Refactor for organization

- Test our new appActions in one go

* Update existing usages of `createDomainObject...` to match the new API

* fix accidental renamed export

* Fix jsdoc return types

* refactor telemetryTable test to use appActions

* Improve selectors

* Refactor test

* improve selector

* add clock mode appActions

* lint

* Fix jsdoc

* Code review comments

* mark failing visual tests as fixme temporarily

* Update package.json (#5601)

* Fix menu style in Snow theme (#5557)

* Include the plan source map when generating the time list/plan hybrid object (#5604)

* Search should indicate in progress and no results states, filter orphaned results (#5599)

* no matching result implemented

* now filtering annotations that are orphaned

* filter object results without valid paths

* add progress bar

* added e2e tests

* removed extraneous click

* fix typos

* fix unit tests

* lint

* address pr comments

* fix tests

* fix tests, centralize logic to object api, check for root instead

* remove debug statement

* lint

* fix documentation

* lint

* fix doc

* made some optimizations after talking with akhenry

* fix test

* update docs

* fix docs

* Have in-memory search indexer use composition API (#5578)

* need to remove tags and objects on composition removal
* had to separate out emits from load as it was causing memory indexer to loop upon itself

* Add parsing for areIdsEqual util to consistently remove folders (#5589)

* Add parsing util to identifier for ID comparison

* Moved firstIdentifier to top of function

* Lint fix

Co-authored-by: Andrew Henry <akhenry@gmail.com>

* Revert "Have in-memory search indexer use composition API (#5578)" (#5609)

This reverts commit 7cf11e177c.

* [e2e] Tests for Display Layout and LAD Tables and telemetry (#5607)

* Check for circular references in originalPath - 5615 (#5619)

* check for circular references

* add test

* fix test

* address PR comments by making comments better

* fix docs...again

* Update version number

* Prevent cyclic references in link & move actions (#5635)

* do not create circular refs

* add negative validation test

* move to plugin

* add link test too

* fix docs

* refactored per john request

* fix path

* use appAction lib

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

* swallow abort errors in search

* [Fault Management] New Example Provider, Unit and e2e tests (#5579)

* added unit tests for fault management plugin

* modified the example fault provider to work out of the box

* updating for new e2e folder structure

* part of the e2e tests

* WIP

* Imagery thumbnail regression fixes - 5327 (#5569)

* Add an active class to thumbnail to indicate current focused image

* Differentiate bg color between real-time and fixed

* scrollIntoView inline: center

* Added watcher for bounds change to trigger thumbnail scroll

* Resolve merge conflict with requestHistory change to telemetry collection

* Split thumbnail into sub component

* Monitor isFixed value to unpause playback status

* updated search to include name, namespace and description added some more e2e tests

* added rest of e2e tests

* fixed my init script, had to disable lint for no-force because it was not working without it, saw online this may be a pw bug

* fix: removing maelstrom theme from application (#5600)

* added some tests for no faults

* visual tests

* added visual tests for fault management

* created utils file for shared functionality between function and visual tests

* updating to 2.0.8

* tryin to remove imagery changes from master

* trying to trigger a refresh

* tryin to refresh

* updated search to include name, namespace and description added some more e2e tests

* added rest of e2e tests

* fix: removing maelstrom theme from application (#5600)

* fixed my init script, had to disable lint for no-force because it was not working without it, saw online this may be a pw bug

* added some tests for no faults

* visual tests

* added visual tests for fault management

* created utils file for shared functionality between function and visual tests

* updating to 2.0.8

* no clue

* still no clue

* removing imports and chaning to requires

* updating utils file to work with require

* fixing paths

* fixing a test I had messed up when adding static exmaple faults

* ONE LAST PATH FIX... hopefully

* typo in files fix

* fix folder typo

* thought I got this one, but apparently not, well I did now! who is laughing now!?

Co-authored-by: Michael Rogers <contact@mhrogers.com>
Co-authored-by: Vitor Henckel <vitor@henckel.com.br>

* Sort tree items locally on rename (#5643)

* fix typo

* Sort the tree items locally on object rename

* Use the navigationPath as a key

- This ensures that objects AND linked objects will be sorted

* add 'tree' and 'treeitem' roles to mct-tree

* WIP tree item reordering test

* Select the first object that matches

* Test that all object links are also reordered

* Get the final uuid before queryParams as notebook sections have uuids

* Make `openObjectTreeContextMenu` more deterministic and update usage

* Add `expandPathToTreeItem` and `expandTreeItemByName` appActions

* add `#tree-pane` id for the tree view

* Add tree visual component test suite and bump percy-cli

* Remove tree appActions

* Better variable name

Co-authored-by: Scott Bell <scott@traclabs.com>

* Mct5549 fix indexer composition error (#5610)

* [Display Layout] Composition and configuration sync (#5669)

LGTM

* [e2e] Stabilize notebook tag tests (#5681)

* Use more deterministic selector

* Hover first to "slow down" e2e actions while in headless mode

* flush hash in case other requests are awaiting debounce

* flush hash in case other requests are awaiting debounce

* the debounce will cause this resolve function to never fire, so force a flush

* lint

* remove debug statements

* add a comment

* Moves condition set fix into 2.0.8 (#5673)

* Set Focused Image index after a imagery is selected from a timestrip - 5632 (#5664)

* Set focused image when timestamp prop is passed in

* Unused var

* Create timestrip with imagery child

* Add equality check for hovered image and view large image url

* Cleanup

* Time List 5534 for release/2.0.8 (#5678)

* Changes to Time List view. Closes #5534.
- Compacted table row spacing.
- Set all timeframes to display by default when creating a new Time List.
- Removed 'Upload plan' file button from properties.

* Changes to Time List view. Closes #5534.
- Better hint text for editing Timeframe Inspector section.

Co-authored-by: Andrew Henry <akhenry@gmail.com>

* [CI] Enable couchdb e2e testing in open source (#5655)

* Handle couch db not found errors so that interceptors are still invoked. (#5654)

* Fix tests for interceptors
* [e2e] Add test for 'mine' folder initialization
* [e2e] don't fail on expected console errors

Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>

* [Docs] Update CouchDB local install documentation (#5692)

* Update local CouchDB install docs to include docker workflow

* reformat to source configuration scripts where possible

* correct couchdb case

Co-authored-by: John Hill <john.c.hill@nasa.gov>

* [Time Conductor] History not working correctly (#5687)

* the check for fixed time vs realtime was not updating, have fixed this

* merging in related changes from master pr #4414

* lint fixes

* Update src/plugins/timeConductor/ConductorHistory.vue

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

* setting time mode directly on load

* fixing issue where realtime history was being wiped on reloads while viewing fixed time

* formatting

* stubbed in some tests

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

* Only index if provider does not support search - mct5690 (#5693)

* only index if provider does not support search

* add some tests

* fix tests

* [e2e] Add search couchdb test for duplicates

* [e2e] Modify existing search test instead

* lint

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>

* fixed flakey test maybe?

* fixed flakey test maybe?

* fail on console errors

* turn on console errors for search

* revert karma code cov change

* couchdb scripting artifacts gitignore

* keep couchdb tests separate

* remove console true as this is the default

* cleanup tests

* [e2e][couchdb] fix docker-compose command

* [e2e][couchdb] ensure script is run using bash

* [e2e][couchdb] try to debug curl in gha

* [e2e][couchdb] try 0.0.0.0

* [e2e][couchdb] add debug step

* [e2e][couchdb] try -L option

* [e2e][couchdb] try 127.0.0.1

* [e2e][couchdb] add http

* [e2e][couchdb] provide initial config

* [e2e][couchdb] bind to 0.0.0.0

* [e2e][couchdb] debug

* [e2e][couchdb] wait for couchdb to start, remove failed hacks

* fix typo

Co-authored-by: Michael Rogers <contact@mhrogers.com>
Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Alize Nguyen <alizenguyen@gmail.com>
Co-authored-by: Shefali <simplyrender@gmail.com>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Vitor Henckel <vitor@henckel.com.br>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2022-08-31 10:25:00 +02:00
153538b6bf Bump eslint from 8.22.0 to 8.23.0 (#5720)
Bumps [eslint](https://github.com/eslint/eslint) from 8.22.0 to 8.23.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.22.0...v8.23.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-29 13:46:16 -07:00
798e2d4337 Bump moment-timezone from 0.5.34 to 0.5.37 (#5714) 2022-08-25 14:38:27 -07:00
e3770dc701 [e2e] Enable CORS for CouchDB in CI (#5702) 2022-08-25 12:19:37 -07:00
0f12aa1eae Update getting started readme (#5712)
Co-authored-by: John <jchill2@gmail.com>
2022-08-24 14:19:15 -07:00
c17efcc157 Audit test.fixmes (#5711)
- Replace `test.fixme` with the `@unstable` annotation for tests with contents

- Replace `test.fixme` with `test.skip` for conditionally skipped tests
2022-08-24 12:31:26 -07:00
90662ce4a7 Merge release/2.0.8 into master (#5709)
* Imagery thumbnail regression fixes - 5327 (#5591)

* Add an active class to thumbnail to indicate current focused image

* Differentiate bg color between real-time and fixed

* scrollIntoView inline: center

* Added watcher for bounds change to trigger thumbnail scroll

* Resolve merge conflict with requestHistory change to telemetry collection

* Split thumbnail into sub component

* Monitor isFixed value to unpause playback status

Co-authored-by: Khalid Adil <khalidadil29@gmail.com>

* [e2e] Improve appActions (#5592)

* update selectors to use aria labels

* Update appActions

- Create new function `getHashUrlToDomainObject` to get the browse url to a given object given its uuid

- Create new function `getFocusedObjectUuid`... self explanatory :)

- Update `createDomainObjectWIthDefaults` to make use of the new url generation

- Update `createDomainObject...`'s arguments to be more organized, and accept a parent object

- Update some docs, still need to clarify some

* Update appActions e2e tests

- Refactor for organization

- Test our new appActions in one go

* Update existing usages of `createDomainObject...` to match the new API

* fix accidental renamed export

* Fix jsdoc return types

* refactor telemetryTable test to use appActions

* Improve selectors

* Refactor test

* improve selector

* add clock mode appActions

* lint

* Fix jsdoc

* Code review comments

* mark failing visual tests as fixme temporarily

* Update package.json (#5601)

* Fix menu style in Snow theme (#5557)

* Include the plan source map when generating the time list/plan hybrid object (#5604)

* Search should indicate in progress and no results states, filter orphaned results (#5599)

* no matching result implemented

* now filtering annotations that are orphaned

* filter object results without valid paths

* add progress bar

* added e2e tests

* removed extraneous click

* fix typos

* fix unit tests

* lint

* address pr comments

* fix tests

* fix tests, centralize logic to object api, check for root instead

* remove debug statement

* lint

* fix documentation

* lint

* fix doc

* made some optimizations after talking with akhenry

* fix test

* update docs

* fix docs

* Have in-memory search indexer use composition API (#5578)

* need to remove tags and objects on composition removal
* had to separate out emits from load as it was causing memory indexer to loop upon itself

* Add parsing for areIdsEqual util to consistently remove folders (#5589)

* Add parsing util to identifier for ID comparison

* Moved firstIdentifier to top of function

* Lint fix

Co-authored-by: Andrew Henry <akhenry@gmail.com>

* Revert "Have in-memory search indexer use composition API (#5578)" (#5609)

This reverts commit 7cf11e177c.

* [e2e] Tests for Display Layout and LAD Tables and telemetry (#5607)

* Check for circular references in originalPath - 5615 (#5619)

* check for circular references

* add test

* fix test

* address PR comments by making comments better

* fix docs...again

* Update version number

* Prevent cyclic references in link & move actions (#5635)

* do not create circular refs

* add negative validation test

* move to plugin

* add link test too

* fix docs

* refactored per john request

* fix path

* use appAction lib

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

* [Condition Set] Add check for empty string being passed to the makeKeyString util by TelemetryCriterion (#5636) (#5663)

* Check telemetry is defined before using makeKeyString util

* Add optional chaining in the check

* Add e2e test

* Add check for undefined

Co-authored-by: Khalid Adil <khalidadil29@gmail.com>

* [Fault Management] New Example Provider, Unit and e2e tests (#5579)

* added unit tests for fault management plugin

* modified the example fault provider to work out of the box

* updating for new e2e folder structure

* part of the e2e tests

* WIP

* Imagery thumbnail regression fixes - 5327 (#5569)

* Add an active class to thumbnail to indicate current focused image

* Differentiate bg color between real-time and fixed

* scrollIntoView inline: center

* Added watcher for bounds change to trigger thumbnail scroll

* Resolve merge conflict with requestHistory change to telemetry collection

* Split thumbnail into sub component

* Monitor isFixed value to unpause playback status

* updated search to include name, namespace and description added some more e2e tests

* added rest of e2e tests

* fixed my init script, had to disable lint for no-force because it was not working without it, saw online this may be a pw bug

* fix: removing maelstrom theme from application (#5600)

* added some tests for no faults

* visual tests

* added visual tests for fault management

* created utils file for shared functionality between function and visual tests

* updating to 2.0.8

* tryin to remove imagery changes from master

* trying to trigger a refresh

* tryin to refresh

* updated search to include name, namespace and description added some more e2e tests

* added rest of e2e tests

* fix: removing maelstrom theme from application (#5600)

* fixed my init script, had to disable lint for no-force because it was not working without it, saw online this may be a pw bug

* added some tests for no faults

* visual tests

* added visual tests for fault management

* created utils file for shared functionality between function and visual tests

* updating to 2.0.8

* no clue

* still no clue

* removing imports and chaning to requires

* updating utils file to work with require

* fixing paths

* fixing a test I had messed up when adding static exmaple faults

* ONE LAST PATH FIX... hopefully

* typo in files fix

* fix folder typo

* thought I got this one, but apparently not, well I did now! who is laughing now!?

Co-authored-by: Michael Rogers <contact@mhrogers.com>
Co-authored-by: Vitor Henckel <vitor@henckel.com.br>

* Sort tree items locally on rename (#5643)

* fix typo

* Sort the tree items locally on object rename

* Use the navigationPath as a key

- This ensures that objects AND linked objects will be sorted

* add 'tree' and 'treeitem' roles to mct-tree

* WIP tree item reordering test

* Select the first object that matches

* Test that all object links are also reordered

* Get the final uuid before queryParams as notebook sections have uuids

* Make `openObjectTreeContextMenu` more deterministic and update usage

* Add `expandPathToTreeItem` and `expandTreeItemByName` appActions

* add `#tree-pane` id for the tree view

* Add tree visual component test suite and bump percy-cli

* Remove tree appActions

* Better variable name

Co-authored-by: Scott Bell <scott@traclabs.com>

* Mct5549 fix indexer composition error (#5610)

* [Display Layout] Composition and configuration sync (#5669)

LGTM

* [e2e] Stabilize notebook tag tests (#5681)

* Use more deterministic selector

* Hover first to "slow down" e2e actions while in headless mode

* Moves condition set fix into 2.0.8 (#5673)

* Set Focused Image index after a imagery is selected from a timestrip - 5632 (#5664)

* Set focused image when timestamp prop is passed in

* Unused var

* Create timestrip with imagery child

* Add equality check for hovered image and view large image url

* Cleanup

* Time List 5534 for release/2.0.8 (#5678)

* Changes to Time List view. Closes #5534.
- Compacted table row spacing.
- Set all timeframes to display by default when creating a new Time List.
- Removed 'Upload plan' file button from properties.

* Changes to Time List view. Closes #5534.
- Better hint text for editing Timeframe Inspector section.

Co-authored-by: Andrew Henry <akhenry@gmail.com>

* [CI] Enable couchdb e2e testing in open source (#5655)

* Handle couch db not found errors so that interceptors are still invoked. (#5654)

* Fix tests for interceptors
* [e2e] Add test for 'mine' folder initialization
* [e2e] don't fail on expected console errors

Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>

* [Docs] Update CouchDB local install documentation (#5692)

* Update local CouchDB install docs to include docker workflow

* reformat to source configuration scripts where possible

* correct couchdb case

Co-authored-by: John Hill <john.c.hill@nasa.gov>

* [Time Conductor] History not working correctly (#5687)

* the check for fixed time vs realtime was not updating, have fixed this

* merging in related changes from master pr #4414

* lint fixes

* Update src/plugins/timeConductor/ConductorHistory.vue

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

* setting time mode directly on load

* fixing issue where realtime history was being wiped on reloads while viewing fixed time

* formatting

* stubbed in some tests

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

* Only index if provider does not support search - mct5690 (#5693)

* only index if provider does not support search

* add some tests

* fix tests

* [e2e] Add search couchdb test for duplicates

* [e2e] Modify existing search test instead

* lint

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>

* Don't re-request historical data on ticks (#5701)

Don't rerequest telemetry on ticks.

* Fix duplicate declaration from merge

Co-authored-by: Michael Rogers <contact@mhrogers.com>
Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: Alize Nguyen <alizenguyen@gmail.com>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Vitor Henckel <vitor@henckel.com.br>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2022-08-24 11:08:17 -07:00
84c1526f5e Bump eslint-plugin-playwright from 0.10.0 to 0.11.1 (#5697)
* Bump eslint-plugin-playwright from 0.10.0 to 0.11.1

Bumps [eslint-plugin-playwright](https://github.com/playwright-community/eslint-plugin-playwright) from 0.10.0 to 0.11.1.
- [Release notes](https://github.com/playwright-community/eslint-plugin-playwright/releases)
- [Commits](https://github.com/playwright-community/eslint-plugin-playwright/compare/v0.10.0...v0.11.1)

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

Signed-off-by: dependabot[bot] <support@github.com>

* Resolve new playwright lint warnings

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 <jesse.d.mazzella@nasa.gov>
2022-08-24 08:51:39 -07:00
d73cb2ef70 [CLA Approved] Fix references to telemetry data "unit" in the plot LEGEND form, top legend display and documentation (#5546)
* [Plot] Rename the `units` attribute to `unit` (#5540)

Rename the `units` attribute to `unit` when referencing the telemetry
data "unit" within:
- the legend-form vue for setting the `valueToShowWhenCollapsed` and
  `showUnitsWhenExpanded` and displaying the available choices in write
  mode,
- the plot-options-browse vue for displaying the selected choices
  within the form in read mode,
- the plot-legend-item-collapsed when checking which field to display.

* [Documentation] Update API documentation (#3562)
2022-08-23 17:06:23 +00:00
07bdbe5108 Bump @percy/cli from 1.7.2 to 1.10.0 (#5689) 2022-08-22 13:19:22 -07:00
291e62687e Master 2.0.7 (#5672) 2022-08-22 11:41:38 -07:00
efadf9036f 5413 - [Notebook] Various visual issues with renaming sections/pages (#5475)
* 5413 - [Notebook] Various visual issues with renaming sections/pages

* 5413 - [Notebook] Various visual issues with renaming sections/pages - 3rd Expectation

* 5413 - [Notebook] Various visual issues with renaming sections/pages - requested changes

* 5413 - [Notebook] Various visual issues with renaming sections/pages - remove a magic number (ENTER)

* 5413 - [Notebook] Various visual issues with renaming sections/pages - key codes

* 5413 - [Notebook] Various visual issues with renaming sections/pages - Separate selectability and editability actions for `Section` and `Page` components

* Fix layout of Notebook nav item to align affordance arrow

* Add e2e test stubs

Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2022-08-18 17:58:34 +00:00
ca928370a4 Bump sass from 1.52.2 to 1.54.4 (#5644) 2022-08-18 08:41:23 -07:00
6820e0d044 Remove lighthouse, jsdoc, and bourbon dependencies (#5081)
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-08-18 16:20:05 +02:00
b8fa89af6e Bump plotly.js-basic-dist from 2.12.0 to 2.14.0 (#5645)
* Bump plotly.js-basic-dist from 2.12.0 to 2.14.0

Bumps [plotly.js-basic-dist](https://github.com/plotly/plotly.js) from 2.12.0 to 2.14.0.
- [Release notes](https://github.com/plotly/plotly.js/releases)
- [Changelog](https://github.com/plotly/plotly.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/plotly/plotly.js/compare/v2.12.0...v2.14.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

* Also update plotly gs2d dist version

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shefali <simplyrender@gmail.com>
2022-08-15 21:14:12 +00:00
bbb84c695d Bump plotly.js-gl2d-dist from 2.12.0 to 2.14.0 (#5667)
Bumps [plotly.js-gl2d-dist](https://github.com/plotly/plotly.js) from 2.12.0 to 2.14.0.
- [Release notes](https://github.com/plotly/plotly.js/releases)
- [Changelog](https://github.com/plotly/plotly.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/plotly/plotly.js/compare/v2.12.0...v2.14.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-15 14:09:01 -07:00
f979e170ee Bump eslint from 8.18.0 to 8.22.0 (#5666)
Bumps [eslint](https://github.com/eslint/eslint) from 8.18.0 to 8.22.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.18.0...v8.22.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2022-08-15 20:55:28 +00:00
1d875cb8ca 4386 - In time conductor history, show them on hover if only milliseconds have changed (#4414)
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2022-08-15 13:01:45 -07:00
a584766618 Bump imports-loader from 0.8.0 to 4.0.1 (#5658) 2022-08-12 15:29:44 -07:00
a671be726b [CI] Update dependabot ignore list for dependencies which will always fail (#5652) 2022-08-12 15:02:15 +00:00
10ff4e1781 fix: removing maelstrom theme from application (#5600) 2022-08-09 13:06:43 -07:00
f1c85933c3 Imagery thumbnail regression fixes - 5327 (#5569)
* Add an active class to thumbnail to indicate current focused image

* Differentiate bg color between real-time and fixed

* scrollIntoView inline: center

* Added watcher for bounds change to trigger thumbnail scroll

* Resolve merge conflict with requestHistory change to telemetry collection

* Split thumbnail into sub component

* Monitor isFixed value to unpause playback status
2022-08-02 12:29:49 -05:00
f2ed518300 Bump @babel/eslint-parser from 7.18.2 to 7.18.9 (#5530)
Bumps [@babel/eslint-parser](https://github.com/babel/babel/tree/HEAD/eslint/babel-eslint-parser) from 7.18.2 to 7.18.9.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.18.9/eslint/babel-eslint-parser)

---
updated-dependencies:
- dependency-name: "@babel/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>
2022-07-31 14:36:52 -07:00
bf8d1561ae Bump @percy/cli from 1.6.4 to 1.7.2 (#5583)
Bumps [@percy/cli](https://github.com/percy/cli/tree/HEAD/packages/cli) from 1.6.4 to 1.7.2.
- [Release notes](https://github.com/percy/cli/releases)
- [Commits](https://github.com/percy/cli/commits/v1.7.2/packages/cli)

---
updated-dependencies:
- dependency-name: "@percy/cli"
  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>
2022-07-31 13:41:15 -07:00
2e1ede1427 Add Visual Test for Snow Theme and add visual tests to PR execution (#5570) 2022-07-29 17:35:43 -07:00
fc3614dfbd [e2e] More VIPER compatibility fixes (#5582) 2022-07-29 16:50:40 -07:00
22924f18fc Better handling of persistence errors (#5576)
* conflict errors in particular
2022-07-29 14:29:34 -07:00
60f20c64d5 Fix multi user notebook (#5563)
* Detect remote changes to notebook object and re-render entries. Detect changes to tags as well

* Do not throw an error when getCurrentUser is called, just return undefined. Code needs a way of testing whether there is a valid user

* Support remote sync of annotations for notebook entries

* Fix bug in notebook spec that prevented multi-user notebook regression being detected

* Fixes edge case where an annotation does not initially exist

* Use structuredClone instead of JSON functions. Fix logical error in entry modification attribution. Address magical value

Co-authored-by: Scott Bell <scott@traclabs.com>
2022-07-29 11:14:09 +02:00
8dc8a1c0a9 Bump jasmine-core from 4.2.0 to 4.3.0 (#5553)
Bumps [jasmine-core](https://github.com/jasmine/jasmine) from 4.2.0 to 4.3.0.
- [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/v4.2.0...v4.3.0)

---
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>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-07-28 20:19:13 -07:00
710259b5f0 Bump eslint-plugin-vue from 9.1.1 to 9.3.0 (#5554)
Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 9.1.1 to 9.3.0.
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v9.1.1...v9.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-07-28 13:56:04 -07:00
e774eb01f3 5211 tests arent using source maps master (#5566)
* removed unused require
* simplify config

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-07-28 11:23:41 -07:00
401 changed files with 23009 additions and 7159 deletions

View File

@ -2,9 +2,11 @@ version: 2.1
executors: executors:
pw-focal-development: pw-focal-development:
docker: docker:
- image: mcr.microsoft.com/playwright:v1.23.0-focal - image: mcr.microsoft.com/playwright:v1.29.0-focal
environment: environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
PERCY_LOGLEVEL: 'debug' # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742)
parameters: parameters:
BUST_CACHE: BUST_CACHE:
description: "Set this with the CircleCI UI Trigger Workflow button (boolean = true) to bust the cache!" description: "Set this with the CircleCI UI Trigger Workflow button (boolean = true) to bust the cache!"
@ -64,7 +66,7 @@ commands:
suite: suite:
type: string type: string
steps: steps:
- run: npm run cov:e2e:report - run: npm run cov:e2e:report || true
- run: npm run cov:e2e:<<parameters.suite>>:publish - run: npm run cov:e2e:<<parameters.suite>>:publish
orbs: orbs:
node: circleci/node@4.9.0 node: circleci/node@4.9.0
@ -153,6 +155,22 @@ jobs:
- store_artifacts: - store_artifacts:
path: html-test-results path: html-test-results
- generate_and_store_version_and_filesystem_artifacts - generate_and_store_version_and_filesystem_artifacts
visual-test:
parameters:
node-version:
type: string
executor: pw-focal-development
steps:
- build_and_install:
node-version: <<parameters.node-version>>
- run: npm run test:e2e:visual
- store_test_results:
path: test-results/results.xml
- store_artifacts:
path: test-results
- store_artifacts:
path: html-test-results
- generate_and_store_version_and_filesystem_artifacts
workflows: workflows:
overall-circleci-commit-status: #These jobs run on every commit overall-circleci-commit-status: #These jobs run on every commit
jobs: jobs:
@ -168,6 +186,9 @@ workflows:
suite: stable suite: stable
- perf-test: - perf-test:
node-version: lts/gallium node-version: lts/gallium
- visual-test:
node-version: lts/gallium
the-nightly: #These jobs do not run on PRs, but against master at night the-nightly: #These jobs do not run on PRs, but against master at night
jobs: jobs:
- unit-test: - unit-test:
@ -185,6 +206,10 @@ workflows:
name: e2e-full-nightly name: e2e-full-nightly
node-version: lts/gallium node-version: lts/gallium
suite: full suite: full
- perf-test:
node-version: lts/gallium
- visual-test:
node-version: lts/gallium
triggers: triggers:
- schedule: - schedule:
cron: "0 0 * * *" cron: "0 0 * * *"

View File

@ -21,9 +21,9 @@ Closes <!--- Insert Issue Number(s) this PR addresses. Start by typing # will op
### Reviewer Checklist ### Reviewer Checklist
* [ ] Changes appear to address issue? * [ ] Changes appear to address issue?
* [ ] Reviewer has tested changes by following the provided instructions?
* [ ] Changes appear not to be breaking changes? * [ ] Changes appear not to be breaking changes?
* [ ] Appropriate unit tests included? * [ ] Appropriate automated tests included?
* [ ] Code style and in-line documentation are appropriate? * [ ] Code style and in-line documentation are appropriate?
* [ ] Commit messages meet standards?
* [ ] Has associated issue been labelled unverified? (only applicable if this PR closes the issue) * [ ] Has associated issue been labelled unverified? (only applicable if this PR closes the issue)
* [ ] Has associated issue been labelled bug? (only applicable if this PR is for a bug fix) * [ ] Has associated issue been labelled bug? (only applicable if this PR is for a bug fix)

1
.github/codeql/codeql-config.yml vendored Normal file
View File

@ -0,0 +1 @@
name: 'Custom CodeQL config'

View File

@ -7,13 +7,24 @@ updates:
interval: "daily" interval: "daily"
open-pull-requests-limit: 10 open-pull-requests-limit: 10
labels: labels:
- "pr:e2e"
- "type:maintenance" - "type:maintenance"
- "dependencies" - "dependencies"
- "pr:e2e"
- "pr:daveit" - "pr:daveit"
- "pr:visual"
- "pr:platform" - "pr:platform"
ignore:
#We have to source the playwright container which is not detected by Dependabot
- dependency-name: "@playwright/test"
- dependency-name: "playwright-core"
#Lots of noise in these type patch releases.
- dependency-name: "@babel/eslint-parser"
update-types: ["version-update:semver-patch"]
- dependency-name: "eslint-plugin-vue"
update-types: ["version-update:semver-patch"]
- dependency-name: "babel-loader"
update-types: ["version-update:semver-patch"]
- dependency-name: "sinon"
update-types: ["version-update:semver-patch"]
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
directory: "/" directory: "/"
schedule: schedule:

View File

@ -1,11 +1,10 @@
name: 'CodeQL'
name: "CodeQL"
on: on:
push: push:
branches: [ master ] branches: [master, 'release/*']
pull_request: pull_request:
branches: [ master ] branches: [master, 'release/*']
paths-ignore: paths-ignore:
- '**/*Spec.js' - '**/*Spec.js'
- '**/*.md' - '**/*.md'
@ -34,7 +33,9 @@ jobs:
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v2
with: with:
config-file: ./.github/codeql/codeql-config.yml
languages: javascript languages: javascript
queries: security-and-quality
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v2 uses: github/codeql-action/autobuild@v2

38
.github/workflows/e2e-couchdb.yml vendored Normal file
View File

@ -0,0 +1,38 @@
name: "e2e-couchdb"
on:
workflow_dispatch:
pull_request:
types:
- labeled
- opened
env:
OPENMCT_DATABASE_NAME: openmct
COUCH_ADMIN_USER: admin
COUCH_ADMIN_PASSWORD: password
COUCH_BASE_LOCAL: http://localhost:5984
COUCH_NODE_NAME: nonode@nohost
jobs:
e2e-couchdb:
if: ${{ github.event.label.name == 'pr:e2e:couchdb' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run : docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
- run : sleep 3 # wait until CouchDB has started (TODO: there must be a better way)
- run : bash src/plugins/persistence/couch/setup-couchdb.sh
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npx playwright@1.29.0 install
- run: npm install
- run: sh src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
- run: npm run test:e2e:couchdb
- run: ls -latr
- name: Archive test results
uses: actions/upload-artifact@v3
with:
path: test-results
- name: Archive html test results
uses: actions/upload-artifact@v3
with:
path: html-test-results

View File

@ -30,7 +30,7 @@ jobs:
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: '16' node-version: '16'
- run: npx playwright@1.23.0 install - run: npx playwright@1.29.0 install
- run: npx playwright install chrome-beta - run: npx playwright install chrome-beta
- run: npm install - run: npm install
- run: npm run test:e2e:full - run: npm run test:e2e:full

View File

@ -1,25 +0,0 @@
name: "e2e-visual"
on:
workflow_dispatch:
pull_request:
types:
- labeled
- opened
schedule:
- cron: '28 21 * * 1-5'
jobs:
e2e-visual:
if: ${{ github.event.label.name == 'pr:visual' }} || ${{ github.event.workflow_dispatch }} || ${{ github.event.schedule }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npx playwright@1.23.0 install
- run: npm install
- name: Run the e2e visual tests
run: npm run test:e2e:visual
env:
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}

View File

@ -1,98 +0,0 @@
name: lighthouse
on:
workflow_dispatch:
inputs:
version:
description: 'Which branch do you want to test?' # Limited to branch for now
required: false
default: 'master'
pull_request:
types:
- labeled
jobs:
lighthouse-pr:
if: ${{ github.event.label.name == 'pr:lighthouse' }}
runs-on: ubuntu-latest
steps:
- name: Checkout Master for Baseline
uses: actions/checkout@v3
with:
ref: master #explicitly checkout master for baseline
- name: Install Node 16
uses: actions/setup-node@v3
with:
node-version: '16'
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
- name: npm install with lighthouse cli
run: npm install && npm install -g @lhci/cli
- name: Run lhci against master to generate baseline and ignore exit codes
run: lhci autorun || true
- name: Perform clean checkout of PR
uses: actions/checkout@v3
with:
clean: true
- name: Install Node version which is compatible with PR
uses: actions/setup-node@v3
- name: npm install with lighthouse cli
run: npm install && npm install -g @lhci/cli
- name: Run lhci with PR
run: lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
lighthouse-nightly:
if: ${{ github.event.schedule }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Node 16
uses: actions/setup-node@v3
with:
node-version: '16'
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
- name: npm install with lighthouse cli
run: npm install && npm install -g @lhci/cli
- name: Run lhci against master to generate baseline
run: lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
lighthouse-dispatch:
if: ${{ github.event.workflow_dispatch }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.inputs.version }}
- name: Install Node 14
uses: actions/setup-node@v3
with:
node-version: '16'
- name: Cache node modules
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
- name: npm install with lighthouse cli
run: npm install && npm install -g @lhci/cli
- name: Run lhci against master to generate baseline
run: lhci autorun

View File

@ -16,7 +16,11 @@ jobs:
with: with:
node-version: 16 node-version: 16
- run: npm install - run: npm install
- run: npm test - run: |
echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" >> ~/.npmrc
npm whoami
npm publish --access=public --tag unstable openmct
# - run: npm test
publish-npm-prerelease: publish-npm-prerelease:
needs: build needs: build
@ -28,6 +32,6 @@ jobs:
node-version: 16 node-version: 16
registry-url: https://registry.npmjs.org/ registry-url: https://registry.npmjs.org/
- run: npm install - run: npm install
- run: npm publish --access public --tag unstable - run: npm publish --access=public --tag unstable
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

4
.gitignore vendored
View File

@ -36,6 +36,10 @@ report.*.json
test-results test-results
html-test-results html-test-results
# couchdb scripting artifacts
src/plugins/persistence/couch/.env.local
index.html.bak
# codecov artifacts # codecov artifacts
.nyc_output .nyc_output
coverage coverage

View File

@ -10,9 +10,6 @@
# https://github.com/nasa/openmct/issues/4992 # https://github.com/nasa/openmct/issues/4992
!/example/**/* !/example/**/*
# We will remove this in https://github.com/nasa/openmct/issues/4922
!/app.js
# ...except for these files in the above folders. # ...except for these files in the above folders.
/src/**/*Spec.js /src/**/*Spec.js
/src/**/test/ /src/**/test/
@ -25,3 +22,9 @@
!index.html !index.html
!openmct.js !openmct.js
!SECURITY.md !SECURITY.md
# Add e2e tests to npm package
!/e2e/**/*
# ... except our test-data folder files.
/e2e/test-data/*.json

176
.webpack/webpack.common.js Normal file
View File

@ -0,0 +1,176 @@
/* global __dirname module */
/*
This is the OpenMCT common webpack file. It is imported by the other three webpack configurations:
- webpack.prod.js - the production configuration for OpenMCT (default)
- webpack.dev.js - the development configuration for OpenMCT
- webpack.coverage.js - imports webpack.dev.js and adds code coverage
There are separate npm scripts to use these configurations, though simply running `npm install`
will use the default production configuration.
*/
const path = require("path");
const packageDefinition = require("../package.json");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const webpack = require("webpack");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const { VueLoaderPlugin } = require("vue-loader");
let gitRevision = "error-retrieving-revision";
let gitBranch = "error-retrieving-branch";
try {
gitRevision = require("child_process")
.execSync("git rev-parse HEAD")
.toString()
.trim();
gitBranch = require("child_process")
.execSync("git rev-parse --abbrev-ref HEAD")
.toString()
.trim();
} catch (err) {
console.warn(err);
}
const projectRootDir = path.resolve(__dirname, "..");
/** @type {import('webpack').Configuration} */
const config = {
context: projectRootDir,
entry: {
openmct: "./openmct.js",
generatorWorker: "./example/generator/generatorWorker.js",
couchDBChangesFeed:
"./src/plugins/persistence/couch/CouchChangesFeed.js",
inMemorySearchWorker: "./src/api/objects/InMemorySearchWorker.js",
espressoTheme: "./src/plugins/themes/espresso-theme.scss",
snowTheme: "./src/plugins/themes/snow-theme.scss"
},
output: {
globalObject: "this",
filename: "[name].js",
path: path.resolve(projectRootDir, "dist"),
library: "openmct",
libraryTarget: "umd",
publicPath: "",
hashFunction: "xxhash64",
clean: true
},
resolve: {
alias: {
"@": path.join(projectRootDir, "src"),
legacyRegistry: path.join(projectRootDir, "src/legacyRegistry"),
saveAs: "file-saver/src/FileSaver.js",
csv: "comma-separated-values",
EventEmitter: "eventemitter3",
bourbon: "bourbon.scss",
"plotly-basic": "plotly.js-basic-dist",
"plotly-gl2d": "plotly.js-gl2d-dist",
"d3-scale": path.join(
projectRootDir,
"node_modules/d3-scale/dist/d3-scale.min.js"
),
printj: path.join(
projectRootDir,
"node_modules/printj/dist/printj.min.js"
),
styles: path.join(projectRootDir, "src/styles"),
MCT: path.join(projectRootDir, "src/MCT"),
testUtils: path.join(projectRootDir, "src/utils/testUtils.js"),
objectUtils: path.join(
projectRootDir,
"src/api/objects/object-utils.js"
),
"kdbush": path.join(projectRootDir, "node_modules/kdbush/kdbush.min.js"),
utils: path.join(projectRootDir, "src/utils")
}
},
plugins: [
new webpack.DefinePlugin({
__OPENMCT_VERSION__: `'${packageDefinition.version}'`,
__OPENMCT_BUILD_DATE__: `'${new Date()}'`,
__OPENMCT_REVISION__: `'${gitRevision}'`,
__OPENMCT_BUILD_BRANCH__: `'${gitBranch}'`
}),
new VueLoaderPlugin(),
new CopyWebpackPlugin({
patterns: [
{
from: "src/images/favicons",
to: "favicons"
},
{
from: "./index.html",
transform: function (content) {
return content.toString().replace(/dist\//g, "");
}
},
{
from: "src/plugins/imagery/layers",
to: "imagery"
}
]
}),
new MiniCssExtractPlugin({
filename: "[name].css",
chunkFilename: "[name].css"
})
],
module: {
rules: [
{
test: /\.(sc|sa|c)ss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader"
},
{
loader: "resolve-url-loader"
},
{
loader: "sass-loader",
options: { sourceMap: true }
}
]
},
{
test: /\.vue$/,
use: "vue-loader"
},
{
test: /\.html$/,
type: "asset/source"
},
{
test: /\.(jpg|jpeg|png|svg)$/,
type: "asset/resource",
generator: {
filename: "images/[name][ext]"
}
},
{
test: /\.ico$/,
type: "asset/resource",
generator: {
filename: "icons/[name][ext]"
}
},
{
test: /\.(woff|woff2?|eot|ttf)$/,
type: "asset/resource",
generator: {
filename: "fonts/[name][ext]"
}
}
]
},
stats: "errors-warnings",
performance: {
// We should eventually consider chunking to decrease
// these values
maxEntrypointSize: 27000000,
maxAssetSize: 27000000
}
};
module.exports = config;

View File

@ -0,0 +1,37 @@
/* global module */
/*
This file extends the webpack.dev.js config to add babel istanbul coverage.
OpenMCT Continuous Integration servers use this configuration to add code coverage
information to pull requests.
*/
const config = require("./webpack.dev");
// eslint-disable-next-line no-undef
const CI = process.env.CI === "true";
config.devtool = CI ? false : undefined;
config.devServer.hot = false;
config.module.rules.push({
test: /\.js$/,
exclude: /(Spec\.js$)|(node_modules)/,
use: {
loader: "babel-loader",
options: {
retainLines: true,
// eslint-disable-next-line no-undef
plugins: [
[
"babel-plugin-istanbul",
{
extension: [".js", ".vue"]
}
]
]
}
}
});
module.exports = config;

59
.webpack/webpack.dev.js Normal file
View File

@ -0,0 +1,59 @@
/* global __dirname module */
/*
This configuration should be used for development purposes. It contains full source map, a
devServer (which be invoked using by `npm start`), and a non-minified Vue.js distribution.
If OpenMCT is to be used for a production server, use webpack.prod.js instead.
*/
const path = require("path");
const webpack = require("webpack");
const { merge } = require("webpack-merge");
const common = require("./webpack.common");
const projectRootDir = path.resolve(__dirname, "..");
module.exports = merge(common, {
mode: "development",
watchOptions: {
// Since we use require.context, webpack is watching the entire directory.
// We need to exclude any files we don't want webpack to watch.
// See: https://webpack.js.org/configuration/watch/#watchoptions-exclude
ignored: [
"**/{node_modules,dist,docs,e2e}", // All files in node_modules, dist, docs, e2e,
"**/{*.yml,Procfile,webpack*.js,babel*.js,package*.json,tsconfig.json}", // Config files
"**/*.{sh,md,png,ttf,woff,svg}", // Non source files
"**/.*" // dotfiles and dotfolders
]
},
resolve: {
alias: {
vue: path.join(projectRootDir, "node_modules/vue/dist/vue.js")
}
},
plugins: [
new webpack.DefinePlugin({
__OPENMCT_ROOT_RELATIVE__: '"dist/"'
})
],
devtool: "eval-source-map",
devServer: {
devMiddleware: {
writeToDisk: (filePathString) => {
const filePath = path.parse(filePathString);
const shouldWrite = !filePath.base.includes("hot-update");
return shouldWrite;
}
},
watchFiles: ["**/*.css"],
static: {
directory: path.join(__dirname, "..", "/dist"),
publicPath: "/dist",
watch: false
},
client: {
progress: true,
overlay: true
}
}
});

27
.webpack/webpack.prod.js Normal file
View File

@ -0,0 +1,27 @@
/* global __dirname module */
/*
This configuration should be used for production installs.
It is the default webpack configuration.
*/
const path = require("path");
const webpack = require("webpack");
const { merge } = require("webpack-merge");
const common = require("./webpack.common");
const projectRootDir = path.resolve(__dirname, "..");
module.exports = merge(common, {
mode: "production",
resolve: {
alias: {
vue: path.join(projectRootDir, "node_modules/vue/dist/vue.min.js")
}
},
plugins: [
new webpack.DefinePlugin({
__OPENMCT_ROOT_RELATIVE__: '""'
})
],
devtool: "source-map"
});

233
API.md
View File

@ -2,10 +2,13 @@
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents** **Table of Contents**
- [Building Applications With Open MCT](#building-applications-with-open-mct) - [Building Applications With Open MCT](#developing-applications-with-open-mct)
- [Scope and purpose of this document](#scope-and-purpose-of-this-document) - [Scope and purpose of this document](#scope-and-purpose-of-this-document)
- [Building From Source](#building-from-source) - [Building From Source](#building-from-source)
- [Starting an Open MCT application](#starting-an-open-mct-application) - [Starting an Open MCT application](#starting-an-open-mct-application)
- [Types](#types)
- [Using Types](#using-types)
- [Limitations](#limitations)
- [Plugins](#plugins) - [Plugins](#plugins)
- [Defining and Installing a New Plugin](#defining-and-installing-a-new-plugin) - [Defining and Installing a New Plugin](#defining-and-installing-a-new-plugin)
- [Domain Objects and Identifiers](#domain-objects-and-identifiers) - [Domain Objects and Identifiers](#domain-objects-and-identifiers)
@ -57,7 +60,7 @@
<!-- END doctoc generated TOC please keep comment here to allow auto update --> <!-- END doctoc generated TOC please keep comment here to allow auto update -->
# Building Applications With Open MCT # Developing Applications With Open MCT
## Scope and purpose of this document ## Scope and purpose of this document
@ -72,8 +75,7 @@ MCT, as well as addressing some common developer use cases.
## Building From Source ## Building From Source
The latest version of Open MCT is available from [our GitHub repository](https://github.com/nasa/openmct). The latest version of Open MCT is available from [our GitHub repository](https://github.com/nasa/openmct).
If you have `git`, and `node` installed, you can build Open MCT with the If you have `git`, and `node` installed, you can build Open MCT with the commands
commands
```bash ```bash
git clone https://github.com/nasa/openmct.git git clone https://github.com/nasa/openmct.git
@ -134,6 +136,48 @@ do anything with the application initially. Any of these plugins can, in
principle, be replaced with a custom plugin. The included plugins are principle, be replaced with a custom plugin. The included plugins are
documented in the [Included Plugins](#included-plugins) section. documented in the [Included Plugins](#included-plugins) section.
## Types
The Open MCT library includes its own TypeScript declaration files which can be
used to provide code hints and typechecking in your own Open MCT application.
Open MCT's type declarations are generated via `tsc` from JSDoc-style comment
blocks. For more information on this, [check out TypeScript's documentation](https://www.typescriptlang.org/docs/handbook/declaration-files/dts-from-js.html).
### Using Types
In order to use Open MCT's provided types in your own application, create a
`jsconfig.js` at the root of your project with this minimal configuration:
```json
{
"compilerOptions": {
"baseUrl": "./",
"target": "es6",
"checkJs": true,
"moduleResolution": "node",
"paths": {
"openmct": ["node_modules/openmct/dist/openmct.d.ts"]
}
}
}
```
Then, simply import and use `openmct` in your application:
```js
import openmct from "openmct";
```
### Limitations
The effort to add types for Open MCT's public API is ongoing, and the provided
type declarations may be incomplete.
If you would like to contribute types to Open MCT, please check out
[TypeScript's documentation](https://www.typescriptlang.org/docs/handbook/declaration-files/dts-from-js.html) on generating type declarations from JSDoc-style comment blocks.
Then read through our [contributing guide](https://github.com/nasa/openmct/blob/f7cf3f72c2efd46da7ce5719c5e52c8806d166f0/CONTRIBUTING.md) and open a PR!
## Plugins ## Plugins
### Defining and Installing a New Plugin ### Defining and Installing a New Plugin
@ -190,10 +234,10 @@ looks like this:
The main attributes to note are the `identifier`, and `type` attributes. The main attributes to note are the `identifier`, and `type` attributes.
* `identifier`: A composite key that provides a universally unique identifier - `identifier`: A composite key that provides a universally unique identifier
for this object. The `namespace` and `key` are used to identify the object. for this object. The `namespace` and `key` are used to identify the object.
The `key` must be unique within the namespace. The `key` must be unique within the namespace.
* `type`: All objects in Open MCT have a type. Types allow you to form an - `type`: All objects in Open MCT have a type. Types allow you to form an
ontology of knowledge and provide an abstraction for grouping, visualizing, ontology of knowledge and provide an abstraction for grouping, visualizing,
and interpreting data. Details on how to define a new object type are and interpreting data. Details on how to define a new object type are
provided below. provided below.
@ -207,6 +251,7 @@ Custom types may be registered via the `addType` function on the Open MCT Type
registry. registry.
eg. eg.
```javascript ```javascript
openmct.types.addType('example.my-type', { openmct.types.addType('example.my-type', {
name: "My Type", name: "My Type",
@ -216,20 +261,21 @@ openmct.types.addType('example.my-type', {
``` ```
The `addType` function accepts two arguments: The `addType` function accepts two arguments:
* A `string` key identifying the type. This key is used when specifying a type
- A `string` key identifying the type. This key is used when specifying a type
for an object. We recommend prefixing your types with a namespace to avoid for an object. We recommend prefixing your types with a namespace to avoid
conflicts with other plugins. conflicts with other plugins.
* An object type specification. An object type definition supports the following - An object type specification. An object type definition supports the following
attributes attributes
* `name`: a `string` naming this object type - `name`: a `string` naming this object type
* `description`: a `string` specifying a longer-form description of this type - `description`: a `string` specifying a longer-form description of this type
* `initialize`: a `function` which initializes the model for new domain objects - `initialize`: a `function` which initializes the model for new domain objects
of this type. This can be used for setting default values on an object when of this type. This can be used for setting default values on an object when
it is instantiated. it is instantiated.
* `creatable`: A `boolean` indicating whether users should be allowed to create - `creatable`: A `boolean` indicating whether users should be allowed to create
this type (default: `false`). This will determine whether the type appears this type (default: `false`). This will determine whether the type appears
in the `Create` menu. in the `Create` menu.
* `cssClass`: A `string` specifying a CSS class to apply to each representation - `cssClass`: A `string` specifying a CSS class to apply to each representation
of this object. This is used for specifying an icon to appear next to each of this object. This is used for specifying an icon to appear next to each
object of this type. object of this type.
@ -247,6 +293,7 @@ as a hierarchy of telemetry-providing domain objects in this fashion.
To do so, use the `addRoot` method of the object API. To do so, use the `addRoot` method of the object API.
eg. eg.
```javascript ```javascript
openmct.objects.addRoot({ openmct.objects.addRoot({
namespace: "example.namespace", namespace: "example.namespace",
@ -261,6 +308,7 @@ level objects, or a function that returns a promise for an identifier or an arra
When using the `getAll` method of the object API, they will be returned in order of priority. When using the `getAll` method of the object API, they will be returned in order of priority.
eg. eg.
```javascript ```javascript
openmct.objects.addRoot(identifier, openmct.priority.LOW); // low = -1000, will appear last in composition or tree openmct.objects.addRoot(identifier, openmct.priority.LOW); // low = -1000, will appear last in composition or tree
openmct.objects.addRoot(otherIdentifier, openmct.priority.HIGH); // high = 1000, will appear first in composition or tree openmct.objects.addRoot(otherIdentifier, openmct.priority.HIGH); // high = 1000, will appear first in composition or tree
@ -288,11 +336,12 @@ openmct.objects.addProvider('example.namespace', {
} }
}); });
``` ```
The `addProvider` function takes two arguments: The `addProvider` function takes two arguments:
* `namespace`: A `string` representing the namespace that this object provider - `namespace`: A `string` representing the namespace that this object provider
will provide objects for. will provide objects for.
* `provider`: An `object` with a single function, `get`. This function accepts an - `provider`: An `object` with a single function, `get`. This function accepts an
[Identifier](#domain-objects-and-identifiers) for the object to be provided. [Identifier](#domain-objects-and-identifiers) for the object to be provided.
It is expected that the `get` function will return a It is expected that the `get` function will return a
[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)
@ -324,12 +373,13 @@ openmct.composition.addProvider({
} }
}); });
``` ```
The `addProvider` function accepts a Composition Provider object as its sole The `addProvider` function accepts a Composition Provider object as its sole
argument. A Composition Provider is a javascript object exposing two functions: argument. A Composition Provider is a javascript object exposing two functions:
* `appliesTo`: A `function` that accepts a `domainObject` argument, and returns - `appliesTo`: A `function` that accepts a `domainObject` argument, and returns
a `boolean` value indicating whether this composition provider applies to the a `boolean` value indicating whether this composition provider applies to the
given object. given object.
* `load`: A `function` that accepts a `domainObject` as an argument, and returns - `load`: A `function` that accepts a `domainObject` as an argument, and returns
a `Promise` that resolves with an array of [Identifier](#domain-objects-and-identifiers). a `Promise` that resolves with an array of [Identifier](#domain-objects-and-identifiers).
These identifiers will be used to fetch Domain Objects from an [Object Provider](#object-provider) These identifiers will be used to fetch Domain Objects from an [Object Provider](#object-provider)
@ -390,7 +440,7 @@ A telemetry object is a domain object with a telemetry property. To take an exa
{ {
"key": "value", "key": "value",
"name": "Value", "name": "Value",
"units": "kilograms", "unit": "kilograms",
"format": "float", "format": "float",
"min": 0, "min": 0,
"max": 100, "max": 100,
@ -425,29 +475,27 @@ attribute | type | flags | notes
`name` | string | optional | a human readable label for this field. If omitted, defaults to `key`. `name` | string | optional | a human readable label for this field. If omitted, defaults to `key`.
`source` | string | optional | identifies the property of a datum where this value is stored. If omitted, defaults to `key`. `source` | string | optional | identifies the property of a datum where this value is stored. If omitted, defaults to `key`.
`format` | string | optional | a specific format identifier, mapping to a formatter. If omitted, uses a default formatter. For enumerations, use `enum`. For timestamps, use `utc` if you are using utc dates, otherwise use a key mapping to your custom date format. `format` | string | optional | a specific format identifier, mapping to a formatter. If omitted, uses a default formatter. For enumerations, use `enum`. For timestamps, use `utc` if you are using utc dates, otherwise use a key mapping to your custom date format.
`units` | string | optional | the units of this value, e.g. `km`, `seconds`, `parsecs` `unit` | string | optional | the unit of this value, e.g. `km`, `seconds`, `parsecs`
`min` | number | optional | the minimum possible value of this measurement. Will be used by plots, gauges, etc to automatically set a min value. `min` | number | optional | the minimum possible value of this measurement. Will be used by plots, gauges, etc to automatically set a min value.
`max` | number | optional | the maximum possible value of this measurement. Will be used by plots, gauges, etc to automatically set a max value. `max` | number | optional | the maximum possible value of this measurement. Will be used by plots, gauges, etc to automatically set a max value.
`enumerations` | array | optional | for objects where `format` is `"enum"`, this array tracks all possible enumerations of the value. Each entry in this array is an object, with a `value` property that is the numerical value of the enumeration, and a `string` property that is the text value of the enumeration. ex: `{"value": 0, "string": "OFF"}`. If you use an enumerations array, `min` and `max` will be set automatically for you. `enumerations` | array | optional | for objects where `format` is `"enum"`, this array tracks all possible enumerations of the value. Each entry in this array is an object, with a `value` property that is the numerical value of the enumeration, and a `string` property that is the text value of the enumeration. ex: `{"value": 0, "string": "OFF"}`. If you use an enumerations array, `min` and `max` will be set automatically for you.
###### Value Hints ###### Value Hints
Each telemetry value description has an object defining hints. Keys in this object represent the hint itself, and the value represents the weight of that hint. A lower weight means the hint has a higher priority. For example, multiple values could be hinted for use as the y-axis of a plot (raw, engineering), but the highest priority would be the default choice. Likewise, a table will use hints to determine the default order of columns. Each telemetry value description has an object defining hints. Keys in this object represent the hint itself, and the value represents the weight of that hint. A lower weight means the hint has a higher priority. For example, multiple values could be hinted for use as the y-axis of a plot (raw, engineering), but the highest priority would be the default choice. Likewise, a table will use hints to determine the default order of columns.
Known hints: Known hints:
* `domain`: Values with a `domain` hint will be used for the x-axis of a plot, and tables will render columns for these values first. - `domain`: Values with a `domain` hint will be used for the x-axis of a plot, and tables will render columns for these values first.
* `range`: Values with a `range` hint will be used as the y-axis on a plot, and tables will render columns for these values after the `domain` values. - `range`: Values with a `range` hint will be used as the y-axis on a plot, and tables will render columns for these values after the `domain` values.
* `image`: Indicates that the value may be interpreted as the URL to an image file, in which case appropriate views will be made available. - `image`: Indicates that the value may be interpreted as the URL to an image file, in which case appropriate views will be made available.
* `imageDownloadName`: Indicates that the value may be interpreted as the name of the image file. - `imageDownloadName`: Indicates that the value may be interpreted as the name of the image file.
##### The Time Conductor and Telemetry ##### The Time Conductor and Telemetry
Open MCT provides a number of ways to pivot through data and link data via time. The Time Conductor helps synchronize multiple views around the same time. Open MCT provides a number of ways to pivot through data and link data via time. The Time Conductor helps synchronize multiple views around the same time.
In order for the time conductor to work, there will always be an active "time system". All telemetry metadata *must* have a telemetry value with a `key` that matches the `key` of the active time system. You can use the `source` attribute on the value metadata to remap this to a different field in the telemetry datum-- especially useful if you are working with disparate datasources that have different field mappings. In order for the time conductor to work, there will always be an active "time system". All telemetry metadata _must_ have a telemetry value with a `key` that matches the `key` of the active time system. You can use the `source` attribute on the value metadata to remap this to a different field in the telemetry datum-- especially useful if you are working with disparate datasources that have different field mappings.
#### Telemetry Providers #### Telemetry Providers
@ -455,14 +503,14 @@ Telemetry providers are responsible for providing historical and real-time telem
A telemetry provider is a javascript object with up to four methods: A telemetry provider is a javascript object with up to four methods:
* `supportsSubscribe(domainObject, callback, options)` optional. Must be implemented to provide realtime telemetry. Should return `true` if the provider supports subscriptions for the given domain object (and request options). - `supportsSubscribe(domainObject, callback, options)` optional. Must be implemented to provide realtime telemetry. Should return `true` if the provider supports subscriptions for the given domain object (and request options).
* `subscribe(domainObject, callback, options)` required if `supportsSubscribe` is implemented. Establish a subscription for realtime data for the given domain object. Should invoke `callback` with a single telemetry datum every time data is received. Must return an unsubscribe function. Multiple views can subscribe to the same telemetry object, so it should always return a new unsubscribe function. - `subscribe(domainObject, callback, options)` required if `supportsSubscribe` is implemented. Establish a subscription for realtime data for the given domain object. Should invoke `callback` with a single telemetry datum every time data is received. Must return an unsubscribe function. Multiple views can subscribe to the same telemetry object, so it should always return a new unsubscribe function.
* `supportsRequest(domainObject, options)` optional. Must be implemented to provide historical telemetry. Should return `true` if the provider supports historical requests for the given domain object. - `supportsRequest(domainObject, options)` optional. Must be implemented to provide historical telemetry. Should return `true` if the provider supports historical requests for the given domain object.
* `request(domainObject, options)` required if `supportsRequest` is implemented. Must return a promise for an array of telemetry datums that fulfills the request. The `options` argument will include a `start`, `end`, and `domain` attribute representing the query bounds. See [Telemetry Requests and Responses](#telemetry-requests-and-responses) for more info on how to respond to requests. - `request(domainObject, options)` required if `supportsRequest` is implemented. Must return a promise for an array of telemetry datums that fulfills the request. The `options` argument will include a `start`, `end`, and `domain` attribute representing the query bounds. See [Telemetry Requests and Responses](#telemetry-requests-and-responses) for more info on how to respond to requests.
* `supportsMetadata(domainObject)` optional. Implement and return `true` for objects that you want to provide dynamic metadata for. - `supportsMetadata(domainObject)` optional. Implement and return `true` for objects that you want to provide dynamic metadata for.
* `getMetadata(domainObject)` required if `supportsMetadata` is implemented. Must return a valid telemetry metadata definition that includes at least one valueMetadata definition. - `getMetadata(domainObject)` required if `supportsMetadata` is implemented. Must return a valid telemetry metadata definition that includes at least one valueMetadata definition.
* `supportsLimits(domainObject)` optional. Implement and return `true` for domain objects that you want to provide a limit evaluator for. - `supportsLimits(domainObject)` optional. Implement and return `true` for domain objects that you want to provide a limit evaluator for.
* `getLimitEvaluator(domainObject)` required if `supportsLimits` is implemented. Must return a valid LimitEvaluator for a given domain object. - `getLimitEvaluator(domainObject)` required if `supportsLimits` is implemented. Must return a valid LimitEvaluator for a given domain object.
Telemetry providers are registered by calling `openmct.telemetry.addProvider(provider)`, e.g. Telemetry providers are registered by calling `openmct.telemetry.addProvider(provider)`, e.g.
@ -475,7 +523,7 @@ openmct.telemetry.addProvider({
Note: it is not required to implement all of the methods on every provider. Depending on the complexity of your implementation, it may be helpful to instantiate and register your realtime, historical, and metadata providers separately. Note: it is not required to implement all of the methods on every provider. Depending on the complexity of your implementation, it may be helpful to instantiate and register your realtime, historical, and metadata providers separately.
#### Telemetry Requests and Responses. #### Telemetry Requests and Responses
Telemetry requests support time bounded queries. A call to a _Telemetry Provider_'s `request` function will include an `options` argument. These are simply javascript objects with attributes for the request parameters. An example of a telemetry request object with a start and end time is included below: Telemetry requests support time bounded queries. A call to a _Telemetry Provider_'s `request` function will include an `options` argument. These are simply javascript objects with attributes for the request parameters. An example of a telemetry request object with a start and end time is included below:
@ -508,6 +556,7 @@ the number of results it desires. The `size` parameter is a hint; views
must not assume the response will have the exact number of results requested. must not assume the response will have the exact number of results requested.
example: example:
```javascript ```javascript
{ {
start: 1487981997240, start: 1487981997240,
@ -523,6 +572,7 @@ This strategy says "I want the latest data point in this time range". A provide
##### `minmax` request strategy ##### `minmax` request strategy
example: example:
```javascript ```javascript
{ {
start: 1487981997240, start: 1487981997240,
@ -540,8 +590,8 @@ MinMax queries are issued by plots, and may be issued by other types as well. T
Telemetry format objects define how to interpret and display telemetry data. Telemetry format objects define how to interpret and display telemetry data.
They have a simple structure: They have a simple structure:
* `key`: A `string` that uniquely identifies this formatter. - `key`: A `string` that uniquely identifies this formatter.
* `format`: A `function` that takes a raw telemetry value, and returns a - `format`: A `function` that takes a raw telemetry value, and returns a
human-readable `string` representation of that value. It has one required human-readable `string` representation of that value. It has one required
argument, and three optional arguments that provide context and can be used argument, and three optional arguments that provide context and can be used
for returning scaled representations of a value. An example of this is for returning scaled representations of a value. An example of this is
@ -549,16 +599,16 @@ They have a simple structure:
are multiple ways of representing a point in time, and by providing a minimum are multiple ways of representing a point in time, and by providing a minimum
scale value, maximum scale value, and a count, it's possible to provide more scale value, maximum scale value, and a count, it's possible to provide more
useful representations of time given the provided limitations. useful representations of time given the provided limitations.
* `value`: The raw telemetry value in its native type. - `value`: The raw telemetry value in its native type.
* `minValue`: An __optional__ argument specifying the minimum displayed - `minValue`: An **optional** argument specifying the minimum displayed
value. value.
* `maxValue`: An __optional__ argument specifying the maximum displayed - `maxValue`: An **optional** argument specifying the maximum displayed
value. value.
* `count`: An __optional__ argument specifying the number of displayed - `count`: An **optional** argument specifying the number of displayed
values. values.
* `parse`: A `function` that takes a `string` representation of a telemetry - `parse`: A `function` that takes a `string` representation of a telemetry
value, and returns the value in its native type. **Note** parse might receive an already-parsed value. This function should be idempotent. value, and returns the value in its native type. **Note** parse might receive an already-parsed value. This function should be idempotent.
* `validate`: A `function` that takes a `string` representation of a telemetry - `validate`: A `function` that takes a `string` representation of a telemetry
value, and returns a `boolean` value indicating whether the provided string value, and returns a `boolean` value indicating whether the provided string
can be parsed. can be parsed.
@ -584,9 +634,9 @@ openmct.telemetry.addFormat({
#### Telemetry Data #### Telemetry Data
A single telemetry point is considered a Datum, and is represented by a standard A single telemetry point is considered a Datum, and is represented by a standard
javascript object. Realtime subscriptions (obtained via __subscribe__) will javascript object. Realtime subscriptions (obtained via **subscribe**) will
invoke the supplied callback once for each telemetry datum recieved. Telemetry invoke the supplied callback once for each telemetry datum recieved. Telemetry
requests (obtained via __request__) will return a promise for an array of requests (obtained via **request**) will return a promise for an array of
telemetry datums. telemetry datums.
##### Telemetry Datums ##### Telemetry Datums
@ -623,7 +673,6 @@ For an example of a limit evaluator, take a look at `examples/generator/Sinewave
The APIs for requesting telemetry from Open MCT -- e.g. for use in custom views -- are currently in draft state and are being revised. If you'd like to experiment with them before they are finalized, please contact the team via the contact-us link on our website. The APIs for requesting telemetry from Open MCT -- e.g. for use in custom views -- are currently in draft state and are being revised. If you'd like to experiment with them before they are finalized, please contact the team via the contact-us link on our website.
## Time API ## Time API
Open MCT provides API for managing the temporal state of the application. Open MCT provides API for managing the temporal state of the application.
@ -665,25 +714,25 @@ is configured and activated by default from `index.html` in the default
installation of Open MCT if you download the source from GitHub. Some details of installation of Open MCT if you download the source from GitHub. Some details of
each of the required properties is provided below. each of the required properties is provided below.
* `key`: A `string` that uniquely identifies this time system. - `key`: A `string` that uniquely identifies this time system.
* `name`: A `string` providing a brief human readable label. If the [Time Conductor](#the-time-conductor) - `name`: A `string` providing a brief human readable label. If the [Time Conductor](#the-time-conductor)
plugin is enabled, this name will identify the time system in a dropdown menu. plugin is enabled, this name will identify the time system in a dropdown menu.
* `cssClass`: A class name `string` that will be applied to the time system when - `cssClass`: A class name `string` that will be applied to the time system when
it appears in the UI. This will be used to represent the time system with an icon. it appears in the UI. This will be used to represent the time system with an icon.
There are a number of built-in icon classes [available in Open MCT](https://github.com/nasa/openmct/blob/master/platform/commonUI/general/res/sass/_glyphs.scss), There are a number of built-in icon classes [available in Open MCT](https://github.com/nasa/openmct/blob/master/platform/commonUI/general/res/sass/_glyphs.scss),
or a custom class can be used here. or a custom class can be used here.
* `timeFormat`: A `string` corresponding to the key of a registered - `timeFormat`: A `string` corresponding to the key of a registered
[telemetry time format](#telemetry-formats). The format will be used for [telemetry time format](#telemetry-formats). The format will be used for
displaying discrete timestamps from telemetry streams when this time system is displaying discrete timestamps from telemetry streams when this time system is
activated. If the [UTCTimeSystem](#included-plugins) is enabled, then the `utc` activated. If the [UTCTimeSystem](#included-plugins) is enabled, then the `utc`
format can be used if this is a utc-based time system format can be used if this is a utc-based time system
* `durationFormat`: A `string` corresponding to the key of a registered - `durationFormat`: A `string` corresponding to the key of a registered
[telemetry time format](#telemetry-formats). The format will be used for [telemetry time format](#telemetry-formats). The format will be used for
displaying time ranges, for example `00:15:00` might be used to represent a time displaying time ranges, for example `00:15:00` might be used to represent a time
period of fifteen minutes. These are used by the Time Conductor plugin to specify period of fifteen minutes. These are used by the Time Conductor plugin to specify
relative time offsets. If the [UTCTimeSystem](#included-plugins) is enabled, relative time offsets. If the [UTCTimeSystem](#included-plugins) is enabled,
then the `duration` format can be used if this is a utc-based time system then the `duration` format can be used if this is a utc-based time system
* `isUTCBased`: A `boolean` that defines whether this time system represents - `isUTCBased`: A `boolean` that defines whether this time system represents
numbers in UTC terrestrial time. numbers in UTC terrestrial time.
#### Getting and Setting the Active Time System #### Getting and Setting the Active Time System
@ -712,10 +761,10 @@ event. If you supplied bounds, a [`'bounds'`](#time-events) event will be trigg
The TimeAPI provides a getter/setter for querying and setting time bounds. Time The TimeAPI provides a getter/setter for querying and setting time bounds. Time
bounds are simply an object with a `start` and an end `end` attribute. 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). - `start`: A `number` representing a moment in time in the active [Time System](#defining-and-registering-time-systems).
This will be used as the beginning of the time period displayed by time-responsive This will be used as the beginning of the time period displayed by time-responsive
telemetry views. telemetry views.
* `end`: A `number` representing a moment in time in the active [Time System](#defining-and-registering-time-systems). - `end`: A `number` representing a moment in time in the active [Time System](#defining-and-registering-time-systems).
This will be used as the end of the time period displayed by time-responsive This will be used as the end of the time period displayed by time-responsive
telemetry views. telemetry views.
@ -754,28 +803,28 @@ context of the active [Time System](#defining-and-registering-time-systems).
A clock is an object that defines certain required metadata and functions: A clock is an object that defines certain required metadata and functions:
* `key`: A `string` uniquely identifying this clock. This can be used later to - `key`: A `string` uniquely identifying this clock. This can be used later to
reference the clock in places such as the [Time Conductor configuration](#time-conductor-configuration) reference the clock in places such as the [Time Conductor configuration](#time-conductor-configuration)
* `cssClass`: A `string` identifying a CSS class to apply to this clock when it's - `cssClass`: A `string` identifying a CSS class to apply to this clock when it's
displayed in the UI. This will be used to represent the time system with an icon. displayed in the UI. This will be used to represent the time system with an icon.
There are a number of built-in icon classes [available in Open MCT](https://github.com/nasa/openmct/blob/master/platform/commonUI/general/res/sass/_glyphs.scss), There are a number of built-in icon classes [available in Open MCT](https://github.com/nasa/openmct/blob/master/platform/commonUI/general/res/sass/_glyphs.scss),
or a custom class can be used here. or a custom class can be used here.
* `name`: A `string` providing a human-readable identifier for the clock source. - `name`: A `string` providing a human-readable identifier for the clock source.
This will be displayed in the clock selector menu in the Time Conductor UI This will be displayed in the clock selector menu in the Time Conductor UI
component, if active. component, if active.
* `description`: An __optional__ `string` providing a longer description of the - `description`: An **optional** `string` providing a longer description of the
clock. The description will be visible in the clock selection menu in the Time clock. The description will be visible in the clock selection menu in the Time
Conductor plugin. Conductor plugin.
* `on`: A `function` supporting registration of a new callback that will be - `on`: A `function` supporting registration of a new callback that will be
invoked when the clock next ticks. It will be invoked with two arguments: invoked when the clock next ticks. It will be invoked with two arguments:
* `eventName`: A `string` specifying the event to listen on. For now, clocks - `eventName`: A `string` specifying the event to listen on. For now, clocks
support one event - `tick`. support one event - `tick`.
* `callback`: A `function` that will be invoked when this clock ticks. The - `callback`: A `function` that will be invoked when this clock ticks. The
function must be invoked with one parameter - a `number` representing a valid function must be invoked with one parameter - a `number` representing a valid
time in the current time system. time in the current time system.
* `off`: A `function` that allows deregistration of a tick listener. It accepts - `off`: A `function` that allows deregistration of a tick listener. It accepts
the same arguments as `on`. the same arguments as `on`.
* `currentValue`: A `function` that returns a `number` representing a point in - `currentValue`: A `function` that returns a `number` representing a point in
time in the active time system. It should be the last value provided by a tick, time in the active time system. It should be the last value provided by a tick,
or some default value if no ticking has yet occurred. or some default value if no ticking has yet occurred.
@ -813,7 +862,6 @@ active clock. [`clockOffsets`](#clock-offsets) must be specified when changing a
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 [`'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().
``` ```
openmct.time.clock(someClock, clockOffsets); openmct.time.clock(someClock, clockOffsets);
``` ```
@ -842,10 +890,10 @@ the current value provided by the active clock (via its `tick` event, or its
Unlike bounds, which represent absolute time values, clock offsets represent Unlike bounds, which represent absolute time values, clock offsets represent
relative time spans. Offsets are defined as an object with two properties: relative time spans. Offsets are defined as an object with two properties:
* `start`: A `number` that must be < 0 and which is used to calculate the start - `start`: A `number` that must be < 0 and which is used to calculate the start
bounds on each clock tick. The start offset will be calculated relative to the bounds on each clock tick. The start offset will be calculated relative to the
value provided by a clock's tick callback, or its `currentValue()` function. 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 - `end`: A `number` that must be >= 0 and which is used to calculate the end
bounds on each clock tick. bounds on each clock tick.
The `clockOffsets` function can be used to get or set clock offsets. For example, The `clockOffsets` function can be used to get or set clock offsets. For example,
@ -860,7 +908,7 @@ openmct.time.clockOffsets({
}) })
``` ```
__Note:__ Setting the clock offsets will trigger an immediate bounds change, as **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. 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 a clock source is active.
@ -880,11 +928,11 @@ openmct.time.on('bounds', function callback (newBounds, tick) {
The events emitted by the Time API are: The events emitted by the Time API are:
* `bounds`: emitted whenever the bounds change. The callback will be invoked - `bounds`: emitted whenever the bounds change. The callback will be invoked
with two arguments: with two arguments:
* `bounds`: A [bounds](#getting-and-setting-bounds) bounds object - `bounds`: A [bounds](#getting-and-setting-bounds) bounds object
representing a new time period bound by the specified start and send times. representing a new time period bound by the specified start and send times.
* `tick`: A `boolean` indicating whether or not this bounds change is due to - `tick`: A `boolean` indicating whether or not this bounds change is due to
a "tick" from a [clock source](#clocks). This information can be useful a "tick" from a [clock source](#clocks). This information can be useful
when determining a strategy for fetching telemetry data in response to a when determining a strategy for fetching telemetry data in response to a
bounds change event. For example, if the bounds change was automatic, and bounds change event. For example, if the bounds change was automatic, and
@ -895,16 +943,15 @@ The events emitted by the Time API are:
If `tick` is false,then the bounds change was not due to an automatic tick, 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 and a query for historical data may be necessary, depending on your data
caching strategy, and how significantly the start bound has changed. 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: - `timeSystem`: 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). - `timeSystem`: The newly active [time system](#defining-and-registering-time-systems).
* `clock`: emitted whenever the clock changes. The callback will be invoked - `clock`: emitted whenever the clock changes. The callback will be invoked
with a single argument: with a single argument:
* `clock`: The newly active [clock](#clocks), or `undefined` if an active - `clock`: The newly active [clock](#clocks), or `undefined` if an active
clock has been deactivated. clock has been deactivated.
* `clockOffsets`: emitted whenever the active clock offsets change. The - `clockOffsets`: emitted whenever the active clock offsets change. The
callback will be invoked with a single argument: callback will be invoked with a single argument:
* `clockOffsets`: The new [clock offsets](#clock-offsets). - `clockOffsets`: The new [clock offsets](#clock-offsets).
### The Time Conductor ### The Time Conductor
@ -929,29 +976,29 @@ array is an object with some properties specifying configuration. The configurat
options specified are slightly different depending on whether or not it is for options specified are slightly different depending on whether or not it is for
an active clock mode. an active clock mode.
__Configuration for Fixed Time Mode (no active clock)__ **Configuration for Fixed Time Mode (no active clock)**
* `timeSystem`: A `string`, the key for the time system that this configuration - `timeSystem`: A `string`, the key for the time system that this configuration
relates to. relates to.
* `bounds`: A [`Time Bounds`](#time-bounds) object. These bounds will be applied - `bounds`: A [`Time Bounds`](#time-bounds) object. These bounds will be applied
when the user selects the time system specified in the previous `timeSystem` when the user selects the time system specified in the previous `timeSystem`
property. property.
* `zoomOutLimit`: An __optional__ `number` specifying the longest period of time - `zoomOutLimit`: An **optional** `number` specifying the longest period of time
that can be represented by the conductor when zooming. If a `zoomOutLimit` is that can be represented by the conductor when zooming. If a `zoomOutLimit` is
provided, then a `zoomInLimit` must also be provided. If provided, the zoom provided, then a `zoomInLimit` must also be provided. If provided, the zoom
slider will automatically become available in the Time Conductor UI. slider will automatically become available in the Time Conductor UI.
* `zoomInLimit`: An __optional__ `number` specifying the shortest period of time - `zoomInLimit`: An **optional** `number` specifying the shortest period of time
that can be represented by the conductor when zooming. If a `zoomInLimit` is that can be represented by the conductor when zooming. If a `zoomInLimit` is
provided, then a `zoomOutLimit` must also be provided. If provided, the zoom provided, then a `zoomOutLimit` must also be provided. If provided, the zoom
slider will automatically become available in the Time Conductor UI. slider will automatically become available in the Time Conductor UI.
__Configuration for Active Clock__ **Configuration for Active Clock**
* `clock`: A `string`, the `key` of the clock that this configuration applies to. - `clock`: A `string`, the `key` of the clock that this configuration applies to.
* `timeSystem`: A `string`, the key for the time system that this configuration - `timeSystem`: A `string`, the key for the time system that this configuration
relates to. Separate configuration must be provided for each time system that you relates to. Separate configuration must be provided for each time system that you
wish to be available to users when they select the specified clock. wish to be available to users when they select the specified clock.
* `clockOffsets`: A [`clockOffsets`](#clock-offsets) object that will be - `clockOffsets`: A [`clockOffsets`](#clock-offsets) object that will be
automatically applied when the combination of clock and time system specified in automatically applied when the combination of clock and time system specified in
this configuration is selected from the UI. this configuration is selected from the UI.
@ -1018,6 +1065,7 @@ A simple indicator with an icon and some text can be created and added with mini
of this type exposes functions for customizing the text, icon, and style of the indicator. of this type exposes functions for customizing the text, icon, and style of the indicator.
eg. eg.
``` javascript ``` javascript
var myIndicator = openmct.indicators.simpleIndicator(); var myIndicator = openmct.indicators.simpleIndicator();
myIndicator.text("Hello World!"); myIndicator.text("Hello World!");
@ -1029,20 +1077,20 @@ By default, the indicator will appear as an information icon. Hovering over the
reveal the text set via the call to `.text()`. The Indicator object returned by the API reveal the text set via the call to `.text()`. The Indicator object returned by the API
call exposes a number of functions for customizing the content and appearance of the indicator: call exposes a number of functions for customizing the content and appearance of the indicator:
* `.text([text])`: Gets or sets the text shown when the user hovers over the indicator. - `.text([text])`: Gets or sets the text shown when the user hovers over the indicator.
Accepts an __optional__ `string` argument that, if provided, will be used to set the text. Accepts an **optional** `string` argument that, if provided, will be used to set the text.
Hovering over the indicator will expand it to its full size, revealing this text alongside Hovering over the indicator will expand it to its full size, revealing this text alongside
the icon. Returns the currently set text as a `string`. the icon. Returns the currently set text as a `string`.
* `.description([description])`: Gets or sets the indicator's description. Accepts an - `.description([description])`: Gets or sets the indicator's description. Accepts an
__optional__ `string` argument that, if provided, will be used to set the text. The description **optional** `string` argument that, if provided, will be used to set the text. The description
allows for more detail to be provided in a tooltip when the user hovers over the indicator. allows for more detail to be provided in a tooltip when the user hovers over the indicator.
Returns the currently set text as a `string`. Returns the currently set text as a `string`.
* `.iconClass([className])`: Gets or sets the CSS class used to define the icon. Accepts an __optional__ - `.iconClass([className])`: Gets or sets the CSS class used to define the icon. Accepts an **optional**
`string` parameter to be used to set the class applied to the indicator. Any of `string` parameter to be used to set the class applied to the indicator. Any of
[the built-in glyphs](https://nasa.github.io/openmct/style-guide/#/browse/styleguide:home/glyphs?view=styleguide.glyphs) [the built-in glyphs](https://nasa.github.io/openmct/style-guide/#/browse/styleguide:home/glyphs?view=styleguide.glyphs)
may be used here, or a custom CSS class can be provided. Returns the currently defined CSS may be used here, or a custom CSS class can be provided. Returns the currently defined CSS
class as a `string`. class as a `string`.
* `.statusClass([className])`: Gets or sets the CSS class used to determine status. Accepts an __optional __ - `.statusClass([className])`: Gets or sets the CSS class used to determine status. Accepts an __optional__
`string` parameter to be used to set a status class applied to the indicator. May be used to apply `string` parameter to be used to set a status class applied to the indicator. May be used to apply
different colors to indicate status. different colors to indicate status.
@ -1069,6 +1117,7 @@ Open MCT provides some built-in priority values that can be used in the applicat
### Priority Types ### Priority Types
Currently, the Open MCT Priority API provides (type: numeric value): Currently, the Open MCT Priority API provides (type: numeric value):
- HIGH: 1000 - HIGH: 1000
- Default: 0 - Default: 0
- LOW: -1000 - LOW: -1000

View File

@ -10,7 +10,7 @@ accept changes from external contributors.
The short version: The short version:
1. Write your contribution or describe your idea in the form of an [GitHub issue](https://github.com/nasa/openmct/issues/new/choose) or [Starting a GitHub Discussion](https://github.com/nasa/openmct/discussions) 1. Write your contribution or describe your idea in the form of a [GitHub issue](https://github.com/nasa/openmct/issues/new/choose) or [start a GitHub discussion](https://github.com/nasa/openmct/discussions).
2. Make sure your contribution meets code, test, and commit message 2. Make sure your contribution meets code, test, and commit message
standards as described below. standards as described below.
3. Submit a pull request from a topic branch back to `master`. Include a check 3. Submit a pull request from a topic branch back to `master`. Include a check
@ -173,7 +173,7 @@ The following guidelines are provided for anyone contributing source code to the
1. Avoid deep nesting (especially of functions), except where necessary 1. Avoid deep nesting (especially of functions), except where necessary
(e.g. due to closure scope). (e.g. due to closure scope).
1. End with a single new-line character. 1. End with a single new-line character.
1. Always use ES6 `Class`es and inheritence rather than the pre-ES6 prototypal 1. Always use ES6 `Class`es and inheritance rather than the pre-ES6 prototypal
pattern. pattern.
1. Within a given function's scope, do not mix declarations and imperative 1. Within a given function's scope, do not mix declarations and imperative
code, and present these in the following order: code, and present these in the following order:

View File

@ -1 +0,0 @@
web: node app.js --port $PORT

View File

@ -1,4 +1,4 @@
# Open MCT [![license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/nasa/openmct.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/nasa/openmct/context:javascript) [![codecov](https://codecov.io/gh/nasa/openmct/branch/master/graph/badge.svg?token=7DQIipp3ej)](https://codecov.io/gh/nasa/openmct) [![This project is using Percy.io for visual regression testing.](https://percy.io/static/images/percy-badge.svg)](https://percy.io/b2e34b17/openmct) [![npm version](https://img.shields.io/npm/v/openmct.svg)](https://www.npmjs.com/package/openmct) # Open MCT [![license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) [![codecov](https://codecov.io/gh/nasa/openmct/branch/master/graph/badge.svg?token=7DQIipp3ej)](https://codecov.io/gh/nasa/openmct) [![This project is using Percy.io for visual regression testing.](https://percy.io/static/images/percy-badge.svg)](https://percy.io/b2e34b17/openmct) [![npm version](https://img.shields.io/npm/v/openmct.svg)](https://www.npmjs.com/package/openmct)
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. 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.
@ -6,10 +6,8 @@ Please visit our [Official Site](https://nasa.github.io/openmct/) and [Getting S
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! 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!
## See Open MCT in Action ![Screen Shot 2022-11-23 at 9 51 36 AM](https://user-images.githubusercontent.com/4215777/203617422-4d912bfc-766f-4074-8324-409d9bbe7c05.png)
Try Open MCT now with our [live demo](https://openmct-demo.herokuapp.com/).
![Demo](https://nasa.github.io/openmct/static/res/images/Open-MCT.Browse.Layout.Mars-Weather-1.jpg)
## Building and Running Open MCT Locally ## Building and Running Open MCT Locally
@ -30,6 +28,8 @@ Building and running Open MCT in your local dev environment is very easy. Be sur
Open MCT is now running, and can be accessed by pointing a web browser at [http://localhost:8080/](http://localhost:8080/) 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/).
## Documentation ## Documentation
Documentation is available on the [Open MCT website](https://nasa.github.io/openmct/documentation/). Documentation is available on the [Open MCT website](https://nasa.github.io/openmct/documentation/).
@ -43,11 +43,9 @@ our documentation.
We want Open MCT to be as easy to use, install, run, and develop for as 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). 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).
## Building Applications With Open MCT ## Developing Applications With Open MCT
Open MCT is built using [`npm`](http://npmjs.com/) and [`webpack`](https://webpack.js.org/). For more on developing with Open MCT, see our documentation for a guide on [Developing Applications with Open MCT](./API.md#starting-an-open-mct-application).
See our documentation for a guide on [building Applications with Open MCT](https://github.com/nasa/openmct/blob/master/API.md#starting-an-open-mct-application).
## Compatibility ## Compatibility
@ -64,7 +62,7 @@ that is intended to be added or removed as a single unit.
As well as providing an extension mechanism, most of the core Open MCT codebase is also As well as providing an extension mechanism, most of the core Open MCT codebase is also
written as plugins. written as plugins.
For information on writing plugins, please see [our API documentation](https://github.com/nasa/openmct/blob/master/API.md#plugins). For information on writing plugins, please see [our API documentation](./API.md#plugins).
## Tests ## Tests
@ -100,7 +98,7 @@ To run the performance tests:
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 localed in `e2e/tests/` ending in `*.e2e.spec.js`. For more about the e2e test suite, please see the [README](./e2e/README.md)
### Security Tests ### Security Tests
Each commit is analyzed for known security vulnerabilities using [CodeQL](https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-javascript/) and our overall security report is available on [LGTM](https://lgtm.com/projects/g/nasa/openmct/) 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).
### Test Reporting and Code Coverage ### Test Reporting and Code Coverage

92
app.js
View File

@ -1,92 +0,0 @@
/*global process*/
/**
* Usage:
*
* npm install minimist express
* node app.js [options]
*/
const options = require('minimist')(process.argv.slice(2));
const express = require('express');
const app = express();
const fs = require('fs');
const request = require('request');
const __DEV__ = !process.env.NODE_ENV || process.env.NODE_ENV === 'development';
// Defaults
options.port = options.port || options.p || 8080;
options.host = options.host || 'localhost';
options.directory = options.directory || options.D || '.';
// Show command line options
if (options.help || options.h) {
console.log("\nUsage: node app.js [options]\n");
console.log("Options:");
console.log(" --help, -h Show this message.");
console.log(" --port, -p <number> Specify port.");
console.log(" --directory, -D <bundle> Serve files from specified directory.");
console.log("");
process.exit(0);
}
app.disable('x-powered-by');
app.use('/proxyUrl', function proxyRequest(req, res, next) {
console.log('Proxying request to: ', req.query.url);
req.pipe(request({
url: req.query.url,
strictSSL: false
}).on('error', next)).pipe(res);
});
class WatchRunPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('WatchRunPlugin', (compilation, callback) => {
console.log('Begin compile at ' + new Date());
callback();
});
}
}
const webpack = require('webpack');
let webpackConfig;
if (__DEV__) {
webpackConfig = require('./webpack.dev');
webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
webpackConfig.entry.openmct = [
'webpack-hot-middleware/client?reload=true',
webpackConfig.entry.openmct
];
webpackConfig.plugins.push(new WatchRunPlugin());
} else {
webpackConfig = require('./webpack.coverage');
}
const compiler = webpack(webpackConfig);
app.use(require('webpack-dev-middleware')(
compiler,
{
publicPath: '/dist',
stats: 'errors-warnings'
}
));
if (__DEV__) {
app.use(require('webpack-hot-middleware')(
compiler,
{}
));
}
// Expose index.html for development users.
app.get('/', function (req, res) {
fs.createReadStream('index.html').pipe(res);
});
// Finally, open the HTTP server and log the instance to the console
app.listen(options.port, options.host, function () {
console.log('Open MCT application running at %s:%s', options.host, options.port);
});

View File

@ -1,9 +0,0 @@
// This is a Babel config that webpack.coverage.js uses in order to instrument
// code with coverage instrumentation.
const babelConfig = {
plugins: [['babel-plugin-istanbul', {
extension: ['.js', '.vue']
}]]
};
module.exports = babelConfig;

View File

@ -1,3 +0,0 @@
<hr>
</body>
</html>

View File

@ -1,209 +0,0 @@
/*****************************************************************************
* 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 require,process,__dirname,GLOBAL*/
/*jslint nomen: false */
// Usage:
// node gendocs.js --in <source directory> --out <dest directory>
var CONSTANTS = {
DIAGRAM_WIDTH: 800,
DIAGRAM_HEIGHT: 500
},
TOC_HEAD = "# Table of Contents";
GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be defined
(function () {
"use strict";
var fs = require("fs"),
mkdirp = require("mkdirp"),
path = require("path"),
glob = require("glob"),
marked = require("marked"),
split = require("split"),
stream = require("stream"),
nomnoml = require('nomnoml'),
toc = require("markdown-toc"),
Canvas = require('canvas'),
header = fs.readFileSync(path.resolve(__dirname, 'header.html')),
footer = fs.readFileSync(path.resolve(__dirname, 'footer.html')),
options = require("minimist")(process.argv.slice(2));
// Convert from nomnoml source to a target PNG file.
function renderNomnoml(source, target) {
var canvas =
new Canvas(CONSTANTS.DIAGRAM_WIDTH, CONSTANTS.DIAGRAM_HEIGHT);
nomnoml.draw(canvas, source, 1.0);
canvas.pngStream().pipe(fs.createWriteStream(target));
}
// Stream transform.
// Pulls out nomnoml diagrams from fenced code blocks and renders them
// as PNG files in the output directory, prefixed with a provided name.
// The fenced code blocks will be replaced with Markdown in the
// output of this stream.
function nomnomlifier(outputDirectory, prefix) {
var transform = new stream.Transform({ objectMode: true }),
isBuilding = false,
counter = 1,
outputPath,
source = "";
transform._transform = function (chunk, encoding, done) {
if (!isBuilding) {
if (chunk.trim().indexOf("```nomnoml") === 0) {
var outputFilename = prefix + '-' + counter + '.png';
outputPath = path.join(outputDirectory, outputFilename);
this.push([
"\n![Diagram ",
counter,
"](",
outputFilename,
")\n\n"
].join(""));
isBuilding = true;
source = "";
counter += 1;
} else {
// Otherwise, pass through
this.push(chunk + '\n');
}
} else {
if (chunk.trim() === "```") {
// End nomnoml
renderNomnoml(source, outputPath);
isBuilding = false;
} else {
source += chunk + '\n';
}
}
done();
};
return transform;
}
// Convert from Github-flavored Markdown to HTML
function gfmifier(renderTOC) {
var transform = new stream.Transform({ objectMode: true }),
markdown = "";
transform._transform = function (chunk, encoding, done) {
markdown += chunk;
done();
};
transform._flush = function (done) {
if (renderTOC){
// Prepend table of contents
markdown =
[ TOC_HEAD, toc(markdown).content, "", markdown ].join("\n");
}
this.push(header);
this.push(marked(markdown));
this.push(footer);
done();
};
return transform;
}
// Custom renderer for marked; converts relative links from md to html,
// and makes headings linkable.
function CustomRenderer() {
var renderer = new marked.Renderer(),
customRenderer = Object.create(renderer);
customRenderer.heading = function (text, level) {
var escapedText = (text || "").trim().toLowerCase().replace(/\W/g, "-"),
aOpen = "<a name=\"" + escapedText + "\" href=\"#" + escapedText + "\">",
aClose = "</a>";
return aOpen + renderer.heading.apply(renderer, arguments) + aClose;
};
// Change links to .md files to .html
customRenderer.link = function (href, title, text) {
// ...but only if they look like relative paths
return (href || "").indexOf(":") === -1 && href[0] !== "/" ?
renderer.link(href.replace(/\.md/, ".html"), title, text) :
renderer.link.apply(renderer, arguments);
};
return customRenderer;
}
options['in'] = options['in'] || options.i;
options.out = options.out || options.o;
marked.setOptions({
renderer: new CustomRenderer(),
gfm: true,
tables: true,
breaks: false,
pedantic: false,
sanitize: true,
smartLists: true,
smartypants: false
});
// Convert all markdown files.
// First, pull out nomnoml diagrams.
// Then, convert remaining Markdown to HTML.
glob(options['in'] + "/**/*.md", {}, function (err, files) {
files.forEach(function (file) {
var destination = file.replace(options['in'], options.out)
.replace(/md$/, "html"),
destPath = path.dirname(destination),
prefix = path.basename(destination).replace(/\.html$/, ""),
//Determine whether TOC should be rendered for this file based
//on regex provided as command line option
renderTOC = file.match(options['suppress-toc'] || "") === null;
mkdirp(destPath, function (err) {
fs.createReadStream(file, { encoding: 'utf8' })
.pipe(split())
.pipe(nomnomlifier(destPath, prefix))
.pipe(gfmifier(renderTOC))
.pipe(fs.createWriteStream(destination, {
encoding: 'utf8'
}));
});
});
});
// Also copy over all HTML, CSS, or PNG files
glob(options['in'] + "/**/*.@(html|css|png)", {}, function (err, files) {
files.forEach(function (file) {
var destination = file.replace(options['in'], options.out),
destPath = path.dirname(destination),
streamOptions = {};
if (file.match(/png$/)) {
streamOptions.encoding = null;
} else {
streamOptions.encoding = 'utf8';
}
mkdirp(destPath, function (err) {
fs.createReadStream(file, streamOptions)
.pipe(fs.createWriteStream(destination, streamOptions));
});
});
});
}());

View File

@ -1,9 +0,0 @@
<html>
<head>
<link rel="stylesheet"
href="//nasa.github.io/openmct/static/res/css/styles.css">
<link rel="stylesheet"
href="//nasa.github.io/openmct/static/res/css/documentation.css">
</head>
<body>

View File

@ -15,8 +15,8 @@
## Sections ## Sections
* The [API](api/) document is generated from inline documentation * The [API](api/) uses inline documentation
using [JSDoc](http://usejsdoc.org/), and describes the JavaScript objects and using [TypeScript](https://www.typescriptlang.org) and some legacy [JSDoc](https://jsdoc.app/). It describes the JavaScript objects and
functions that make up the software platform. functions that make up the software platform.
* The [Development Process](process/) document describes the * The [Development Process](process/) document describes the

View File

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

View File

@ -8,7 +8,7 @@ This document is designed to capture on the What, Why, and How's of writing and
1. [Getting Started](#getting-started) 1. [Getting Started](#getting-started)
2. [Types of Testing](#types-of-e2e-testing) 2. [Types of Testing](#types-of-e2e-testing)
3. [Architecture](#architecture) 3. [Architecture](#test-architecture-and-ci)
## Getting Started ## Getting Started
@ -23,21 +23,23 @@ If this is your first time ever using the Playwright framework, we recommend goi
Once you've got an understanding of Playwright, you'll need a baseline understanding of Open MCT: Once you've got an understanding of Playwright, you'll need a baseline understanding of Open MCT:
1. Follow the steps [Building and Running Open MCT Locally](../README.md#building-and-running-open-mct-locally) 1. Follow the steps [Building and Running Open MCT Locally](../README.md#building-and-running-open-mct-locally)
2. Once you're serving Open MCT locally, create an Example Telemetry Object (e.g.: 'Sine Wave Generator') 2. Once you're serving Open MCT locally, create a 'Display Layout' object. Save it.
3. Create a 'Plot' Object (e.g.: 'Stacked Plot') 3. Create a 'Plot' Object (e.g.: 'Stacked Plot')
4. Expand the Tree on the left-hand nav and drag and drop the Example Telemetry Object into the Plot Object 4. Create an Example Telemetry Object (e.g.: 'Sine Wave Generator')
5. Create a 'Display Layout' object 5. Expand the Tree and note the hierarchy of objects which were created.
6. From the Tree, Drag the Plot object into the Display Layout 6. Navigate to the Demo Display Layout Object to edit and modify the embedded plot.
7. Modify the embedded plot with Telemetry Data.
What you've created is a display which mimics the display that a mission control operator might use to understand and model telemetry data. What you've created is a display which mimics the display that a mission control operator might use to understand and model telemetry data.
Recreate the steps above with Playwright's codegen tool: Recreate the steps above with Playwright's codegen tool:
1. `npm run start` in a terminal window 1. `npm run start` in a terminal window to serve Open MCT locally
2. Open another terminal window and start the Playwright codegen application `npx playwright codegen` 2. `npx @playwright/test install` to install playwright and dependencies
3. Navigate the browser to `http://localhost:8080` 3. Open another terminal window and start the Playwright codegen application `npx playwright codegen`
4. Click the Create button and notice how your actions in the browser are being recorded in the Playwright Inspector 4. Navigate the browser to `http://localhost:8080`
5. Continue through the steps 2-6 above 5. Click the Create button and notice how your actions in the browser are being recorded in the Playwright Inspector
6. Continue through the steps 2-6 above
What you've created is an automated test which mimics the creation of a mission control display. What you've created is an automated test which mimics the creation of a mission control display.
@ -73,59 +75,80 @@ To read about how to write a good visual test, please see [How to write a great
`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` 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.
#### Percy.io #### Percy.io
To make this possible, we're leveraging a 3rd party service, [Percy](https://percy.io/). This service maintains a copy of all changes, users, scm-metadata, and baselines to verify that the application looks and feels the same _unless approved by a Open MCT developer_. To request a Percy API token, please reach out to the Open MCT Dev team on GitHub. For more information, please see the official [Percy documentation](https://docs.percy.io/docs/visual-testing-basics) To make this possible, we're leveraging a 3rd party service, [Percy](https://percy.io/). This service maintains a copy of all changes, users, scm-metadata, and baselines to verify that the application looks and feels the same _unless approved by a Open MCT developer_. To request a Percy API token, please reach out to the Open MCT Dev team on GitHub. For more information, please see the official [Percy documentation](https://docs.percy.io/docs/visual-testing-basics)
### (Advanced) Snapshot Testing ### (Advanced) Snapshot Testing
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. 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.
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). 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).
Read more about [Playwright Snapshots](https://playwright.dev/docs/test-snapshots) Read more about [Playwright Snapshots](https://playwright.dev/docs/test-snapshots)
Open MCT's implementation #### Open MCT's implementation
-Our Snapshot tests receive a @snapshot tag.
-Snapshots need to be executed within the official playwright container to ensure we're using the exact rendering platform in CI and locally
``` - Our Snapshot tests receive a `@snapshot` tag.
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:[GET THIS VERSION FROM OUR CIRCLECI CONFIG FILE]-focal /bin/bash - Snapshots need to be executed within the official Playwright container to ensure we're using the exact rendering platform in CI and locally. To do a valid comparison locally:
```sh
// Replace {X.X.X} with the current Playwright version
// from our package.json or circleCI configuration file
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v{X.X.X}-focal /bin/bash
npm install npm install
npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot
``` ```
(WIP) Updating Snapshots ### Updating Snapshots
When the @snapshot tests fail, they will need to be evaluated to see if the failure is an acceptable change or
When the `@snapshot` tests fail, they will need to be evaluated to determine if the failure is an acceptable and desireable or an unintended regression.
To compare a snapshot, run a test and open the html report with the 'Expected' vs 'Actual' screenshot. If the actual screenshot is preferred, then the source-controlled 'Expected' snapshots will need to be updated with the following scripts.
MacOS
```
npm run test:e2e:updatesnapshots
```
Linux/CI
```sh
// Replace {X.X.X} with the current Playwright version
// from our package.json or circleCI configuration file
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v{X.X.X}-focal /bin/bash
npm install
npm run test:e2e:updatesnapshots
```
## Performance Testing ## 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 mostly as a contract for the locator logic, functionality, and assumptions will work in our downstream, closed source test suites.
They're found in the `/e2e/tests/performance` repo and are to be executed with the following npm script: They're found under `./e2e/tests/performance` and are to be executed with the following npm script:
```npm run test:perf``` `npm run test:perf`
These tests are expected to become blocking and gating with assertions as we extend the capabilities of playwright. These tests are expected to become blocking and gating with assertions as we extend the capabilities of Playwright.
## Test Architecture and CI ## Test Architecture and CI
### Architecture (TODO) ### Architecture (TODO)
### File Structure ### File Structure
Our file structure follows the type of type of testing being excercised at the e2e layer and files containing test suites which matcher application behavior or our `src` and `example` layout. This area is not well refined as we figure out what works best for closed source and downstream projects. This may change altogether if we move `e2e` to it's own npm package. Our file structure follows the type of type of testing being excercised at the e2e layer and files containing test suites which matcher application behavior or our `src` and `example` layout. This area is not well refined as we figure out what works best for closed source and downstream projects. This may change altogether if we move `e2e` to it's own npm package.
- `./helper` - contains helper functions or scripts which are leveraged directly within the testsuites. i.e. non-default plugin scripts injected into DOM - `./helper` - contains helper functions or scripts which are leveraged directly within the testsuites. i.e. non-default plugin scripts injected into DOM
- `./test-data` - contains test data which is leveraged or generated in the functional, performance, or visual test suites. i.e. localStorage data - `./test-data` - contains test data which is leveraged or generated in the functional, performance, or visual test suites. i.e. localStorage data
- `./tests/functional` - the bulk of the tests are contained within this folder to verify the functionality of open mct - `./tests/functional` - the bulk of the tests are contained within this folder to verify the functionality of open mct
- `./tests/functional/example/` - tests which specifically verify the example plugins - `./tests/functional/example/` - tests which specifically verify the example plugins
- `./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/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 testframework functionality and assumptions will continue to work based on further refactoring or playwright version changes - `./tests/framework/` - tests which verify that our testframework functionality and assumptions will continue to work based on further refactoring or playwright version changes
- `./tests/performance/` - performance tests - `./tests/performance/` - performance tests
- `./tests/visual/` - Visual tests - `./tests/visual/` - Visual tests
- `./appActions.js` - Contains common fixtures which can be leveraged by testcase authors to quickly move through the application when writing new tests. - `./appActions.js` - Contains common fixtures which can be leveraged by testcase authors to quickly move through the application when writing new tests.
- `./baseFixture.js` - Contains base fixtures which only extend default `@playwright/test` functionality. The goal is to remove these fixtures as native Playwright APIs improve. - `./baseFixture.js` - Contains base fixtures which only extend default `@playwright/test` functionality. The goal is to remove these fixtures as native Playwright APIs improve.
Our functional tests end in `*.e2e.spec.js`, visual tests in `*.visual.spec.js` and performance tests in `*.perf.spec.js`. Our functional tests end in `*.e2e.spec.js`, visual tests in `*.visual.spec.js` and performance tests in `*.perf.spec.js`.
@ -134,51 +157,64 @@ Our functional tests end in `*.e2e.spec.js`, visual tests in `*.visual.spec.js`
Where possible, we try to run Open MCT without modification or configuration change so that the Open MCT doesn't fail exclusively in "test mode" or in "production mode". Where possible, we try to run Open MCT without modification or configuration change so that the Open MCT doesn't fail exclusively in "test mode" or in "production mode".
Open MCT is leveraging the [config file](https://playwright.dev/docs/test-configuration) pattern to describe the capabilities of Open MCT e2e _where_ it's run Open MCT is leveraging the [config file](https://playwright.dev/docs/test-configuration) pattern to describe the capabilities of Open MCT e2e _where_ it's run
- `./playwright-ci.config.js` - Used when running in CI or to debug CI issues locally - `./playwright-ci.config.js` - Used when running in CI or to debug CI issues locally
- `./playwright-local.config.js` - Used when running locally - `./playwright-local.config.js` - Used when running locally
- `./playwright-performance.config.js` - Used when running performance tests in CI or locally - `./playwright-performance.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 - `./playwright-visual.config.js` - Used to run the visual tests in CI or locally
#### Test Tags #### Test Tags
Test tags are a great way of organizing tests outside of a file structure. To learn more see the official documentation [here](https://playwright.dev/docs/test-annotations#tag-tests) Test tags are a great way of organizing tests outside of a file structure. To learn more see the official documentation [here](https://playwright.dev/docs/test-annotations#tag-tests).
Current list of test tags: Current list of test tags:
- `@ipad` - Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no Create button). - `@ipad` - Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no Create button).
- `@gds` - Denotes a GDS Test Case used in the VIPER Mission. - `@gds` - Denotes a GDS Test Case used in the VIPER Mission.
- `@addInit` - Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of app.js. - `@addInit` - Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of `npm start`.
- `@localStorage` - Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB). - `@localStorage` - Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB).
- `@snapshot` - Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of the playwright container. - `@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. - `@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.
### Continuous Integration ### Continuous Integration
The cheapest time to catch a bug is Pre-merge. Unfortuantely, this is the most expensive time to run all of the tests since each Merge event can consistent of hundreds of commits. For this reason, we're selective in _what_ we run as much as _when_ we run it. The cheapest time to catch a bug is pre-merge. Unfortuantely, this is the most expensive time to run all of the tests since each merge event can consist of hundreds of commits. For this reason, we're selective in _what we run_ as much as _when we run it_.
We leverage CircleCI to run tests against each commit and inject the Test Reports which are generated by playwright so that they team can keep track of flaky and [historical Test Trends](https://app.circleci.com/insights/github/nasa/openmct/workflows/overall-circleci-commit-status/tests?branch=master&reporting-window=last-30-days) We leverage CircleCI to run tests against each commit and inject the Test Reports which are generated by Playwright so that they team can keep track of flaky and [historical test trends](https://app.circleci.com/insights/github/nasa/openmct/workflows/overall-circleci-commit-status/tests?branch=master&reporting-window=last-30-days)
We leverage Github Actions / Workflows to execute tests as it gives us the ability to run against multiple operating systems with greater control over git event triggers (i.e. Run on a PR Comment event). We leverage Github Actions / Workflows to execute tests as it gives us the ability to run against multiple operating systems with greater control over git event triggers (i.e. Run on a PR Comment event).
Our CI environment consists of 3 main modes of operation: Our CI environment consists of 3 main modes of operation:
#### 1. Per-Commit Testing #### 1. Per-Commit Testing
CircleCI CircleCI
- Stable e2e tests against ubuntu and chrome - Stable e2e tests against ubuntu and chrome
- Performance tests against ubuntu and chrome - Performance tests against ubuntu and chrome
- e2e tests are linted - e2e tests are linted
#### 2. Per-Merge Testing #### 2. Per-Merge Testing
Github Actions / Workflow Github Actions / Workflow
- Full suite against all browsers/projects. Triggered with Github Label Event 'pr:e2e' - Full suite against all browsers/projects. Triggered with Github Label Event 'pr:e2e'
- Visual Tests. Triggered with Github Label Event 'pr:visual' - Visual Tests. Triggered with Github Label Event 'pr:visual'
#### 3. Scheduled / Batch Testing #### 3. Scheduled / Batch Testing
Nightly Testing in Circle CI Nightly Testing in Circle CI
- Full e2e suite against ubuntu and chrome - Full e2e suite against ubuntu and chrome
- Performance tests against ubuntu and chrome - Performance tests against ubuntu and chrome
Github Actions / Workflow Github Actions / Workflow
- Visual Test baseline generation. - Visual Test baseline generation.
#### Parallelism and Fast Feedback #### Parallelism and Fast Feedback
In order to provide fast feedback in the Per-Commit context, we try to keep total test feedback at 5 minutes or less. That is to say, A developer should have a pass/fail result in under 5 minutes. In order to provide fast feedback in the Per-Commit context, we try to keep total test feedback at 5 minutes or less. That is to say, A developer should have a pass/fail result in under 5 minutes.
Playwright has native support for semi-intelligent sharding. Read about it [here](https://playwright.dev/docs/test-parallel#shard-tests-between-multiple-machines). Playwright has native support for semi-intelligent sharding. Read about it [here](https://playwright.dev/docs/test-parallel#shard-tests-between-multiple-machines).
@ -190,6 +226,7 @@ In addition to the Parallelization of Test Runners (Sharding), we're also runnin
So for every commit, Playwright is effectively running 4 x 2 concurrent browsercontexts to keep the overall runtime to a miminum. So for every commit, Playwright is effectively running 4 x 2 concurrent browsercontexts to keep the overall runtime to a miminum.
At the same time, we don't want to waste CI resources on parallel runs, so we've configured each shard to fail after 5 test failures. Test failure logs are recorded and stored to allow fast triage. At the same time, we don't want to waste CI resources on parallel runs, so we've configured each shard to fail after 5 test failures. Test failure logs are recorded and stored to allow fast triage.
#### Test Promotion #### Test Promotion
In order to maintain fast and reliable feedback, tests go through a promotion process. All new test cases or test suites must be labeled with the `@unstable` annotation. The Open MCT dev team runs these unstable tests in our private repos to ensure they work downstream and are reliable. In order to maintain fast and reliable feedback, tests go through a promotion process. All new test cases or test suites must be labeled with the `@unstable` annotation. The Open MCT dev team runs these unstable tests in our private repos to ensure they work downstream and are reliable.
@ -197,34 +234,98 @@ In order to maintain fast and reliable feedback, tests go through a promotion pr
To run the stable tests, use the ```npm run test:e2e:stable``` command. To run the new and flaky tests, use the ```npm run test:e2e:unstable``` command. To run the stable tests, use the ```npm run test:e2e:stable``` command. To run the new and flaky tests, use the ```npm run test:e2e:unstable``` command.
A testcase and testsuite are to be unmarked as @unstable when: A testcase and testsuite are to be unmarked as @unstable when:
1. They run as part of "full" run 5 times without failure. 1. They run as part of "full" run 5 times without failure.
2. They've been by a Open MCT Developer 5 times in the closed source repo without failure. 2. They've been by a Open MCT Developer 5 times in the closed source repo without failure.
### Cross-browser and Cross-operating system ### Cross-browser and Cross-operating system
- Where is it tested #### **What's supported:**
- What's supported
- Mobile We are leveraging the `browserslist` project to declare our supported list of browsers.
#### **Where it's tested:**
We lint on `browserslist` to ensure that we're not implementing deprecated browser APIs and are aware of browser API improvements over time.
We also have the need to execute our e2e tests across this published list of browsers. Our browsers and browser version matrix is found inside of our `./playwright-*.config.js`, but mostly follows in order of bleeding edge to stable:
- `playwright-chromium channel:beta`
- A beta version of Chromium from official chromium channels. As close to the bleeding edge as we can get.
- `playwright-chromium`
- 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.
#### **Mobile**
We have the Mission-need to support iPad. To run our iPad suite, please see our `playwright-*.config.js` with the 'iPad' project.
#### **Skipping or executing tests based on browser, os, and/os browser version:**
Conditionally skipping tests based on browser (**RECOMMENDED**):
```js
test('Can adjust image brightness/contrast by dragging the sliders', async ({ page, browserName }) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(browserName === 'firefox', 'This test needs to be updated to work with firefox');
// ...
```
Conditionally skipping tests based on OS:
```js
test('Can adjust image brightness/contrast by dragging the sliders', async ({ page }) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(process.platform === 'darwin', 'This test needs to be updated to work with MacOS');
// ...
```
Skipping based on browser version (Rarely used): <https://github.com/microsoft/playwright/discussions/17318>
## Test Design, Best Practices, and Tips & Tricks ## Test Design, Best Practices, and Tips & Tricks
### Test Design (TODO) ### Test Design (TODO)
- How to make tests robust to function in other contexts (VISTA, VIPER, etc.) - How to make tests robust to function in other contexts (VISTA, VIPER, etc.)
- Leverage the use of appActions.js like getOrCreateDomainObject - Leverage the use of `appActions.js` methods such as `createDomainObjectWithDefaults()`
- How to make tests faster and more resilient - How to make tests faster and more resilient
- When possible, navigate directly by URL - When possible, navigate directly by URL
- Leverage ```await page.goto('/', { waitUntil: 'networkidle' });``` - Leverage `await page.goto('./', { waitUntil: 'networkidle' });`
- Avoid repeated setup to test to test a single assertion. Write longer tests with multiple soft assertions. - Avoid repeated setup to test to test a single assertion. Write longer tests with multiple soft assertions.
### How to write a great test (TODO) ### How to write a great test (WIP)
- Use our [App Actions](./appActions.js) for performing common actions whenever applicable.
- 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`:
```js
// Fill the "Notes" section with information about the
// currently running test and its project.
const { testNotes } = page;
const notesInput = page.locator('form[name="mctForm"] #notes-textarea');
await notesInput.fill(testNotes);
```
#### How to write a great visual test (TODO) #### How to write a great visual test (TODO)
#### How to write a great network test
- Where possible, it is best to mock out third-party network activity to ensure we are testing application behavior of Open MCT.
- It is best to be as specific as possible about the expected network request/response structures in creating your mocks.
- Make sure to only mock requests which are relevant to the specific behavior being tested.
- Where possible, network requests and responses should be treated in an order-agnostic manner, as the order in which certain requests/responses happen is dynamic and subject to change.
Some examples of mocking network responses in regards to CouchDB can be found in our [couchdb.e2e.spec.js](./tests/functional/couchdb.e2e.spec.js) test file.
### Best Practices ### Best Practices
For now, our best practices exist as self-tested, living documentation in our [exampleTemplate.e2e.spec.js](./tests/framework/exampleTemplate.e2e.spec.js) file. For now, our best practices exist as self-tested, living documentation in our [exampleTemplate.e2e.spec.js](./tests/framework/exampleTemplate.e2e.spec.js) file.
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 (TODO)
The following contains a list of tips and tricks which don't exactly fit into a FAQ or Best Practices doc. The following contains a list of tips and tricks which don't exactly fit into a FAQ or Best Practices doc.
@ -237,6 +338,7 @@ There are instances where multiple browser pages will need to be opened to verif
Test Reporting is done through official Playwright reporters and the CI Systems which execute them. Test Reporting is done through official Playwright reporters and the CI Systems which execute them.
We leverage the following official Playwright reporters: We leverage the following official Playwright reporters:
- HTML - HTML
- junit - junit
- github annotations - github annotations
@ -246,6 +348,7 @@ We leverage the following official Playwright reporters:
When running the tests locally with the `npm run test:local` command, the html report will open automatically on failure. Inside this HTML report will be a complete summary of the finished tests. If the tests failed, you'll see embedded links to screenshot failure, execution logs, and the Tracefile. When running the tests locally with the `npm run test:local` command, the html report will open automatically on failure. Inside this HTML report will be a complete summary of the finished tests. If the tests failed, you'll see embedded links to screenshot failure, execution logs, and the Tracefile.
When looking at the reports run in CI, you'll leverage this same HTML Report which is hosted either in CircleCI or Github Actions as a build artifact. When looking at the reports run in CI, you'll leverage this same HTML Report which is hosted either in CircleCI or Github Actions as a build artifact.
### e2e Code Coverage ### e2e Code Coverage
Code coverage is collected during test execution using our custom [baseFixture](./baseFixtures.js). The raw coverage files are stored in a `.nyc_report` directory to be converted into a lcov file with the following [nyc](https://github.com/istanbuljs/nyc) command: Code coverage is collected during test execution using our custom [baseFixture](./baseFixtures.js). The raw coverage files are stored in a `.nyc_report` directory to be converted into a lcov file with the following [nyc](https://github.com/istanbuljs/nyc) command:
@ -261,6 +364,7 @@ or
Codecov.io will combine each of the above commands with [Codecov.io Flags](https://docs.codecov.com/docs/flags). Effectively, this allows us to combine multiple reports which are run at various stages of our CI Pipeline or run as part of a parallel process. Codecov.io will combine each of the above commands with [Codecov.io Flags](https://docs.codecov.com/docs/flags). Effectively, this allows us to combine multiple reports which are run at various stages of our CI Pipeline or run as part of a parallel process.
This e2e coverage is combined with our unit test report to give a comprehensive (if flawed) view of line coverage. This e2e coverage is combined with our unit test report to give a comprehensive (if flawed) view of line coverage.
## Other ## Other
### About e2e testing ### About e2e testing
@ -313,6 +417,26 @@ A single e2e test in Open MCT is extended to run:
- Why is my test failing on CI and not locally? - Why is my test failing on CI and not locally?
- How can I view the failing tests on CI? - How can I view the failing tests on CI?
- Tests won't start because 'Error: http://localhost:8080/# is already used...' - Tests won't start because 'Error: <http://localhost:8080/># is already used...'
This error will appear when running the tests locally. Sometimes, the webserver is left in an orphaned state and needs to be cleaned up. To clear up the orphaned webserver, execute the following from your Terminal: This error will appear when running the tests locally. Sometimes, the webserver is left in an orphaned state and needs to be cleaned up. To clear up the orphaned webserver, execute the following from your Terminal:
```lsof -n -i4TCP:8080 | awk '{print$2}' | tail -1 | xargs kill -9``` ```lsof -n -i4TCP:8080 | awk '{print$2}' | tail -1 | xargs kill -9```
### Upgrading Playwright
In order to upgrade from one version of Playwright to another, the version should be updated in several places in both `openmct` and `openmct-yamcs` repos. An easy way to identify these locations is to search for the current version in all files and find/replace.
For reference, all of the locations where the version should be updated are listed below:
#### **In `openmct`:**
- `package.json`
- Both packages `@playwright/test` and `playwright-core` should be updated to the same target version.
- `.circleci/config.yml`
- `.github/workflows/e2e-couchdb.yml`
- `.github/workflows/e2e-pr.yml`
#### **In `openmct-yamcs`:**
- `package.json`
- `@playwright/test` should be updated to the target version.
- `.github/workflows/yamcs-quickstart-e2e.yml`

View File

@ -30,54 +30,371 @@
*/ */
/** /**
* This common function creates a `domainObject` with default options. It is the preferred way of creating objects * Defines parameters to be used in the creation of a domain object.
* in the e2e suite when uninterested in properties of the objects themselves. * @typedef {Object} CreateObjectOptions
* @param {import('@playwright/test').Page} page * @property {string} type the type of domain object to create (e.g.: "Sine Wave Generator").
* @param {string} type * @property {string} [name] the desired name of the created domain object.
* @param {string | undefined} name * @property {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the Identifier or uuid of the parent object.
*/ */
async function createDomainObjectWithDefaults(page, type, name) {
/**
* Contains information about the newly created domain object.
* @typedef {Object} CreatedObjectInfo
* @property {string} name the name of the created object
* @property {string} uuid the uuid of the created object
* @property {string} url the relative url to the object (for use with `page.goto()`)
*/
/**
* Defines parameters to be used in the creation of a notification.
* @typedef {Object} CreateNotificationOptions
* @property {string} message the message
* @property {'info' | 'alert' | 'error'} severity the severity
* @property {import('../src/api/notifications/NotificationAPI').NotificationOptions} [notificationOptions] additional options
*/
const Buffer = require('buffer').Buffer;
const genUuid = require('uuid').v4;
/**
* This common function creates a domain object with the default options. It is the preferred way of creating objects
* in the e2e suite when uninterested in properties of the objects themselves.
*
* @param {import('@playwright/test').Page} page
* @param {CreateObjectOptions} options
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
*/
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) {
if (!name) {
name = `${type}:${genUuid()}`;
}
const parentUrl = await getHashUrlToDomainObject(page, parent);
// Navigate to the parent object. This is necessary to create the object
// in the correct location, such as a folder, layout, or plot.
await page.goto(`${parentUrl}?hideTree=true`);
await page.waitForLoadState('networkidle');
//Click the Create button //Click the Create button
await page.click('button:has-text("Create")'); await page.click('button:has-text("Create")');
// Click the object specified by 'type' // Click the object specified by 'type'
await page.click(`text=${type}`); await page.click(`li[role='menuitem']:text("${type}")`);
// Modify the name input field of the domain object to accept 'name' // Modify the name input field of the domain object to accept 'name'
if (name) { const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
const nameInput = page.locator('input[type="text"]').nth(2);
await nameInput.fill(""); await nameInput.fill("");
await nameInput.fill(name); await nameInput.fill(name);
if (page.testNotes) {
// Fill the "Notes" section with information about the
// currently running test and its project.
const notesInput = page.locator('form[name="mctForm"] #notes-textarea');
await notesInput.fill(page.testNotes);
} }
// Click OK button and wait for Navigate event // Click OK button and wait for Navigate event
await Promise.all([ await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}), page.waitForLoadState(),
page.click('text=OK') page.click('[aria-label="Save"]'),
// Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]); ]);
// Wait until the URL is updated
await page.waitForURL(`**/${parent}/*`);
const uuid = await getFocusedObjectUuid(page);
const objectUrl = await getHashUrlToDomainObject(page, uuid);
if (await _isInEditMode(page, uuid)) {
// Save (exit edit mode)
await page.locator('button[title="Save"]').click();
await page.locator('li[title="Save and Finish Editing"]').click();
}
return {
name,
uuid,
url: objectUrl
};
}
/**
* Generate a notification with the given options.
* @param {import('@playwright/test').Page} page
* @param {CreateNotificationOptions} createNotificationOptions
*/
async function createNotification(page, createNotificationOptions) {
await page.evaluate((_createNotificationOptions) => {
const { message, severity, options } = _createNotificationOptions;
const notificationApi = window.openmct.notifications;
if (severity === 'info') {
notificationApi.info(message, options);
} else if (severity === 'alert') {
notificationApi.alert(message, options);
} else {
notificationApi.error(message, options);
}
}, createNotificationOptions);
}
/**
* @param {import('@playwright/test').Page} page
* @param {string} name
*/
async function expandTreePaneItemByName(page, name) {
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
const expandTriangle = treeItem.locator('.c-disclosure-triangle');
await expandTriangle.click();
}
/**
* Create a Plan object from JSON with the provided options.
* @param {import('@playwright/test').Page} page
* @param {*} options
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
*/
async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
const parentUrl = await getHashUrlToDomainObject(page, parent);
// Navigate to the parent object. This is necessary to create the object
// in the correct location, such as a folder, layout, or plot.
await page.goto(`${parentUrl}?hideTree=true`);
//Click the Create button
await page.click('button:has-text("Create")');
// Click 'Plan' menu option
await page.click(`li:text("Plan")`);
// Modify the name input field of the domain object to accept 'name'
if (name) {
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
await nameInput.fill("");
await nameInput.fill(name);
}
// Upload buffer from memory
await page.locator('input#fileElem').setInputFiles({
name: 'plan.txt',
mimeType: 'text/plain',
buffer: Buffer.from(JSON.stringify(json))
});
// Click OK button and wait for Navigate event
await Promise.all([
page.waitForLoadState(),
page.click('[aria-label="Save"]'),
// Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
// Wait until the URL is updated
await page.waitForURL(`**/mine/*`);
const uuid = await getFocusedObjectUuid(page);
const objectUrl = await getHashUrlToDomainObject(page, uuid);
return {
uuid,
name,
url: objectUrl
};
} }
/** /**
* Open the given `domainObject`'s context menu from the object tree. * Open the given `domainObject`'s context menu from the object tree.
* Expands the 'My Items' folder if it is not already expanded. * Expands the path to the object and scrolls to it if necessary.
*
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {string} myItemsFolderName the name of the "My Items" folder * @param {string} url the url to the object
* @param {string} domainObjectName the display name of the `domainObject`
*/ */
async function openObjectTreeContextMenu(page, myItemsFolderName, domainObjectName) { async function openObjectTreeContextMenu(page, url) {
const myItemsFolder = page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3); await page.goto(url);
const className = await myItemsFolder.getAttribute('class'); await page.click('button[title="Show selected item in tree"]');
if (!className.includes('c-disclosure-triangle--expanded')) { await page.locator('.is-navigated-object').click({
await myItemsFolder.click();
}
await page.locator(`a:has-text("${domainObjectName}")`).click({
button: 'right' button: 'right'
}); });
} }
/**
* Expands the entire object tree (every expandable tree item).
* @param {import('@playwright/test').Page} page
* @param {"Main Tree" | "Create Modal Tree"} [treeName="Main Tree"]
*/
async function expandEntireTree(page, treeName = "Main Tree") {
const treeLocator = page.getByRole('tree', {
name: treeName
});
const collapsedTreeItems = treeLocator.getByRole('treeitem', {
expanded: false
}).locator('span.c-disclosure-triangle.is-enabled');
while (await collapsedTreeItems.count() > 0) {
await collapsedTreeItems.nth(0).click();
// FIXME: Replace hard wait with something event-driven.
// Without the wait, this fails periodically due to a race condition
// with Vue rendering (loop exits prematurely).
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(200);
}
}
/**
* Gets the UUID of the currently focused object by parsing the current URL
* and returning the last UUID in the path.
* @param {import('@playwright/test').Page} page
* @returns {Promise<string>} the uuid of the focused object
*/
async function getFocusedObjectUuid(page) {
const UUIDv4Regexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi;
const focusedObjectUuid = await page.evaluate((regexp) => {
return window.location.href.split('?')[0].match(regexp).at(-1);
}, UUIDv4Regexp);
return focusedObjectUuid;
}
/**
* Returns the hashUrl to the domainObject given its uuid.
* Useful for directly navigating to the given domainObject.
*
* 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
* @returns {Promise<string>} the url of the object
*/
async function getHashUrlToDomainObject(page, uuid) {
const hashUrl = await page.evaluate(async (objectUuid) => {
const path = await window.openmct.objects.getOriginalPath(objectUuid);
let url = './#/browse/' + [...path].reverse()
.map((object) => window.openmct.objects.makeKeyString(object.identifier))
.join('/');
// Drop the vestigial '/ROOT' if it exists
if (url.includes('/ROOT')) {
url = url.split('/ROOT').join('');
}
return url;
}, uuid);
return hashUrl;
}
/**
* Utilizes the OpenMCT API to detect if the UI is in Edit mode.
* @private
* @param {import('@playwright/test').Page} page
* @return {Promise<boolean>} true if the Open MCT is in Edit Mode
*/
async function _isInEditMode(page, identifier) {
// eslint-disable-next-line no-return-await
return await page.evaluate(() => window.openmct.editor.isEditing());
}
/**
* Set the time conductor mode to either fixed timespan or realtime mode.
* @param {import('@playwright/test').Page} page
* @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true
*/
async function setTimeConductorMode(page, isFixedTimespan = true) {
// Click 'mode' button
await page.locator('.c-mode-button').click();
// Switch time conductor mode
if (isFixedTimespan) {
await page.locator('data-testid=conductor-modeOption-fixed').click();
} else {
await page.locator('data-testid=conductor-modeOption-realtime').click();
}
}
/**
* Set the time conductor to fixed timespan mode
* @param {import('@playwright/test').Page} page
*/
async function setFixedTimeMode(page) {
await setTimeConductorMode(page, true);
}
/**
* Set the time conductor to realtime mode
* @param {import('@playwright/test').Page} page
*/
async function setRealTimeMode(page) {
await setTimeConductorMode(page, false);
}
/**
* @typedef {Object} OffsetValues
* @property {string | undefined} hours
* @property {string | undefined} mins
* @property {string | undefined} secs
*/
/**
* Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
* @param {import('@playwright/test').Locator} offsetButton
*/
async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) {
await offsetButton.click();
if (hours) {
await page.fill('.pr-time-controls__hrs', hours);
}
if (mins) {
await page.fill('.pr-time-controls__mins', mins);
}
if (secs) {
await page.fill('.pr-time-controls__secs', secs);
}
// Click the check button
await page.locator('.pr-time__buttons .icon-check').click();
}
/**
* Set the values (hours, mins, secs) for the start time offset when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
*/
async function setStartOffset(page, offset) {
const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
await setTimeConductorOffset(page, offset, startOffsetButton);
}
/**
* Set the values (hours, mins, secs) for the end time offset when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
*/
async function setEndOffset(page, offset) {
const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
await setTimeConductorOffset(page, offset, endOffsetButton);
}
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
module.exports = { module.exports = {
createDomainObjectWithDefaults, createDomainObjectWithDefaults,
openObjectTreeContextMenu createNotification,
expandTreePaneItemByName,
expandEntireTree,
createPlanFromJSON,
openObjectTreeContextMenu,
getHashUrlToDomainObject,
getFocusedObjectUuid,
setFixedTimeMode,
setRealTimeMode,
setStartOffset,
setEndOffset
}; };

View File

@ -0,0 +1,28 @@
/*****************************************************************************
* 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.
*****************************************************************************/
// This should be used to install the Example Fault Provider, this will also install the FaultManagementPlugin (neither of which are installed by default).
document.addEventListener('DOMContentLoaded', () => {
const openmct = window.openmct;
openmct.install(openmct.plugins.example.ExampleFaultSource());
});

View File

@ -0,0 +1,30 @@
/*****************************************************************************
* 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.
*****************************************************************************/
// This should be used to install the Example Fault Provider, this will also install the FaultManagementPlugin (neither of which are installed by default).
document.addEventListener('DOMContentLoaded', () => {
const openmct = window.openmct;
const staticFaults = true;
openmct.install(openmct.plugins.example.ExampleFaultSource(staticFaults));
});

View File

@ -0,0 +1,27 @@
/*****************************************************************************
* 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 should be used to install the Example User
document.addEventListener('DOMContentLoaded', () => {
const openmct = window.openmct;
openmct.install(openmct.plugins.example.ExampleUser());
});

View File

@ -0,0 +1,28 @@
/*****************************************************************************
* 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.
*****************************************************************************/
// This should be used to install the Example Fault Provider, this will also install the FaultManagementPlugin (neither of which are installed by default).
document.addEventListener('DOMContentLoaded', () => {
const openmct = window.openmct;
openmct.install(openmct.plugins.FaultManagement());
});

View File

@ -0,0 +1,76 @@
class DomainObjectViewProvider {
constructor(openmct) {
this.key = 'doViewProvider';
this.name = 'Domain Object View Provider';
this.openmct = openmct;
}
canView(domainObject) {
return domainObject.type === 'imageFileInput'
|| domainObject.type === 'jsonFileInput';
}
view(domainObject, objectPath) {
let content;
return {
show: function (element) {
const body = domainObject.selectFile.body;
const type = typeof body;
content = document.createElement('div');
content.id = 'file-input-type';
content.textContent = JSON.stringify(type);
element.appendChild(content);
},
destroy: function (element) {
element.removeChild(content);
content = undefined;
}
};
}
}
document.addEventListener('DOMContentLoaded', () => {
const openmct = window.openmct;
openmct.types.addType('jsonFileInput', {
key: 'jsonFileInput',
name: "JSON File Input Object",
creatable: true,
form: [
{
name: 'Upload File',
key: 'selectFile',
control: 'file-input',
required: true,
text: 'Select File...',
type: 'application/json',
property: [
"selectFile"
]
}
]
});
openmct.types.addType('imageFileInput', {
key: 'imageFileInput',
name: "Image File Input Object",
creatable: true,
form: [
{
name: 'Upload File',
key: 'selectFile',
control: 'file-input',
required: true,
text: 'Select File...',
type: 'image/*',
property: [
"selectFile"
]
}
]
});
openmct.objectViews.addProvider(new DomainObjectViewProvider(openmct));
});

View File

@ -0,0 +1,32 @@
/*****************************************************************************
* 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.
*****************************************************************************/
// This should be used to install the re-instal default Notebook plugin with a simple url whitelist.
// e.g.
// await page.addInitScript({ path: path.join(__dirname, 'addInitNotebookWithUrls.js') });
const NOTEBOOK_NAME = 'Notebook';
const URL_WHITELIST = ['google.com'];
document.addEventListener('DOMContentLoaded', () => {
const openmct = window.openmct;
openmct.install(openmct.plugins.Notebook(NOTEBOOK_NAME, URL_WHITELIST));
});

View File

@ -0,0 +1,27 @@
/*****************************************************************************
* 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 should be used to install the Operator Status
document.addEventListener('DOMContentLoaded', () => {
const openmct = window.openmct;
openmct.install(openmct.plugins.OperatorStatus());
});

277
e2e/helper/faultUtils.js Normal file
View File

@ -0,0 +1,277 @@
/*****************************************************************************
* 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.
*****************************************************************************/
const path = require('path');
/**
* @param {import('@playwright/test').Page} page
*/
async function navigateToFaultManagementWithExample(page) {
// eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, './', 'addInitExampleFaultProvider.js') });
await navigateToFaultItemInTree(page);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function navigateToFaultManagementWithStaticExample(page) {
// eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, './', 'addInitExampleFaultProviderStatic.js') });
await navigateToFaultItemInTree(page);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function navigateToFaultManagementWithoutExample(page) {
// eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, './', 'addInitFaultManagementPlugin.js') });
await navigateToFaultItemInTree(page);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function navigateToFaultItemInTree(page) {
await page.goto('./', { waitUntil: 'networkidle' });
// Click text=Fault Management
await page.click('text=Fault Management'); // this verifies the plugin has been added
}
/**
* @param {import('@playwright/test').Page} page
*/
async function acknowledgeFault(page, rowNumber) {
await openFaultRowMenu(page, rowNumber);
await page.locator('.c-menu >> text="Acknowledge"').click();
// Click [aria-label="Save"]
await page.locator('[aria-label="Save"]').click();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function shelveMultipleFaults(page, ...nums) {
const selectRows = nums.map((num) => {
return selectFaultItem(page, num);
});
await Promise.all(selectRows);
await page.locator('button:has-text("Shelve")').click();
await page.locator('[aria-label="Save"]').click();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function acknowledgeMultipleFaults(page, ...nums) {
const selectRows = nums.map((num) => {
return selectFaultItem(page, num);
});
await Promise.all(selectRows);
await page.locator('button:has-text("Acknowledge")').click();
await page.locator('[aria-label="Save"]').click();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function shelveFault(page, rowNumber) {
await openFaultRowMenu(page, rowNumber);
await page.locator('.c-menu >> text="Shelve"').click();
// Click [aria-label="Save"]
await page.locator('[aria-label="Save"]').click();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function changeViewTo(page, view) {
await page.locator('.c-fault-mgmt__search-row select').first().selectOption(view);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function sortFaultsBy(page, sort) {
await page.locator('.c-fault-mgmt__list-header-sortButton select').selectOption(sort);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function enterSearchTerm(page, term) {
await page.locator('.c-fault-mgmt-search [aria-label="Search Input"]').fill(term);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function clearSearch(page) {
await enterSearchTerm(page, '');
}
/**
* @param {import('@playwright/test').Page} page
*/
async function selectFaultItem(page, rowNumber) {
// eslint-disable-next-line playwright/no-force-option
await page.check(`.c-fault-mgmt-item > input >> nth=${rowNumber - 1}`, { force: true }); // this will not work without force true, saw this may be a pw bug
}
/**
* @param {import('@playwright/test').Page} page
*/
async function getHighestSeverity(page) {
const criticalCount = await page.locator('[title=CRITICAL]').count();
const warningCount = await page.locator('[title=WARNING]').count();
if (criticalCount > 0) {
return 'CRITICAL';
} else if (warningCount > 0) {
return 'WARNING';
}
return 'WATCH';
}
/**
* @param {import('@playwright/test').Page} page
*/
async function getLowestSeverity(page) {
const warningCount = await page.locator('[title=WARNING]').count();
const watchCount = await page.locator('[title=WATCH]').count();
if (watchCount > 0) {
return 'WATCH';
} else if (warningCount > 0) {
return 'WARNING';
}
return 'CRITICAL';
}
/**
* @param {import('@playwright/test').Page} page
*/
async function getFaultResultCount(page) {
const count = await page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').count();
return count;
}
/**
* @param {import('@playwright/test').Page} page
*/
function getFault(page, rowNumber) {
const fault = page.locator(`.c-faults-list-view-item-body > .c-fault-mgmt__list >> nth=${rowNumber - 1}`);
return fault;
}
/**
* @param {import('@playwright/test').Page} page
*/
function getFaultByName(page, name) {
const fault = page.locator(`.c-fault-mgmt__list-faultname:has-text("${name}")`);
return fault;
}
/**
* @param {import('@playwright/test').Page} page
*/
async function getFaultName(page, rowNumber) {
const faultName = await page.locator(`.c-fault-mgmt__list-faultname >> nth=${rowNumber - 1}`).textContent();
return faultName;
}
/**
* @param {import('@playwright/test').Page} page
*/
async function getFaultSeverity(page, rowNumber) {
const faultSeverity = await page.locator(`.c-faults-list-view-item-body .c-fault-mgmt__list-severity >> nth=${rowNumber - 1}`).getAttribute('title');
return faultSeverity;
}
/**
* @param {import('@playwright/test').Page} page
*/
async function getFaultNamespace(page, rowNumber) {
const faultNamespace = await page.locator(`.c-fault-mgmt__list-path >> nth=${rowNumber - 1}`).textContent();
return faultNamespace;
}
/**
* @param {import('@playwright/test').Page} page
*/
async function getFaultTriggerTime(page, rowNumber) {
const faultTriggerTime = await page.locator(`.c-fault-mgmt__list-trigTime >> nth=${rowNumber - 1} >> .c-fault-mgmt-item__value`).textContent();
return faultTriggerTime.toString().trim();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function openFaultRowMenu(page, rowNumber) {
// select
await page.locator(`.c-fault-mgmt-item > .c-fault-mgmt__list-action-button >> nth=${rowNumber - 1}`).click();
}
// eslint-disable-next-line no-undef
module.exports = {
navigateToFaultManagementWithExample,
navigateToFaultManagementWithStaticExample,
navigateToFaultManagementWithoutExample,
navigateToFaultItemInTree,
acknowledgeFault,
shelveMultipleFaults,
acknowledgeMultipleFaults,
shelveFault,
changeViewTo,
sortFaultsBy,
enterSearchTerm,
clearSearch,
selectFaultItem,
getHighestSeverity,
getLowestSeverity,
getFaultResultCount,
getFault,
getFaultByName,
getFaultName,
getFaultSeverity,
getFaultNamespace,
getFaultTriggerTime,
openFaultRowMenu
};

View File

@ -0,0 +1,60 @@
/*****************************************************************************
* 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.
*****************************************************************************/
const { createDomainObjectWithDefaults } = require('../appActions');
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
/**
* @param {import('@playwright/test').Page} page
*/
async function enterTextEntry(page, text) {
// Click .c-notebook__drag-area
await page.locator(NOTEBOOK_DROP_AREA).click();
// enter text
await page.locator('div.c-ne__text').click();
await page.locator('div.c-ne__text').fill(text);
await page.locator('div.c-ne__text').press('Enter');
}
/**
* @param {import('@playwright/test').Page} page
*/
async function dragAndDropEmbed(page, notebookObject) {
// Create example telemetry object
const swg = await createDomainObjectWithDefaults(page, {
type: "Sine Wave Generator"
});
// Navigate to notebook
await page.goto(notebookObject.url);
// Expand the tree to reveal the notebook
await page.click('button[title="Show selected item in tree"]');
// Drag and drop the SWG into the notebook
await page.dragAndDrop(`text=${swg.name}`, NOTEBOOK_DROP_AREA);
}
// eslint-disable-next-line no-undef
module.exports = {
enterTextEntry,
dragAndDropEmbed
};

View File

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

View File

@ -0,0 +1,51 @@
/* eslint-disable no-undef */
// playwright.config.js
// @ts-check
/** @type {import('@playwright/test').PlaywrightTestConfig<{ theme: string }>} */
const config = {
retries: 0, // Visual tests should never retry due to snapshot comparison errors. Leaving as a shim
testDir: 'tests/a11y',
testMatch: '**/*.a11y.spec.js', // only run visual tests
timeout: 60 * 1000,
workers: 1, //Lower stress on Circle CI Agent for Visual tests https://github.com/percy/cli/discussions/1067
webServer: {
command: 'npm run start:coverage',
url: 'http://localhost:8080/#',
timeout: 200 * 1000,
reuseExistingServer: !process.env.CI
},
use: {
baseURL: 'http://localhost:8080/',
headless: true, // this needs to remain headless to avoid visual changes due to GPU rendering in headed browsers
ignoreHTTPSErrors: true,
screenshot: 'only-on-failure',
trace: 'on-first-retry',
video: 'off'
},
projects: [
{
name: 'chrome',
use: {
browserName: 'chromium'
}
},
{
name: 'chrome-snow-theme', //Runs the same visual tests but with snow-theme enabled
use: {
browserName: 'chromium',
theme: 'snow'
}
}
],
reporter: [
['list'],
['junit', { outputFile: '../test-results/results.xml' }],
['html', {
open: 'never',
outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
}]
]
};
module.exports = config;

View File

@ -14,7 +14,7 @@ const config = {
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
timeout: 60 * 1000, timeout: 60 * 1000,
webServer: { webServer: {
command: 'cross-env NODE_ENV=test npm run start', command: 'npm run start:coverage',
url: 'http://localhost:8080/#', url: 'http://localhost:8080/#',
timeout: 200 * 1000, timeout: 200 * 1000,
reuseExistingServer: false reuseExistingServer: false
@ -32,6 +32,7 @@ const config = {
projects: [ projects: [
{ {
name: 'chrome', name: 'chrome',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
use: { use: {
browserName: 'chromium' browserName: 'chromium'
} }
@ -72,7 +73,7 @@ const config = {
open: 'never', open: 'never',
outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840 outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
}], }],
['junit', { outputFile: 'test-results/results.xml' }], ['junit', { outputFile: '../test-results/results.xml' }],
['github'] ['github']
] ]
}; };

View File

@ -12,10 +12,7 @@ const config = {
testIgnore: '**/*.perf.spec.js', testIgnore: '**/*.perf.spec.js',
timeout: 30 * 1000, timeout: 30 * 1000,
webServer: { webServer: {
env: { command: 'npm run start:coverage',
NODE_ENV: 'test'
},
command: 'npm run start',
url: 'http://localhost:8080/#', url: 'http://localhost:8080/#',
timeout: 120 * 1000, timeout: 120 * 1000,
reuseExistingServer: true reuseExistingServer: true

View File

@ -6,12 +6,12 @@ const CI = process.env.CI === 'true';
/** @type {import('@playwright/test').PlaywrightTestConfig} */ /** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = { const config = {
retries: 1, //Only for debugging purposes because trace is enabled only on first retry retries: 1, //Only for debugging purposes for trace: 'on-first-retry'
testDir: 'tests/performance/', testDir: 'tests/performance/',
timeout: 60 * 1000, timeout: 60 * 1000,
workers: 1, //Only run in serial with 1 worker workers: 1, //Only run in serial with 1 worker
webServer: { webServer: {
command: 'cross-env NODE_ENV=test npm run start', command: 'npm run start', //coverage not generated
url: 'http://localhost:8080/#', url: 'http://localhost:8080/#',
timeout: 200 * 1000, timeout: 200 * 1000,
reuseExistingServer: !CI reuseExistingServer: !CI
@ -35,8 +35,8 @@ const config = {
], ],
reporter: [ reporter: [
['list'], ['list'],
['junit', { outputFile: 'test-results/results.xml' }], ['junit', { outputFile: '../test-results/results.xml' }],
['json', { outputFile: 'test-results/results.json' }] ['json', { outputFile: '../test-results/results.json' }]
] ]
}; };

View File

@ -2,30 +2,49 @@
// playwright.config.js // playwright.config.js
// @ts-check // @ts-check
/** @type {import('@playwright/test').PlaywrightTestConfig} */ /** @type {import('@playwright/test').PlaywrightTestConfig<{ theme: string }>} */
const config = { const config = {
retries: 0, // visual tests should never retry due to snapshot comparison errors retries: 0, // Visual tests should never retry due to snapshot comparison errors. Leaving as a shim
testDir: 'tests/visual', testDir: 'tests/visual',
timeout: 90 * 1000, testMatch: '**/*.visual.spec.js', // only run visual tests
workers: 1, // visual tests should never run in parallel due to test pollution timeout: 60 * 1000,
workers: 1, //Lower stress on Circle CI Agent for Visual tests https://github.com/percy/cli/discussions/1067
webServer: { webServer: {
command: 'cross-env NODE_ENV=test npm run start', command: 'npm run start:coverage',
url: 'http://localhost:8080/#', url: 'http://localhost:8080/#',
timeout: 200 * 1000, timeout: 200 * 1000,
reuseExistingServer: !process.env.CI reuseExistingServer: !process.env.CI
}, },
use: { use: {
browserName: "chromium",
baseURL: 'http://localhost:8080/', baseURL: 'http://localhost:8080/',
headless: true, // this needs to remain headless to avoid visual changes due to GPU headless: true, // this needs to remain headless to avoid visual changes due to GPU rendering in headed browsers
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
screenshot: 'on', screenshot: 'only-on-failure',
trace: 'off', trace: 'on-first-retry',
video: 'off' video: 'off'
}, },
projects: [
{
name: 'chrome',
use: {
browserName: 'chromium'
}
},
{
name: 'chrome-snow-theme', //Runs the same visual tests but with snow-theme enabled
use: {
browserName: 'chromium',
theme: 'snow'
}
}
],
reporter: [ reporter: [
['list'], ['list'],
['junit', { outputFile: 'test-results/results.xml' }] ['junit', { outputFile: '../test-results/results.xml' }],
['html', {
open: 'on-failure',
outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
}]
] ]
}; };

View File

@ -27,7 +27,8 @@
*/ */
const { test, expect } = require('./baseFixtures'); const { test, expect } = require('./baseFixtures');
const { createDomainObjectWithDefaults } = require('./appActions'); // const { createDomainObjectWithDefaults } = require('./appActions');
const path = require('path');
/** /**
* @typedef {Object} ObjectCreateOptions * @typedef {Object} ObjectCreateOptions
@ -36,12 +37,16 @@ const { createDomainObjectWithDefaults } = require('./appActions');
*/ */
/** /**
* **NOTE: This feature is a work-in-progress and should not currently be used.**
*
* Used to create a new domain object as a part of getOrCreateDomainObject. * Used to create a new domain object as a part of getOrCreateDomainObject.
* @type {Map<string, string>} * @type {Map<string, string>}
*/ */
const createdObjects = new Map(); // 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 * 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 * 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 * a new file. The intent is to move object creation out of test suites which are not explicitly worried
@ -50,27 +55,29 @@ const createdObjects = new Map();
* @param {ObjectCreateOptions} options * @param {ObjectCreateOptions} options
* @returns {Promise<string>} uuid of the domain object * @returns {Promise<string>} uuid of the domain object
*/ */
async function getOrCreateDomainObject(page, options) { // async function getOrCreateDomainObject(page, options) {
const { type, name } = options; // const { type, name } = options;
const objectName = name ? `${type}:${name}` : type; // const objectName = name ? `${type}:${name}` : type;
if (createdObjects.has(objectName)) { // if (createdObjects.has(objectName)) {
return createdObjects.get(objectName); // return createdObjects.get(objectName);
} // }
await createDomainObjectWithDefaults(page, type, name); // await createDomainObjectWithDefaults(page, type, name);
// Once object is created, get the uuid from the url // // Once object is created, get the uuid from the url
const uuid = await page.evaluate(() => { // 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]; // 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];
}); // });
createdObjects.set(objectName, uuid); // createdObjects.set(objectName, uuid);
return uuid; // return uuid;
} // }
/** /**
* **NOTE: This feature is a work-in-progress and should not currently be used.**
*
* If provided, these options will be used to get or create the desired domain object before * If provided, these options will be used to get or create the desired domain object before
* any tests or test hooks have run. * any tests or test hooks have run.
* The `uuid` of the `domainObject` will then be available to use within the scoped tests. * The `uuid` of the `domainObject` will then be available to use within the scoped tests.
@ -87,7 +94,24 @@ async function getOrCreateDomainObject(page, options) {
* ``` * ```
* @type {ObjectCreateOptions} * @type {ObjectCreateOptions}
*/ */
const objectCreateOptions = null; // const objectCreateOptions = null;
/**
* The default theme for VIPER and Open MCT is the 'espresso' theme. Overriding this value with 'snow' in our playwright config.js
* will override the default theme by injecting the 'snow' theme on launch.
*
* ### Example:
* ```js
* projects: [
* {
* name: 'chrome-snow-theme',
* use: {
* browserName: 'chromium',
* theme: 'snow'
* ```
* @type {'snow' | 'espresso'}
*/
const theme = 'espresso';
/** /**
* The name of the "My Items" folder in the domain object tree. * The name of the "My Items" folder in the domain object tree.
@ -99,27 +123,30 @@ const objectCreateOptions = null;
const myItemsFolderName = "My Items"; const myItemsFolderName = "My Items";
exports.test = test.extend({ exports.test = test.extend({
// This should follow in the Project's configuration. Can be set to 'snow' in playwright config.js
theme: [theme, { option: true }],
// eslint-disable-next-line no-shadow
page: async ({ page, theme }, use, testInfo) => {
// eslint-disable-next-line playwright/no-conditional-in-test
if (theme === 'snow') {
//inject snow theme
await page.addInitScript({ path: path.join(__dirname, './helper', './useSnowTheme.js') });
}
// Attach info about the currently running test and its project.
// This will be used by appActions to fill in the created
// domain object's notes.
page.testNotes = [
`${testInfo.titlePath.join('\n')}`,
`${testInfo.project.name}`
].join('\n');
await use(page);
},
myItemsFolderName: [myItemsFolderName, { option: true }], myItemsFolderName: [myItemsFolderName, { option: true }],
// eslint-disable-next-line no-shadow // eslint-disable-next-line no-shadow
openmctConfig: async ({ myItemsFolderName }, use) => { openmctConfig: async ({ myItemsFolderName }, use) => {
await use({ myItemsFolderName }); await use({ myItemsFolderName });
},
objectCreateOptions: [objectCreateOptions, {option: true}],
// eslint-disable-next-line no-shadow
domainObject: [async ({ page, objectCreateOptions }, use) => {
// FIXME: This is a false-positive caused by a bug in the eslint-plugin-playwright rule.
// eslint-disable-next-line playwright/no-conditional-in-test
if (objectCreateOptions === null) {
await use(page);
return;
} }
//Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
const uuid = await getOrCreateDomainObject(page, objectCreateOptions);
await use({ uuid });
}, { auto: true }]
}); });
exports.expect = expect; exports.expect = expect;

File diff suppressed because one or more lines are too long

BIN
e2e/test-data/rick.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,129 @@
/*****************************************************************************
* 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.
*****************************************************************************/
const { test, expect } = require('../../pluginFixtures');
const { createDomainObjectWithDefaults, expandTreePaneItemByName } = require('../../appActions');
const { injectAxe, checkA11y, getViolations, reportViolations } = require('axe-playwright');
const { createHtmlReport } = require('axe-html-reporter');
const AxeBuilder = require('@axe-core/playwright').default; // 1
test.describe('Visual - Default', () => {
test.beforeEach(async ({ page }) => {
//Go to baseURL and Hide Tree
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
await injectAxe(page);
});
test.only('axe-playwright - basic reporting', async ({ page, theme }) => {
// Verify that Create button is actionable
await expect(page.locator('button:has-text("Create")')).toBeEnabled();
await checkA11y(page, null, {
detailedReport: true,
detailedReportOptions: { html: true }
});
});
test('axe-playwright - notebook', async ({ page, theme, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
// Create Notebook
const notebook = await createDomainObjectWithDefaults(page, {
type: 'Notebook',
name: "Embed Test Notebook"
});
// Create Overlay Plot
await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
name: "Dropped Overlay Plot"
});
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})`);
await checkA11y(page, null, {
detailedReport: true,
detailedReportOptions: { html: true }
});
});
test('axe-playwright - html reporting with wcag2aa rules 2', async ({ page, theme }) => {
// Verify that Create button is actionable
await expect(page.locator('button:has-text("Create")')).toBeEnabled();
await checkA11y(page, null,
{
axeOptions: {
runOnly: {
type: 'tag',
values: ['wcag2aa']
}
}
},
true, 'default',
{
outputDirPath: './results',
outputDir: 'accessibility',
reportFileName: 'accessibility-audit.html'
}
);
});
test('axe-playwright - html reporting with wcag2aa rules', async ({ page, theme }) => {
// Verify that Create button is actionable
await expect(page.locator('button:has-text("Create")')).toBeEnabled();
await checkA11y(page, null,
{
axeOptions: {
runOnly: {
type: 'tag',
values: ['wcag2aa']
}
}
},
true, 'default',
{ reporter: 'html' }
);
await createHtmlReport(
{ results: { violations: args.violations } }
);
});
test('axeBuilder - should not have any automatically detectable accessibility issues', async ({ page }, testInfo) => {
const accessibilityScanResults = await new AxeBuilder({ page }).withTags(['wcag2aa']).analyze(); // 4
await testInfo.attach('accessibility-scan-results', {
body: JSON.stringify(accessibilityScanResults, null, 2),
contentType: 'application/json'
});
expect(accessibilityScanResults.violations).toEqual([]); // 5
});
});

View File

@ -0,0 +1,165 @@
/*****************************************************************************
* 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.
*****************************************************************************/
const { test, expect } = require('../../pluginFixtures.js');
const { createDomainObjectWithDefaults, createNotification, expandEntireTree } = require('../../appActions.js');
test.describe('AppActions', () => {
test('createDomainObjectsWithDefaults', async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
const e2eFolder = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'e2e folder'
});
await test.step('Create multiple flat objects in a row', async () => {
const timer1 = await createDomainObjectWithDefaults(page, {
type: 'Timer',
name: 'Timer Foo',
parent: e2eFolder.uuid
});
const timer2 = await createDomainObjectWithDefaults(page, {
type: 'Timer',
name: 'Timer Bar',
parent: e2eFolder.uuid
});
const timer3 = await createDomainObjectWithDefaults(page, {
type: 'Timer',
name: 'Timer Baz',
parent: e2eFolder.uuid
});
await page.goto(timer1.url);
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer1.name);
await page.goto(timer2.url);
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer2.name);
await page.goto(timer3.url);
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer3.name);
});
await test.step('Create multiple nested objects in a row', async () => {
const folder1 = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Folder Foo',
parent: e2eFolder.uuid
});
const folder2 = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Folder Bar',
parent: folder1.uuid
});
const folder3 = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Folder Baz',
parent: folder2.uuid
});
await page.goto(folder1.url);
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder1.name);
await page.goto(folder2.url);
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder2.name);
await page.goto(folder3.url);
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder3.name);
expect(folder1.url).toBe(`${e2eFolder.url}/${folder1.uuid}`);
expect(folder2.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}`);
expect(folder3.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}/${folder3.uuid}`);
});
});
test("createNotification", async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await createNotification(page, {
message: 'Test info notification',
severity: 'info'
});
await expect(page.locator('.c-message-banner__message')).toHaveText('Test info notification');
await expect(page.locator('.c-message-banner')).toHaveClass(/info/);
await page.locator('[aria-label="Dismiss"]').click();
await createNotification(page, {
message: 'Test alert notification',
severity: 'alert'
});
await expect(page.locator('.c-message-banner__message')).toHaveText('Test alert notification');
await expect(page.locator('.c-message-banner')).toHaveClass(/alert/);
await page.locator('[aria-label="Dismiss"]').click();
await createNotification(page, {
message: 'Test error notification',
severity: 'error'
});
await expect(page.locator('.c-message-banner__message')).toHaveText('Test error notification');
await expect(page.locator('.c-message-banner')).toHaveClass(/error/);
await page.locator('[aria-label="Dismiss"]').click();
});
test('expandEntireTree', async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
const rootFolder = await createDomainObjectWithDefaults(page, {
type: 'Folder'
});
const folder1 = await createDomainObjectWithDefaults(page, {
type: 'Folder',
parent: rootFolder.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Clock',
parent: folder1.uuid
});
const folder2 = await createDomainObjectWithDefaults(page, {
type: 'Folder',
parent: folder1.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Folder',
parent: folder1.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
parent: folder2.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Folder',
parent: folder2.uuid
});
await page.goto('./#/browse/mine');
await expandEntireTree(page);
const treePane = page.getByRole('tree', {
name: "Main Tree"
});
const treePaneCollapsedItems = treePane.getByRole('treeitem', { expanded: false });
expect(await treePaneCollapsedItems.count()).toBe(0);
await page.goto('./#/browse/mine');
//Click the Create button
await page.click('button:has-text("Create")');
// Click the object specified by 'type'
await page.click(`li[role='menuitem']:text("Clock")`);
await expandEntireTree(page, "Create Modal Tree");
const locatorTree = page.getByRole("tree", {
name: "Create Modal Tree"
});
const locatorTreeCollapsedItems = locatorTree.locator('role=treeitem[expanded=false]');
expect(await locatorTreeCollapsedItems.count()).toBe(0);
});
});

View File

@ -23,13 +23,13 @@
/* /*
This test suite is dedicated to testing our use of the playwright framework as it This test suite is dedicated to testing our use of the playwright framework as it
relates to how we've extended it (i.e. ./e2e/baseFixtures.js) and assumptions made in our dev environment relates to how we've extended it (i.e. ./e2e/baseFixtures.js) and assumptions made in our dev environment
(app.js and ./e2e/webpack-dev-middleware.js) (`npm start` and ./e2e/webpack-dev-middleware.js)
*/ */
const { test } = require('../../baseFixtures.js'); const { test } = require('../../baseFixtures.js');
test.describe('baseFixtures tests', () => { test.describe('baseFixtures tests', () => {
test('Verify that tests fail if console.error is thrown @framework', async ({ page }) => { test('Verify that tests fail if console.error is thrown', async ({ page }) => {
test.fail(); test.fail();
//Go to baseURL //Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
@ -41,7 +41,7 @@ test.describe('baseFixtures tests', () => {
]); ]);
}); });
test('Verify that tests pass if console.warn is thrown @framework', async ({ page }) => { test('Verify that tests pass if console.warn is thrown', async ({ page }) => {
//Go to baseURL //Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });

View File

@ -21,12 +21,11 @@
*****************************************************************************/ *****************************************************************************/
/* /*
* This test suite template is to be used when creating new testsuites. It will be kept up to date with the latest improvements * This test suite template is to be used when creating new test suites. It will be kept up to date with the latest improvements
* made by the Open MCT team. It will also follow our best pratices as those evolve. Please use this structure as a _reference_ and clear * made by the Open MCT team. It will also follow our best pratices as those evolve. Please use this structure as a _reference_ and clear
* or update any references when creating a new test suite! * or update any references when creating a new test suite!
* *
* To illustrate current best practices, we've included a mocked up test suite for Renaming a Timer domain object. In this example * To illustrate current best practices, we've included a mocked up test suite for Renaming a Timer domain object.
* this test suite should be cloned and renamed as /e2e/tests/plugins/timer/renameTimer.e2e.spec.js
* *
* Demonstrated: * Demonstrated:
* - Using appActions to leverage existing functions * - Using appActions to leverage existing functions
@ -43,56 +42,73 @@
* -> test2 * -> test2
* -> test3(stub) * -> test3(stub)
* 4. Any custom functions * 4. Any custom functions
*
*/ */
//Structure: Some standard Imports. Please update the required pathing // Structure: Some standard Imports. Please update the required pathing.
const { test, expect } = require('../../baseFixtures'); const { test, expect } = require('../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../appActions'); const { createDomainObjectWithDefaults } = require('../../appActions');
// Structure: Try to keep a single describe block per logical groups of tests. If your test runtime exceeds 5 minutes or 500 lines, it's likely that it will need to be split. /**
// Annotations: Please use the @unstable tag so that our automation can pick it up as a part of our test promotion pipeline. * Structure:
test.describe('Renaming Timer Object @unstable', () => { * Try to keep a single describe block per logical groups of tests.
//Create a testcase name which will be obvious when it fails in CI * If your test runtime exceeds 5 minutes or 500 lines, it's likely that it will need to be split.
test('Can create a new Timer object and rename it from actions Menu', async ({ page }) => { *
//Open a browser, navigate to the main page, and wait until all networkevents to resolve * Annotations:
* Please use the @unstable tag at the end of the test title so that our automation can pick it up
* as a part of our test promotion pipeline.
*/
test.describe('Renaming Timer Object', () => {
// Top-level declaration of the Timer object created in beforeEach().
// We can then use this throughout the entire test suite.
let timer;
test.beforeEach(async ({ page }) => {
// Open a browser, navigate to the main page, and wait until all network events to resolve
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
//We provide some helper functions in appActions like createDomainObjectWithDefaults. This example will create a Timer object
await createDomainObjectWithDefaults(page, 'Timer');
//Assert the object to be created and check it's name in the title
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer');
const newObjectName = "Renamed Timer"; // We provide some helper functions in appActions like `createDomainObjectWithDefaults()`.
//We've created an example of a shared function which pases the page and newObjectName values // This example will create a Timer object with default properties, under the root folder:
await renameObjectFrom3DotMenu(page, newObjectName); timer = await createDomainObjectWithDefaults(page, { type: 'Timer' });
//Assert that the name has changed in the browser bar to the value we assigned above
await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName);
// Assert the object to be created and check its name in the title
await expect(page.locator('.l-browse-bar__object-name')).toContainText(timer.name);
}); });
test('An existing Timer object can be renamed twice', async ({ page }) => {
//Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'networkidle' });
//We provide some helper functions in appActions like createDomainObjectWithDefaults. This example will create a Timer object
await createDomainObjectWithDefaults(page, 'Timer');
//Expect the object to be created and check it's name in the title
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer');
/**
* Make sure to use testcase names which are descriptive and easy to understand.
* A good testcase name concisely describes the test's goal(s) and should give
* some hint as to what went wrong if the test fails.
*/
test('An existing Timer object can be renamed via the 3dot actions menu', async ({ page }) => {
const newObjectName = "Renamed Timer";
// We've created an example of a shared function which pases the page and newObjectName values
await renameTimerFrom3DotMenu(page, timer.url, newObjectName);
// Assert that the name has changed in the browser bar to the value we assigned above
await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName);
});
test('An existing Timer object can be renamed twice', async ({ page }) => {
const newObjectName = "Renamed Timer"; const newObjectName = "Renamed Timer";
const newObjectName2 = "Re-Renamed Timer"; const newObjectName2 = "Re-Renamed Timer";
//We've created an example of a shared function which pases the page and newObjectName values
await renameObjectFrom3DotMenu(page, newObjectName);
//Assert that the name has changed in the browser bar to the value we assigned above await renameTimerFrom3DotMenu(page, timer.url, newObjectName);
// Assert that the name has changed in the browser bar to the value we assigned above
await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName); await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName);
await renameObjectFrom3DotMenu(page, newObjectName2); // Rename the Timer object again
await renameTimerFrom3DotMenu(page, timer.url, newObjectName2);
//Assert that the name has changed in the browser bar to the second value // Assert that the name has changed in the browser bar to the second value
await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName2); await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName2);
}); });
//If you run out of time to write new tests, please stub in the missing tests in place with a test.fixme and BDD-style test steps. Someone will carry the baton! /**
* If you run out of time to write new tests, please stub in the missing tests
* in-place with a test.fixme and BDD-style test steps.
* Someone will carry the baton!
*/
test.fixme('Can Rename Timer Object from Tree', async ({ page }) => { test.fixme('Can Rename Timer Object from Tree', async ({ page }) => {
//Create a new object //Create a new object
//Copy this object //Copy this object
@ -101,24 +117,32 @@ test.describe('Renaming Timer Object @unstable', () => {
}); });
}); });
//Structure: custom functions should be declared last. We are leaning on JSDoc pretty heavily to describe functionality. It is not required, but heavily recommended. /**
* Structure:
* Custom functions should be declared last.
* We are leaning on JSDoc pretty heavily to describe functionality. It is not required, but highly recommended.
*/
/** /**
* This is an example of a function which is shared between testcases in this test suite. When refactoring, we'll be looking * This is an example of a function which is shared between testcases in this test suite. When refactoring, we'll be looking
* for common functionality which makes sense to generalize for the entire test framework. * for common functionality which makes sense to generalize for the entire test framework.
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {string} newNameForTimer New Name for object * @param {string} timerUrl The URL of the timer object to be renamed
* @param {string} newNameForTimer New name for object
*/ */
async function renameObjectFrom3DotMenu(page, newNameForTimer) { async function renameTimerFrom3DotMenu(page, timerUrl, newNameForTimer) {
// Navigate to the timer object
await page.goto(timerUrl);
// Click on 3 Dot Menu // Click on 3 Dot Menu
await page.locator('button[title="More options"]').click(); await page.locator('button[title="More options"]').click();
// Click text=Edit Properties... // Click text=Edit Properties...
await page.locator('text=Edit Properties...').click(); await page.locator('text=Edit Properties...').click();
// Rename the object with newNameForTimer variable which is passed into this function // Rename the timer object
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(newNameForTimer); await page.locator('text=Properties Title Notes >> input[type="text"]').fill(newNameForTimer);
// Click Ok button to Save // Click Ok button to Save
await page.locator('text=OK').click(); await page.locator('button:has-text("OK")').click();
} }

View File

@ -31,54 +31,34 @@ TODO: Provide additional validation of object properties as it grows.
*/ */
const { createDomainObjectWithDefaults } = require('../../appActions.js');
const { test, expect } = require('../../pluginFixtures.js'); const { test, expect } = require('../../pluginFixtures.js');
test('Generate Visual Test Data @localStorage', async ({ page, context, openmctConfig }) => { test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
const { myItemsFolderName } = openmctConfig;
//Go to baseURL //Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
const overlayPlot = await createDomainObjectWithDefaults(page, { type: 'Overlay Plot' });
await page.locator('button:has-text("Create")').click();
// add overlay plot with defaults
await page.locator('li:has-text("Overlay Plot")').click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear1
page.waitForSelector('.c-message-banner__message')
]);
// 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 // click create button
await page.locator('button:has-text("Create")').click(); await page.locator('button:has-text("Create")').click();
// add sine wave generator with defaults // add sine wave generator with defaults
await page.locator('li:has-text("Sine Wave Generator")').click(); await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
//Add a 5000 ms Delay //Add a 5000 ms Delay
await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000'); await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000');
await Promise.all([ await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),
page.locator('text=OK').click(), page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear1 //Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message') page.waitForSelector('.c-message-banner__message')
]); ]);
// focus the overlay plot // focus the overlay plot
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click(); await page.goto(overlayPlot.url);
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Overlay Plot').first().click()
]);
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot'); await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlot.name);
//Save localStorage for future test execution //Save localStorage for future test execution
await context.storageState({ path: './e2e/test-data/VisualTestData_storage.json' }); await context.storageState({ path: './e2e/test-data/VisualTestData_storage.json' });
}); });

View File

@ -25,21 +25,22 @@ This test suite is dedicated to testing our use of our custom fixtures to verify
that they are working as expected. that they are working as expected.
*/ */
const { test, expect } = require('../../pluginFixtures.js'); const { test } = require('../../pluginFixtures.js');
test.describe('pluginFixtures tests', () => { // eslint-disable-next-line playwright/no-skipped-test
test.use({ domainObjectName: 'Timer' }); test.describe.skip('pluginFixtures tests', () => {
let timerUUID; // test.use({ domainObjectName: 'Timer' });
// let timerUUID;
test('Creates a timer object @framework @unstable', ({ domainObject }) => { // test('Creates a timer object @framework @unstable', ({ domainObject }) => {
const { uuid } = domainObject; // const { uuid } = domainObject;
const uuidRegexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/; // const uuidRegexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/;
expect(uuid).toMatch(uuidRegexp); // expect(uuid).toMatch(uuidRegexp);
timerUUID = uuid; // timerUUID = uuid;
}); // });
test('Provides same uuid for subsequent uses of the same object @framework', ({ domainObject }) => { // test('Provides same uuid for subsequent uses of the same object @framework', ({ domainObject }) => {
const { uuid } = domainObject; // const { uuid } = domainObject;
expect(uuid).toEqual(timerUUID); // expect(uuid).toEqual(timerUUID);
}); // });
}); });

View File

@ -38,14 +38,14 @@ test.describe('Branding tests', () => {
await expect(page.locator('.c-about__image')).toBeVisible(); await expect(page.locator('.c-about__image')).toBeVisible();
// Modify the Build information in 'about' Modal // Modify the Build information in 'about' Modal
const versionInformationLocator = page.locator('ul.t-info.l-info.s-info'); const versionInformationLocator = page.locator('ul.t-info.l-info.s-info').first();
await expect(versionInformationLocator).toBeEnabled(); await expect(versionInformationLocator).toBeEnabled();
await expect.soft(versionInformationLocator).toContainText(/Version: \d/); await expect.soft(versionInformationLocator).toContainText(/Version: \d/);
await expect.soft(versionInformationLocator).toContainText(/Build Date: ((?:Mon|Tue|Wed|Thu|Fri|Sat|Sun))/); await expect.soft(versionInformationLocator).toContainText(/Build Date: ((?:Mon|Tue|Wed|Thu|Fri|Sat|Sun))/);
await expect.soft(versionInformationLocator).toContainText(/Revision: \b[0-9a-f]{5,40}\b/); await expect.soft(versionInformationLocator).toContainText(/Revision: \b[0-9a-f]{5,40}\b/);
await expect.soft(versionInformationLocator).toContainText(/Branch: ./); await expect.soft(versionInformationLocator).toContainText(/Branch: ./);
}); });
test('Verify Links in About Modal', async ({ page }) => { test('Verify Links in About Modal @2p', async ({ page }) => {
// Go to baseURL // Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });

View File

@ -0,0 +1,111 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
* This test suite is meant to be executed against a couchdb container. More doc to come
*
*/
const { test, expect } = require('../../pluginFixtures');
test.describe("CouchDB Status Indicator with mocked responses @couchdb", () => {
test.use({ failOnConsoleError: false });
//TODO BeforeAll Verify CouchDB Connectivity with APIContext
test('Shows green if connected', async ({ page }) => {
await page.route('**/openmct/mine', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({})
});
});
//Go to baseURL
await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', { waitUntil: 'networkidle' });
await expect(page.locator('div:has-text("CouchDB is connected")').nth(3)).toBeVisible();
});
test('Shows red if not connected', async ({ page }) => {
await page.route('**/openmct/**', route => {
route.fulfill({
status: 503,
contentType: 'application/json',
body: JSON.stringify({})
});
});
//Go to baseURL
await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', { waitUntil: 'networkidle' });
await expect(page.locator('div:has-text("CouchDB is offline")').nth(3)).toBeVisible();
});
test('Shows unknown if it receives an unexpected response code', async ({ page }) => {
await page.route('**/openmct/mine', route => {
route.fulfill({
status: 418,
contentType: 'application/json',
body: JSON.stringify({})
});
});
//Go to baseURL
await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', { waitUntil: 'networkidle' });
await expect(page.locator('div:has-text("CouchDB connectivity unknown")').nth(3)).toBeVisible();
});
});
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 = {
status: 404,
contentType: 'application/json',
body: JSON.stringify({})
};
// Override the first request to GET openmct/mine to return a 404.
// This simulates the case of starting Open MCT with a fresh database
// and no "My Items" folder created yet.
await page.route('**/mine', route => {
route.fulfill(mockedMissingObjectResponsefromCouchDB);
}, { times: 1 });
// Set up promise to verify that a PUT request to create "My Items"
// folder was made.
const putMineFolderRequest = page.waitForRequest(req =>
req.url().endsWith('/mine')
&& req.method() === 'PUT');
// Set up promise to verify that a GET request to retrieve "My Items"
// folder was made.
const getMineFolderRequest = page.waitForRequest(req =>
req.url().endsWith('/mine')
&& req.method() === 'GET');
// Go to baseURL.
await page.goto('./', { waitUntil: 'networkidle' });
// Wait for both requests to resolve.
await Promise.all([
putMineFolderRequest,
getMineFolderRequest
]);
});
});

View File

@ -24,7 +24,7 @@
This test suite is dedicated to tests which verify the basic operations surrounding the example event generator. This test suite is dedicated to tests which verify the basic operations surrounding the example event generator.
*/ */
const { test, expect } = require('../../../baseFixtures'); const { test, expect } = require('../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../appActions'); const { createDomainObjectWithDefaults } = require('../../../appActions');
test.describe('Example Event Generator CRUD Operations', () => { test.describe('Example Event Generator CRUD Operations', () => {
@ -35,7 +35,10 @@ test.describe('Example Event Generator CRUD Operations', () => {
//Create a name for the object //Create a name for the object
const newObjectName = 'Test Event Generator'; const newObjectName = 'Test Event Generator';
await createDomainObjectWithDefaults(page, 'Event Message Generator', newObjectName); await createDomainObjectWithDefaults(page, {
type: 'Event Message Generator',
name: newObjectName
});
//Assertions against newly created object which define standard behavior //Assertions against newly created object which define standard behavior
await expect(page.waitForURL(/.*&view=table/)).toBeTruthy(); await expect(page.waitForURL(/.*&view=table/)).toBeTruthy();

View File

@ -28,7 +28,8 @@ const { test, expect } = require('../../../../baseFixtures');
test.describe('Sine Wave Generator', () => { test.describe('Sine Wave Generator', () => {
test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ page, browserName }) => { test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ page, browserName }) => {
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox'); // eslint-disable-next-line playwright/no-skipped-test
test.skip(browserName === 'firefox', 'This test needs to be updated to work with firefox');
//Go to baseURL //Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
@ -95,7 +96,7 @@ test.describe('Sine Wave Generator', () => {
//Click text=OK //Click text=OK
await Promise.all([ await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),
page.click('text=OK') page.click('button:has-text("OK")')
]); ]);
// Verify that the Sine Wave Generator is displayed and correct // Verify that the Sine Wave Generator is displayed and correct

View File

@ -24,10 +24,14 @@
This test suite is dedicated to tests which verify form functionality in isolation This test suite is dedicated to tests which verify form functionality in isolation
*/ */
const { test, expect } = require('../../baseFixtures'); const { test, expect } = require('../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../appActions');
const genUuid = require('uuid').v4;
const path = require('path'); const path = require('path');
const TEST_FOLDER = 'test folder'; const TEST_FOLDER = 'test folder';
const jsonFilePath = 'e2e/test-data/ExampleLayouts.json';
const imageFilePath = 'e2e/test-data/rick.jpg';
test.describe('Form Validation Behavior', () => { test.describe('Form Validation Behavior', () => {
test('Required Field indicators appear if title is empty and can be corrected', async ({ page }) => { test('Required Field indicators appear if title is empty and can be corrected', async ({ page }) => {
@ -43,7 +47,7 @@ test.describe('Form Validation Behavior', () => {
await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab'); await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab');
//Required Field Form Validation //Required Field Form Validation
await expect(page.locator('text=OK')).toBeDisabled(); await expect(page.locator('button:has-text("OK")')).toBeDisabled();
await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/); await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/);
//Correct Form Validation for missing title and trigger validation with 'Tab' //Correct Form Validation for missing title and trigger validation with 'Tab'
@ -52,13 +56,13 @@ test.describe('Form Validation Behavior', () => {
await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab'); await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab');
//Required Field Form Validation is corrected //Required Field Form Validation is corrected
await expect(page.locator('text=OK')).toBeEnabled(); await expect(page.locator('button:has-text("OK")')).toBeEnabled();
await expect(page.locator('.c-form-row__state-indicator').first()).not.toHaveClass(/invalid/); await expect(page.locator('.c-form-row__state-indicator').first()).not.toHaveClass(/invalid/);
//Finish Creating Domain Object //Finish Creating Domain Object
await Promise.all([ await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),
page.click('text=OK') page.click('button:has-text("OK")')
]); ]);
//Verify that the Domain Object has been created with the corrected title property //Verify that the Domain Object has been created with the corrected title property
@ -66,6 +70,41 @@ test.describe('Form Validation Behavior', () => {
}); });
}); });
test.describe('Form File Input Behavior', () => {
test.beforeEach(async ({ page }) => {
// eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, '../../helper', 'addInitFileInputObject.js') });
});
test('Can select a JSON file type', async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.getByRole('button', { name: ' Create ' }).click();
await page.getByRole('menuitem', { name: 'JSON File Input Object' }).click();
await page.setInputFiles('#fileElem', jsonFilePath);
await page.getByRole('button', { name: 'Save' }).click();
const type = await page.locator('#file-input-type').textContent();
await expect(type).toBe(`"string"`);
});
test('Can select an image file type', async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.getByRole('button', { name: ' Create ' }).click();
await page.getByRole('menuitem', { name: 'Image File Input Object' }).click();
await page.setInputFiles('#fileElem', imageFilePath);
await page.getByRole('button', { name: 'Save' }).click();
const type = await page.locator('#file-input-type').textContent();
await expect(type).toBe(`"object"`);
});
});
test.describe('Persistence operations @addInit', () => { test.describe('Persistence operations @addInit', () => {
// add non persistable root item // add non persistable root item
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
@ -91,6 +130,146 @@ test.describe('Persistence operations @addInit', () => {
}); });
}); });
test.describe('Persistence operations @couchdb', () => {
test.use({ failOnConsoleError: false });
test('Editing object properties should generate a single persistence operation', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5616'
});
await page.goto('./', { waitUntil: 'networkidle' });
// Create a new 'Clock' object with default settings
const clock = await createDomainObjectWithDefaults(page, {
type: 'Clock'
});
// Count all persistence operations (PUT requests) for this specific object
let putRequestCount = 0;
page.on('request', req => {
if (req.method() === 'PUT' && req.url().endsWith(clock.uuid)) {
putRequestCount += 1;
}
});
// Open the edit form for the clock object
await page.click('button[title="More options"]');
await page.click('li[title="Edit properties of this object."]');
// Modify the display format from default 12hr -> 24hr and click 'Save'
await page.locator('select[aria-label="12 or 24 hour clock"]').selectOption({ value: 'clock24' });
await page.click('button[aria-label="Save"]');
await expect.poll(() => putRequestCount, {
message: 'Verify a single PUT request was made to persist the object',
timeout: 1000
}).toEqual(1);
});
test('Can create an object after a conflict error @couchdb @2p', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5982'
});
const page2 = await page.context().newPage();
// Both pages: Go to baseURL
await Promise.all([
page.goto('./', { waitUntil: 'networkidle' }),
page2.goto('./', { waitUntil: 'networkidle' })
]);
// Both pages: Click the Create button
await Promise.all([
page.click('button:has-text("Create")'),
page2.click('button:has-text("Create")')
]);
// Both pages: Click "Clock" in the Create menu
await Promise.all([
page.click(`li[role='menuitem']:text("Clock")`),
page2.click(`li[role='menuitem']:text("Clock")`)
]);
// Generate unique names for both objects
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
const nameInput2 = page2.locator('form[name="mctForm"] .first input[type="text"]');
// Both pages: Fill in the 'Name' form field.
await Promise.all([
nameInput.fill(""),
nameInput.fill(`Clock:${genUuid()}`),
nameInput2.fill(""),
nameInput2.fill(`Clock:${genUuid()}`)
]);
// Both pages: Fill the "Notes" section with information about the
// currently running test and its project.
const testNotes = page.testNotes;
const notesInput = page.locator('form[name="mctForm"] #notes-textarea');
const notesInput2 = page2.locator('form[name="mctForm"] #notes-textarea');
await Promise.all([
notesInput.fill(testNotes),
notesInput2.fill(testNotes)
]);
// Page 2: Click "OK" to create the domain object and wait for navigation.
// This will update the composition of the parent folder, setting the
// conditions for a conflict error from the first page.
await Promise.all([
page2.waitForLoadState(),
page2.click('[aria-label="Save"]'),
// Wait for Save Banner to appear
page2.waitForSelector('.c-message-banner__message')
]);
// Close Page 2, we're done with it.
await page2.close();
// Page 1: Click "OK" to create the domain object and wait for navigation.
// This will trigger a conflict error upon attempting to update
// the composition of the parent folder.
await Promise.all([
page.waitForLoadState(),
page.click('[aria-label="Save"]'),
// Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
// Page 1: Verify that the conflict has occurred and an error notification is displayed.
await expect(page.locator('.c-message-banner__message', {
hasText: "Conflict detected while saving mine"
})).toBeVisible();
// Page 1: Start logging console errors from this point on
let errors = [];
page.on('console', (msg) => {
if (msg.type() === 'error') {
errors.push(msg.text());
}
});
// Page 1: Try to create a clock with the page that received the conflict.
const clockAfterConflict = await createDomainObjectWithDefaults(page, {
type: 'Clock'
});
// Page 1: Wait for save progress dialog to appear/disappear
await page.locator('.c-message-banner__message', {
hasText: 'Do not navigate away from this page or close this browser tab while this message is displayed.',
state: 'visible'
}).waitFor({ state: 'hidden' });
// Page 1: Navigate to 'My Items' and verify that the second clock was created
await page.goto('./#/browse/mine');
await expect(page.locator(`.c-grid-item__name[title="${clockAfterConflict.name}"]`)).toBeVisible();
// Verify no console errors occurred
expect(errors).toHaveLength(0);
});
});
test.describe('Form Correctness by Object Type', () => { test.describe('Form Correctness by Object Type', () => {
test.fixme('Verify correct behavior of number object (SWG)', async ({page}) => {}); test.fixme('Verify correct behavior of number object (SWG)', async ({page}) => {});
test.fixme('Verify correct behavior of number object Timer', async ({page}) => {}); test.fixme('Verify correct behavior of number object Timer', async ({page}) => {});

View File

@ -42,7 +42,7 @@ test.describe('Persistence operations @addInit', () => {
button: 'right' button: 'right'
}); });
const menuOptions = page.locator('.c-menu ul'); const menuOptions = page.locator('.c-menu li');
await expect.soft(menuOptions).toContainText(['Open In New Tab', 'View', 'Create Link']); await expect.soft(menuOptions).toContainText(['Open In New Tab', 'View', 'Create Link']);
await expect(menuOptions).not.toContainText(['Move', 'Duplicate', 'Remove', 'Add New Folder', 'Edit Properties...', 'Export as JSON', 'Import from JSON']); await expect(menuOptions).not.toContainText(['Move', 'Duplicate', 'Remove', 'Add New Folder', 'Edit Properties...', 'Export as JSON', 'Import from JSON']);

View File

@ -0,0 +1,276 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify the basic operations surrounding moving & linking objects.
*/
const { test, expect } = require('../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../appActions');
test.describe('Move & link item tests', () => {
test('Create a basic object and verify that it can be moved to another folder', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
// Go to Open MCT
await page.goto('./');
const parentFolder = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Parent Folder'
});
const childFolder = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Child Folder',
parent: parentFolder.uuid
});
const grandchildFolder = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Grandchild Folder',
parent: childFolder.uuid
});
// Attempt to move parent to its own grandparent
await page.locator('button[title="Show selected item in tree"]').click();
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
await treePane.getByRole('treeitem', {
name: 'Parent Folder'
}).click({
button: 'right'
});
await page.getByRole('menuitem', {
name: /Move/
}).click();
const createModalTree = page.getByRole('tree', {
name: "Create Modal Tree"
});
const myItemsLocatorTreeItem = createModalTree.getByRole('treeitem', {
name: myItemsFolderName
});
await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click();
await myItemsLocatorTreeItem.click();
const parentFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {
name: parentFolder.name
});
await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
await parentFolderLocatorTreeItem.click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
const childFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {
name: new RegExp(childFolder.name)
});
await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
await childFolderLocatorTreeItem.click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
const grandchildFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {
name: grandchildFolder.name
});
await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
await grandchildFolderLocatorTreeItem.click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
await parentFolderLocatorTreeItem.click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
await page.locator('[aria-label="Cancel"]').click();
// Move Child Folder from Parent Folder to My Items
await treePane.getByRole('treeitem', {
name: new RegExp(childFolder.name)
}).click({
button: 'right'
});
await page.getByRole('menuitem', {
name: /Move/
}).click();
await myItemsLocatorTreeItem.click();
await page.locator('[aria-label="Save"]').click();
const myItemsPaneTreeItem = treePane.getByRole('treeitem', {
name: myItemsFolderName
});
// Expect that Child Folder is in My Items, the root folder
expect(myItemsPaneTreeItem.locator('nth=0:has(text=Child Folder)')).toBeTruthy();
});
test('Create a basic object and verify that it cannot be moved to telemetry object without Composition Provider', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
// Go to Open MCT
await page.goto('./');
// Create Telemetry Table
let telemetryTable = 'Test Telemetry Table';
await page.locator('button:has-text("Create")').click();
await page.locator('li[role="menuitem"]:has-text("Telemetry Table")').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable);
await page.locator('button:has-text("OK")').click();
// Finish editing and save Telemetry Table
await page.locator('.c-button--menu.c-button--major.icon-save').click();
await page.locator('text=Save and Finish Editing').click();
// Create New Folder Basic Domain Object
let folder = 'Test Folder';
await page.locator('button:has-text("Create")').click();
await page.locator('li[role="menuitem"]:has-text("Folder")').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder);
// See if it's possible to put the folder in the Telemetry object during creation (Soft Assert)
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
let okButton = page.locator('button.c-button.c-button--major:has-text("OK")');
let okButtonStateDisabled = await okButton.isDisabled();
expect.soft(okButtonStateDisabled).toBeTruthy();
// Continue test regardless of assertion and create it in My Items
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
await page.locator('button:has-text("OK")').click();
// Open My Items
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
// Select Folder Object and select Move from context menu
await Promise.all([
page.waitForNavigation(),
page.locator(`a:has-text("${folder}")`).click()
]);
await page.locator('.c-tree__item.is-navigated-object .c-tree__item__label .c-tree__item__type-icon').click({
button: 'right'
});
await page.locator('li.icon-move').click();
// See if it's possible to put the folder in the Telemetry object after creation
await page.locator(`text=Location Open MCT ${myItemsFolderName} >> span`).nth(3).click();
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
let okButton2 = page.locator('button.c-button.c-button--major:has-text("OK")');
let okButtonStateDisabled2 = await okButton2.isDisabled();
expect(okButtonStateDisabled2).toBeTruthy();
});
test('Create a basic object and verify that it can be linked to another folder', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
// Go to Open MCT
await page.goto('./');
const parentFolder = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Parent Folder'
});
const childFolder = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Child Folder',
parent: parentFolder.uuid
});
const grandchildFolder = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Grandchild Folder',
parent: childFolder.uuid
});
// Attempt to move parent to its own grandparent
await page.locator('button[title="Show selected item in tree"]').click();
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
await treePane.getByRole('treeitem', {
name: 'Parent Folder'
}).click({
button: 'right'
});
await page.getByRole('menuitem', {
name: /Move/
}).click();
const createModalTree = page.getByRole('tree', {
name: "Create Modal Tree"
});
const myItemsLocatorTreeItem = createModalTree.getByRole('treeitem', {
name: myItemsFolderName
});
await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click();
await myItemsLocatorTreeItem.click();
const parentFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {
name: parentFolder.name
});
await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
await parentFolderLocatorTreeItem.click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
const childFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {
name: new RegExp(childFolder.name)
});
await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
await childFolderLocatorTreeItem.click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
const grandchildFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {
name: grandchildFolder.name
});
await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
await grandchildFolderLocatorTreeItem.click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
await parentFolderLocatorTreeItem.click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
await page.locator('[aria-label="Cancel"]').click();
// Move Child Folder from Parent Folder to My Items
await treePane.getByRole('treeitem', {
name: new RegExp(childFolder.name)
}).click({
button: 'right'
});
await page.getByRole('menuitem', {
name: /Link/
}).click();
await myItemsLocatorTreeItem.click();
await page.locator('[aria-label="Save"]').click();
const myItemsPaneTreeItem = treePane.getByRole('treeitem', {
name: myItemsFolderName
});
// Expect that Child Folder is in My Items, the root folder
expect(myItemsPaneTreeItem.locator('nth=0:has(text=Child Folder)')).toBeTruthy();
});
});
test.fixme('Cannot move a previously created domain object to non-peristable object in Move Modal', async ({ page }) => {
//Create a domain object
//Save Domain object
//Move Object and verify that cannot select non-persistable object
//Move Object to My Items
//Verify successful move
});

View File

@ -1,148 +0,0 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify the basic operations surrounding moving objects.
*/
const { test, expect } = require('../../pluginFixtures');
test.describe('Move item tests', () => {
test('Create a basic object and verify that it can be moved to another folder', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
// Go to Open MCT
await page.goto('./');
// Create a new folder in the root my items folder
let folder1 = "Folder1";
await page.locator('button:has-text("Create")').click();
await page.locator('li.icon-folder').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder1);
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
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'});
// Create another folder with a new name at default location, which is currently inside Folder 1
let folder2 = "Folder2";
await page.locator('button:has-text("Create")').click();
await page.locator('li.icon-folder').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder2);
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
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'});
// Move Folder 2 from Folder 1 to My Items
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
await page.locator('.c-tree__scrollable div div:nth-child(2) .c-tree__item .c-tree__item__view-control').click();
await page.locator(`a:has-text("${folder2}")`).click({
button: 'right'
});
await page.locator('li.icon-move').click();
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
await page.locator('text=OK').click();
// Expect that Folder 2 is in My Items, the root folder
expect(page.locator(`text=${myItemsFolderName} >> nth=0:has(text=${folder2})`)).toBeTruthy();
});
test('Create a basic object and verify that it cannot be moved to telemetry object without Composition Provider', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
// Go to Open MCT
await page.goto('./');
// Create Telemetry Table
let telemetryTable = 'Test Telemetry Table';
await page.locator('button:has-text("Create")').click();
await page.locator('li:has-text("Telemetry Table")').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable);
await page.locator('text=OK').click();
// Finish editing and save Telemetry Table
await page.locator('.c-button--menu.c-button--major.icon-save').click();
await page.locator('text=Save and Finish Editing').click();
// Create New Folder Basic Domain Object
let folder = 'Test Folder';
await page.locator('button:has-text("Create")').click();
await page.locator('li:has-text("Folder")').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder);
// See if it's possible to put the folder in the Telemetry object during creation (Soft Assert)
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
let okButton = await page.locator('button.c-button.c-button--major:has-text("OK")');
let okButtonStateDisabled = await okButton.isDisabled();
expect.soft(okButtonStateDisabled).toBeTruthy();
// Continue test regardless of assertion and create it in My Items
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
await page.locator('text=OK').click();
// Open My Items
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
// Select Folder Object and select Move from context menu
await Promise.all([
page.waitForNavigation(),
page.locator(`a:has-text("${folder}")`).click()
]);
await page.locator('.c-tree__item.is-navigated-object .c-tree__item__label .c-tree__item__type-icon').click({
button: 'right'
});
await page.locator('li.icon-move').click();
// See if it's possible to put the folder in the Telemetry object after creation
await page.locator(`text=Location Open MCT ${myItemsFolderName} >> span`).nth(3).click();
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
let okButton2 = await page.locator('button.c-button.c-button--major:has-text("OK")');
let okButtonStateDisabled2 = await okButton2.isDisabled();
expect(okButtonStateDisabled2).toBeTruthy();
});
});
test.fixme('Cannot move a previously created domain object to non-peristable object in Move Modal', async ({ page }) => {
//Create a domain object
//Save Domain object
//Move Object and verify that cannot select non-persistable object
//Move Object to My Items
//Verify successful move
});

View File

@ -0,0 +1,112 @@
/*****************************************************************************
* 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 which verify Open MCT's Notification functionality
*/
const { createDomainObjectWithDefaults, createNotification } = require('../../appActions');
const { test, expect } = require('../../pluginFixtures');
test.describe('Notifications List', () => {
test('Notifications can be dismissed individually', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6122'
});
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
// Create an error notification with the message "Error message"
await createNotification(page, {
severity: 'error',
message: 'Error message'
});
// Create an alert notification with the message "Alert message"
await createNotification(page, {
severity: 'alert',
message: 'Alert message'
});
// Verify that there is a button with aria-label "Review 2 Notifications"
expect(await page.locator('button[aria-label="Review 2 Notifications"]').count()).toBe(1);
// Click on button with aria-label "Review 2 Notifications"
await page.click('button[aria-label="Review 2 Notifications"]');
// Click on button with aria-label="Dismiss notification of Error message"
await page.click('button[aria-label="Dismiss notification of Error message"]');
// Verify there is no a notification (listitem) with the text "Error message" since it was dismissed
expect(await page.locator('div[role="dialog"] div[role="listitem"]').innerText()).not.toContain('Error message');
// Verify there is still a notification (listitem) with the text "Alert message"
expect(await page.locator('div[role="dialog"] div[role="listitem"]').innerText()).toContain('Alert message');
// Click on button with aria-label="Dismiss notification of Alert message"
await page.click('button[aria-label="Dismiss notification of Alert message"]');
// Verify that there is no dialog since the notification overlay was closed automatically after all notifications were dismissed
expect(await page.locator('div[role="dialog"]').count()).toBe(0);
});
});
test.describe('Notification Overlay', () => {
test('Closing notification list after notification banner disappeared does not cause it to open automatically', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6130'
});
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
// Create a new Display Layout object
await createDomainObjectWithDefaults(page, { type: 'Display Layout' });
// Click on the button "Review 1 Notification"
await page.click('button[aria-label="Review 1 Notification"]');
// Verify that Notification List is open
expect(await page.locator('div[role="dialog"]').isVisible()).toBe(true);
// Wait until there is no Notification Banner
await page.waitForSelector('div[role="alert"]', { state: 'detached'});
// Click on the "Close" button of the Notification List
await page.click('button[aria-label="Close"]');
// On the Display Layout object, click on the "Edit" button
await page.click('button[title="Edit"]');
// Click on the "Save" button
await page.click('button[title="Save"]');
// Click on the "Save and Finish Editing" option
await page.click('li[title="Save and Finish Editing"]');
// Verify that Notification List is NOT open
expect(await page.locator('div[role="dialog"]').isVisible()).toBe(false);
});
});

View File

@ -0,0 +1,87 @@
/*****************************************************************************
* 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.
*****************************************************************************/
const { test, expect } = require('../../../pluginFixtures');
const { createPlanFromJSON } = require('../../../appActions');
const testPlan = {
"TEST_GROUP": [
{
"name": "Past event 1",
"start": 1660320408000,
"end": 1660343797000,
"type": "TEST-GROUP",
"color": "orange",
"textColor": "white"
},
{
"name": "Past event 2",
"start": 1660406808000,
"end": 1660429160000,
"type": "TEST-GROUP",
"color": "orange",
"textColor": "white"
},
{
"name": "Past event 3",
"start": 1660493208000,
"end": 1660503981000,
"type": "TEST-GROUP",
"color": "orange",
"textColor": "white"
},
{
"name": "Past event 4",
"start": 1660579608000,
"end": 1660624108000,
"type": "TEST-GROUP",
"color": "orange",
"textColor": "white"
},
{
"name": "Past event 5",
"start": 1660666008000,
"end": 1660681529000,
"type": "TEST-GROUP",
"color": "orange",
"textColor": "white"
}
]
};
test.describe("Plan", () => {
test("Create a Plan and display all plan events @unstable", async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
const plan = await createPlanFromJSON(page, {
name: 'Test Plan',
json: testPlan
});
const startBound = testPlan.TEST_GROUP[0].start;
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
// Switch to fixed time mode with all plan events within the bounds
await page.goto(`${plan.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=plan.view`);
const eventCount = await page.locator('.activity-bounds').count();
expect(eventCount).toEqual(testPlan.TEST_GROUP.length);
});
});

View File

@ -0,0 +1,181 @@
/*****************************************************************************
* 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.
*****************************************************************************/
const { test, expect } = require('../../../pluginFixtures');
const { createDomainObjectWithDefaults, createPlanFromJSON } = require('../../../appActions');
const testPlan = {
"TEST_GROUP": [
{
"name": "Past event 1",
"start": 1660320408000,
"end": 1660343797000,
"type": "TEST-GROUP",
"color": "orange",
"textColor": "white"
},
{
"name": "Past event 2",
"start": 1660406808000,
"end": 1660429160000,
"type": "TEST-GROUP",
"color": "orange",
"textColor": "white"
},
{
"name": "Past event 3",
"start": 1660493208000,
"end": 1660503981000,
"type": "TEST-GROUP",
"color": "orange",
"textColor": "white"
},
{
"name": "Past event 4",
"start": 1660579608000,
"end": 1660624108000,
"type": "TEST-GROUP",
"color": "orange",
"textColor": "white"
},
{
"name": "Past event 5",
"start": 1660666008000,
"end": 1660681529000,
"type": "TEST-GROUP",
"color": "orange",
"textColor": "white"
}
]
};
test.describe("Time Strip", () => {
test("Create two Time Strips, add a single Plan to both, and verify they can have separate Indepdenent Time Contexts @unstable", async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5627'
});
// Constant locators
const independentTimeConductorInputs = page.locator('.l-shell__main-independent-time-conductor .c-input--datetime');
const activityBounds = page.locator('.activity-bounds');
// Goto baseURL
await page.goto('./', { waitUntil: 'networkidle' });
const timestrip = await test.step("Create a Time Strip", async () => {
const createdTimeStrip = await createDomainObjectWithDefaults(page, { type: 'Time Strip' });
const objectName = await page.locator('.l-browse-bar__object-name').innerText();
expect(objectName).toBe(createdTimeStrip.name);
return createdTimeStrip;
});
const plan = await test.step("Create a Plan and add it to the timestrip", async () => {
const createdPlan = await createPlanFromJSON(page, {
name: 'Test Plan',
json: testPlan
});
await page.goto(timestrip.url);
// Expand the tree to show the plan
await page.click("button[title='Show selected item in tree']");
await page.dragAndDrop(`role=treeitem[name=/${createdPlan.name}/]`, '.c-object-view');
await page.click("button[title='Save']");
await page.click("li[title='Save and Finish Editing']");
const startBound = testPlan.TEST_GROUP[0].start;
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
// Switch to fixed time mode with all plan events within the bounds
await page.goto(`${timestrip.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=time-strip.view`);
// Verify all events are displayed
const eventCount = await page.locator('.activity-bounds').count();
expect(eventCount).toEqual(testPlan.TEST_GROUP.length);
return createdPlan;
});
await test.step("TimeStrip can use the Independent Time Conductor", async () => {
// Activate Independent Time Conductor in Fixed Time Mode
await page.click('.c-toggle-switch__slider');
expect(await activityBounds.count()).toEqual(0);
// Set the independent time bounds so that only one event is shown
const startBound = testPlan.TEST_GROUP[0].start;
const endBound = testPlan.TEST_GROUP[0].end;
const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
await independentTimeConductorInputs.nth(0).fill('');
await independentTimeConductorInputs.nth(0).fill(startBoundString);
await page.keyboard.press('Enter');
await independentTimeConductorInputs.nth(1).fill('');
await independentTimeConductorInputs.nth(1).fill(endBoundString);
await page.keyboard.press('Enter');
expect(await activityBounds.count()).toEqual(1);
});
await test.step("Can have multiple TimeStrips with the same plan linked and different Independent Time Contexts", async () => {
// Create another Time Strip and verify that it has been created
const createdTimeStrip = await createDomainObjectWithDefaults(page, {
type: 'Time Strip',
name: "Another Time Strip"
});
const objectName = await page.locator('.l-browse-bar__object-name').innerText();
expect(objectName).toBe(createdTimeStrip.name);
// Drag the existing Plan onto the newly created Time Strip, and save.
await page.dragAndDrop(`role=treeitem[name=/${plan.name}/]`, '.c-object-view');
await page.click("button[title='Save']");
await page.click("li[title='Save and Finish Editing']");
// Activate Independent Time Conductor in Fixed Time Mode
await page.click('.c-toggle-switch__slider');
// All events should be displayed at this point because the
// initial independent context bounds will match the global bounds
expect(await activityBounds.count()).toEqual(5);
// Set the independent time bounds so that two events are shown
const startBound = testPlan.TEST_GROUP[0].start;
const endBound = testPlan.TEST_GROUP[1].end;
const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
await independentTimeConductorInputs.nth(0).fill('');
await independentTimeConductorInputs.nth(0).fill(startBoundString);
await page.keyboard.press('Enter');
await independentTimeConductorInputs.nth(1).fill('');
await independentTimeConductorInputs.nth(1).fill(endBoundString);
await page.keyboard.press('Enter');
// Verify that two events are displayed
expect(await activityBounds.count()).toEqual(2);
// Switch to the previous Time Strip and verify that only one event is displayed
await page.goto(timestrip.url);
expect(await activityBounds.count()).toEqual(1);
});
});
});

View File

@ -50,7 +50,7 @@ test.describe('Clock Generator CRUD Operations', () => {
await page.locator('.icon-arrow-down').click(); await page.locator('.icon-arrow-down').click();
// Verify clicking on the autocomplete arrow collapses the dropdown // Verify clicking on the autocomplete arrow collapses the dropdown
await expect(page.locator(".c-input--autocomplete__options")).not.toBeVisible(); await expect(page.locator(".c-input--autocomplete__options")).toBeHidden();
// Click timezone input to open dropdown // Click timezone input to open dropdown
await page.locator('.c-input--autocomplete__input').click(); await page.locator('.c-input--autocomplete__input').click();
@ -60,7 +60,7 @@ test.describe('Clock Generator CRUD Operations', () => {
// Verify clicking outside the autocomplete dropdown collapses it // Verify clicking outside the autocomplete dropdown collapses it
await page.locator('text=Timezone').click(); await page.locator('text=Timezone').click();
// Verify clicking on the autocomplete arrow collapses the dropdown // Verify clicking on the autocomplete arrow collapses the dropdown
await expect(page.locator(".c-input--autocomplete__options")).not.toBeVisible(); await expect(page.locator(".c-input--autocomplete__options")).toBeHidden();
}); });
}); });

View File

@ -27,6 +27,7 @@ demonstrate some playwright for test developers. This pattern should not be re-u
*/ */
const { test, expect } = require('../../../../pluginFixtures.js'); const { test, expect } = require('../../../../pluginFixtures.js');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
let conditionSetUrl; let conditionSetUrl;
let getConditionSetIdentifierFromUrl; let getConditionSetIdentifierFromUrl;
@ -39,11 +40,11 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
await page.click('button:has-text("Create")'); await page.click('button:has-text("Create")');
await page.locator('li:has-text("Condition Set")').click(); await page.locator('li[role="menuitem"]:has-text("Condition Set")').click();
await Promise.all([ await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),
page.click('text=OK') page.click('button:has-text("OK")')
]); ]);
//Save localStorage for future test execution //Save localStorage for future test execution
@ -97,8 +98,8 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
await page.locator('text=Conditions View Snapshot >> button').nth(3).click(); await page.locator('text=Conditions View Snapshot >> button').nth(3).click();
//Edit Condition Set Name from main view //Edit Condition Set Name from main view
await page.locator('text=Unnamed Condition Set').first().fill('Renamed Condition Set'); await page.locator('.l-browse-bar__object-name').filter({ hasText: 'Unnamed Condition Set' }).first().fill('Renamed Condition Set');
await page.locator('text=Renamed Condition Set').first().press('Enter'); await page.locator('.l-browse-bar__object-name').filter({ hasText: 'Renamed Condition Set' }).first().press('Enter');
// Click Save Button // Click Save Button
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
// Click Save and Finish Editing Option // Click Save and Finish Editing Option
@ -162,9 +163,9 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
// Click hamburger button // Click hamburger button
await page.locator('[title="More options"]').click(); await page.locator('[title="More options"]').click();
// Click text=Remove // Click 'Remove' and press OK
await page.locator('text=Remove').click(); await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('text=OK').click(); await page.locator('button:has-text("OK")').click();
//Expect Unnamed Condition Set to be removed in Main View //Expect Unnamed Condition Set to be removed in Main View
const numberOfConditionSetsAtEnd = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count(); const numberOfConditionSetsAtEnd = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count();
@ -178,3 +179,71 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
}); });
}); });
test.describe('Basic Condition Set Use', () => {
test.beforeEach(async ({ page }) => {
// Open a browser, navigate to the main page, and wait until all network events to resolve
await page.goto('./', { waitUntil: 'networkidle' });
});
test('Can add a condition', async ({ page }) => {
// Create a new condition set
await createDomainObjectWithDefaults(page, {
type: 'Condition Set',
name: "Test Condition Set"
});
// 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();
expect(numOfUnnamedConditions).toEqual(1);
});
test('ConditionSet should display appropriate view options', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5924'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: "Alpha Sine Wave Generator"
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: "Beta Sine Wave Generator"
});
const conditionSet1 = await createDomainObjectWithDefaults(page, {
type: 'Condition Set',
name: "Test Condition Set"
});
// 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', {
name: 'Main Tree'
});
const alphaGeneratorTreeItem = treePane.getByRole('treeitem', { name: "Alpha Sine Wave Generator"});
const betaGeneratorTreeItem = treePane.getByRole('treeitem', { name: "Beta Sine Wave Generator"});
const conditionCollection = page.locator('#conditionCollection');
await alphaGeneratorTreeItem.dragTo(conditionCollection);
await betaGeneratorTreeItem.dragTo(conditionCollection);
const saveButtonLocator = page.locator('button[title="Save"]');
await saveButtonLocator.click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.click('button[title="Change the current view"]');
await expect(page.getByRole('menuitem', { name: /Lad Table/ })).toBeHidden();
await expect(page.getByRole('menuitem', { name: /Conditions View/ })).toBeVisible();
await expect(page.getByRole('menuitem', { name: /Plot/ })).toBeVisible();
await expect(page.getByRole('menuitem', { name: /Telemetry Table/ })).toBeVisible();
});
});

View File

@ -0,0 +1,217 @@
/*****************************************************************************
* 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.
*****************************************************************************/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions');
test.describe('Display Layout', () => {
/** @type {import('../../../../appActions').CreatedObjectInfo} */
let sineWaveObject;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await setRealTimeMode(page);
// Create Sine Wave Generator
sineWaveObject = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator'
});
});
test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in real time', async ({ page }) => {
// Create a Display Layout
await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: "Test Display Layout"
});
// Edit Display 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 Display Layout and save changes
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
const layoutGridHolder = page.locator('.l-layout__grid-holder');
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
// Subscribe to the Sine Wave Generator data
// On getting data, check if the value found in the Display Layout is the most recent value
// from the Sine Wave Generator
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
const formattedTelemetryValue = getTelemValuePromise;
const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`);
const displayLayoutValue = await displayLayoutValuePromise.textContent();
const trimmedDisplayValue = displayLayoutValue.trim();
expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
});
test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in fixed time', async ({ page }) => {
// Create a Display Layout
await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: "Test Display Layout"
});
// Edit Display 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 Display Layout and save changes
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
const layoutGridHolder = page.locator('.l-layout__grid-holder');
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
// Subscribe to the Sine Wave Generator data
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
// Set an offset of 1 minute and then change the time mode to fixed to set a 1 minute historical window
await setStartOffset(page, { mins: '1' });
await setFixedTimeMode(page);
// On getting data, check if the value found in the Display Layout is the most recent value
// from the Sine Wave Generator
const formattedTelemetryValue = getTelemValuePromise;
const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`);
const displayLayoutValue = await displayLayoutValuePromise.textContent();
const trimmedDisplayValue = displayLayoutValue.trim();
expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
});
test('items in a display layout can be removed with object tree context menu when viewing the display layout', async ({ page }) => {
// Create a Display Layout
await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: "Test Display Layout"
});
// Edit Display 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 Display Layout and save changes
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
const layoutGridHolder = page.locator('.l-layout__grid-holder');
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);
// Expand the Display Layout so we can remove the sine wave generator
await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
// Bring up context menu and remove
await sineWaveGeneratorTreeItem.nth(1).click({ button: 'right' });
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('button:has-text("OK")').click();
// delete
expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
});
test('items in a display layout can be removed with object tree context menu when viewing another item', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/3117'
});
// Create a Display Layout
const displayLayout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout'
});
// Edit Display 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 Display Layout and save changes
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
const layoutGridHolder = page.locator('.l-layout__grid-holder');
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);
// Expand the Display Layout so we can remove the sine wave generator
await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
// Go to the original Sine Wave Generator to navigate away from the Display Layout
await page.goto(sineWaveObject.url);
// Bring up context menu and remove
await sineWaveGeneratorTreeItem.first().click({ button: 'right' });
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('button:has-text("OK")').click();
// navigate back to the display layout to confirm it has been removed
await page.goto(displayLayout.url);
expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
});
});
/**
* Util for subscribing to a telemetry object by object identifier
* Limitations: Currently only works to return telemetry once to the node scope
* To Do: See if there's a way to await this multiple times to allow for multiple
* values to be returned over time
* @param {import('@playwright/test').Page} page
* @param {string} objectIdentifier identifier for object
* @returns {Promise<string>} the formatted sin telemetry value
*/
async function subscribeToTelemetry(page, objectIdentifier) {
const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getTelemValue', resolve));
await page.evaluate(async (telemetryIdentifier) => {
const telemetryObject = await window.openmct.objects.get(telemetryIdentifier);
const metadata = window.openmct.telemetry.getMetadata(telemetryObject);
const formats = await window.openmct.telemetry.getFormatMap(metadata);
window.openmct.telemetry.subscribe(telemetryObject, (obj) => {
const sinVal = obj.sin;
const formattedSinVal = formats.sin.format(sinVal);
window.getTelemValue(formattedSinVal);
});
}, objectIdentifier);
return getTelemValuePromise;
}

View File

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

View File

@ -0,0 +1,153 @@
/*****************************************************************************
* 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.
*****************************************************************************/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
test.describe('Flexible Layout', () => {
let sineWaveObject;
let clockObject;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
// Create Sine Wave Generator
sineWaveObject = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator'
});
// Create Clock Object
clockObject = await createDomainObjectWithDefaults(page, {
type: 'Clock'
});
});
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'
});
// 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').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'));
// Check that panes can be dragged while Flexible Layout is in Edit mode
let dragWrapper = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
await expect(dragWrapper).toHaveAttribute('draggable', 'true');
// Save Flexible Layout
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
// Check that panes are not draggable while Flexible Layout is in Browse mode
dragWrapper = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
await expect(dragWrapper).toHaveAttribute('draggable', 'false');
});
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'
});
// 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').first().click();
// Add the Sine Wave Generator to the Flexible Layout and save changes
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1);
// Expand the Flexible Layout so we can remove the sine wave generator
await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
// Bring up context menu and remove
await sineWaveGeneratorTreeItem.first().click({ button: 'right' });
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('button:has-text("OK")').click();
// Verify that the item has been removed from the layout
expect(await page.locator('.c-fl-container__frame').count()).toEqual(0);
});
test('items in a flexible layout can be removed with object tree context menu when viewing another item', async ({ page }) => {
test.info().annotations.push({
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'
});
// 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
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1);
// Expand the Flexible Layout so we can remove the sine wave generator
await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
// Go to the original Sine Wave Generator to navigate away from the Flexible Layout
await page.goto(sineWaveObject.url);
// Bring up context menu and remove
await sineWaveGeneratorTreeItem.first().click({ button: 'right' });
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('button:has-text("OK")').click();
// navigate back to the display layout to confirm it has been removed
await page.goto(flexibleLayout.url);
// Verify that the item has been removed from the layout
expect(await page.locator('.c-fl-container__frame').count()).toEqual(0);
});
});

View File

@ -0,0 +1,124 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
* This test suite is dedicated to testing the Gauge component.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const uuid = require('uuid').v4;
test.describe('Gauge', () => {
test.beforeEach(async ({ page }) => {
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'networkidle' });
});
test('Can add and remove telemetry sources @unstable', async ({ page }) => {
// Create the gauge with defaults
const gauge = await createDomainObjectWithDefaults(page, { type: 'Gauge' });
const editButtonLocator = page.locator('button[title="Edit"]');
const saveButtonLocator = page.locator('button[title="Save"]');
// Create a sine wave generator within the gauge
const swg1 = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: `swg-${uuid()}`,
parent: gauge.uuid
});
// Navigate to the gauge and verify that
// the SWG appears in the elements pool
await page.goto(gauge.url);
await editButtonLocator.click();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible();
await saveButtonLocator.click();
await page.locator('li[title="Save and Finish Editing"]').click();
// Create another sine wave generator within the gauge
const swg2 = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: `swg-${uuid()}`,
parent: gauge.uuid
});
// Verify that the 'Replace telemetry source' modal appears and accept it
await expect.soft(page.locator('text=This action will replace the current telemetry source. Do you want to continue?')).toBeVisible();
await page.click('text=Ok');
// Navigate to the gauge and verify that the new SWG
// appears in the elements pool and the old one is gone
await page.goto(gauge.url);
await editButtonLocator.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 saveButtonLocator.click();
// Right click on the new SWG in the elements pool and delete it
await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({
button: 'right'
});
await page.locator('li[title="Remove this object from its containing object."]').click();
// Verify that the 'Remove object' confirmation modal appears and accept it
await expect.soft(page.locator('text=Warning! This action will remove this object. Are you sure you want to continue?')).toBeVisible();
await page.click('text=Ok');
// Verify that the elements pool shows no elements
await expect(page.locator('text="No contained elements"')).toBeVisible();
});
test('Can create a non-default Gauge', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5356'
});
//Click the Create button
await page.click('button:has-text("Create")');
// Click the object specified by 'type'
await page.click(`li[role='menuitem']:text("Gauge")`);
// FIXME: We need better selectors for these custom form controls
const displayCurrentValueSwitch = page.locator('.c-toggle-switch__slider >> nth=0');
await displayCurrentValueSwitch.setChecked(false);
await page.click('button[aria-label="Save"]');
// TODO: Verify changes in the UI
});
test('Can edit a single Gauge-specific property', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5985'
});
// Create the gauge with defaults
await createDomainObjectWithDefaults(page, { type: 'Gauge' });
await page.click('button[title="More options"]');
await page.click('li[role="menuitem"]:has-text("Edit Properties")');
// FIXME: We need better selectors for these custom form controls
const displayCurrentValueSwitch = page.locator('.c-toggle-switch__slider >> nth=0');
await displayCurrentValueSwitch.setChecked(false);
await page.click('button[aria-label="Save"]');
// TODO: Verify changes in the UI
});
});

View File

@ -25,67 +25,39 @@ This test suite is dedicated to tests which verify the basic operations surround
but only assume that example imagery is present. but only assume that example imagery is present.
*/ */
/* globals process */ /* globals process */
const { waitForAnimations } = require('../../../../baseFixtures'); const { waitForAnimations } = require('../../../../baseFixtures');
const { test, expect } = require('../../../../pluginFixtures'); const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const backgroundImageSelector = '.c-imagery__main-image__background-image'; const backgroundImageSelector = '.c-imagery__main-image__background-image';
const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt']; const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan'; const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
const thumbnailUrlParamsRegexp = /\?w=100&h=100/;
//The following block of tests verifies the basic functionality of example imagery and serves as a template for Imagery objects embedded in other objects. //The following block of tests verifies the basic functionality of example imagery and serves as a template for Imagery objects embedded in other objects.
test.describe('Example Imagery Object', () => { test.describe('Example Imagery Object', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
//Go to baseURL //Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
//Click the Create button // Create a default 'Example Imagery' object
await page.click('button:has-text("Create")'); const exampleImagery = await createDomainObjectWithDefaults(page, { type: 'Example Imagery' });
// Click text=Example Imagery // Verify that the created object is focused
await page.click('text=Example Imagery'); await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name);
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('text=OK'),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
// Close Banner
await page.locator('.c-message-banner__close-button').click();
//Wait until Save Banner is gone
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
await page.locator(backgroundImageSelector).hover({trial: true}); await page.locator(backgroundImageSelector).hover({trial: true});
}); });
test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => { test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
const deltaYStep = 100; //equivalent to 1x zoom // Zoom in x2 and assert
await page.locator(backgroundImageSelector).hover({trial: true}); await mouseZoomOnImageAndAssert(page, 2);
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
// zoom in
await page.locator(backgroundImageSelector).hover({trial: true});
await page.mouse.wheel(0, deltaYStep * 2);
// wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true});
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
// zoom out
await page.locator(backgroundImageSelector).hover({trial: true});
await page.mouse.wheel(0, -deltaYStep);
// wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true});
const imageMouseZoomedOut = await page.locator(backgroundImageSelector).boundingBox();
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height); // Zoom out x2 and assert
expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width); await mouseZoomOnImageAndAssert(page, -2);
expect(imageMouseZoomedOut.height).toBeLessThan(imageMouseZoomedIn.height);
expect(imageMouseZoomedOut.width).toBeLessThan(imageMouseZoomedIn.width);
}); });
test('Can adjust image brightness/contrast by dragging the sliders', async ({ page, browserName }) => { test('Can adjust image brightness/contrast by dragging the sliders', async ({ page, browserName }) => {
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox'); // eslint-disable-next-line playwright/no-skipped-test
test.skip(browserName === 'firefox', 'This test needs to be updated to work with firefox');
// Open the image filter menu // Open the image filter menu
await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click(); await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click();
@ -158,30 +130,7 @@ test.describe('Example Imagery Object', () => {
}); });
test('Can use + - buttons to zoom on the image @unstable', async ({ page }) => { test('Can use + - buttons to zoom on the image @unstable', async ({ page }) => {
// Get initial image dimensions await buttonZoomOnImageAndAssert(page);
const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
// Zoom in twice via button
await zoomIntoImageryByButton(page);
await zoomIntoImageryByButton(page);
// Get and assert zoomed in image dimensions
const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
// Zoom out once via button
await zoomOutOfImageryByButton(page);
// Get and assert zoomed out image dimensions
const zoomedOutBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
// Zoom out again via button, assert against the initial image dimensions
await zoomOutOfImageryByButton(page);
const finalBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
expect(finalBoundingBox).toEqual(initialBoundingBox);
}); });
test('Can use the reset button to reset the image @unstable', async ({ page }, testInfo) => { test('Can use the reset button to reset the image @unstable', async ({ page }, testInfo) => {
@ -208,7 +157,7 @@ test.describe('Example Imagery Object', () => {
const pausePlayButton = page.locator('.c-button.pause-play'); const pausePlayButton = page.locator('.c-button.pause-play');
// open the time conductor drop down // open the time conductor drop down
await page.locator('button:has-text("Fixed Timespan")').click(); await page.locator('.c-mode-button').click();
// Click local clock // Click local clock
await page.locator('[data-testid="conductor-modeOption-realtime"]').click(); await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
@ -219,28 +168,27 @@ test.describe('Example Imagery Object', () => {
await expect(pausePlayButton).not.toHaveClass(/is-paused/); await expect(pausePlayButton).not.toHaveClass(/is-paused/);
}); });
test('Uses low fetch priority', async ({ page }) => {
const priority = await page.locator('.js-imageryView-image').getAttribute('fetchpriority');
await expect(priority).toBe('low');
});
}); });
// The following test case will cover these scenarios test.describe('Example Imagery in Display Layout', () => {
// ('Can use Mouse Wheel to zoom in and out of previous image'); let displayLayout;
// ('Can use alt+drag to move around image once zoomed in'); test.beforeEach(async ({ page }) => {
// ('Clicking on the left arrow should pause the imagery and go to previous image');
// ('If the imagery view is in pause mode, it should not be updated when new images come in');
// ('If the imagery view is not in pause mode, it should be updated when new images come in');
test('Example Imagery in Display layout @unstable', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5265'
});
// Go to baseURL // Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
displayLayout = await createDomainObjectWithDefaults(page, { type: 'Display Layout' });
await page.goto(displayLayout.url);
/* Create Sine Wave Generator with minimum Image Load Delay */
// Click the Create button // Click the Create button
await page.click('button:has-text("Create")'); await page.click('button:has-text("Create")');
// Click text=Example Imagery // Click text=Example Imagery
await page.click('text=Example Imagery'); await page.click('li[role="menuitem"]:has-text("Example Imagery")');
// Clear and set Image load delay to minimum value // Clear and set Image load delay to minimum value
await page.locator('input[type="number"]').fill(''); await page.locator('input[type="number"]').fill('');
@ -249,15 +197,264 @@ test('Example Imagery in Display layout @unstable', async ({ page }) => {
// Click text=OK // Click text=OK
await Promise.all([ await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}), page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('text=OK'), page.click('button:has-text("OK")'),
//Wait for Save Banner to appear //Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message') page.waitForSelector('.c-message-banner__message')
]); ]);
// Wait until Save Banner is gone
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery'); await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
await page.locator(backgroundImageSelector).hover({trial: true});
await page.goto(displayLayout.url);
});
test('View Large action pauses imagery when in realtime and returns to realtime', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/3647'
});
// Click time conductor mode button
await page.locator('.c-mode-button').click();
// set realtime mode
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
// pause/play button
const pausePlayButton = await page.locator('.c-button.pause-play');
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
// Open context menu and click view large menu item
await page.locator('button[title="View menu items"]').click();
await page.locator('li[title="View Large"]').click();
await expect(pausePlayButton).toHaveClass(/is-paused/);
await page.locator('[aria-label="Close"]').click();
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
});
test('View Large action leaves keeps realtime mode paused', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/3647'
});
// Click time conductor mode button
await page.locator('.c-mode-button').click();
// set realtime mode
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
// pause/play button
const pausePlayButton = await page.locator('.c-button.pause-play');
await pausePlayButton.click();
await expect.soft(pausePlayButton).toHaveClass(/is-paused/);
// Open context menu and click view large menu item
await page.locator('button[title="View menu items"]').click();
await page.locator('li[title="View Large"]').click();
await expect(pausePlayButton).toHaveClass(/is-paused/);
await page.locator('[aria-label="Close"]').click();
await expect.soft(pausePlayButton).toHaveClass(/is-paused/);
});
test('Imagery View operations @unstable', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5265'
});
// Edit mode
await page.click('button[title="Edit"]');
// Click on example imagery to expose toolbar
await page.locator('.c-so-view__header').click();
// Adjust object height
await page.locator('div[title="Resize object height"] > input').click();
await page.locator('div[title="Resize object height"] > input').fill('50');
// Adjust object width
await page.locator('div[title="Resize object width"] > input').click();
await page.locator('div[title="Resize object width"] > input').fill('50');
await performImageryViewOperationsAndAssert(page);
});
test('Resizing the layout changes thumbnail visibility and size', async ({ page }) => {
const thumbsWrapperLocator = page.locator('.c-imagery__thumbs-wrapper');
// Edit mode
await page.click('button[title="Edit"]');
// Click on example imagery to expose toolbar
await page.locator('.c-so-view__header').click();
// expect thumbnails not be visible when first added
expect.soft(thumbsWrapperLocator.isHidden()).toBeTruthy();
// Resize the example imagery vertically to change the thumbnail visibility
/*
The following arbitrary values are added to observe the separate visual
conditions of the thumbnails (hidden, small thumbnails, regular thumbnails).
Specifically, height is set to 50px for small thumbs and 100px for regular
*/
await page.locator('div[title="Resize object height"] > input').click();
await page.locator('div[title="Resize object height"] > input').fill('50');
expect(thumbsWrapperLocator.isVisible()).toBeTruthy();
await expect(thumbsWrapperLocator).toHaveClass(/is-small-thumbs/);
// Resize the example imagery vertically to change the thumbnail visibility
await page.locator('div[title="Resize object height"] > input').click();
await page.locator('div[title="Resize object height"] > input').fill('100');
expect(thumbsWrapperLocator.isVisible()).toBeTruthy();
await expect(thumbsWrapperLocator).not.toHaveClass(/is-small-thumbs/);
});
});
test.describe('Example Imagery in Flexible layout', () => {
let flexibleLayout;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
flexibleLayout = await createDomainObjectWithDefaults(page, { type: 'Flexible Layout' });
await page.goto(flexibleLayout.url);
/* Create Sine Wave Generator with minimum Image Load Delay */
// Click the Create button
await page.click('button:has-text("Create")');
// Click text=Example Imagery
await page.click('li[role="menuitem"]:has-text("Example Imagery")');
// Clear and set Image load delay to minimum value
await page.locator('input[type="number"]').fill('');
await page.locator('input[type="number"]').fill('5000');
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('button:has-text("OK")'),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
await page.goto(flexibleLayout.url);
});
test('Imagery View operations @unstable', async ({ page, browserName }) => {
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5326'
});
await performImageryViewOperationsAndAssert(page);
});
});
test.describe('Example Imagery in Tabs View', () => {
let tabsView;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
tabsView = await createDomainObjectWithDefaults(page, { type: 'Tabs View' });
await page.goto(tabsView.url);
/* Create Sine Wave Generator with minimum Image Load Delay */
// Click the Create button
await page.click('button:has-text("Create")');
// Click text=Example Imagery
await page.click('li[role="menuitem"]:has-text("Example Imagery")');
// Clear and set Image load delay to minimum value
await page.locator('input[type="number"]').fill('');
await page.locator('input[type="number"]').fill('5000');
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('button:has-text("OK")'),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
await page.goto(tabsView.url);
});
test('Imagery View operations @unstable', async ({ page }) => {
await performImageryViewOperationsAndAssert(page);
});
});
test.describe('Example Imagery in Time Strip', () => {
let timeStripObject;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
timeStripObject = await createDomainObjectWithDefaults(page, {
type: 'Time Strip'
});
await createDomainObjectWithDefaults(page, {
type: 'Example Imagery',
parent: timeStripObject.uuid
});
// Navigate to timestrip
await page.goto(timeStripObject.url);
});
test('Clicking a thumbnail loads the image in large view', async ({ page, browserName }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5632'
});
// Hover over the timestrip to reveal a thumbnail image
await page.locator('.c-imagery-tsv-container').hover();
// Get the img src of the hovered image thumbnail
const hoveredThumbnailImg = page.locator('.c-imagery-tsv div.c-imagery-tsv__image-wrapper:hover img');
const hoveredThumbnailImgSrc = await hoveredThumbnailImg.getAttribute('src');
// Verify that imagery timestrip view uses the thumbnailUrl as img src for thumbnails
expect(hoveredThumbnailImgSrc).toBeTruthy();
expect(hoveredThumbnailImgSrc).toMatch(thumbnailUrlParamsRegexp);
// Click on the hovered thumbnail to open "View Large" view
await page.locator('.c-imagery-tsv-container').click();
// Get the img src of the large view image
const viewLargeImg = page.locator('img.c-imagery__main-image__image');
const viewLargeImgSrc = await viewLargeImg.getAttribute('src');
expect(viewLargeImgSrc).toBeTruthy();
// Verify that the image in the large view is the same as the hovered thumbnail
expect(viewLargeImgSrc).toEqual(hoveredThumbnailImgSrc.split('?')[0]);
});
});
/**
* Perform the common actions and assertions for the Imagery View.
* This function verifies the following in order:
* 1. Can zoom in/out using the zoom buttons
* 2. Can zoom in/out using the mouse wheel
* 3. Can pan the image using the pan hotkey + mouse drag
* 4. Clicking on the left arrow button pauses imagery and moves to the previous image
* 5. Imagery is updated as new images stream in, regardless of pause status
* 6. Old images are discarded when new images stream in
* 7. Image brightness/contrast can be adjusted by dragging the sliders
* @param {import('@playwright/test').Page} page
*/
async function performImageryViewOperationsAndAssert(page) {
// Verify that imagery thumbnails use a thumbnail url
const thumbnailImages = page.locator('.c-thumb__image');
const mainImage = page.locator('.c-imagery__main-image__image');
await expect(thumbnailImages.first()).toHaveAttribute('src', thumbnailUrlParamsRegexp);
await expect(mainImage).not.toHaveAttribute('src', thumbnailUrlParamsRegexp);
// Click previous image button // Click previous image button
const previousImageButton = page.locator('.c-nav--prev'); const previousImageButton = page.locator('.c-nav--prev');
@ -267,27 +464,17 @@ test('Example Imagery in Display layout @unstable', async ({ page }) => {
const selectedImage = page.locator('.selected'); const selectedImage = page.locator('.selected');
await expect(selectedImage).toBeVisible(); await expect(selectedImage).toBeVisible();
// Zoom in // Use the zoom buttons to zoom in and out
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox(); await buttonZoomOnImageAndAssert(page);
await page.locator(backgroundImageSelector).hover({trial: true});
const deltaYStep = 100; // equivalent to 1x zoom
await page.mouse.wheel(0, deltaYStep * 2);
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
// Wait for zoom animation to finish // Use Mouse Wheel to zoom in to previous image
await page.locator(backgroundImageSelector).hover({trial: true}); await mouseZoomOnImageAndAssert(page, 2);
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
// Center the mouse pointer // Use alt+drag to move around image once zoomed in
await page.mouse.move(imageCenterX, imageCenterY); await panZoomAndAssertImageProperties(page);
// Pan Imagery Hints // Use Mouse Wheel to zoom out of previous image
const imageryHintsText = await page.locator('.c-imagery__hints').innerText(); await mouseZoomOnImageAndAssert(page, -2);
expect(expectedAltText).toEqual(imageryHintsText);
// Click next image button // Click next image button
const nextImageButton = page.locator('.c-nav--next'); const nextImageButton = page.locator('.c-nav--next');
@ -300,21 +487,14 @@ test('Example Imagery in Display layout @unstable', async ({ page }) => {
await page.locator('[data-testid=conductor-modeOption-realtime]').click(); await page.locator('[data-testid=conductor-modeOption-realtime]').click();
// Zoom in on next image // Zoom in on next image
await page.locator(backgroundImageSelector).hover({trial: true}); await mouseZoomOnImageAndAssert(page, 2);
await page.mouse.wheel(0, deltaYStep * 2);
// Wait for zoom animation to finish // Clicking on the left arrow should pause the imagery and go to previous image
await page.locator(backgroundImageSelector).hover({trial: true});
const imageNextMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
expect(imageNextMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
expect(imageNextMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
// Click previous image button
await previousImageButton.click(); await previousImageButton.click();
await expect(page.locator('.c-button.pause-play')).toHaveClass(/is-paused/);
// Verify previous image
await expect(selectedImage).toBeVisible(); await expect(selectedImage).toBeVisible();
// The imagery view should be updated when new images come in
const imageCount = await page.locator('.c-imagery__thumb').count(); const imageCount = await page.locator('.c-imagery__thumb').count();
await expect.poll(async () => { await expect.poll(async () => {
const newImageCount = await page.locator('.c-imagery__thumb').count(); const newImageCount = await page.locator('.c-imagery__thumb').count();
@ -322,7 +502,7 @@ test('Example Imagery in Display layout @unstable', async ({ page }) => {
return newImageCount; return newImageCount;
}, { }, {
message: "verify that old images are discarded", message: "verify that old images are discarded",
timeout: 6 * 1000 timeout: 7 * 1000
}).toBe(imageCount); }).toBe(imageCount);
// Verify selected image is still displayed // Verify selected image is still displayed
@ -340,253 +520,6 @@ test('Example Imagery in Display layout @unstable', async ({ page }) => {
// Drag the brightness and contrast sliders around and assert filter values // Drag the brightness and contrast sliders around and assert filter values
await dragBrightnessSliderAndAssertFilterValues(page); await dragBrightnessSliderAndAssertFilterValues(page);
await dragContrastSliderAndAssertFilterValues(page); await dragContrastSliderAndAssertFilterValues(page);
});
test.describe('Example imagery thumbnails resize in display layouts', () => {
test('Resizing the layout changes thumbnail visibility and size', async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
const thumbsWrapperLocator = page.locator('.c-imagery__thumbs-wrapper');
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Display Layout")
await page.locator('li:has-text("Display Layout")').click();
const displayLayoutTitleField = page.locator('text=Properties Title Notes Horizontal grid (px) Vertical grid (px) Horizontal size ( >> input[type="text"]');
await displayLayoutTitleField.click();
await displayLayoutTitleField.fill('Thumbnail Display Layout');
// Click text=OK
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click()
]);
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
// Click text=Save and Finish Editing
await page.locator('text=Save and Finish Editing').click();
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Example Imagery")
await page.locator('li:has-text("Example Imagery")').click();
const imageryTitleField = page.locator('text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]');
// Click text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]
await imageryTitleField.click();
// Fill text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]
await imageryTitleField.fill('Thumbnail Example Imagery');
// Click text=OK
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click()
]);
// Click text=Thumbnail Example Imagery Imagery Layout Snapshot >> button >> nth=0
await Promise.all([
page.waitForNavigation(),
page.locator('text=Thumbnail Example Imagery Imagery Layout Snapshot >> button').first().click()
]);
// Edit mode
await page.locator('text=Thumbnail Display Layout Snapshot >> button').nth(3).click();
// Click on example imagery to expose toolbar
await page.locator('text=Thumbnail Example Imagery Snapshot Large View').click();
// expect thumbnails not be visible when first added
expect.soft(thumbsWrapperLocator.isHidden()).toBeTruthy();
// Resize the example imagery vertically to change the thumbnail visibility
/*
The following arbitrary values are added to observe the separate visual
conditions of the thumbnails (hidden, small thumbnails, regular thumbnails).
Specifically, height is set to 50px for small thumbs and 100px for regular
*/
// Click #mct-input-id-103
await page.locator('#mct-input-id-103').click();
// Fill #mct-input-id-103
await page.locator('#mct-input-id-103').fill('50');
expect(thumbsWrapperLocator.isVisible()).toBeTruthy();
await expect(thumbsWrapperLocator).toHaveClass(/is-small-thumbs/);
// Resize the example imagery vertically to change the thumbnail visibility
// Click #mct-input-id-103
await page.locator('#mct-input-id-103').click();
// Fill #mct-input-id-103
await page.locator('#mct-input-id-103').fill('100');
expect(thumbsWrapperLocator.isVisible()).toBeTruthy();
await expect(thumbsWrapperLocator).not.toHaveClass(/is-small-thumbs/);
});
});
// test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
// test.fixme('Can use alt+drag to move around image once zoomed in');
// test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
// test.fixme('If the imagery view is in pause mode, images still come in');
// test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
test.describe('Example Imagery in Flexible layout', () => {
test('Example Imagery in Flexible layout @unstable', async ({ page, browserName, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5326'
});
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
// Click the Create button
await page.click('button:has-text("Create")');
// Click text=Example Imagery
await page.click('text=Example Imagery');
// Clear and set Image load delay (milliseconds)
await page.click('input[type="number"]', {clickCount: 3});
await page.type('input[type="number"]', "20");
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('text=OK'),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
// Wait until Save Banner is gone
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
await page.locator(backgroundImageSelector).hover({trial: true});
// Click the Create button
await page.click('button:has-text("Create")');
// Click text=Flexible Layout
await page.click('text=Flexible Layout');
// Assert Flexible layout
await expect(page.locator('.js-form-title')).toHaveText('Create a New Flexible Layout');
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
// Click My Items
await Promise.all([
page.locator('text=OK').click(),
page.waitForNavigation({waitUntil: 'networkidle'})
]);
// Click My Items
await page.locator('.c-disclosure-triangle').click();
// Right click example imagery
await page.click(('text=Unnamed Example Imagery'), { button: 'right' });
// Click move
await page.locator('.icon-move').click();
// Click triangle to open sub menu
await page.locator('.c-form__section .c-disclosure-triangle').click();
// Click Flexable Layout
await page.click('.c-overlay__outer >> text=Unnamed Flexible Layout');
// Click text=OK
await page.locator('text=OK').click();
// Save template
await saveTemplate(page);
// Zoom in
await mouseZoomIn(page);
// Center the mouse pointer
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
await page.mouse.move(imageCenterX, imageCenterY);
// Pan zoom
await panZoomAndAssertImageProperties(page);
// Click previous image button
const previousImageButton = page.locator('.c-nav--prev');
await previousImageButton.click();
// Verify previous image
const selectedImage = page.locator('.selected');
await expect(selectedImage).toBeVisible();
// Click time conductor mode button
await page.locator('.c-mode-button').click();
// Select local clock mode
await page.locator('[data-testid=conductor-modeOption-realtime]').click();
// Zoom in on next image
await mouseZoomIn(page);
// Click previous image button
await previousImageButton.click();
// Verify previous image
await expect(selectedImage).toBeVisible();
const imageCount = await page.locator('.c-imagery__thumb').count();
await expect.poll(async () => {
const newImageCount = await page.locator('.c-imagery__thumb').count();
return newImageCount;
}, {
message: "verify that old images are discarded",
timeout: 6 * 1000
}).toBe(imageCount);
// Verify selected image is still displayed
await expect(selectedImage).toBeVisible();
// Unpause imagery
await page.locator('.pause-play').click();
//Get background-image url from background-image css prop
await assertBackgroundImageUrlFromBackgroundCss(page);
// Open the image filter menu
await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click();
// Drag the brightness and contrast sliders around and assert filter values
await dragBrightnessSliderAndAssertFilterValues(page);
await dragContrastSliderAndAssertFilterValues(page);
});
});
test.describe('Example Imagery in Tabs view', () => {
test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
test.fixme('Can use alt+drag to move around image once zoomed in');
test.fixme('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
test.fixme('Can zoom into a previous image from thumbstrip in real-time or fixed-time');
test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in');
test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
});
/**
* @param {import('@playwright/test').Page} page
*/
async function saveTemplate(page) {
await page.locator('.c-button--menu.c-button--major.icon-save').click();
await page.locator('text=Save and Finish Editing').click();
} }
/** /**
@ -669,7 +602,7 @@ async function assertBackgroundImageUrlFromBackgroundCss(page) {
return backgroundImageUrl2; return backgroundImageUrl2;
}, { }, {
message: "verify next image has updated", message: "verify next image has updated",
timeout: 6 * 1000 timeout: 7 * 1000
}).not.toBe(backgroundImageUrl1); }).not.toBe(backgroundImageUrl1);
console.log('backgroundImageUrl2 ' + backgroundImageUrl2); console.log('backgroundImageUrl2 ' + backgroundImageUrl2);
} }
@ -723,14 +656,17 @@ async function panZoomAndAssertImageProperties(page) {
} }
/** /**
* Use the mouse wheel to zoom in or out of an image and assert that the image
* has successfully zoomed in or out.
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {number} [factor = 2] The zoom factor. Positive for zoom in, negative for zoom out.
*/ */
async function mouseZoomIn(page) { async function mouseZoomOnImageAndAssert(page, factor = 2) {
// Zoom in // Zoom in
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox(); const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
await page.locator(backgroundImageSelector).hover({trial: true}); await page.locator(backgroundImageSelector).hover({trial: true});
const deltaYStep = 100; // equivalent to 1x zoom const deltaYStep = 100; // equivalent to 1x zoom
await page.mouse.wheel(0, deltaYStep * 2); await page.mouse.wheel(0, deltaYStep * factor);
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
@ -740,9 +676,47 @@ async function mouseZoomIn(page) {
// Wait for zoom animation to finish // Wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true}); await page.locator(backgroundImageSelector).hover({trial: true});
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox(); const imageMouseZoomed = await page.locator(backgroundImageSelector).boundingBox();
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width); if (factor > 0) {
expect(imageMouseZoomed.height).toBeGreaterThan(originalImageDimensions.height);
expect(imageMouseZoomed.width).toBeGreaterThan(originalImageDimensions.width);
} else {
expect(imageMouseZoomed.height).toBeLessThan(originalImageDimensions.height);
expect(imageMouseZoomed.width).toBeLessThan(originalImageDimensions.width);
}
}
/**
* Zoom in and out of the image using the buttons, and assert that the image has
* been successfully zoomed in or out.
* @param {import('@playwright/test').Page} page
*/
async function buttonZoomOnImageAndAssert(page) {
// Get initial image dimensions
const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
// Zoom in twice via button
await zoomIntoImageryByButton(page);
await zoomIntoImageryByButton(page);
// Get and assert zoomed in image dimensions
const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
// Zoom out once via button
await zoomOutOfImageryByButton(page);
// Get and assert zoomed out image dimensions
const zoomedOutBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
// Zoom out again via button, assert against the initial image dimensions
await zoomOutOfImageryByButton(page);
const finalBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
expect(finalBoundingBox).toEqual(initialBoundingBox);
} }
/** /**

View File

@ -0,0 +1,120 @@
/*****************************************************************************
* 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.
*****************************************************************************/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions');
test.describe('Testing LAD table @unstable', () => {
let sineWaveObject;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await setRealTimeMode(page);
// Create Sine Wave Generator
sineWaveObject = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: "Test Sine Wave Generator"
});
});
test('telemetry value exactly matches latest telemetry value received in real time', async ({ page }) => {
// Create LAD table
await createDomainObjectWithDefaults(page, {
type: 'LAD Table',
name: "Test LAD Table"
});
// Edit LAD table
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 LAD table and save changes
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-lad-table-wrapper');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
// Subscribe to the Sine Wave Generator data
// On getting data, check if the value found in the LAD table is the most recent value
// from the Sine Wave Generator
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
const subscribeTelemValue = await getTelemValuePromise;
const ladTableValuePromise = await page.waitForSelector(`text="${subscribeTelemValue}"`);
const ladTableValue = await ladTableValuePromise.textContent();
expect(ladTableValue).toBe(subscribeTelemValue);
});
test('telemetry value exactly matches latest telemetry value received in fixed time', async ({ page }) => {
// Create LAD table
await createDomainObjectWithDefaults(page, {
type: 'LAD Table',
name: "Test LAD Table"
});
// Edit LAD table
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 LAD table and save changes
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-lad-table-wrapper');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
// Subscribe to the Sine Wave Generator data
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
// Set an offset of 1 minute and then change the time mode to fixed to set a 1 minute historical window
await setStartOffset(page, { mins: '1' });
await setFixedTimeMode(page);
// On getting data, check if the value found in the LAD table is the most recent value
// from the Sine Wave Generator
const subscribeTelemValue = await getTelemValuePromise;
const ladTableValuePromise = await page.waitForSelector(`text="${subscribeTelemValue}"`);
const ladTableValue = await ladTableValuePromise.textContent();
expect(ladTableValue).toBe(subscribeTelemValue);
});
});
/**
* Util for subscribing to a telemetry object by object identifier
* Limitations: Currently only works to return telemetry once to the node scope
* To Do: See if there's a way to await this multiple times to allow for multiple
* values to be returned over time
* @param {import('@playwright/test').Page} page
* @param {string} objectIdentifier identifier for object
* @returns {Promise<string>} the formatted sin telemetry value
*/
async function subscribeToTelemetry(page, objectIdentifier) {
const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getTelemValue', resolve));
await page.evaluate(async (telemetryIdentifier) => {
const telemetryObject = await window.openmct.objects.get(telemetryIdentifier);
const metadata = window.openmct.telemetry.getMetadata(telemetryObject);
const formats = await window.openmct.telemetry.getFormatMap(metadata);
window.openmct.telemetry.subscribe(telemetryObject, (obj) => {
const sinVal = obj.sin;
const formattedSinVal = formats.sin.format(sinVal);
window.getTelemValue(formattedSinVal);
});
}, objectIdentifier);
return getTelemValuePromise;
}

View File

@ -24,9 +24,12 @@
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks. This test suite is dedicated to tests which verify the basic operations surrounding Notebooks.
*/ */
// FIXME: Remove this eslint exception once tests are implemented const { test, expect } = require('../../../../pluginFixtures');
// eslint-disable-next-line no-unused-vars const { createDomainObjectWithDefaults } = require('../../../../appActions');
const { test, expect } = require('../../../../baseFixtures'); const nbUtils = require('../../../../helper/notebookUtils');
const path = require('path');
const NOTEBOOK_NAME = 'Notebook';
test.describe('Notebook CRUD Operations', () => { test.describe('Notebook CRUD Operations', () => {
test.fixme('Can create a Notebook Object', async ({ page }) => { test.fixme('Can create a Notebook Object', async ({ page }) => {
@ -67,10 +70,31 @@ test.describe('Default Notebook', () => {
test.describe('Notebook section tests', () => { test.describe('Notebook section tests', () => {
//The following test cases are associated with Notebook Sections //The following test cases are associated with Notebook Sections
test.fixme('New sections are automatically named Unnamed Section with Unnamed Page', async ({ page }) => { test.beforeEach(async ({ page }) => {
//Create new notebook A //Navigate to baseURL
//Add section await page.goto('./', { waitUntil: 'networkidle' });
//Verify new section and new page details
// Create Notebook
await createDomainObjectWithDefaults(page, {
type: NOTEBOOK_NAME
});
});
test('Default and new sections are automatically named Unnamed Section with Unnamed Page', async ({ page }) => {
// Check that the default section and page are created and the name matches the defaults
const defaultSectionName = await page.locator('.c-notebook__sections .c-list__item__name').textContent();
expect(defaultSectionName).toBe('Unnamed Section');
const defaultPageName = await page.locator('.c-notebook__pages .c-list__item__name').textContent();
expect(defaultPageName).toBe('Unnamed Page');
// Expand sidebar and add a section
await page.locator('.c-notebook__toggle-nav-button').click();
await page.locator('.js-sidebar-sections .c-icon-button.icon-plus').click();
// Check that new section and page within the new section match the defaults
const newSectionName = await page.locator('.c-notebook__sections .c-list__item__name').nth(1).textContent();
expect(newSectionName).toBe('Unnamed Section');
const newPageName = await page.locator('.c-notebook__pages .c-list__item__name').textContent();
expect(newPageName).toBe('Unnamed Page');
}); });
test.fixme('Section selection operations and associated behavior', async ({ page }) => { test.fixme('Section selection operations and associated behavior', async ({ page }) => {
//Create new notebook A //Create new notebook A
@ -86,10 +110,58 @@ test.describe('Notebook section tests', () => {
//Delete 3rd section //Delete 3rd section
//1st is selected and there is no default notebook //1st is selected and there is no default notebook
}); });
test.fixme('Section rename operations', async ({ page }) => {
// Create a new notebook
// Add a section
// Rename the section but do not confirm
// Keyboard press 'Escape'
// Verify that the section name reverts to the default name
// Rename the section but do not confirm
// Keyboard press 'Enter'
// Verify that the section name is updated
// Rename the section to "" (empty string)
// Keyboard press 'Enter' to confirm
// Verify that the section name reverts to the default name
// Rename the section to something long that overflows the text box
// Verify that the section name is not truncated while input is active
// Confirm the section name edit
// Verify that the section name is truncated now that input is not active
});
}); });
test.describe('Notebook page tests', () => { test.describe('Notebook page tests', () => {
//The following test cases are associated with Notebook Pages //The following test cases are associated with Notebook Pages
test.beforeEach(async ({ page }) => {
//Navigate to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
// Create Notebook
await createDomainObjectWithDefaults(page, {
type: NOTEBOOK_NAME
});
});
//Test will need to be implemented after a refactor in #5713
// eslint-disable-next-line playwright/no-skipped-test
test.skip('Delete page popup is removed properly on clicking dropdown again', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5713'
});
// Expand sidebar and add a second page
await page.locator('.c-notebook__toggle-nav-button').click();
await page.locator('text=Page Add >> button').click();
// Click on the 2nd page dropdown button and expect the Delete Page option to appear
await page.locator('button[title="Open context menu"]').nth(2).click();
await expect(page.locator('text=Delete Page')).toBeEnabled();
// Clicking on the same page a second time causes the same Delete Page option to recreate
await page.locator('button[title="Open context menu"]').nth(2).click();
await expect(page.locator('text=Delete Page')).toBeEnabled();
// Clicking on the first page causes the first delete button to detach and recreate on the first page
await page.locator('button[title="Open context menu"]').nth(1).click();
const numOfDeletePagePopups = await page.locator('li[title="Delete Page"]').count();
expect(numOfDeletePagePopups).toBe(1);
});
test.fixme('Page selection operations and associated behavior', async ({ page }) => { test.fixme('Page selection operations and associated behavior', async ({ page }) => {
//Create new notebook A //Create new notebook A
//Delete existing Page //Delete existing Page
@ -107,6 +179,23 @@ test.describe('Notebook page tests', () => {
//Delete 3rd page //Delete 3rd page
//First is now selected and there is no default notebook //First is now selected and there is no default notebook
}); });
test.fixme('Page rename operations', async ({ page }) => {
// Create a new notebook
// Add a page
// Rename the page but do not confirm
// Keyboard press 'Escape'
// Verify that the page name reverts to the default name
// Rename the page but do not confirm
// Keyboard press 'Enter'
// Verify that the page name is updated
// Rename the page to "" (empty string)
// Keyboard press 'Enter' to confirm
// Verify that the page name reverts to the default name
// Rename the page to something long that overflows the text box
// Verify that the page name is not truncated while input is active
// Confirm the page name edit
// Verify that the page name is truncated now that input is not active
});
}); });
test.describe('Notebook search tests', () => { test.describe('Notebook search tests', () => {
@ -119,82 +208,173 @@ test.describe('Notebook search tests', () => {
}); });
test.describe('Notebook entry tests', () => { test.describe('Notebook entry tests', () => {
test.fixme('When a new entry is created, it should be focused', async ({ page }) => {}); // Create Notebook with URL Whitelist
test.fixme('When a telemetry object is dropped into a notebook, a new entry is created and it should be focused', async ({ page }) => { let notebookObject;
// Drag and drop any telmetry object on 'drop object' test.beforeEach(async ({ page }) => {
// new entry gets created with telemtry object // eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitNotebookWithUrls.js') });
await page.goto('./', { waitUntil: 'networkidle' });
notebookObject = await createDomainObjectWithDefaults(page, {
type: NOTEBOOK_NAME
}); });
test.fixme('When a telemetry object is dropped into a notebooks existing entry, it should be focused', async ({ page }) => { });
// Drag and drop any telemetry object onto existing entry test.fixme('When a new entry is created, it should be focused', async ({ page }) => {});
// Entry updated with object and snapshot test('When an object is dropped into a notebook, a new entry is created and it should be focused @unstable', async ({ page }) => {
// Create Overlay Plot
await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
// Navigate to the notebook object
await page.goto(notebookObject.url);
// Reveal the notebook in the tree
await page.getByTitle('Show selected item in tree').click();
await page.dragAndDrop('role=treeitem[name=/Dropped Overlay Plot/]', '.c-notebook__drag-area');
const embed = page.locator('.c-ne__embed__link');
const embedName = await embed.textContent();
await expect(embed).toHaveClass(/icon-plot-overlay/);
expect(embedName).toBe('Dropped Overlay Plot');
});
test('When an object is dropped into a notebooks existing entry, it should be focused @unstable', async ({ page }) => {
// Create Overlay Plot
await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
// Navigate to the notebook object
await page.goto(notebookObject.url);
// Reveal the notebook in the tree
await page.getByTitle('Show selected item in tree').click();
await nbUtils.enterTextEntry(page, 'Entry to drop into');
await page.dragAndDrop('role=treeitem[name=/Dropped Overlay Plot/]', 'text=Entry to drop into');
const existingEntry = page.locator('.c-ne__content', { has: page.locator('text="Entry to drop into"') });
const embed = existingEntry.locator('.c-ne__embed__link');
const embedName = await embed.textContent();
await expect(embed).toHaveClass(/icon-plot-overlay/);
expect(embedName).toBe('Dropped Overlay Plot');
}); });
test.fixme('new entries persist through navigation events without save', async ({ page }) => {}); test.fixme('new entries persist through navigation events without save', async ({ page }) => {});
test.fixme('previous and new entries can be deleted', async ({ page }) => {}); test.fixme('previous and new entries can be deleted', async ({ page }) => {});
}); test('when a valid link is entered into a notebook entry, it becomes clickable when viewing', async ({ page }) => {
const TEST_LINK = 'http://www.google.com';
test.describe('Snapshot Menu tests', () => { // Navigate to the notebook object
test.fixme('When no default notebook is selected, Snapshot Menu dropdown should only have a single option', async ({ page }) => { await page.goto(notebookObject.url);
// There should be no default notebook
// Clear default notebook if exists using `localStorage.setItem('notebook-storage', null);`
// refresh page
// Click on 'Notebook Snaphot Menu'
// 'save to Notebook Snapshots' should be only option there
});
test.fixme('When default notebook is updated selected, Snapshot Menu dropdown should list it as the newest option', async ({ page }) => {
// Create 2a notebooks
// Set Notebook A as Default
// Open Snapshot Menu and note that Notebook A is listed
// Close Snapshot Menu
// Set Default Notebook to Notebook B
// Open Snapshot Notebook and note that Notebook B is listed
// Select Default Notebook Option and verify that Snapshot is added to Notebook B
});
test.fixme('Can add Snapshots via Snapshot Menu and details are correct', async ({ page }) => {
//Note this should be a visual test, too
// Create Telemetry object
// Create A notebook with many pages and sections.
// Set page and section defaults to be between first and last of many. i.e. 3 of 5
// Navigate to Telemetry object
// Select Default Notebook Option and verify that Snapshot is added to Notebook A
// Verify Snapshot Details appear correctly
});
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
// 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
});
});
test.describe('Snapshot Container tests', () => { // Reveal the notebook in the tree
test.fixme('5 Snapshots can be added to a container', async ({ page }) => {}); await page.getByTitle('Show selected item in tree').click();
test.fixme('5 Snapshots can be added to a container and Deleted with Delete All action', async ({ page }) => {});
test.fixme('A snapshot can be Deleted from Container', async ({ page }) => {}); await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`);
test.fixme('A snapshot can be Previewed from Container', async ({ page }) => {});
test.fixme('A snapshot Container can be open and closed', async ({ page }) => {}); const validLink = page.locator(`a[href="${TEST_LINK}"]`);
test.fixme('Can add object to Snapshot container and pull into notebook and create a new entry', async ({ page }) => {
//Create Notebook // Start waiting for popup before clicking. Note no await.
//Create Telemetry Object const popupPromise = page.waitForEvent('popup');
//From Telemetry Object, use 'save to Notebook Snapshots'
//Snapshots indicator should blink, click on it to view snapshots await validLink.click();
//Navigate to Notebook const popup = await popupPromise;
//Drag and Drop onto droppable area for new entry
//New Entry created with given snapshot added // Wait for the popup to load.
//Snapshot removed from container? await popup.waitForLoadState();
expect.soft(popup.url()).toContain('www.google.com');
expect(await validLink.count()).toBe(1);
}); });
test.fixme('Can add object to Snapshot container and pull into notebook and existing entry', async ({ page }) => { test('when an invalid link is entered into a notebook entry, it does not become clickable when viewing', async ({ page }) => {
//Create Notebook const TEST_LINK = 'www.google.com';
//Create Telemetry Object
//From Telemetry Object, use 'save to Notebook Snapshots' // Navigate to the notebook object
//Snapshots indicator should blink, click on it to view snapshots await page.goto(notebookObject.url);
//Navigate to Notebook
//Drag and Drop into exiting entry // Reveal the notebook in the tree
//Existing Entry updated with given snapshot await page.getByTitle('Show selected item in tree').click();
//Snapshot removed from container?
await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`);
const invalidLink = page.locator(`a[href="${TEST_LINK}"]`);
expect(await invalidLink.count()).toBe(0);
}); });
test.fixme('Verify Embedded options for PNG, JPG, and Annotate work correctly', async ({ page }) => { test('when a link is entered, but it is not in the whitelisted urls, it does not become clickable when viewing', async ({ page }) => {
//Add snapshot to container const TEST_LINK = 'http://www.bing.com';
//Verify PNG, JPG, and Annotate buttons work correctly
// Navigate to the notebook object
await page.goto(notebookObject.url);
// Reveal the notebook in the tree
await page.getByTitle('Show selected item in tree').click();
await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`);
const invalidLink = page.locator(`a[href="${TEST_LINK}"]`);
expect(await invalidLink.count()).toBe(0);
});
test('when a valid link with a subdomain and a valid domain in the whitelisted urls is entered into a notebook entry, it becomes clickable when viewing', async ({ page }) => {
const INVALID_TEST_LINK = 'http://bing.google.com';
// Navigate to the notebook object
await page.goto(notebookObject.url);
// Reveal the notebook in the tree
await page.getByTitle('Show selected item in tree').click();
await nbUtils.enterTextEntry(page, `This should be a link: ${INVALID_TEST_LINK} is it?`);
const validLink = page.locator(`a[href="${INVALID_TEST_LINK}"]`);
expect(await validLink.count()).toBe(1);
});
test('when a valid secure link is entered into a notebook entry, it becomes clickable when viewing', async ({ page }) => {
const TEST_LINK = 'https://www.google.com';
// Navigate to the notebook object
await page.goto(notebookObject.url);
// Reveal the notebook in the tree
await page.getByTitle('Show selected item in tree').click();
await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`);
const validLink = page.locator(`a[href="${TEST_LINK}"]`);
// Start waiting for popup before clicking. Note no await.
const popupPromise = page.waitForEvent('popup');
await validLink.click();
const popup = await popupPromise;
// 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 }) => {
const TEST_LINK = 'http://www.google.com?bad=';
const TEST_LINK_BAD = `http://www.google.com?bad=<script>alert('gimme your cookies')</script>`;
// Navigate to the notebook object
await page.goto(notebookObject.url);
// Reveal the notebook in the tree
await page.getByTitle('Show selected item in tree').click();
await nbUtils.enterTextEntry(page, `This should be a link, BUT not a bad link: ${TEST_LINK_BAD} is it?`);
const sanitizedLink = page.locator(`a[href="${TEST_LINK}"]`);
const unsanitizedLink = page.locator(`a[href="${TEST_LINK_BAD}"]`);
expect.soft(await sanitizedLink.count()).toBe(1);
expect(await unsanitizedLink.count()).toBe(0);
}); });
}); });

View File

@ -0,0 +1,134 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks.
*/
const { test, expect } = require('../../../../pluginFixtures');
// const { expandTreePaneItemByName, createDomainObjectWithDefaults } = require('../../../../appActions');
// const nbUtils = require('../../../../helper/notebookUtils');
test.describe('Snapshot Menu tests', () => {
test.fixme('When no default notebook is selected, Snapshot Menu dropdown should only have a single option', async ({ page }) => {
// There should be no default notebook
// Clear default notebook if exists using `localStorage.setItem('notebook-storage', null);`
// refresh page
// Click on 'Notebook Snaphot Menu'
// 'save to Notebook Snapshots' should be only option there
});
test.fixme('When default notebook is updated selected, Snapshot Menu dropdown should list it as the newest option', async ({ page }) => {
// Create 2a notebooks
// Set Notebook A as Default
// Open Snapshot Menu and note that Notebook A is listed
// Close Snapshot Menu
// Set Default Notebook to Notebook B
// Open Snapshot Notebook and note that Notebook B is listed
// Select Default Notebook Option and verify that Snapshot is added to Notebook B
});
test.fixme('Can add Snapshots via Snapshot Menu and details are correct', async ({ page }) => {
//Note this should be a visual test, too
// Create Telemetry object
// Create A notebook with many pages and sections.
// Set page and section defaults to be between first and last of many. i.e. 3 of 5
// Navigate to Telemetry object
// Select Default Notebook Option and verify that Snapshot is added to Notebook A
// Verify Snapshot Details appear correctly
});
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
// 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
});
});
test.describe('Snapshot Container tests', () => {
test.beforeEach(async ({ page }) => {
//Navigate to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
// Create Notebook
// const notebook = await createDomainObjectWithDefaults(page, {
// type: 'Notebook',
// name: "Test Notebook"
// });
// // Create Overlay Plot
// const snapShotObject = await createDomainObjectWithDefaults(page, {
// type: 'Overlay Plot',
// name: "Dropped Overlay Plot"
// });
await page.getByRole('button', { name: ' Snapshot ' }).click();
await page.getByRole('menuitem', { name: ' Save to Notebook Snapshots' }).click();
await page.getByRole('button', { name: 'Show' }).click();
});
test.fixme('5 Snapshots can be added to a container', async ({ page }) => {});
test.fixme('5 Snapshots can be added to a container and Deleted with Delete All action', async ({ page }) => {});
test.fixme('A snapshot can be Deleted from Container with 3 dot action menu', async ({ page }) => {});
test.fixme('A snapshot can be Viewed, Annotated, display deleted, and saved from Container with 3 dot action menu', async ({ page }) => {
await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More options').click();
await page.getByRole('menuitem', { name: ' View Snapshot' }).click();
await expect(page.locator('.c-overlay__outer')).toBeVisible();
await page.getByTitle('Annotate').click();
await expect(page.locator('#snap-annotation-canvas')).toBeVisible();
await page.getByRole('button', { name: '' }).click();
// await expect(page.locator('#snap-annotation-canvas')).not.toBeVisible();
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: 'Done' }).click();
//await expect(await page.locator)
});
test('A snapshot can be Quick Viewed from Container with 3 dot action menu', async ({ page }) => {
await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More options').click();
await page.getByRole('menuitem', { name: 'Quick View' }).click();
await expect(page.locator('.c-overlay__outer')).toBeVisible();
});
test.fixme('A snapshot can be Navigated To from Container with 3 dot action menu', async ({ page }) => {});
test.fixme('A snapshot can be Navigated To Item in Time from Container with 3 dot action menu', async ({ page }) => {});
test.fixme('A snapshot Container can be open and closed', async ({ page }) => {});
test.fixme('Can add object to Snapshot container and pull into notebook and create a new entry', async ({ page }) => {
//Create Notebook
//Create Telemetry Object
//From Telemetry Object, use 'save to Notebook Snapshots'
//Snapshots indicator should blink, click on it to view snapshots
//Navigate to Notebook
//Drag and Drop onto droppable area for new entry
//New Entry created with given snapshot added
//Snapshot removed from container?
});
test.fixme('Can add object to Snapshot container and pull into notebook and existing entry', async ({ page }) => {
//Create Notebook
//Create Telemetry Object
//From Telemetry Object, use 'save to Notebook Snapshots'
//Snapshots indicator should blink, click on it to view snapshots
//Navigate to Notebook
//Drag and Drop into exiting entry
//Existing Entry updated with given snapshot
//Snapshot removed from container?
});
test.fixme('Verify Embedded options for PNG, JPG, and Annotate work correctly', async ({ page }) => {
//Add snapshot to container
//Verify PNG, JPG, and Annotate buttons work correctly
});
});

View File

@ -0,0 +1,277 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks with CouchDB.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
test.describe('Notebook Tests with CouchDB @couchdb', () => {
let testNotebook;
test.beforeEach(async ({ page }) => {
//Navigate to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
// Create Notebook
testNotebook = await createDomainObjectWithDefaults(page, {
type: 'Notebook',
name: "TestNotebook"
});
});
test('Inspect Notebook Entry Network Requests', async ({ page }) => {
await page.getByText('Annotations').click();
// Expand sidebar
await page.locator('.c-notebook__toggle-nav-button').click();
// Collect all request events to count and assert after notebook action
let addingNotebookElementsRequests = [];
page.on('request', (request) => addingNotebookElementsRequests.push(request));
let [notebookUrlRequest, allDocsRequest] = await Promise.all([
// Waits for the next request with the specified url
page.waitForRequest(`**/openmct/${testNotebook.uuid}`),
page.waitForRequest('**/openmct/_all_docs?include_docs=true'),
// Triggers the request
page.click('[aria-label="Add Page"]'),
// Ensures that there are no other network requests
page.waitForLoadState('networkidle')
]);
// Assert that only two requests are made
// Network Requests are:
// 1) The actual POST to create the page
// 2) The shared worker event from 👆 request
expect(addingNotebookElementsRequests.length).toBe(2);
// Assert on request object
expect(notebookUrlRequest.postDataJSON().metadata.name).toBe('TestNotebook');
expect(notebookUrlRequest.postDataJSON().model.persisted).toBeGreaterThanOrEqual(notebookUrlRequest.postDataJSON().model.modified);
expect(allDocsRequest.postDataJSON().keys).toContain(testNotebook.uuid);
// Add an entry
// Network Requests are:
// 1) The actual POST to create the entry
// 2) The shared worker event from 👆 POST request
addingNotebookElementsRequests = [];
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
await page.locator('[aria-label="Notebook Entry Input"]').click();
await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`);
await page.locator('[aria-label="Notebook Entry Input"]').press('Enter');
await page.waitForLoadState('networkidle');
expect(addingNotebookElementsRequests.length).toBeLessThanOrEqual(2);
// Add some tags
// Network Requests are for each tag creation are:
// 1) Getting the original path of the parent object
// 2) Getting the original path of the grandparent object (recursive call)
// 3) Creating the annotation/tag object
// 4) The shared worker event from 👆 POST request
// 5) Mutate notebook domain object's annotationModified property
// 6) The shared worker event from 👆 POST request
// 7) Notebooks fetching new annotations due to annotationModified changed
// 8) The update of the notebook domain's object's modified property
// 9) The shared worker event from 👆 POST request
// 10) Entry is timestamped
// 11) The shared worker event from 👆 POST request
addingNotebookElementsRequests = [];
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")');
page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(11);
addingNotebookElementsRequests = [];
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Drilling').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")');
page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(11);
addingNotebookElementsRequests = [];
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")');
page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(11);
// Delete all the tags
// Network requests are:
// 1) Send POST to mutate _delete property to true on annotation with tag
// 2) The shared worker event from 👆 POST request
// 3) Timestamp update on entry
// 4) The shared worker event from 👆 POST request
// This happens for 3 tags so 12 requests
addingNotebookElementsRequests = [];
await page.hover('[aria-label="Tag"]:has-text("Driving")');
await page.locator('[aria-label="Remove tag Driving"]').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")', {state: 'hidden'});
await page.hover('[aria-label="Tag"]:has-text("Drilling")');
await page.locator('[aria-label="Remove tag Drilling"]').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")', {state: 'hidden'});
page.hover('[aria-label="Tag"]:has-text("Science")');
await page.locator('[aria-label="Remove tag Science"]').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")', {state: 'hidden'});
page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(12);
// Add two more pages
await page.click('[aria-label="Add Page"]');
await page.click('[aria-label="Add Page"]');
// Add three entries
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
await page.locator('[aria-label="Notebook Entry Input"]').click();
await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`);
await page.locator('[aria-label="Notebook Entry Input"]').press('Enter');
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').fill(`Second Entry`);
await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').press('Enter');
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').fill(`Third Entry`);
await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').press('Enter');
// Add three tags
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")');
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Drilling').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")');
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")');
page.waitForLoadState('networkidle');
// Add a fourth entry
// Network requests are:
// 1) Send POST to add new entry
// 2) The shared worker event from 👆 POST request
// 3) Timestamp update on entry
// 4) The shared worker event from 👆 POST request
addingNotebookElementsRequests = [];
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=3').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=3').fill(`Fourth Entry`);
await page.locator('[aria-label="Notebook Entry Input"] >> nth=3').press('Enter');
page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(4);
// Add a fifth entry
// Network requests are:
// 1) Send POST to add new entry
// 2) The shared worker event from 👆 POST request
// 3) Timestamp update on entry
// 4) The shared worker event from 👆 POST request
addingNotebookElementsRequests = [];
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=4').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=4').fill(`Fifth Entry`);
await page.locator('[aria-label="Notebook Entry Input"] >> nth=4').press('Enter');
page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(4);
// Add a sixth entry
// 1) Send POST to add new entry
// 2) The shared worker event from 👆 POST request
// 3) Timestamp update on entry
// 4) The shared worker event from 👆 POST request
addingNotebookElementsRequests = [];
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=5').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=5').fill(`Sixth Entry`);
await page.locator('[aria-label="Notebook Entry Input"] >> nth=5').press('Enter');
page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(4);
});
test('Search tests', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/akhenry/openmct-yamcs/issues/69'
});
await page.getByText('Annotations').click();
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
await page.locator('[aria-label="Notebook Entry Input"]').click();
await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`);
await page.locator('[aria-label="Notebook Entry Input"]').press('Enter');
// Add three tags
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")');
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Drilling').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")');
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")');
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"]').first()).toContainText("Science");
await expect(page.locator('[aria-label="Search Result"]').first()).not.toContainText("Driving");
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq');
await expect(page.locator('text=No results found')).toBeVisible();
});
});
// Try to reduce indeterminism of browser requests by only returning fetch requests.
// Filter out preflight CORS, fetching stylesheets, page icons, etc. that can occur during tests
function filterNonFetchRequests(requests) {
return requests.filter(request => {
return (request.resourceType() === 'fetch');
});
}

View File

@ -21,43 +21,42 @@
*****************************************************************************/ *****************************************************************************/
const { test, expect } = require('../../../../pluginFixtures'); const { test, expect } = require('../../../../pluginFixtures');
const { openObjectTreeContextMenu } = require('../../../../appActions'); const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions');
const path = require('path'); const path = require('path');
const nbUtils = require('../../../../helper/notebookUtils');
const TEST_TEXT = 'Testing text for entries.'; const TEST_TEXT = 'Testing text for entries.';
const TEST_TEXT_NAME = 'Test Page'; const TEST_TEXT_NAME = 'Test Page';
const CUSTOM_NAME = 'CUSTOM_NAME'; const CUSTOM_NAME = 'CUSTOM_NAME';
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
test.describe('Restricted Notebook', () => { test.describe('Restricted Notebook', () => {
let notebook;
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await startAndAddRestrictedNotebookObject(page); notebook = await startAndAddRestrictedNotebookObject(page);
}); });
test('Can be renamed @addInit', async ({ page }) => { test('Can be renamed @addInit', async ({ page }) => {
await expect(page.locator('.l-browse-bar__object-name')).toContainText(`Unnamed ${CUSTOM_NAME}`); await expect(page.locator('.l-browse-bar__object-name')).toContainText(`${notebook.name}`);
}); });
test('Can be deleted if there are no locked pages @addInit', async ({ page, openmctConfig }) => { test('Can be deleted if there are no locked pages @addInit', async ({ page }) => {
const { myItemsFolderName } = openmctConfig; await openObjectTreeContextMenu(page, notebook.url);
await openObjectTreeContextMenu(page, myItemsFolderName, `Unnamed ${CUSTOM_NAME}`);
const menuOptions = page.locator('.c-menu ul'); const menuOptions = page.locator('.c-menu ul');
await expect.soft(menuOptions).toContainText('Remove'); await expect.soft(menuOptions).toContainText('Remove');
const restrictedNotebookTreeObject = page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`); const restrictedNotebookTreeObject = page.locator(`a:has-text("${notebook.name}")`);
// notebook tree object exists // notebook tree object exists
expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1); expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1);
// Click Remove Text // Click Remove Text
await page.locator('text=Remove').click(); await page.locator('li[role="menuitem"]:has-text("Remove")').click();
// Click 'OK' on confirmation window and wait for save banner to appear // Click 'OK' on confirmation window and wait for save banner to appear
await Promise.all([ await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),
page.locator('text=OK').click(), page.locator('button:has-text("OK")').click(),
page.waitForSelector('.c-message-banner__message') page.waitForSelector('.c-message-banner__message')
]); ]);
@ -67,7 +66,7 @@ test.describe('Restricted Notebook', () => {
test('Can be locked if at least one page has one entry @addInit', async ({ page }) => { test('Can be locked if at least one page has one entry @addInit', async ({ page }) => {
await enterTextEntry(page); await nbUtils.enterTextEntry(page, TEST_TEXT);
const commitButton = page.locator('button:has-text("Commit Entries")'); const commitButton = page.locator('button:has-text("Commit Entries")');
expect(await commitButton.count()).toEqual(1); expect(await commitButton.count()).toEqual(1);
@ -76,19 +75,19 @@ test.describe('Restricted Notebook', () => {
}); });
test.describe('Restricted Notebook with at least one entry and with the page locked @addInit', () => { test.describe('Restricted Notebook with at least one entry and with the page locked @addInit', () => {
let notebook;
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await startAndAddRestrictedNotebookObject(page); notebook = await startAndAddRestrictedNotebookObject(page);
await enterTextEntry(page); await nbUtils.enterTextEntry(page, TEST_TEXT);
await lockPage(page); await lockPage(page);
// open sidebar // open sidebar
await page.locator('button.c-notebook__toggle-nav-button').click(); await page.locator('button.c-notebook__toggle-nav-button').click();
}); });
test('Locked page should now be in a locked state @addInit @unstable', async ({ page, openmctConfig }, testInfo) => { test('Locked page should now be in a locked state @addInit @unstable', async ({ page }, testInfo) => {
test.fixme(testInfo.project === 'chrome-beta', "Test is unreliable on chrome-beta"); // eslint-disable-next-line playwright/no-skipped-test
const { myItemsFolderName } = openmctConfig; test.skip(testInfo.project === 'chrome-beta', "Test is unreliable on chrome-beta");
// main lock message on page // main lock message on page
const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed'); const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed');
expect.soft(await lockMessage.count()).toEqual(1); expect.soft(await lockMessage.count()).toEqual(1);
@ -98,7 +97,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
expect.soft(await pageLockIcon.count()).toEqual(1); expect.soft(await pageLockIcon.count()).toEqual(1);
// no way to remove a restricted notebook with a locked page // no way to remove a restricted notebook with a locked page
await openObjectTreeContextMenu(page, myItemsFolderName, `Unnamed ${CUSTOM_NAME}`); await openObjectTreeContextMenu(page, notebook.url);
const menuOptions = page.locator('.c-menu ul'); const menuOptions = page.locator('.c-menu ul');
await expect(menuOptions).not.toContainText('Remove'); await expect(menuOptions).not.toContainText('Remove');
@ -122,7 +121,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
expect.soft(newPageCount).toEqual(1); expect.soft(newPageCount).toEqual(1);
// enter test text // enter test text
await enterTextEntry(page); await nbUtils.enterTextEntry(page, TEST_TEXT);
// expect new page to be lockable // expect new page to be lockable
const commitButton = page.locator('BUTTON:HAS-TEXT("COMMIT ENTRIES")'); const commitButton = page.locator('BUTTON:HAS-TEXT("COMMIT ENTRIES")');
@ -135,7 +134,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
// Click text=Ok // Click text=Ok
await Promise.all([ await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),
page.locator('text=Ok').click() page.locator('button:has-text("OK")').click()
]); ]);
// deleted page, should no longer exist // deleted page, should no longer exist
@ -146,15 +145,14 @@ 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.describe('Restricted Notebook with a page locked and with an embed @addInit', () => {
test.beforeEach(async ({ page, openmctConfig }) => { test.beforeEach(async ({ page }) => {
const { myItemsFolderName } = openmctConfig; const notebook = await startAndAddRestrictedNotebookObject(page);
await startAndAddRestrictedNotebookObject(page); await nbUtils.dragAndDropEmbed(page, notebook);
await dragAndDropEmbed(page, myItemsFolderName);
}); });
test('Allows embeds to be deleted if page unlocked @addInit', async ({ page }) => { test('Allows embeds to be deleted if page unlocked @addInit', async ({ page }) => {
// Click .c-ne__embed__name .c-popup-menu-button // Click .c-ne__embed__name .c-popup-menu-button
await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu await page.locator('.c-ne__embed__name .c-icon-button').click(); // embed popup menu
const embedMenu = page.locator('body >> .c-menu'); const embedMenu = page.locator('body >> .c-menu');
await expect(embedMenu).toContainText('Remove This Embed'); await expect(embedMenu).toContainText('Remove This Embed');
@ -163,7 +161,7 @@ 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 }) => { test('Disallows embeds to be deleted if page locked @addInit', async ({ page }) => {
await lockPage(page); await lockPage(page);
// Click .c-ne__embed__name .c-popup-menu-button // Click .c-ne__embed__name .c-popup-menu-button
await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu await page.locator('.c-ne__embed__name .c-icon-button').click(); // embed popup menu
const embedMenu = page.locator('body >> .c-menu'); const embedMenu = page.locator('body >> .c-menu');
await expect(embedMenu).not.toContainText('Remove This Embed'); await expect(embedMenu).not.toContainText('Remove This Embed');
@ -178,49 +176,8 @@ async function startAndAddRestrictedNotebookObject(page) {
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitRestrictedNotebook.js') }); await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitRestrictedNotebook.js') });
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
await page.click('button:has-text("Create")');
await page.click(`text=${CUSTOM_NAME}`); // secondarily tests renamability also
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('text=OK')
]);
}
/** return createDomainObjectWithDefaults(page, { type: CUSTOM_NAME });
* @param {import('@playwright/test').Page} page
*/
async function enterTextEntry(page) {
// Click .c-notebook__drag-area
await page.locator(NOTEBOOK_DROP_AREA).click();
// enter text
await page.locator('div.c-ne__text').click();
await page.locator('div.c-ne__text').fill(TEST_TEXT);
await page.locator('div.c-ne__text').press('Enter');
}
/**
* @param {import('@playwright/test').Page} page
*/
async function dragAndDropEmbed(page, myItemsFolderName) {
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Sine Wave Generator")
await page.locator('li:has-text("Sine Wave Generator")').click();
// Click form[name="mctForm"] >> text=My Items
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
// Click text=OK
await page.locator('text=OK').click();
// Click text=Open MCT My Items >> span >> nth=3
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
// Click text=Unnamed CUSTOM_NAME
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed CUSTOM_NAME').click()
]);
await page.dragAndDrop('text=UNNAMED SINE WAVE GENERATOR', NOTEBOOK_DROP_AREA);
} }
/** /**

View File

@ -25,34 +25,29 @@ This test suite is dedicated to tests which verify form functionality.
*/ */
const { test, expect } = require('../../../../pluginFixtures'); const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
/** /**
* Creates a notebook object and adds an entry. * Creates a notebook object and adds an entry.
* @param {import('@playwright/test').Page} - page to load * @param {import('@playwright/test').Page} - page to load
* @param {number} [iterations = 1] - the number of entries to create * @param {number} [iterations = 1] - the number of entries to create
*/ */
async function createNotebookAndEntry(page, myItemsFolderName, iterations = 1) { async function createNotebookAndEntry(page, iterations = 1) {
//Go to baseURL //Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
// Click button:has-text("Create") const notebook = createDomainObjectWithDefaults(page, { type: 'Notebook' });
await page.locator('button:has-text("Create")').click();
await page.locator('[title="Create and save timestamped notes with embedded object snapshots."]').click();
// Click button:has-text("OK")
await Promise.all([
page.waitForNavigation(),
page.locator(`[name="mctForm"] >> text=${myItemsFolderName}`).click(),
page.locator('button:has-text("OK")').click()
]);
for (let iteration = 0; iteration < iterations; iteration++) { for (let iteration = 0; iteration < iterations; iteration++) {
// Click text=To start a new entry, click here or drag and drop any object // Create an entry
await page.locator('text=To start a new entry, click here or drag and drop any object').click(); 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 = ${iteration}`; const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = ${iteration}`;
await page.locator(entryLocator).click(); await page.locator(entryLocator).click();
await page.locator(entryLocator).fill(`Entry ${iteration}`); await page.locator(entryLocator).fill(`Entry ${iteration}`);
await page.locator(entryLocator).press('Enter');
} }
return notebook;
} }
/** /**
@ -60,122 +55,149 @@ async function createNotebookAndEntry(page, myItemsFolderName, iterations = 1) {
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {number} [iterations = 1] - the number of entries (and tags) to create * @param {number} [iterations = 1] - the number of entries (and tags) to create
*/ */
async function createNotebookEntryAndTags(page, myItemsFolderName, iterations = 1) { async function createNotebookEntryAndTags(page, iterations = 1) {
await createNotebookAndEntry(page, myItemsFolderName, iterations); const notebook = await createNotebookAndEntry(page, iterations);
await page.locator('text=Annotations').click();
for (let iteration = 0; iteration < iterations; iteration++) { for (let iteration = 0; iteration < iterations; iteration++) {
// Click text=To start a new entry, click here or drag and drop any object // Hover and click "Add Tag" button
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click(); // 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 [placeholder="Type to select tag"] // Click inside the tag search input
await page.locator('[placeholder="Type to select tag"]').click(); await page.locator('[placeholder="Type to select tag"]').click();
// Click text=Driving // Select the "Driving" tag
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click(); await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
// Click button:has-text("Add Tag") // Hover and click "Add Tag" button
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click(); // Hover is needed here to "slow down" the actions while running in headless mode
// Click [placeholder="Type to select tag"] 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(); await page.locator('[placeholder="Type to select tag"]').click();
// Click text=Science // Select the "Science" tag
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click(); await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
} }
return notebook;
} }
test.describe('Tagging in Notebooks', () => { test.describe('Tagging in Notebooks @addInit', () => {
test('Can load tags', async ({ page, openmctConfig }) => { test('Can load tags', async ({ page }) => {
const { myItemsFolderName } = openmctConfig; await createNotebookAndEntry(page);
await page.locator('text=Annotations').click();
await createNotebookAndEntry(page, myItemsFolderName);
// Click text=To start a new entry, click here or drag and drop any object
await page.locator('button:has-text("Add Tag")').click(); await page.locator('button:has-text("Add Tag")').click();
// Click [placeholder="Type to select tag"]
await page.locator('[placeholder="Type to select tag"]').click(); await page.locator('[placeholder="Type to select tag"]').click();
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Science"); await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Science");
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling"); await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling");
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Driving"); await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Driving");
}); });
test('Can add tags', async ({ page, openmctConfig }) => { test('Can add tags', async ({ page }) => {
const { myItemsFolderName } = openmctConfig; await createNotebookEntryAndTags(page);
await createNotebookEntryAndTags(page, myItemsFolderName);
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science"); await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving"); await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving");
// Click button:has-text("Add Tag")
await page.locator('button:has-text("Add Tag")').click(); await page.locator('button:has-text("Add Tag")').click();
// Click [placeholder="Type to select tag"]
await page.locator('[placeholder="Type to select tag"]').click(); await page.locator('[placeholder="Type to select tag"]').click();
await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Science"); await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Science");
await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Driving"); await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Driving");
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling"); await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling");
}); });
test('Can search for tags', async ({ page, openmctConfig }) => { test('Can search for tags and preview works properly', async ({ page }) => {
const { myItemsFolderName } = openmctConfig; await createNotebookEntryAndTags(page);
await createNotebookEntryAndTags(page, myItemsFolderName);
// Click [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); 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('sc'); await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science"); await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science");
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving"); await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
// Click [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); 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('Sc'); await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc');
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science"); await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science");
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving"); await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
// Click [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); 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('Xq'); await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq');
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible(); await expect(page.locator('text=No results found')).toBeVisible();
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
await createDomainObjectWithDefaults(page, {
type: 'Display Layout'
}); });
test('Can delete tags', async ({ page, openmctConfig }) => { // Go back into edit mode for the display layout
const { myItemsFolderName } = openmctConfig; await page.locator('button[title="Edit"]').click();
await createNotebookEntryAndTags(page, myItemsFolderName); await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="Notebook Entries"]').click(); await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc');
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science");
await page.getByText('Entry 0').click();
await expect(page.locator('.js-preview-window')).toBeVisible();
});
test('Can delete tags', async ({ page }) => {
await createNotebookEntryAndTags(page);
// Delete Driving // Delete Driving
await page.locator('text=Science Driving Add Tag >> button').nth(1).click(); await page.hover('[aria-label="Tag"]:has-text("Driving")');
await page.locator('[aria-label="Remove tag Driving"]').click();
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science"); await expect(page.locator('[aria-label="Tags Inspector"]')).toContainText("Science");
await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving"); await expect(page.locator('[aria-label="Tags Inspector"]')).not.toContainText("Driving");
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving"); await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
}); });
test('Tags persist across reload', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
test('Can delete entries without tags', async ({ page }) => {
test.info().annotations.push({
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.hover('[aria-label="Notebook Entry Input"] >> 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?')).toBeVisible();
await page.locator('button:has-text("Ok")').click();
await expect(page.locator('text=This action will permanently delete this entry. Do you wish to continue?')).toBeHidden();
});
test('Can delete objects with tags and neither return in search', async ({ page }) => {
await createNotebookEntryAndTags(page);
// Delete Notebook
await page.locator('button[title="More options"]').click();
await page.locator('li[title="Remove this object from its containing object."]').click();
await page.locator('button:has-text("OK")').click();
await page.goto('./', { waitUntil: 'networkidle' });
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed');
await expect(page.locator('text=No results found')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sci');
await expect(page.locator('text=No results found')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('dri');
await expect(page.locator('text=No results found')).toBeVisible();
});
test('Tags persist across reload', async ({ page }) => {
//Go to baseURL //Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
// Create a clock object we can navigate to const clock = await createDomainObjectWithDefaults(page, { type: 'Clock' });
await page.click('button:has-text("Create")');
// Click Clock
await page.click('text=Clock');
// Click button:has-text("OK")
await Promise.all([
page.waitForNavigation(),
page.locator(`[name="mctForm"] >> text=${myItemsFolderName}`).click(),
page.locator('button:has-text("OK")').click()
]);
await page.click('.c-disclosure-triangle');
const ITERATIONS = 4; const ITERATIONS = 4;
await createNotebookEntryAndTags(page, myItemsFolderName, ITERATIONS); const notebook = await createNotebookEntryAndTags(page, ITERATIONS);
for (let iteration = 0; iteration < ITERATIONS; iteration++) { for (let iteration = 0; iteration < ITERATIONS; iteration++) {
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`; const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
@ -183,11 +205,23 @@ test.describe('Tagging in Notebooks', () => {
await expect(page.locator(entryLocator)).toContainText("Driving"); await expect(page.locator(entryLocator)).toContainText("Driving");
} }
// Click Unnamed Clock await Promise.all([
await page.click('text="Unnamed Clock"'); page.waitForNavigation(),
page.goto('./#/browse/mine?hideTree=false'),
page.click('.c-disclosure-triangle')
]);
// Click Unnamed Notebook const treePane = page.getByRole('tree', {
await page.click('text="Unnamed Notebook"'); name: 'Main Tree'
});
// Click Clock
await treePane.getByRole('treeitem', {
name: clock.name
}).click();
// Click Notebook
await page.getByRole('treeitem', {
name: notebook.name
}).click();
for (let iteration = 0; iteration < ITERATIONS; iteration++) { for (let iteration = 0; iteration < ITERATIONS; iteration++) {
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`; const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
@ -201,14 +235,34 @@ test.describe('Tagging in Notebooks', () => {
page.waitForLoadState('networkidle') page.waitForLoadState('networkidle')
]); ]);
// Click Unnamed Notebook // Click Notebook
await page.click('text="Unnamed Notebook"'); await page.click(`text="${notebook.name}"`);
for (let iteration = 0; iteration < ITERATIONS; iteration++) { for (let iteration = 0; iteration < ITERATIONS; iteration++) {
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`; const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
await expect(page.locator(entryLocator)).toContainText("Science"); await expect(page.locator(entryLocator)).toContainText("Science");
await expect(page.locator(entryLocator)).toContainText("Driving"); await expect(page.locator(entryLocator)).toContainText("Driving");
} }
});
test('Can cancel adding a tag', async ({ page }) => {
await createNotebookAndEntry(page);
// Click on Annotations tab
await page.locator('.c-inspector__tab', { hasText: "Annotations" }).click();
// Click on the "Add Tag" button
await page.locator('button:has-text("Add Tag")').click();
// 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)
await page.locator('div.c-inspect-properties__header:has-text("Tags")').click();
// Verify there is a button with text "Add Tag"
await expect(page.locator('button:has-text("Add Tag")')).toBeVisible();
// Verify the AutoComplete field is hidden
await expect(page.locator('[placeholder="Type to select tag"]')).toBeHidden();
}); });
}); });

View File

@ -0,0 +1,156 @@
/*****************************************************************************
* 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 testing the operator status plugin.
*/
const path = require('path');
const { test, expect } = require('../../../../pluginFixtures');
/*
Precondition: Inject Example User, Operator Status Plugins
Verify that user 1 sees updates from user/role 2 (Not possible without openmct-yamcs implementation)
Clear Role Status of single user test
STUB (test.fixme) Rolling through each
*/
test.describe('Operator Status', () => {
test.beforeEach(async ({ page }) => {
// FIXME: determine if plugins will be added to index.html or need to be injected
// eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitExampleUser.js')});
// eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitOperatorStatus.js')});
await page.goto('./', { waitUntil: 'networkidle' });
});
// verify that operator status is visible
test('operator status is visible and expands when clicked', async ({ page }) => {
await expect(page.locator('div[title="Set my operator status"]')).toBeVisible();
await page.locator('div[title="Set my operator status"]').click();
// expect default status to be 'GO'
await expect(page.locator('.c-status-poll-panel')).toBeVisible();
});
test('poll question indicator remains when blank poll set', async ({ page }) => {
await expect(page.locator('div[title="Set the current poll question"]')).toBeVisible();
await page.locator('div[title="Set the current poll question"]').click();
// set to blank
await page.getByRole('button', { name: 'Update' }).click();
// should still be visible
await expect(page.locator('div[title="Set the current poll question"]')).toBeVisible();
});
// Verify that user 1 sees updates from user/role 2 (Not possible without openmct-yamcs implementation)
test('operator status table reflects answered values', async ({ page }) => {
// user navigates to operator status poll
const statusPollIndicator = page.locator('div[title="Set my operator status"]');
await statusPollIndicator.click();
// get user role value
const userRole = page.locator('.c-status-poll-panel__user-role');
const userRoleText = await userRole.innerText();
// get selected status value
const selectStatus = page.locator('select[name="setStatus"]');
await selectStatus.selectOption({ index: 1});
const initialStatusValue = await selectStatus.inputValue();
// open manage status poll
const manageStatusPollIndicator = page.locator('div[title="Set the current poll question"]');
await manageStatusPollIndicator.click();
// parse the table row values
const row = page.locator(`tr:has-text("${userRoleText}")`);
const rowValues = await row.innerText();
const rowValuesArr = rowValues.split('\t');
const COLUMN_STATUS_INDEX = 1;
// check initial set value matches status table
expect(rowValuesArr[COLUMN_STATUS_INDEX].toLowerCase())
.toEqual(initialStatusValue.toLowerCase());
// change user status
await statusPollIndicator.click();
// FIXME: might want to grab a dynamic option instead of arbitrary
await page.locator('select[name="setStatus"]').selectOption({ index: 2});
const updatedStatusValue = await selectStatus.inputValue();
// verify user status is reflected in table
await manageStatusPollIndicator.click();
const updatedRow = page.locator(`tr:has-text("${userRoleText}")`);
const updatedRowValues = await updatedRow.innerText();
const updatedRowValuesArr = updatedRowValues.split('\t');
expect(updatedRowValuesArr[COLUMN_STATUS_INDEX].toLowerCase())
.toEqual(updatedStatusValue.toLowerCase());
});
test('clear poll button removes poll responses', async ({ page }) => {
// user navigates to operator status poll
const statusPollIndicator = page.locator('div[title="Set my operator status"]');
await statusPollIndicator.click();
// get user role value
const userRole = page.locator('.c-status-poll-panel__user-role');
const userRoleText = await userRole.innerText();
// get selected status value
const selectStatus = page.locator('select[name="setStatus"]');
// FIXME: might want to grab a dynamic option instead of arbitrary
await selectStatus.selectOption({ index: 1});
const initialStatusValue = await selectStatus.inputValue();
// open manage status poll
const manageStatusPollIndicator = page.locator('div[title="Set the current poll question"]');
await manageStatusPollIndicator.click();
// parse the table row values
const row = page.locator(`tr:has-text("${userRoleText}")`);
const rowValues = await row.innerText();
const rowValuesArr = rowValues.split('\t');
const COLUMN_STATUS_INDEX = 1;
// check initial set value matches status table
expect(rowValuesArr[COLUMN_STATUS_INDEX].toLowerCase())
.toEqual(initialStatusValue.toLowerCase());
// clear the poll
await page.locator('button[title="Clear the previous poll question"]').click();
const updatedRow = page.locator(`tr:has-text("${userRoleText}")`);
const updatedRowValues = await updatedRow.innerText();
const updatedRowValuesArr = updatedRowValues.split('\t');
const UNSET_VALUE_LABEL = 'Not set';
expect(updatedRowValuesArr[COLUMN_STATUS_INDEX])
.toEqual(UNSET_VALUE_LABEL);
});
test.fixme('iterate through all possible response values', async ({ page }) => {
// test all possible respone values for the poll
});
});

View File

@ -32,7 +32,7 @@ test.use({
} }
}); });
test.describe('ExportAsJSON', () => { test.describe('Autoscale', () => {
test('User can set autoscale with a valid range @snapshot', async ({ page, openmctConfig }) => { test('User can set autoscale with a valid range @snapshot', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig; const { myItemsFolderName } = openmctConfig;
@ -47,16 +47,32 @@ test.describe('ExportAsJSON', () => {
await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']); await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']);
// enter edit mode
await page.click('button[title="Edit"]');
await turnOffAutoscale(page); await turnOffAutoscale(page);
// Make sure that after turning off autoscale, the user selected range values start at the same values the plot had prior. await setUserDefinedMinAndMax(page, '-2', '2');
await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']);
// save
await page.click('button[title="Save"]');
await Promise.all([
page.locator('li[title = "Save and Finish Editing"]').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
// Make sure that after turning off autoscale, the user entered range values are reflexted in the ticks.
await testYTicks(page, ['-2.00', '-1.50', '-1.00', '-0.50', '0.00', '0.50', '1.00', '1.50', '2.00']);
const canvas = page.locator('canvas').nth(1); const canvas = page.locator('canvas').nth(1);
await canvas.hover({trial: true}); await canvas.hover({trial: true});
expect(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-prepan'); expect.soft(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-prepan.png', { animations: 'disabled' });
//Alt Drag Start //Alt Drag Start
await page.keyboard.down('Alt'); await page.keyboard.down('Alt');
@ -76,11 +92,12 @@ test.describe('ExportAsJSON', () => {
await page.keyboard.up('Alt'); await page.keyboard.up('Alt');
// Ensure the drag worked. // Ensure the drag worked.
await testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']); await testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00', '2.50', '3.00', '3.50']);
//Wait for canvas to stablize.
await canvas.hover({trial: true}); await canvas.hover({trial: true});
expect(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-panned'); expect.soft(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-panned.png', { animations: 'disabled' });
}); });
}); });
@ -110,10 +127,10 @@ async function createSinewaveOverlayPlot(page, myItemsFolderName) {
await page.locator('button:has-text("Create")').click(); await page.locator('button:has-text("Create")').click();
// add overlay plot with defaults // add overlay plot with defaults
await page.locator('li:has-text("Overlay Plot")').click(); await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click();
await Promise.all([ await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),
page.locator('text=OK').click(), page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear1 //Wait for Save Banner to appear1
page.waitForSelector('.c-message-banner__message') page.waitForSelector('.c-message-banner__message')
]); ]);
@ -129,10 +146,10 @@ async function createSinewaveOverlayPlot(page, myItemsFolderName) {
await page.locator('button:has-text("Create")').click(); await page.locator('button:has-text("Create")').click();
// add sine wave generator with defaults // add sine wave generator with defaults
await page.locator('li:has-text("Sine Wave Generator")').click(); await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
await Promise.all([ await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),
page.locator('text=OK').click(), page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear1 //Wait for Save Banner to appear1
page.waitForSelector('.c-message-banner__message') page.waitForSelector('.c-message-banner__message')
]); ]);
@ -152,22 +169,25 @@ async function createSinewaveOverlayPlot(page, myItemsFolderName) {
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function turnOffAutoscale(page) { async function turnOffAutoscale(page) {
// enter edit mode
await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
// uncheck autoscale // uncheck autoscale
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"] >> nth=1').uncheck(); await page.getByRole('listitem').filter({ hasText: 'Auto scale' }).getByRole('checkbox').uncheck();
}
// save /**
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); * @param {import('@playwright/test').Page} page
await Promise.all([ * @param {string} min
page.locator('text=Save and Finish Editing').click(), * @param {string} max
//Wait for Save Banner to appear */
page.waitForSelector('.c-message-banner__message') async function setUserDefinedMinAndMax(page, min, max) {
]); // set minimum value
//Wait until Save Banner is gone const minRangeInput = page.getByRole('listitem').filter({ hasText: 'Minimum Value' }).locator('input[type="number"]');
await page.locator('.c-message-banner__close-button').click(); await minRangeInput.click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); await minRangeInput.fill(min);
// set maximum value
const maxRangeInput = page.getByRole('listitem').filter({ hasText: 'Maximum Value' }).locator('input[type="number"]');
await maxRangeInput.click();
await maxRangeInput.fill(max);
} }
/** /**
@ -179,7 +199,7 @@ async function testYTicks(page, values) {
let promises = [yTicks.count().then(c => expect(c).toBe(values.length))]; let promises = [yTicks.count().then(c => expect(c).toBe(values.length))];
for (let i = 0, l = values.length; i < l; i += 1) { for (let i = 0, l = values.length; i < l; i += 1) {
promises.push(expect(yTicks.nth(i)).toHaveText(values[i])); // eslint-disable-line promises.push(expect.soft(yTicks.nth(i)).toHaveText(values[i])); // eslint-disable-line
} }
await Promise.all(promises); await Promise.all(promises);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -88,10 +88,10 @@ async function makeOverlayPlot(page, myItemsFolderName) {
// create overlay plot // create overlay plot
await page.locator('button.c-create-button').click(); await page.locator('button.c-create-button').click();
await page.locator('li:has-text("Overlay Plot")').click(); await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click();
await Promise.all([ await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}), page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('text=OK').click(), page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear //Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message') page.waitForSelector('.c-message-banner__message')
]); ]);
@ -106,7 +106,7 @@ async function makeOverlayPlot(page, myItemsFolderName) {
// create a sinewave generator // create a sinewave generator
await page.locator('button.c-create-button').click(); await page.locator('button.c-create-button').click();
await page.locator('li:has-text("Sine Wave Generator")').click(); await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
// set amplitude to 6, offset 4, period 2 // set amplitude to 6, offset 4, period 2
@ -123,7 +123,7 @@ async function makeOverlayPlot(page, myItemsFolderName) {
await Promise.all([ await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}), page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('text=OK').click(), page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear //Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message') page.waitForSelector('.c-message-banner__message')
]); ]);
@ -160,35 +160,16 @@ async function testRegularTicks(page) {
*/ */
async function testLogTicks(page) { async function testLogTicks(page) {
const yTicks = await page.locator('.gl-plot-y-tick-label'); const yTicks = await page.locator('.gl-plot-y-tick-label');
expect(await yTicks.count()).toBe(28); expect(await yTicks.count()).toBe(9);
await expect(yTicks.nth(0)).toHaveText('-2.98'); await expect(yTicks.nth(0)).toHaveText('-2.98');
await expect(yTicks.nth(1)).toHaveText('-2.50'); await expect(yTicks.nth(1)).toHaveText('-1.51');
await expect(yTicks.nth(2)).toHaveText('-2.00'); await expect(yTicks.nth(2)).toHaveText('-0.58');
await expect(yTicks.nth(3)).toHaveText('-1.51'); await expect(yTicks.nth(3)).toHaveText('-0.00');
await expect(yTicks.nth(4)).toHaveText('-1.20'); await expect(yTicks.nth(4)).toHaveText('0.58');
await expect(yTicks.nth(5)).toHaveText('-1.00'); await expect(yTicks.nth(5)).toHaveText('1.51');
await expect(yTicks.nth(6)).toHaveText('-0.80'); await expect(yTicks.nth(6)).toHaveText('2.98');
await expect(yTicks.nth(7)).toHaveText('-0.58'); await expect(yTicks.nth(7)).toHaveText('5.31');
await expect(yTicks.nth(8)).toHaveText('-0.40'); await expect(yTicks.nth(8)).toHaveText('9.00');
await expect(yTicks.nth(9)).toHaveText('-0.20');
await expect(yTicks.nth(10)).toHaveText('-0.00');
await expect(yTicks.nth(11)).toHaveText('0.20');
await expect(yTicks.nth(12)).toHaveText('0.40');
await expect(yTicks.nth(13)).toHaveText('0.58');
await expect(yTicks.nth(14)).toHaveText('0.80');
await expect(yTicks.nth(15)).toHaveText('1.00');
await expect(yTicks.nth(16)).toHaveText('1.20');
await expect(yTicks.nth(17)).toHaveText('1.51');
await expect(yTicks.nth(18)).toHaveText('2.00');
await expect(yTicks.nth(19)).toHaveText('2.50');
await expect(yTicks.nth(20)).toHaveText('2.98');
await expect(yTicks.nth(21)).toHaveText('3.50');
await expect(yTicks.nth(22)).toHaveText('4.00');
await expect(yTicks.nth(23)).toHaveText('4.50');
await expect(yTicks.nth(24)).toHaveText('5.31');
await expect(yTicks.nth(25)).toHaveText('7.00');
await expect(yTicks.nth(26)).toHaveText('8.00');
await expect(yTicks.nth(27)).toHaveText('9.00');
} }
/** /**
@ -205,7 +186,8 @@ async function enableEditMode(page) {
*/ */
async function enableLogMode(page) { async function enableLogMode(page) {
// turn on log mode // turn on log mode
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check(); await page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox').check();
// await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check();
} }
/** /**
@ -213,7 +195,7 @@ async function enableLogMode(page) {
*/ */
async function disableLogMode(page) { async function disableLogMode(page) {
// turn off log mode // turn off log mode
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().uncheck(); await page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox').uncheck();
} }
/** /**

View File

@ -28,9 +28,10 @@ const { test, expect } = require('../../../../pluginFixtures');
test.describe('Handle missing object for plots', () => { test.describe('Handle missing object for plots', () => {
test('Displays empty div for missing stacked plot item @unstable', async ({ page, browserName, openmctConfig }) => { test('Displays empty div for missing stacked plot item @unstable', async ({ page, browserName, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig; // eslint-disable-next-line playwright/no-skipped-test
test.skip(browserName === 'firefox', 'Firefox failing due to console events being missed');
test.fixme(browserName === 'firefox', 'Firefox failing due to console events being missed'); const { myItemsFolderName } = openmctConfig;
const errorLogs = []; const errorLogs = [];
page.on("console", (message) => { page.on("console", (message) => {
@ -87,11 +88,11 @@ async function makeStackedPlot(page, myItemsFolderName) {
// create stacked plot // create stacked plot
await page.locator('button.c-create-button').click(); await page.locator('button.c-create-button').click();
await page.locator('li:has-text("Stacked Plot")').click(); await page.locator('li[role="menuitem"]:has-text("Stacked Plot")').click();
await Promise.all([ await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}), page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('text=OK').click(), page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear //Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message') page.waitForSelector('.c-message-banner__message')
]); ]);
@ -145,11 +146,11 @@ async function saveStackedPlot(page) {
async function createSineWaveGenerator(page) { async function createSineWaveGenerator(page) {
//Create sine wave generator //Create sine wave generator
await page.locator('button.c-create-button').click(); await page.locator('button.c-create-button').click();
await page.locator('li:has-text("Sine Wave Generator")').click(); await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
await Promise.all([ await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}), page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('text=OK').click(), page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear //Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message') page.waitForSelector('.c-message-banner__message')
]); ]);

View File

@ -0,0 +1,200 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
Tests to verify log plot functionality. Note this test suite if very much under active development and should not
necessarily be used for reference when writing new tests in this area.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
test.describe('Overlay Plot', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
});
test('Plot legend color is in sync with plot series color', async ({ page }) => {
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: "Overlay Plot"
});
await createDomainObjectWithDefaults(page, {
type: "Sine Wave Generator",
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url);
// navigate to plot series color palette
await page.click('.l-browse-bar__actions__edit');
await page.locator('li.c-tree__item.menus-to-left .c-disclosure-triangle').click();
await page.locator('.c-click-swatch--menu').click();
await page.locator('.c-palette__item[style="background: rgb(255, 166, 61);"]').click();
// gets color for swatch located in legend
const element = await page.waitForSelector('.plot-series-color-swatch');
const color = await element.evaluate((el) => {
return window.getComputedStyle(el).getPropertyValue('background-color');
});
expect(color).toBe('rgb(255, 166, 61)');
});
test('The elements pool supports dragging series into multiple y-axis buckets', async ({ page }) => {
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: "Overlay Plot"
});
const swgA = await createDomainObjectWithDefaults(page, {
type: "Sine Wave Generator",
parent: overlayPlot.uuid
});
const swgB = await createDomainObjectWithDefaults(page, {
type: "Sine Wave Generator",
parent: overlayPlot.uuid
});
const swgC = await createDomainObjectWithDefaults(page, {
type: "Sine Wave Generator",
parent: overlayPlot.uuid
});
const swgD = await createDomainObjectWithDefaults(page, {
type: "Sine Wave Generator",
parent: overlayPlot.uuid
});
const swgE = await createDomainObjectWithDefaults(page, {
type: "Sine Wave Generator",
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url);
await page.click('button[title="Edit"]');
// Expand the elements pool vertically
await page.locator('.l-pane.l-pane--vertical-handle-before', {
hasText: 'Elements'
}).locator('.l-pane__handle').hover();
await page.mouse.down();
await page.mouse.move(0, 100);
await page.mouse.up();
const yAxis1PropertyGroup = page.locator('[aria-label="Y Axis Properties"]');
const yAxis2PropertyGroup = page.locator('[aria-label="Y Axis 2 Properties"]');
const yAxis3PropertyGroup = page.locator('[aria-label="Y Axis 3 Properties"]');
// Assert that Y Axis 1 property group is visible only
await expect(yAxis1PropertyGroup).toBeVisible();
await expect(yAxis2PropertyGroup).toBeHidden();
await expect(yAxis3PropertyGroup).toBeHidden();
// Drag swg a, c, e into Y Axis 2
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]'));
await page.locator(`#inspector-elements-tree >> text=${swgC.name}`).dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]'));
await page.locator(`#inspector-elements-tree >> text=${swgE.name}`).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 expect(yAxis1PropertyGroup).toBeVisible();
await expect(yAxis2PropertyGroup).toBeVisible();
await expect(yAxis3PropertyGroup).toBeHidden();
const yAxis1Group = page.getByLabel("Y Axis 1");
const yAxis2Group = page.getByLabel("Y Axis 2");
const yAxis3Group = page.getByLabel("Y Axis 3");
// Drag swg b into Y Axis 3
await page.locator(`#inspector-elements-tree >> text=${swgB.name}`).dragTo(page.locator('[aria-label="Element Item Group Y Axis 3"]'));
// Assert that all Y Axis property groups are visible
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
expect(yAxis1Group.getByRole('listitem', { name: swgD.name })).toBeTruthy();
expect(yAxis1Group.getByRole('listitem').nth(0).getByText(swgD.name)).toBeTruthy();
expect(yAxis2Group.getByRole('listitem', { name: swgE.name })).toBeTruthy();
expect(yAxis2Group.getByRole('listitem').nth(0).getByText(swgE.name)).toBeTruthy();
expect(yAxis2Group.getByRole('listitem', { name: swgC.name })).toBeTruthy();
expect(yAxis2Group.getByRole('listitem').nth(1).getByText(swgC.name)).toBeTruthy();
expect(yAxis2Group.getByRole('listitem', { name: swgA.name })).toBeTruthy();
expect(yAxis2Group.getByRole('listitem').nth(2).getByText(swgA.name)).toBeTruthy();
expect(yAxis3Group.getByRole('listitem', { name: swgB.name })).toBeTruthy();
expect(yAxis3Group.getByRole('listitem').nth(0).getByText(swgB.name)).toBeTruthy();
});
test('Clicking on an item in the elements pool brings up the plot preview with data points', async ({ page }) => {
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: "Overlay Plot"
});
const swgA = await createDomainObjectWithDefaults(page, {
type: "Sine Wave Generator",
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url);
await page.click('button[title="Edit"]');
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click();
await page.locator('.js-overlay canvas').nth(1);
const plotPixelSize = await getCanvasPixelsWithData(page);
expect(plotPixelSize).toBeGreaterThan(0);
});
});
/**
* @param {import('@playwright/test').Page} page
*/
async function getCanvasPixelsWithData(page) {
const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getCanvasValue', resolve));
await page.evaluate(() => {
// The document canvas is where the plot points and lines are drawn.
// The only way to access the canvas is using document (using page.evaluate)
let data;
let canvas;
let ctx;
canvas = document.querySelector('.js-overlay canvas');
ctx = canvas.getContext('2d');
data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
const imageDataValues = Object.values(data);
let plotPixels = [];
// Each pixel consists of four values within the ImageData.data array. The for loop iterates by multiples of four.
// The values associated with each pixel are R (red), G (green), B (blue), and A (alpha), in that order.
for (let i = 0; i < imageDataValues.length;) {
if (imageDataValues[i] > 0) {
plotPixels.push({
startIndex: i,
endIndex: i + 3,
value: `rgb(${imageDataValues[i]}, ${imageDataValues[i + 1]}, ${imageDataValues[i + 2]}, ${imageDataValues[i + 3]})`
});
}
i = i + 4;
}
window.getCanvasValue(plotPixels.length);
});
return getTelemValuePromise;
}

View File

@ -0,0 +1,110 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
Tests to verify log plot functionality. Note this test suite if very much under active development and should not
necessarily be used for reference when writing new tests in this area.
*/
const { test, expect } = require('../../../../pluginFixtures');
test.describe('Legend color in sync with plot color', () => {
test('Testing', async ({ page }) => {
await makeOverlayPlot(page);
// navigate to plot series color palette
await page.click('.l-browse-bar__actions__edit');
await page.locator('li.c-tree__item.menus-to-left .c-disclosure-triangle').click();
await page.locator('.c-click-swatch--menu').click();
await page.locator('.c-palette__item[style="background: rgb(255, 166, 61);"]').click();
// gets color for swatch located in legend
const element = await page.waitForSelector('.plot-series-color-swatch');
const color = await element.evaluate((el) => {
return window.getComputedStyle(el).getPropertyValue('background-color');
});
expect(color).toBe('rgb(255, 166, 61)');
});
});
async function saveOverlayPlot(page) {
// save overlay plot
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
await Promise.all([
page.locator('text=Save and Finish Editing').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
}
async function makeOverlayPlot(page) {
// fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
await page.goto('/', { waitUntil: 'networkidle' });
// create overlay plot
await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
//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 the overlay plot
await saveOverlayPlot(page);
// create a sinewave generator
await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
// Click OK to make generator
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
// click on overlay plot
await page.locator('text=Open MCT My Items >> span').nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Overlay Plot').first().click()
]);
}

View File

@ -0,0 +1,139 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
* This test suite is dedicated to testing the rendering and interaction of plots.
*
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults} = require('../../../../appActions');
test.describe('Plot Integrity Testing @unstable', () => {
let sineWaveGeneratorObject;
test.beforeEach(async ({ page }) => {
//Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'networkidle' });
sineWaveGeneratorObject = await createDomainObjectWithDefaults(page, { type: 'Sine Wave Generator' });
});
test('Plots do not re-request data when a plot is clicked', async ({ page }) => {
//Navigate to Sine Wave Generator
await page.goto(sineWaveGeneratorObject.url);
//Click on the plot canvas
await page.locator('canvas').nth(1).click();
//No request was made to get historical data
const createMineFolderRequests = [];
page.on('request', req => {
// eslint-disable-next-line playwright/no-conditional-in-test
createMineFolderRequests.push(req);
});
expect(createMineFolderRequests.length).toEqual(0);
});
test('Plot is rendered when infinity values exist', async ({ page }) => {
// Edit Plot
await editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject);
//Get pixel data from Canvas
const plotPixelSize = await getCanvasPixelsWithData(page);
expect(plotPixelSize).toBeGreaterThan(0);
});
});
/**
* This function edits a sine wave generator with the default options and enables the infinity values option.
*
* @param {import('@playwright/test').Page} page
* @param {import('../../../../appActions').CreateObjectInfo} sineWaveGeneratorObject
* @returns {Promise<CreatedObjectInfo>} An object containing information about the edited domain object.
*/
async function editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject) {
await page.goto(sineWaveGeneratorObject.url);
// Edit LAD table
await page.locator('[title="More options"]').click();
await page.locator('[title="Edit properties of this object."]').click();
// Modify the infinity option to true
const infinityInput = page.locator('[aria-label="Include Infinity Values"]');
await infinityInput.click();
// 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')
]);
// FIXME: Changes to SWG properties should be reflected on save, but they're not?
// Thus, navigate away and back to the object.
await page.goto('./#/browse/mine');
await page.goto(sineWaveGeneratorObject.url);
await page.locator('c-progress-bar c-telemetry-table__progress-bar').waitFor({
state: 'hidden'
});
// FIXME: The progress bar disappears on series data load, not on plot render,
// so wait for a half a second before evaluating the canvas.
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(500);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function getCanvasPixelsWithData(page) {
const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getCanvasValue', resolve));
await page.evaluate(() => {
// The document canvas is where the plot points and lines are drawn.
// The only way to access the canvas is using document (using page.evaluate)
let data;
let canvas;
let ctx;
canvas = document.querySelector('canvas');
ctx = canvas.getContext('2d');
data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
const imageDataValues = Object.values(data);
let plotPixels = [];
// Each pixel consists of four values within the ImageData.data array. The for loop iterates by multiples of four.
// The values associated with each pixel are R (red), G (green), B (blue), and A (alpha), in that order.
for (let i = 0; i < imageDataValues.length;) {
if (imageDataValues[i] > 0) {
plotPixels.push({
startIndex: i,
endIndex: i + 3,
value: `rgb(${imageDataValues[i]}, ${imageDataValues[i + 1]}, ${imageDataValues[i + 2]}, ${imageDataValues[i + 3]})`
});
}
i = i + 4;
}
window.getCanvasValue(plotPixels.length);
});
return getTelemValuePromise;
}

View File

@ -0,0 +1,93 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
* This test suite is dedicated to testing the Scatter Plot component.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const uuid = require('uuid').v4;
test.describe('Scatter Plot', () => {
let scatterPlot;
test.beforeEach(async ({ page }) => {
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'networkidle' });
// Create the Scatter Plot
scatterPlot = await createDomainObjectWithDefaults(page, { type: 'Scatter Plot' });
});
test('Can add and remove telemetry sources', async ({ page }) => {
const editButtonLocator = page.locator('button[title="Edit"]');
const saveButtonLocator = page.locator('button[title="Save"]');
// Create a sine wave generator within the scatter plot
const swg1 = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: `swg-${uuid()}`,
parent: scatterPlot.uuid
});
// Navigate to the scatter plot and verify that
// the SWG appears in the elements pool
await page.goto(scatterPlot.url);
await editButtonLocator.click();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible();
await saveButtonLocator.click();
await page.locator('li[title="Save and Finish Editing"]').click();
// Create another sine wave generator within the scatter plot
const swg2 = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: `swg-${uuid()}`,
parent: scatterPlot.uuid
});
// Verify that the 'Replace telemetry source' modal appears and accept it
await expect.soft(page.locator('text=This action will replace the current telemetry source. Do you want to continue?')).toBeVisible();
await page.click('text=Ok');
// Navigate to the scatter plot and verify that the new SWG
// appears in the elements pool and the old one is gone
await page.goto(scatterPlot.url);
await editButtonLocator.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 saveButtonLocator.click();
// Right click on the new SWG in the elements pool and delete it
await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({
button: 'right'
});
await page.locator('li[title="Remove this object from its containing object."]').click();
// Verify that the 'Remove object' confirmation modal appears and accept it
await expect.soft(page.locator('text=Warning! This action will remove this object. Are you sure you want to continue?')).toBeVisible();
await page.click('text=Ok');
// Verify that the elements pool shows no elements
await expect(page.locator('text="No contained elements"')).toBeVisible();
});
});

View File

@ -0,0 +1,190 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
Tests to verify log plot functionality. Note this test suite if very much under active development and should not
necessarily be used for reference when writing new tests in this area.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
test.describe('Stacked Plot', () => {
let stackedPlot;
let swgA;
let swgB;
let swgC;
test.beforeEach(async ({ page }) => {
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'networkidle' });
stackedPlot = await createDomainObjectWithDefaults(page, {
type: "Stacked Plot"
});
swgA = await createDomainObjectWithDefaults(page, {
type: "Sine Wave Generator",
parent: stackedPlot.uuid
});
swgB = await createDomainObjectWithDefaults(page, {
type: "Sine Wave Generator",
parent: stackedPlot.uuid
});
swgC = await createDomainObjectWithDefaults(page, {
type: "Sine Wave Generator",
parent: stackedPlot.uuid
});
});
test('Using the remove action removes the correct plot', async ({ page }) => {
const swgAElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgA.name });
const swgBElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgB.name });
const swgCElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgC.name });
await page.goto(stackedPlot.url);
await page.click('button[title="Edit"]');
// Expand the elements pool vertically
await page.locator('.l-pane__handle').nth(2).hover({ trial: true });
await page.mouse.down();
await page.mouse.move(0, 100);
await page.mouse.up();
await swgBElementsPoolItem.click({ button: 'right' });
await page.getByRole('menuitem').filter({ hasText: /Remove/ }).click();
await page.getByRole('button').filter({ hasText: "OK" }).click();
await expect(page.locator('#inspector-elements-tree .js-elements-pool__item')).toHaveCount(2);
// Confirm that the elements pool contains the items we expect
await expect(swgAElementsPoolItem).toHaveCount(1);
await expect(swgBElementsPoolItem).toHaveCount(0);
await expect(swgCElementsPoolItem).toHaveCount(1);
});
test('Can reorder Stacked Plot items', async ({ page }) => {
const swgAElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgA.name });
const swgBElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgB.name });
const swgCElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgC.name });
await page.goto(stackedPlot.url);
await page.click('button[title="Edit"]');
// Expand the elements pool vertically
await page.locator('.l-pane__handle').nth(2).hover({ trial: true });
await page.mouse.down();
await page.mouse.move(0, 100);
await page.mouse.up();
const stackedPlotItem1 = page.locator('.c-plot--stacked-container').nth(0);
const stackedPlotItem2 = page.locator('.c-plot--stacked-container').nth(1);
const stackedPlotItem3 = page.locator('.c-plot--stacked-container').nth(2);
// assert initial plot order - [swgA, swgB, swgC]
await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`);
await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`);
await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`);
// Drag and drop to reorder - [swgB, swgA, swgC]
await swgBElementsPoolItem.dragTo(swgAElementsPoolItem);
// assert plot order after reorder - [swgB, swgA, swgC]
await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`);
await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`);
await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`);
// Drag and drop to reorder - [swgB, swgC, swgA]
await swgCElementsPoolItem.dragTo(swgAElementsPoolItem);
// assert plot order after second reorder - [swgB, swgC, swgA]
await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`);
await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`);
await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`);
// Save (exit edit mode)
await page.locator('button[title="Save"]').click();
await page.locator('li[title="Save and Finish Editing"]').click();
// assert plot order persists after save - [swgB, swgC, swgA]
await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`);
await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`);
await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`);
});
test('Selecting a child plot while in browse and edit modes shows its properties in the inspector', async ({ page }) => {
await page.goto(stackedPlot.url);
// Click on the 1st plot
await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"] canvas`).nth(1).click();
// Assert that the inspector shows the Y Axis properties for swgA
await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series");
await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgA.name);
// Click on the 2nd plot
await page.locator(`[aria-label="Stacked Plot Item ${swgB.name}"] canvas`).nth(1).click();
// Assert that the inspector shows the Y Axis properties for swgB
await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series");
await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgB.name);
// Click on the 3rd plot
await page.locator(`[aria-label="Stacked Plot Item ${swgC.name}"] canvas`).nth(1).click();
// Assert that the inspector shows the Y Axis properties for swgC
await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series");
await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgC.name);
// Go into edit mode
await page.click('button[title="Edit"]');
// Click on canvas for the 1st plot
await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"]`).click();
// Assert that the inspector shows the Y Axis properties for swgA
await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series");
await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgA.name);
//Click on canvas for the 2nd plot
await page.locator(`[aria-label="Stacked Plot Item ${swgB.name}"]`).click();
// Assert that the inspector shows the Y Axis properties for swgB
await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series");
await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgB.name);
//Click on canvas for the 3rd plot
await page.locator(`[aria-label="Stacked Plot Item ${swgC.name}"]`).click();
// Assert that the inspector shows the Y Axis properties for swgC
await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series");
await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgC.name);
});
});

View File

@ -0,0 +1,223 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
Tests to verify plot tagging functionality.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults, setRealTimeMode, setFixedTimeMode } = require('../../../../appActions');
test.describe('Plot Tagging', () => {
async function createTags({page, canvas, xEnd, yEnd}) {
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 stablize.
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();
}
async function testTelemetryItem(page, canvas, 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();
//Wait for canvas to stablize.
await canvas.hover({trial: true});
// click on the tagged plot point
await canvas.click({
position: {
x: 325,
y: 377
}
});
await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Driving')).toBeHidden();
}
async function basicTagsTests(page, canvas) {
// 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");
// Delete Driving
await page.hover('[aria-label="Tag"]:has-text("Driving")');
await page.locator('[aria-label="Remove tag Driving"]').click();
await expect(page.locator('[aria-label="Tags Inspector"]')).toContainText("Science");
await expect(page.locator('[aria-label="Tags Inspector"]')).not.toContainText("Driving");
// 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 Promise.all([
page.reload(),
page.waitForLoadState('networkidle')
]);
// wait for plot progress bar to disappear
await page.locator('.l-view-section.c-progress-bar').waitFor({ state: 'detached' });
await page.getByText('Annotations').click();
await expect(page.getByText('No tags to display for this item')).toBeVisible();
// 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: 'networkidle' });
});
test('Tags work with Overlay Plots', async ({ page }) => {
//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,
xEnd: 700,
yEnd: 480
});
await setFixedTimeMode(page);
// changing to fixed time mode rebuilds canvas?
canvas = page.locator('canvas').nth(1);
await basicTagsTests(page, canvas);
await testTelemetryItem(page, canvas, alphaSineWave);
});
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,
xEnd: 700,
yEnd: 480
});
await basicTagsTests(page, canvas);
});
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: 215
});
await basicTagsTests(page, canvas);
await testTelemetryItem(page, canvas, alphaSineWave);
});
});

View File

@ -20,55 +20,26 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const { test, expect } = require('../../../../pluginFixtures'); const { test, expect } = require('../../../../pluginFixtures');
test.describe('Telemetry Table', () => { test.describe('Telemetry Table', () => {
test('unpauses and filters data when paused by button and user changes bounds', async ({ page, openmctConfig }) => { test('unpauses and filters data when paused by button and user changes bounds', async ({ page }) => {
test.info().annotations.push({ test.info().annotations.push({
type: 'issue', type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5113' description: 'https://github.com/nasa/openmct/issues/5113'
}); });
const { myItemsFolderName } = openmctConfig;
const bannerMessage = '.c-message-banner__message';
const createButton = 'button:has-text("Create")';
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
// Click create button const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
await page.locator(createButton).click(); await createDomainObjectWithDefaults(page, {
await page.locator('li:has-text("Telemetry Table")').click(); type: 'Sine Wave Generator',
parent: table.uuid
await Promise.all([ });
page.waitForNavigation(),
page.locator('text=OK').click(),
// Wait for Save Banner to appear
page.waitForSelector(bannerMessage)
]);
// Save (exit edit mode)
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(3).click();
await page.locator('text=Save and Finish Editing').click();
// Click create button
await page.locator(createButton).click();
// add Sine Wave Generator with defaults
await page.locator('li:has-text("Sine Wave Generator")').click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
// Wait for Save Banner to appear
page.waitForSelector(bannerMessage)
]);
// focus the Telemetry Table // focus the Telemetry Table
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click(); page.goto(table.url);
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Telemetry Table').first().click()
]);
// Click pause button // Click pause button
const pauseButton = page.locator('button.c-button.icon-pause'); const pauseButton = page.locator('button.c-button.icon-pause');

View File

@ -20,7 +20,8 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
const { test, expect } = require('../../../../baseFixtures'); const { test, expect } = require('../../../../pluginFixtures');
const { setFixedTimeMode, setRealTimeMode, setStartOffset, setEndOffset } = require('../../../../appActions');
test.describe('Time conductor operations', () => { test.describe('Time conductor operations', () => {
test('validate start time does not exceeds end time', async ({ page }) => { test('validate start time does not exceeds end time', async ({ page }) => {
@ -142,93 +143,48 @@ test.describe('Time conductor input fields real-time mode', () => {
await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01'); await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01');
// Verify url parameters persist after mode switch // Verify url parameters persist after mode switch
await page.waitForNavigation(); await page.waitForNavigation({ waitUntil: 'networkidle' });
expect(page.url()).toContain(`startDelta=${startDelta}`); expect(page.url()).toContain(`startDelta=${startDelta}`);
expect(page.url()).toContain(`endDelta=${endDelta}`); expect(page.url()).toContain(`endDelta=${endDelta}`);
}); });
test.fixme('time conductor history in fixed time mode will track changing start and end times', async ({ page }) => {
// change start time, verify it's tracked in history
// change end time, verify it's tracked in history
});
test.fixme('time conductor history in realtime mode will track changing start and end times', async ({ page }) => {
// change start offset, verify it's tracked in history
// change end offset, verify it's tracked in history
});
test.fixme('time conductor history allows you to set a historical timeframe', async ({ page }) => {
// make sure there are historical history options
// select an option and make sure the time conductor start and end bounds are updated correctly
});
test.fixme('time conductor history allows you to set a realtime offsets', async ({ page }) => {
// make sure there are realtime history options
// select an option and verify the offsets are updated correctly
});
}); });
/** test.describe('Time Conductor History', () => {
* @typedef {Object} OffsetValues test("shows milliseconds on hover @unstable", async ({ page }) => {
* @property {string | undefined} hours test.info().annotations.push({
* @property {string | undefined} mins type: 'issue',
* @property {string | undefined} secs description: 'https://github.com/nasa/openmct/issues/4386'
*/ });
// Navigate to Open MCT in Fixed Time Mode, UTC Time System
// with startBound at 2022-01-01 00:00:00.000Z
// and endBound at 2022-01-01 00:00:00.200Z
await page.goto('./#/browse/mine?view=grid&tc.mode=fixed&tc.startBound=1640995200000&tc.endBound=1640995200200&tc.timeSystem=utc&hideInspector=true', { waitUntil: 'networkidle' });
await page.locator("[aria-label='Time Conductor History']").hover({ trial: true});
await page.locator("[aria-label='Time Conductor History']").click();
/** // Validate history item format
* Set the values (hours, mins, secs) for the start time offset when in realtime mode const historyItem = page.locator('text="2022-01-01 00:00:00 + 200ms"');
* @param {import('@playwright/test').Page} page await expect(historyItem).toBeEnabled();
* @param {OffsetValues} offset await expect(historyItem).toHaveAttribute('title', '2022-01-01 00:00:00.000 - 2022-01-01 00:00:00.200');
*/ });
async function setStartOffset(page, offset) { });
const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
await setTimeConductorOffset(page, offset, startOffsetButton);
}
/**
* Set the values (hours, mins, secs) for the end time offset when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
*/
async function setEndOffset(page, offset) {
const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
await setTimeConductorOffset(page, offset, endOffsetButton);
}
/**
* Set the time conductor to fixed timespan mode
* @param {import('@playwright/test').Page} page
*/
async function setFixedTimeMode(page) {
await setTimeConductorMode(page, true);
}
/**
* Set the time conductor to realtime mode
* @param {import('@playwright/test').Page} page
*/
async function setRealTimeMode(page) {
await setTimeConductorMode(page, false);
}
/**
* Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
* @param {import('@playwright/test').Locator} offsetButton
*/
async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) {
await offsetButton.click();
if (hours) {
await page.fill('.pr-time-controls__hrs', hours);
}
if (mins) {
await page.fill('.pr-time-controls__mins', mins);
}
if (secs) {
await page.fill('.pr-time-controls__secs', secs);
}
// Click the check button
await page.locator('.icon-check').click();
}
/**
* Set the time conductor mode to either fixed timespan or realtime mode.
* @param {import('@playwright/test').Page} page
* @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true
*/
async function setTimeConductorMode(page, isFixedTimespan = true) {
// Click 'mode' button
await page.locator('.c-mode-button').click();
// Switch time conductor mode
if (isFixedTimespan) {
await page.locator('data-testid=conductor-modeOption-fixed').click();
} else {
await page.locator('data-testid=conductor-modeOption-realtime').click();
}
}

View File

@ -21,27 +21,28 @@
*****************************************************************************/ *****************************************************************************/
const { test, expect } = require('../../../../pluginFixtures'); const { test, expect } = require('../../../../pluginFixtures');
const { openObjectTreeContextMenu } = require('../../../../appActions'); const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions');
const options = {
type: 'Timer'
};
test.describe('Timer', () => { test.describe('Timer', () => {
test.use({ objectCreateOptions: options }); let timer;
test('Can perform actions on the Timer', async ({ page, openmctConfig }) => { test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
});
test('Can perform actions on the Timer', async ({ page }) => {
test.info().annotations.push({ test.info().annotations.push({
type: 'issue', type: 'issue',
description: 'https://github.com/nasa/openmct/issues/4313' description: 'https://github.com/nasa/openmct/issues/4313'
}); });
const { myItemsFolderName } = await openmctConfig; const timerUrl = timer.url;
await test.step("From the tree context menu", async () => { await test.step("From the tree context menu", async () => {
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Start'); await triggerTimerContextMenuAction(page, timerUrl, 'Start');
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Pause'); await triggerTimerContextMenuAction(page, timerUrl, 'Pause');
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Restart at 0'); await triggerTimerContextMenuAction(page, timerUrl, 'Restart at 0');
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Stop'); await triggerTimerContextMenuAction(page, timerUrl, 'Stop');
}); });
await test.step("From the 3dot menu", async () => { await test.step("From the 3dot menu", async () => {
@ -74,9 +75,9 @@ test.describe('Timer', () => {
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {TimerAction} action * @param {TimerAction} action
*/ */
async function triggerTimerContextMenuAction(page, myItemsFolderName, action) { async function triggerTimerContextMenuAction(page, timerUrl, action) {
const menuAction = `.c-menu ul li >> text="${action}"`; const menuAction = `.c-menu ul li >> text="${action}"`;
await openObjectTreeContextMenu(page, myItemsFolderName, "Unnamed Timer"); await openObjectTreeContextMenu(page, timerUrl);
await page.locator(menuAction).click(); await page.locator(menuAction).click();
assertTimerStateAfterAction(page, action); assertTimerStateAfterAction(page, action);
} }

View File

@ -0,0 +1,255 @@
/*****************************************************************************
* 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('../../pluginFixtures.js');
const { createDomainObjectWithDefaults } = require('../../appActions.js');
const { waitForAnimations } = require('../../baseFixtures.js');
test.describe('Recent Objects', () => {
/** @type {import('@playwright/test').Locator} */
let recentObjectsList;
/** @type {import('@playwright/test').Locator} */
let clock;
/** @type {import('@playwright/test').Locator} */
let folderA;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
// Set Recent Objects List locator for subsequent tests
recentObjectsList = page.getByRole('list', {
name: 'Recent Objects'
});
// Create a folder and nest a Clock within it
folderA = await createDomainObjectWithDefaults(page, {
type: 'Folder'
});
clock = await createDomainObjectWithDefaults(page, {
type: 'Clock',
parent: folderA.uuid
});
// Drag the Recent Objects panel up a bit
await page.locator('.l-pane.l-pane--vertical-handle-before', {
hasText: 'Recently Viewed'
}).locator('.l-pane__handle').hover();
await page.mouse.down();
await page.mouse.move(0, 100);
await page.mouse.up();
});
test('Navigated objects show up in recents, object renames and deletions are reflected', async ({ page }) => {
// Verify that both created objects appear in the list and are in the correct order
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();
// 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
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, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6151'
});
await page.goto('./#/browse/mine');
// Navigate to the folder by clicking on its entry in the Clock's breadcrumb
const waitForFolderNavigation = page.waitForURL(`**/${folderA.uuid}?*`);
await page.getByRole('navigation', {
name: clock.name
}).locator('a').filter({
hasText: folderA.name
}).click();
// Verify that the hash URL updates correctly
await waitForFolderNavigation;
expect(page.url()).toMatch(new RegExp(`.*${folderA.uuid}?.*`));
// Navigate to My Items by clicking on its entry in the Clock's breadcrumb
const waitForMyItemsNavigation = page.waitForURL(`**/mine?*`);
await page.getByRole('navigation', {
name: clock.name
}).locator('a').filter({
hasText: myItemsFolderName
}).click();
// Verify that the hash URL updates correctly
await waitForMyItemsNavigation;
expect(page.url()).toMatch(new RegExp(`.*mine?.*`));
});
test("Clicking on the 'target button' scrolls the object into view in the tree and highlights it", async ({ page }) => {
const clockTreeItem = page.getByRole('tree', { name: 'Main Tree'}).getByRole('treeitem', { name: clock.name });
const folderTreeItem = page.getByRole('tree', { name: 'Main Tree'})
.getByRole('treeitem', {
name: folderA.name,
expanded: true
});
// Click the "Target" button for the Clock which is nested in a folder
await page.getByRole('button', { name: `Open and scroll to ${clock.name}`}).click();
// Assert that the Clock parent folder has expanded and the Clock is visible)
await expect(folderTreeItem.locator('.c-disclosure-triangle')).toHaveClass(/--expanded/);
await expect(clockTreeItem).toBeVisible();
// Assert that the Clock treeitem is highlighted
await expect(clockTreeItem.locator('.c-tree__item')).toHaveClass(/is-targeted-item/);
// Wait for highlight animation to end
await waitForAnimations(clockTreeItem.locator('.c-tree__item'));
// Assert that the Clock treeitem is no longer highlighted
await expect(clockTreeItem.locator('.c-tree__item')).not.toHaveClass(/is-targeted-item/);
});
test("Persists on refresh", async ({ page }) => {
assertInitialRecentObjectsListState();
await page.reload();
assertInitialRecentObjectsListState();
});
test("Displays objects and aliases uniquely", async ({ page }) => {
const mainTree = page.getByRole('tree', { name: 'Main Tree'});
// Navigate to the clock and reveal it in the tree
await page.goto(clock.url);
await page.getByTitle('Show selected item in tree').click();
// Right click the clock and create an alias using the "link" context menu action
const clockTreeItem = page.getByRole('tree', {
name: 'Main Tree'
}).getByRole('treeitem', {
name: clock.name
});
await clockTreeItem.click({
button: 'right'
});
await page.getByRole('menuitem', {
name: /Create Link/
}).click();
await page.getByRole('tree', { name: 'Create Modal Tree'}).getByRole('treeitem').first().click();
await page.getByRole('button', { name: 'Save' }).click();
// Click the newly created object alias in the tree
await mainTree.getByRole('treeitem', {
name: new RegExp(clock.name)
}).filter({
has: page.locator('.is-alias')
}).click();
// Assert that two recent objects are displayed and one of them is an alias
expect(await recentObjectsList.getByRole('listitem', { name: clock.name }).count()).toBe(2);
expect(await recentObjectsList.locator('.is-alias').count()).toBe(1);
// Assert that the alias and the original's breadcrumbs are different
const clockBreadcrumbs = recentObjectsList.getByRole('listitem', {name: clock.name}).getByRole('navigation');
expect(await clockBreadcrumbs.count()).toBe(2);
expect(await clockBreadcrumbs.nth(0).innerText()).not.toEqual(await clockBreadcrumbs.nth(1).innerText());
});
test("Enforces a limit of 20 recent objects", async ({ page }) => {
// Creating 21 objects takes a while, so increase the timeout
test.slow();
// Assert that the list initially contains 3 objects (clock, folder, my items)
expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(3);
let lastFolder;
let lastClock;
// Create 19 more objects (3 in beforeEach() + 18 new = 21 total)
for (let i = 0; i < 9; i++) {
lastFolder = await createDomainObjectWithDefaults(page, {
type: "Folder",
parent: lastFolder?.uuid
});
lastClock = await createDomainObjectWithDefaults(page, {
type: "Clock",
parent: lastFolder?.uuid
});
}
// Assert that the list contains 20 objects
expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(20);
// Collapse the tree
await page.getByTitle("Collapse all tree items").click();
const lastFolderTreeItem = page.getByRole('tree', { name: 'Main Tree'})
.getByRole('treeitem', {
name: lastFolder.name,
expanded: true
});
const lastClockTreeItem = page.getByRole('tree', { name: 'Main Tree'})
.getByRole('treeitem', {
name: lastClock.name
});
// Test "Open and Scroll To" in a deeply nested tree, while we're here
await page.getByRole('button', { name: `Open and scroll to ${lastClock.name}`}).click();
// Assert that the Clock parent folder has expanded and the Clock is visible)
await expect(lastFolderTreeItem.locator('.c-disclosure-triangle')).toHaveClass(/--expanded/);
await expect(lastClockTreeItem).toBeVisible();
// Assert that the Clock treeitem is highlighted
await expect(lastClockTreeItem.locator('.c-tree__item')).toHaveClass(/is-targeted-item/);
// Wait for highlight animation to end
await waitForAnimations(lastClockTreeItem.locator('.c-tree__item'));
// Assert that the Clock treeitem is no longer highlighted
await expect(lastClockTreeItem.locator('.c-tree__item')).not.toHaveClass(/is-targeted-item/);
});
function assertInitialRecentObjectsListState() {
expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeTruthy();
expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy();
expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeTruthy();
expect(recentObjectsList.getByRole('listitem').nth(0).getByText(clock.name)).toBeTruthy();
expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeTruthy();
expect(recentObjectsList.getByRole('listitem').nth(1).getByText(folderA.name)).toBeTruthy();
}
});

View File

@ -24,12 +24,22 @@
*/ */
const { test, expect } = require('../../pluginFixtures'); const { test, expect } = require('../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../appActions');
const { v4: uuid } = require('uuid');
test.describe('Grand Search', () => { test.describe('Grand Search', () => {
const searchResultSelector = '.c-gsearch-result__title';
const searchResultDropDownSelector = '.c-gsearch__results';
test.beforeEach(async ({ page }) => {
// Go to baseURL
await page.goto("./", { waitUntil: "networkidle" });
});
test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page, openmctConfig }) => { test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig; const { myItemsFolderName } = openmctConfig;
await createObjectsForSearch(page, myItemsFolderName); const createdObjects = await createObjectsForSearch(page);
// Click [aria-label="OpenMCT Search"] input[type="search"] // Click [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
@ -39,9 +49,9 @@ test.describe('Grand Search', () => {
await expect(page.locator('[aria-label="Search Result"] >> nth=1')).toContainText(`Clock B ${myItemsFolderName} Red Folder Blue Folder`); await expect(page.locator('[aria-label="Search Result"] >> nth=1')).toContainText(`Clock B ${myItemsFolderName} Red Folder Blue Folder`);
await expect(page.locator('[aria-label="Search Result"] >> nth=2')).toContainText(`Clock C ${myItemsFolderName} Red Folder Blue Folder`); await expect(page.locator('[aria-label="Search Result"] >> nth=2')).toContainText(`Clock C ${myItemsFolderName} Red Folder Blue Folder`);
await expect(page.locator('[aria-label="Search Result"] >> nth=3')).toContainText(`Clock D ${myItemsFolderName} Red Folder Blue Folder`); await expect(page.locator('[aria-label="Search Result"] >> nth=3')).toContainText(`Clock D ${myItemsFolderName} Red Folder Blue Folder`);
// Click text=Elements >> nth=0 // Click the Elements pool to dismiss the search menu
await page.locator('text=Elements').first().click(); await page.locator('.l-pane__label:has-text("Elements")').click();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toBeVisible(); await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click(); await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
await page.locator('[aria-label="Clock A clock result"] >> text=Clock A').click(); await page.locator('[aria-label="Clock A clock result"] >> text=Clock A').click();
@ -54,11 +64,11 @@ test.describe('Grand Search', () => {
// Click [aria-label="OpenMCT Search"] a >> nth=0 // Click [aria-label="OpenMCT Search"] a >> nth=0
await page.locator('[aria-label="OpenMCT Search"] a').first().click(); await page.locator('[aria-label="OpenMCT Search"] a').first().click();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toBeVisible(); await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
// Fill [aria-label="OpenMCT Search"] input[type="search"] // Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('foo'); await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('foo');
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toBeVisible(); 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 // Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
@ -70,12 +80,12 @@ test.describe('Grand Search', () => {
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl'); await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl');
await Promise.all([ await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),
page.locator('text=Clock A').click() page.locator('[aria-label="Clock A clock result"] >> text=Clock A').click()
]); ]);
await expect(page.locator('.is-object-type-clock')).toBeVisible(); await expect(page.locator('.is-object-type-clock')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Disp'); await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Disp');
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText('Unnamed Display Layout'); await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText(createdObjects.displayLayout.name);
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toContainText('Folder'); await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toContainText('Folder');
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Clock C'); await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Clock C');
@ -87,15 +97,8 @@ test.describe('Grand Search', () => {
await expect(page.locator('[aria-label="Search Result"] >> nth=2')).toContainText(`Clock C ${myItemsFolderName} Red Folder Blue Folder`); await expect(page.locator('[aria-label="Search Result"] >> nth=2')).toContainText(`Clock C ${myItemsFolderName} Red Folder Blue Folder`);
await expect(page.locator('[aria-label="Search Result"] >> nth=3')).toContainText(`Clock D ${myItemsFolderName} Red Folder Blue Folder`); await expect(page.locator('[aria-label="Search Result"] >> nth=3')).toContainText(`Clock D ${myItemsFolderName} Red Folder Blue Folder`);
}); });
});
test.describe("Search Tests @unstable", () => {
const searchResultSelector = '.c-gsearch-result__title';
test('Validate empty search result', async ({ page }) => { test('Validate empty search result', async ({ page }) => {
// Go to baseURL
await page.goto("./", { waitUntil: "networkidle" });
// Invalid search for objects // Invalid search for objects
await page.type("input[type=search]", 'not found'); await page.type("input[type=search]", 'not found');
@ -103,19 +106,22 @@ test.describe("Search Tests @unstable", () => {
await waitForSearchCompletion(page); await waitForSearchCompletion(page);
// Get the search results // Get the search results
const searchResults = await page.locator(searchResultSelector); const searchResults = page.locator(searchResultSelector);
// Verify that no results are found // Verify that no results are found
expect(await searchResults.count()).toBe(0); expect(await searchResults.count()).toBe(0);
// Verify proper message appears
await expect(page.locator('text=No results found')).toBeVisible();
}); });
test('Validate single object in search result', async ({ page }) => { test('Validate single object in search result @couchdb', async ({ page }) => {
//Go to baseURL
await page.goto("./", { waitUntil: "networkidle" });
// Create a folder object // Create a folder object
const folderName = 'testFolder'; const folderName = uuid();
await createFolderObject(page, folderName); await createDomainObjectWithDefaults(page, {
type: 'folder',
name: folderName
});
// Full search for object // Full search for object
await page.type("input[type=search]", folderName); await page.type("input[type=search]", folderName);
@ -124,28 +130,63 @@ test.describe("Search Tests @unstable", () => {
await waitForSearchCompletion(page); await waitForSearchCompletion(page);
// Get the search results // Get the search results
const searchResults = await page.locator(searchResultSelector); const searchResults = page.locator(searchResultSelector);
// Verify that one result is found // Verify that one result is found
expect(await searchResults.count()).toBe(1); expect(await searchResults.count()).toBe(1);
await expect(searchResults).toHaveText(folderName); await expect(searchResults).toHaveText(folderName);
}); });
test('Search results are debounced @couchdb', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6179'
});
await createObjectsForSearch(page);
let networkRequests = [];
page.on('request', (request) => {
const searchRequest = request.url().endsWith('_find');
const fetchRequest = request.resourceType() === 'fetch';
if (searchRequest && fetchRequest) {
networkRequests.push(request);
}
});
// Full search for object
await page.type("input[type=search]", 'Clock', { delay: 100 });
// Wait for search to finish
await waitForSearchCompletion(page);
// Network requests for the composite telemetry with multiple items should be:
// 1. batched request for latest telemetry using the bulk API
expect(networkRequests.length).toBe(1);
const searchResultDropDown = await page.locator(searchResultDropDownSelector);
await expect(searchResultDropDown).toContainText('Clock A');
});
test("Validate multiple objects in search results return partial matches", async ({ page }) => { test("Validate multiple objects in search results return partial matches", async ({ page }) => {
test.info().annotations.push({ test.info().annotations.push({
type: 'issue', type: 'issue',
description: 'https://github.com/nasa/openmct/issues/4667' description: 'https://github.com/nasa/openmct/issues/4667'
}); });
// Go to baseURL
await page.goto("/", { waitUntil: "networkidle" });
// Create folder objects // Create folder objects
const folderName = "e928a26e-e924-4ea0"; const folderName1 = "e928a26e-e924-4ea0";
const folderName2 = "e928a26e-e924-4001"; const folderName2 = "e928a26e-e924-4001";
await createFolderObject(page, folderName); await createDomainObjectWithDefaults(page, {
await createFolderObject(page, folderName2); type: 'Folder',
name: folderName1
});
await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: folderName2
});
// Partial search for objects // Partial search for objects
await page.type("input[type=search]", 'e928a26e'); await page.type("input[type=search]", 'e928a26e');
@ -153,111 +194,75 @@ test.describe("Search Tests @unstable", () => {
// Wait for search to finish // Wait for search to finish
await waitForSearchCompletion(page); await waitForSearchCompletion(page);
// Get the search results const searchResultDropDown = await page.locator(searchResultDropDownSelector);
const searchResults = await page.locator(searchResultSelector);
// Verify that the search result/s correctly match the search query // Verify that the search result/s correctly match the search query
await expect(searchResultDropDown).toContainText(folderName1);
await expect(searchResultDropDown).toContainText(folderName2);
// Get the search results
const searchResults = page.locator(searchResultSelector);
// Verify that two results are found
expect(await searchResults.count()).toBe(2); expect(await searchResults.count()).toBe(2);
await expect(await searchResults.first()).toHaveText(folderName);
await expect(await searchResults.last()).toHaveText(folderName2);
}); });
}); });
async function createFolderObject(page, folderName) {
// Open Create menu
await page.locator('button:has-text("Create")').click();
// Select Folder object
await page.locator('text=Folder').nth(1).click();
// Click folder title to enter edit mode
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
// Enter folder name
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folderName);
// Create folder object
await page.locator('text=OK').click();
}
async function waitForSearchCompletion(page) { async function waitForSearchCompletion(page) {
// Wait loading spinner to disappear // Wait loading spinner to disappear
await page.waitForSelector('.c-tree-and-search__loading', { state: 'detached' }); await page.waitForSelector('.search-finished');
} }
/** /**
* Creates some domain objects for searching * Creates some domain objects for searching
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
*/ */
async function createObjectsForSearch(page, myItemsFolderName) { async function createObjectsForSearch(page) {
//Go to baseURL const redFolder = await createDomainObjectWithDefaults(page, {
await page.goto('./', { waitUntil: 'networkidle' }); type: 'Folder',
name: 'Red Folder'
});
await page.locator('button:has-text("Create")').click(); const blueFolder = await createDomainObjectWithDefaults(page, {
await page.locator('li:has-text("Folder") >> nth=1').click(); type: 'Folder',
await Promise.all([ name: 'Blue Folder',
page.waitForNavigation(), parent: redFolder.uuid
await page.locator('text=Properties Title Notes >> input[type="text"]').fill('Red Folder'), });
await page.locator(`text=Save In Open MCT ${myItemsFolderName} >> span`).nth(3).click(),
page.locator('button:has-text("OK")').click()
]);
await page.locator('button:has-text("Create")').click(); const clockA = await createDomainObjectWithDefaults(page, {
await page.locator('li:has-text("Folder") >> nth=2').click(); type: 'Clock',
await Promise.all([ name: 'Clock A',
page.waitForNavigation(), parent: blueFolder.uuid
await page.locator('text=Properties Title Notes >> input[type="text"]').fill('Blue Folder'), });
await page.locator('form[name="mctForm"] >> text=Red Folder').click(), const clockB = await createDomainObjectWithDefaults(page, {
page.locator('button:has-text("OK")').click() type: 'Clock',
]); name: 'Clock B',
parent: blueFolder.uuid
});
const clockC = await createDomainObjectWithDefaults(page, {
type: 'Clock',
name: 'Clock C',
parent: blueFolder.uuid
});
const clockD = await createDomainObjectWithDefaults(page, {
type: 'Clock',
name: 'Clock D',
parent: blueFolder.uuid
});
await page.locator('button:has-text("Create")').click(); const displayLayout = await createDomainObjectWithDefaults(page, {
await page.locator('li[title="A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts."]').click(); type: 'Display Layout'
await Promise.all([ });
page.waitForNavigation(),
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock A'),
await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
page.locator('button:has-text("OK")').click()
]);
await page.locator('button:has-text("Create")').click(); // Go back into edit mode for the display layout
await page.locator('li[title="A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts."]').click(); await page.locator('button[title="Edit"]').click();
await Promise.all([
page.waitForNavigation(),
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock B'),
await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
page.locator('button:has-text("OK")').click()
]);
await page.locator('button:has-text("Create")').click(); return {
await page.locator('li[title="A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts."]').click(); redFolder,
await Promise.all([ blueFolder,
page.waitForNavigation(), clockA,
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock C'), clockB,
await page.locator('form[name="mctForm"] >> text=Blue Folder').click(), clockC,
page.locator('button:has-text("OK")').click() clockD,
]); displayLayout
};
await page.locator('button:has-text("Create")').click();
await page.locator('li[title="A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts."]').click();
await Promise.all([
page.waitForNavigation(),
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock D'),
await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
page.locator('button:has-text("OK")').click()
]);
await Promise.all([
page.waitForNavigation(),
page.locator(`a:has-text("${myItemsFolderName}") >> nth=0`).click()
]);
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Notebook")
await page.locator('li:has-text("Display Layout")').click();
// Click button:has-text("OK")
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click()
]);
} }

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