Compare commits

...

79 Commits

Author SHA1 Message Date
John Hill
429e0a90c6 enable a11y on all visual suites 2023-12-21 11:21:17 -08:00
Jesse Mazzella
715a44864e Reduce bundle size (#7246)
* chore: use ESModule imports for d3 libraries

* chore: add d3 types

* chore: use minified plotly

* chore: use ESModule style imports for printj

* chore: use `terser-webpack-plugin` to minimize

* Revert "chore: use minified plotly"

This reverts commit 0ae9b39d41b6e38f0fe38cd89a2cd73869f31c36.

* Revert "Revert "chore: use minified plotly""

This reverts commit 08973a2d2e6675206907f678d447717ac6526613.

* fix: use default minification options

* test: stabilize notebook image drop e2e test

* test(fix): remove .only()

* refactor: convert TelemetryValueFormatter to es6 class

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
2023-12-20 11:23:24 -08:00
John Hill
0d97675a0a [CI] Add a11y checks to our visual testing suite (#7047)
* Add a VISUAL_URL constant and remove all vestiges of hide inspector and tree

* hide timer and add concurrency

* turn off concurrency

* factor out old appAction

* Add expand button to panes

* remove old slow annotations

* fix fault

* update domcontentloaded

* missed refactor

* driveby: setTimeBounds private

* add comments to the percyCSS

* suggest MISSION_TIME

* more notes

* regen

* clean up test

* driveby: clean up order

* restructure

* add new suite now that i'ts hidden

* use mission time everywhere possible

* driveby

* rerun generatedata

* comments

* lint fix

* add inital pass of a11y tests

* first pass for fixing a11y problems

* update build

* add copyright

* check for slashes

* rename files

* update testcases

* update to latest

* updates

* section 508

* final version

* remove leftover

* comments

* documentation

* bad merge

* comment

* use current ruleset

* typo

* feedback

* remove time conductor due to false positives

* default to closed tabs

* add some more accessiblity checking

* change to a condition widget and update search

* lint fix

* turns this into a single function

* update doc to match single function

* update to single function

* update to new function

* lint

* update locator for search input

* fix extra page

* why

* comments

* comments

* refacotr

* wrong paths and fixes
2023-12-19 14:16:08 -08:00
Scott Bell
ec910dcbdc Add tests for inspector data pivoting (#7282)
* inspector view needs renderWhenVisible function

* add a default visualization source

* add plugin to exercise data pivotting

* use correct key string

* test skeleton

* add e2e test

---------

Co-authored-by: David Tsay <david.e.tsay@nasa.gov>
Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
2023-12-19 04:33:50 -06:00
John Hill
0ce36c8297 [CI] Remove unneeded parameterization and increase parallelism (#7310)
* remove unneeded parameterization and increase parallelism

* wrong scripts

* rename

* refactor: rename job

* fix: woops

---------

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2023-12-18 16:48:13 -08:00
Scott Bell
3fccac0bfc Automatically check additional views for memory leaks on navigation (#7300)
* add new objects for navigation testing

* add test for remaining objects

* cleanup plotly on dismount

* lint

* remove vestigial object

* do not need to call destroy here

* do not need to call destroy here

* refactor: ensure path to test file always resolves

* refactor: better locators

---------

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2023-12-15 10:13:41 +01:00
Scott Bell
2675220452 Defer intersection monitoring until needed to prevent race conditions (#7278)
* defer visibility rendering until actually used to prevent race conditions

* remove extrace space
2023-12-15 09:40:36 +01:00
John Hill
4075a31d96 PR Cop 2.0 (#5815)
* PR Cop

* Update the PR Template

* address review comments
2023-12-14 10:55:51 -08:00
Andrew Henry
7f95325816 Add CodeQL badge to readme (#6803)
Because we're passing and we should be proud of that!

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-12-14 07:31:31 -08:00
dependabot[bot]
e07ba61c4c chore(deps-dev): bump marked from 9.1.5 to 11.1.0 (#7299)
Bumps [marked](https://github.com/markedjs/marked) from 9.1.5 to 11.1.0.
- [Release notes](https://github.com/markedjs/marked/releases)
- [Changelog](https://github.com/markedjs/marked/blob/master/.releaserc.json)
- [Commits](https://github.com/markedjs/marked/compare/v9.1.5...v11.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-14 07:29:27 -08:00
Charles Hacskaylo
97bffc554f Fix Notebook entry hover problem (#7106)
Closes #7105
- Removed `:not(:focus)` CSS check for hover.
- New theme constant for a more subdued hover effect to differentiate
from active editing mode.

Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-12-14 06:39:09 -08:00
Shefali Joshi
250db8d7f9 Allow specification of swimlanes via configuration (#7200)
* Use specified group order for plans

* Allow groupIds to be a function

* Fix typo in if statement

* Check that activities are present for a given group

* Change refresh to emit the new model

* Update domainobject on change

* Revert changes for domainObject

* Revert groupIds as functions. Check if groups are objects with names instead.

* Add e2e test for plan swim lane order

* Address review comments - improve if statement

* Move function to right util helper

* Fix path for imported code

* Remove focused test

* Change the name of the ordered group configuration
2023-12-14 06:19:42 -08:00
dependabot[bot]
3520a929a9 chore(deps): bump github/codeql-action from 2 to 3 (#7296)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-14 14:03:20 +00:00
Jesse Mazzella
800b03ad60 fix: define main entry point in package.json (#7298) 2023-12-14 05:50:17 -08:00
Scott Bell
902ed0274a Update API Readme to indicate renderWhenVisible function is optional (#7285)
* using less normative languange

* grammar
2023-12-13 09:57:48 +00:00
David Tsay
9ed8d4f5a5 Provide own renderWhenVisible function since manually creating an object view (#7281)
inspector view needs renderWhenVisible function
2023-12-08 18:33:49 +01:00
Scott Bell
93e5219917 Handle aborted get requests and null domain objects when using ObjectAPI (#7276)
* handle null domain objects

* add some test coverage for aborting search results

* to make test independent
2023-12-05 17:43:49 +00:00
Scott Bell
2d9c0414f7 Inconsistent behavior with multiple annotations in imagery (#7261)
* fix opacity issue

* wip, though selection still weird

* remove debugging

* plots still have issue with last tag

* add some better tests

* Apply suggestions from code review

Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>

* remove hardlined classnames

* case sensitivity

* good job tests finding issue

---------

Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-12-04 19:12:24 -08:00
dependabot[bot]
a3e0a0f694 chore(deps-dev): bump @vue/compiler-sfc from 3.3.8 to 3.3.10 (#7270)
Bumps [@vue/compiler-sfc](https://github.com/vuejs/core/tree/HEAD/packages/compiler-sfc) from 3.3.8 to 3.3.10.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/commits/v3.3.10/packages/compiler-sfc)

---
updated-dependencies:
- dependency-name: "@vue/compiler-sfc"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-12-04 19:29:58 -05:00
Jesse Mazzella
5ec155c7ce chore: bump version to 3.3.0-next (#7273)
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-12-04 15:58:43 -08:00
Sarah McClelland
cfb190fb68 wrote an e2e test for can create a notebook object (#7236)
* wrote an e2e test for can create a notebook object

* made suggested changes to notebook.e2e.spec.js

* made suggested changes to notebook.e2e.spec.js

* made changes to newly created notebook

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-12-04 22:15:55 +00:00
Scott Bell
72e0621ecd When searching, build the path objects asynchronously while returning the results (#7265)
* build paths as fast as we can

* fix tests

* add abort controllers and async load tags
2023-12-04 13:40:28 -08:00
Scott Bell
e7b9481aa9 Destroy canvas in plots if not visible (#7263)
* first draft

* add some more debugging

* add test and remove debug

* Remove debug function

* consolidate destroy

* add better canvas name and handle if gl has gone missing

* extra check for extension
2023-12-04 21:28:24 +00:00
Jesse Mazzella
2dc1388737 chore(package.json): fix warning during npm publish (#7253)
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-12-01 09:44:03 -08:00
Scott Bell
41bee3111c Update API documentation for Visibility-Based Rendering (#7262)
update API with documentation for Visibility-Based Rendering
2023-12-01 10:35:41 +01:00
Jesse Mazzella
97cb783c4b chore: bump d3-scale and use ESModule imports (#7245)
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-11-28 14:07:34 -08:00
John Hill
39a31617b8 [Build] Update to Node 20 and remove 16 (EOL) (#7260)
attempt one
2023-11-28 13:05:28 -08:00
Scott Bell
415b65237b Prevent rubber-banding in Telemetry Table filter input (#7248)
* should debounce the filtering of the telemetry, not the setting of the input

* add some laggy typing to check for debouncing issues

* revert test
2023-11-28 17:39:34 +01:00
dependabot[bot]
28bfc90036 chore(deps-dev): bump eslint from 8.53.0 to 8.54.0 (#7250)
Bumps [eslint](https://github.com/eslint/eslint) from 8.53.0 to 8.54.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.53.0...v8.54.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-11-21 12:35:42 -08:00
Scott Bell
7ce3ed5597 Provide visibility based rendering as part of the view api (#7241)
* first draft

* in preview mode, just show it

* fix unit tests
2023-11-20 09:19:00 -08:00
Jesse Mazzella
b9ae461b7d fix(#7234): Fix frame deletion in Flexible Layouts (#7244)
* fix: use the correct event name for frame deletion

* test: add test for frame removal

* refactor: update test locators, add a11y

* test: upgrade locator

* test: assert dialog text
2023-11-17 18:02:58 +00:00
Charles Hacskaylo
15ee8303e4 Gauge fixes for NaN and composition policy (#5608)
* Closes #5536, #5538
- Significant changes to code flow for better handling of missing telemetry values; closes #5538.
- Changes to flow to handle range display when updating composition and when ranges are set by limits.
- Added `GaugeCompositionPolicy.js`; closes #5536.

* Closes #5536, #5538
- Code cleanup, linting.

* Closes #5538
- Linting fixes.

* Closes #5538
- Added test for 'Gauge does not display NaN when data not available'.

* Closes #5538
- Refined test.

* Closes #5538
- Refined 'NaN' test.
- Added test for 'Gauge enforces composition policy';

* Closes #5538
- Fix linting issues.

* Closes #5538
- Suggested changes from PR review.

* Closes #5538
- Suggested changes from PR review.

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

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

* chore: lint:fix

---------

Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2023-11-16 10:40:48 -08:00
David Tsay
a914e4f1f7 Only show marquee for selected item (#7180)
* only show marquee for selected item

* Revert "only show marquee for selected item"

This reverts commit d17af210c2.

* revert change made in #6767

* create framework for displayLayout visual test

* WIP create display layout for test

* only show marquee for selected

* fix selection of object in nested layout

* fix grid and code cleanup

* add child layouts side by side

* code cleanup

* externalize setup for reuse in multiple tests

* write marquee and grid tests

* fix object in layout locator

* fix nested layout selector

* add aria label to layouts

* fix layout locator

* add jsdoc for test setup function

* make test more efficient

* cleanup and linting

* update locator

* update locators

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-11-16 10:31:35 -08:00
Jesse Mazzella
cdd772aa87 fix(#7234): 'Remove Container' button not working in Flexible Layout toolbar (#7240)
* refactor: rename prop for clarity

- `orientation` -> `dragOrientation`

* fix(#7234): fix event name for flexible layout toolbar action

* test(e2e): add tests for flexible layout toolbar actions

* test: add `@localStorage` tags
2023-11-16 09:21:23 -08:00
Michael Rogers
7f8262b882 Changed global time to use time context current value for ITC (#7191)
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-11-13 15:42:54 -06:00
Scott Bell
deacd91078 Defer rendering for inactive tabs in open mct tabbed view (#7149)
* simple prototype

* add a few examples

* revert to original

* only check first element

* only print when we're firing

* need to return status

* ignore polling logic if not visible

* convert to es6 classes

* add private variables

* remove debug code

* revert on this branch webgl changes

* fix draw loader import

* do not use v-model for search component

* remove flakey unit tests and add e2e tests for same behavior

* remove fdescribe

* add test word

* add simple functional test for tabs

* add performance test for tabs

* make tab selection more explict

* better describe expects

* lint

* switch back to fixed time

* fix perf test for webpacked version

* lint

* relax condition

* relax condition

* resolve PR comments

* address PR review comments

* typo on role vs locator
2023-11-13 18:27:50 +00:00
Even Stensberg
29b7c389ad fix(index.html): use defer and move script to head (#6999) 2023-11-09 14:13:24 -08:00
dependabot[bot]
591b5745a8 chore(deps-dev): bump vue from 3.3.4 to 3.3.8 (#7214)
Bumps [vue](https://github.com/vuejs/core) from 3.3.4 to 3.3.8.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.3.4...v3.3.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-11-09 11:27:49 -08:00
dependabot[bot]
4b0abdf54f chore(deps-dev): bump eslint-plugin-unicorn from 48.0.1 to 49.0.0 (#7218)
Bumps [eslint-plugin-unicorn](https://github.com/sindresorhus/eslint-plugin-unicorn) from 48.0.1 to 49.0.0.
- [Release notes](https://github.com/sindresorhus/eslint-plugin-unicorn/releases)
- [Commits](https://github.com/sindresorhus/eslint-plugin-unicorn/compare/v48.0.1...v49.0.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-unicorn
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-09 08:06:08 -08:00
dependabot[bot]
0c19260028 chore(deps-dev): bump webpack from 5.88.0 to 5.89.0 (#7186)
Bumps [webpack](https://github.com/webpack/webpack) from 5.88.0 to 5.89.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.88.0...v5.89.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-09 08:05:46 -08:00
John Hill
2cc00271b1 [CI] Add CI Troubleshooting doc (#6988)
* document how to bust cache

* Update TESTING.md

Co-authored-by: David 'Epper' Marshall <epper.marshall@gmail.com>

* add approapriate link

* Update missing troublehshooting doc and combine code-cov docs

* comments

---------

Co-authored-by: David 'Epper' Marshall <epper.marshall@gmail.com>
2023-11-09 08:04:52 -08:00
dependabot[bot]
3c933eaa19 chore(deps-dev): bump @types/jasmine from 4.3.4 to 5.1.2 (#7219)
Bumps [@types/jasmine](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jasmine) from 4.3.4 to 5.1.2.
- [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-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-09 07:51:38 -08:00
John Hill
b829735d64 [CI] Dependabot skip rebasing on every merge (#7216)
Skip rebasing on every merge
2023-11-08 15:28:32 -08:00
Scott Bell
51eb2a4f59 Add static limit values to LAD tables (#7193)
* limits shown

* use more verbose way for uniqueness

* when adding items, check for what columns we need

* make header logic much simpler

* add test coverage

* fix test and lint

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

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

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

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

* change to getByTitle

* lint and change to getByLabel

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-11-08 17:56:59 +00:00
dependabot[bot]
09b7873fbd chore(deps-dev): bump eslint from 8.48.0 to 8.53.0 (#7211)
Bumps [eslint](https://github.com/eslint/eslint) from 8.48.0 to 8.53.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.48.0...v8.53.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-11-07 17:16:08 +00:00
dependabot[bot]
c3eef44beb chore(deps-dev): bump @percy/cli from 1.26.0 to 1.27.4 (#7212)
Bumps [@percy/cli](https://github.com/percy/cli/tree/HEAD/packages/cli) from 1.26.0 to 1.27.4.
- [Release notes](https://github.com/percy/cli/releases)
- [Commits](https://github.com/percy/cli/commits/v1.27.4/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>
2023-11-07 07:56:06 -08:00
dependabot[bot]
34d3ba0cff chore(deps-dev): bump eslint-plugin-you-dont-need-lodash-underscore from 6.12.0 to 6.13.0 (#7213)
chore(deps-dev): bump eslint-plugin-you-dont-need-lodash-underscore

Bumps [eslint-plugin-you-dont-need-lodash-underscore](https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore) from 6.12.0 to 6.13.0.
- [Release notes](https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore/releases)
- [Commits](https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore/compare/v6.12.0...v6.13.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-you-dont-need-lodash-underscore
  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-11-07 07:41:02 -08:00
John Hill
5fd24cb689 [Dependency] Update to skip regular releases of marked (#7209)
* update to latest

* skip major versions
2023-11-06 16:19:12 -08:00
Jesse Mazzella
a64faae394 docs: add warning about deploying devServer to prod environment (#7203)
* docs: add warning about using dev server in
production environment

* docs: fix formatting
2023-11-06 16:12:10 -08:00
Jesse Mazzella
d44e06d598 docs: add related repos section to README.md (#7111)
* docs: add related repos section to README.md

* docs: update related repos section
2023-11-06 16:10:37 -08:00
dependabot[bot]
bfcab6b327 chore(deps-dev): bump webpack-merge from 5.9.0 to 5.10.0 (#7205)
Bumps [webpack-merge](https://github.com/survivejs/webpack-merge) from 5.9.0 to 5.10.0.
- [Changelog](https://github.com/survivejs/webpack-merge/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/survivejs/webpack-merge/compare/v5.9.0...v5.10.0)

---
updated-dependencies:
- dependency-name: webpack-merge
  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-11-07 00:09:51 +00:00
dependabot[bot]
1f24cbed1f chore(deps-dev): bump @vue/compiler-sfc from 3.3.4 to 3.3.8 (#7208)
Bumps [@vue/compiler-sfc](https://github.com/vuejs/core/tree/HEAD/packages/compiler-sfc) from 3.3.4 to 3.3.8.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/commits/v3.3.8/packages/compiler-sfc)

---
updated-dependencies:
- dependency-name: "@vue/compiler-sfc"
  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-11-06 15:52:17 -08:00
dependabot[bot]
da7d0df736 chore(deps-dev): bump uuid from 9.0.0 to 9.0.1 (#7207)
Bumps [uuid](https://github.com/uuidjs/uuid) from 9.0.0 to 9.0.1.
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v9.0.0...v9.0.1)

---
updated-dependencies:
- dependency-name: uuid
  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-11-06 15:33:06 -08:00
Jesse Mazzella
8e7c02069e chore: bump Playwright to v1.39.0 (#7201) 2023-11-04 17:20:35 -07:00
dependabot[bot]
4dbca9cb09 chore(deps): bump actions/checkout from 3 to 4 (#7034)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-11-02 16:39:54 -07:00
dependabot[bot]
bc0c0d63c1 chore(deps-dev): bump npm-run-all2 from 6.0.6 to 6.1.1 (#7185)
Bumps [npm-run-all2](https://github.com/bcomnes/npm-run-all2) from 6.0.6 to 6.1.1.
- [Release notes](https://github.com/bcomnes/npm-run-all2/releases)
- [Changelog](https://github.com/bcomnes/npm-run-all2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bcomnes/npm-run-all2/compare/v6.0.6...v6.1.1)

---
updated-dependencies:
- dependency-name: npm-run-all2
  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-11-02 15:37:38 -07:00
Jesse Mazzella
0d27938843 docs: update Telemetry Formats section (#7173) 2023-11-02 14:35:36 -07:00
Jesse Mazzella
02f1013770 fix: DisplayLayout and FlexibleLayout toolbar actions only apply to selected layout (#7184)
* refactor: convert to ES6 function

* fix: include `keyString` in event name

- This negates the need for complicated logic in determining which objectView the action was intended for

* fix: handle the case of currentView being null

* fix: add keyString to flexibleLayout toolbar events

* fix: properly unregister listeners

* fix: remove unused imports

* fix: revert parameter reorder

* refactor: replace usage of `arguments` with `...args`

* fix: add a11y to display layout + toolbar

* test: add first cut of layout toolbar suite

* test: cleanup a bit and add Image test

* test: add stubs

* fix: remove unused variable

* refactor(DisplayLayoutToolbar): convert to ES6 class

* test: generate localStorage data for display layout tests

* fix: clarify "Add" button label

* test: cleanup and don't parameterize tests

* test: fix path for recycled_local_storage.json

* fix: path to local storage file

* docs: add documentation for
utilizing localStorage in e2e

* fix: path to recycled_local_storage.json

* docs: add note hyperlink
2023-11-02 20:42:37 +00:00
dependabot[bot]
bdff210a9c chore(deps-dev): bump eslint-plugin-vue from 9.17.0 to 9.18.1 (#7188)
Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 9.17.0 to 9.18.1.
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v9.17.0...v9.18.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-02 18:08:58 +00:00
Shefali Joshi
ae22920576 Refine display options and add Independent Time Conductor option for Time List view (#7161)
* Apply sort settings immediately - even when in edit mode.

* Adds test for sort order

* Enable independent time conductor for time list view

* Remove time frame duration options.

* Remove immediate sorting in edit mode.

* Closes #7113
- Color of current events changed to bring more in-line with color conventions.
- Changed Time List rgba colors to solids.
- Removed bolding on current events text.

* Fix tests to include new changes

---------

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
2023-11-01 15:47:43 +00:00
Michael Rogers
a0fd1f0171 Removed errant brace in ObjectAPI Error (#7192)
Removed errant brace
2023-10-31 07:28:20 -07:00
dependabot[bot]
c7fd584b58 chore(deps-dev): bump @braintree/sanitize-url from 6.0.2 to 6.0.4 (#7190)
Bumps [@braintree/sanitize-url](https://github.com/braintree/sanitize-url) from 6.0.2 to 6.0.4.
- [Changelog](https://github.com/braintree/sanitize-url/blob/main/CHANGELOG.md)
- [Commits](https://github.com/braintree/sanitize-url/compare/v6.0.2...v6.0.4)

---
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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-30 23:05:33 +00:00
dependabot[bot]
16ca994cfa chore(deps-dev): bump sinon from 15.1.0 to 17.0.0 (#7155)
Bumps [sinon](https://github.com/sinonjs/sinon) from 15.1.0 to 17.0.0.
- [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/v15.1.0...v17.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-29 09:13:09 -07:00
dependabot[bot]
ebe5323f82 chore(deps-dev): bump painterro from 1.2.78 to 1.2.87 (#7165)
Bumps [painterro](https://github.com/devforth/painterro) from 1.2.78 to 1.2.87.
- [Release notes](https://github.com/devforth/painterro/releases)
- [Changelog](https://github.com/devforth/painterro/blob/master/Release.md)
- [Commits](https://github.com/devforth/painterro/compare/v1.2.78...v1.2.87)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-29 15:55:57 +00:00
dependabot[bot]
7a8a6d3649 chore(deps-dev): bump eslint-plugin-unicorn from 44.0.2 to 48.0.1 (#7163)
Bumps [eslint-plugin-unicorn](https://github.com/sindresorhus/eslint-plugin-unicorn) from 44.0.2 to 48.0.1.
- [Release notes](https://github.com/sindresorhus/eslint-plugin-unicorn/releases)
- [Commits](https://github.com/sindresorhus/eslint-plugin-unicorn/compare/v44.0.2...v48.0.1)

---
updated-dependencies:
- dependency-name: eslint-plugin-unicorn
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-29 06:55:09 -07:00
dependabot[bot]
25e7a16c77 chore(deps-dev): bump cspell from 7.3.6 to 7.3.8 (#7162)
Bumps [cspell](https://github.com/streetsidesoftware/cspell) from 7.3.6 to 7.3.8.
- [Release notes](https://github.com/streetsidesoftware/cspell/releases)
- [Changelog](https://github.com/streetsidesoftware/cspell/blob/main/CHANGELOG.md)
- [Commits](https://github.com/streetsidesoftware/cspell/compare/v7.3.6...v7.3.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-28 14:57:47 +00:00
dependabot[bot]
141939295a chore(deps): bump actions/setup-node from 3 to 4 (#7166)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-10-24 16:52:17 -07:00
dependabot[bot]
2c1040c7c0 chore(deps-dev): bump sass from 1.63.4 to 1.68.0 (#7086)
Bumps [sass](https://github.com/sass/dart-sass) from 1.63.4 to 1.68.0.
- [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.63.4...1.68.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rukmini Bose (Ruki) <48999852+rukmini-bose@users.noreply.github.com>
2023-10-24 16:26:27 -07:00
Jamie V
d94fe8806b [Plots] Gracefully handle Float32Array breaking values (#7138)
* WIP

* guaranteeing float32breaking values for swgs when option is set

* cleaning up and clarity

* more clarity

* removing randomization of float breaking number, as it is not necessary

* logging the values that could not be plotted for awareness

* remving auto-added imports

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-10-24 23:07:43 +00:00
Jamie V
7bf983210c [Filters] Fix view based filters when string input is enabled (#7050)
* de-reactifying some objects for clarity, handling strings for filters, some vue 3 formatting

* removing debug, fixing string value persistence

* remove unnecessary change

* removing vue utils from non vue files

* nipping proxy objects in the bud

* linting

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-10-24 15:33:08 -07:00
Khalid Adil
8f92cd4206 [Tooltips] Finish tests for gauges, telemetry tables, recently viewed items, and time strips (#7145)
Finish tests for gauge, telemetry tables, recently viewed items, and time strips

Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
2023-10-24 22:06:40 +00:00
Scott Bell
13311b9fc8 Prevent infinite loop when updating a table row in place (#7154)
* bump index on update row in place

* add test

* Removing problematic test

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-20 23:20:32 +00:00
dependabot[bot]
954c72b100 chore(deps-dev): bump eslint-plugin-vue from 9.15.0 to 9.17.0 (#6907)
Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 9.15.0 to 9.17.0.
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v9.15.0...v9.17.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-20 19:01:12 +00:00
Jesse Mazzella
43338f3980 chore: remove vue/compat and complete Vue 3 migration (#7133)
* chore: remove custom `compatConfig` settings

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

* chore: explicitly define Vue feature flags

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

* fix(e2e): revert to original test procedures

* refactor: replace final instances of `$set`

* refactor: remove `Vue` imports from tests

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

* refactor: actually push the changes...

* test: replace unit test with e2e test

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

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

* test(fix): more `$refs`

* fix(test): more `$refs`

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

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

---------

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-18 15:49:29 +00:00
Jesse Mazzella
9849e0398e fix(#7143): add eslint-plugin-no-sanitize and fix errors (#7144) 2023-10-16 09:26:12 -07:00
David Tsay
76889cf60d Rename all configuration inspector tabs to Config (#7140) 2023-10-12 13:48:13 -07:00
221 changed files with 5426 additions and 2895 deletions

View File

@@ -2,7 +2,7 @@ version: 2.1
executors:
pw-focal-development:
docker:
- image: mcr.microsoft.com/playwright:v1.36.2-focal
- image: mcr.microsoft.com/playwright:v1.39.0-focal
environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
@@ -120,15 +120,13 @@ jobs:
- generate_and_store_version_and_filesystem_artifacts
e2e-test:
parameters:
node-version:
type: string
suite: #stable or full
type: string
executor: pw-focal-development
parallelism: 4
parallelism: 6
steps:
- build_and_install:
node-version: <<parameters.node-version>>
node-version: lts/hydrogen
- when: #Only install chrome-beta when running the 'full' suite to save $$$
condition:
equal: ['full', <<parameters.suite>>]
@@ -155,14 +153,11 @@ jobs:
steps:
- generate_and_store_version_and_filesystem_artifacts
e2e-couchdb:
parameters:
node-version:
type: string
executor: ubuntu
steps:
- build_and_install:
node-version: <<parameters.node-version>>
- run: npx playwright@1.36.2 install #Necessary for bare ubuntu machine
node-version: lts/hydrogen
- run: npx playwright@1.39.0 install #Necessary for bare ubuntu machine
- run: |
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
@@ -189,15 +184,28 @@ jobs:
equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_and_store_version_and_filesystem_artifacts
perf-test:
parameters:
node-version:
type: string
mem-test:
executor: pw-focal-development
steps:
- build_and_install:
node-version: <<parameters.node-version>>
node-version: lts/hydrogen
- run: npm run test:perf:memory
- store_test_results:
path: test-results/results.xml
- store_artifacts:
path: test-results
- store_artifacts:
path: html-test-results
- when:
condition:
equal: [42, 42] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_and_store_version_and_filesystem_artifacts
perf-test:
executor: pw-focal-development
steps:
- build_and_install:
node-version: lts/hydrogen
- run: npm run test:perf:localhost
- run: npm run test:perf:contract
- store_test_results:
@@ -211,16 +219,14 @@ jobs:
equal: [42, 42] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_and_store_version_and_filesystem_artifacts
visual-test:
visual-a11y-tests:
parameters:
node-version:
type: string
suite:
type: string # ci or full
executor: pw-focal-development
steps:
- build_and_install:
node-version: <<parameters.node-version>>
node-version: lts/hydrogen
- run: npm run test:e2e:visual:<<parameters.suite>>
- store_test_results:
path: test-results/results.xml
@@ -237,27 +243,25 @@ workflows:
overall-circleci-commit-status: #These jobs run on every commit
jobs:
- lint:
name: node16-lint
node-version: lts/gallium
name: node20-lint
node-version: lts/iron
- unit-test:
name: node18-chrome
node-version: lts/hydrogen
- e2e-test:
name: e2e-stable
node-version: lts/hydrogen
suite: stable
- perf-test:
node-version: lts/hydrogen
- visual-test:
- mem-test
- perf-test
- visual-a11y-tests:
name: visual-test-ci
suite: ci
node-version: lts/hydrogen
the-nightly: #These jobs do not run on PRs, but against master at night
jobs:
- unit-test:
name: node16-chrome-nightly
node-version: lts/gallium
name: node20-chrome-nightly
node-version: lts/iron
- unit-test:
name: node18-chrome
node-version: lts/hydrogen
@@ -265,16 +269,13 @@ workflows:
node-version: lts/hydrogen
- e2e-test:
name: e2e-full-nightly
node-version: lts/hydrogen
suite: full
- perf-test:
node-version: lts/hydrogen
- visual-test:
- mem-test
- perf-test
- visual-a11y-tests:
name: visual-test-nightly
suite: full
node-version: lts/hydrogen
- e2e-couchdb:
node-version: lts/hydrogen
- e2e-couchdb
triggers:
- schedule:
cron: '0 0 * * *'

View File

@@ -487,7 +487,11 @@
"blockquote",
"blockquotes",
"Blockquote",
"Blockquotes"
"Blockquotes",
"oger",
"lcovonly",
"gcov",
"WCAG"
],
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US"],
"ignorePaths": [
@@ -500,4 +504,4 @@
"html-test-results",
"test-results"
]
}
}

View File

@@ -15,7 +15,8 @@ module.exports = {
'plugin:compat/recommended',
'plugin:vue/vue3-recommended',
'plugin:you-dont-need-lodash-underscore/compatible',
'plugin:prettier/recommended'
'plugin:prettier/recommended',
'plugin:no-unsanitized/DOM'
],
parser: 'vue-eslint-parser',
parserOptions: {

View File

@@ -14,8 +14,10 @@ Closes <!--- Insert Issue Number(s) this PR addresses. Start by typing # will op
* [ ] Changes address original issue?
* [ ] Tests included and/or updated with changes?
* [ ] Command line build passes?
* [ ] Has this been smoke tested?
* [ ] Have you associated this PR with a `type:` label? Note: this is not necessarily the same as the original issue.
* [ ] Have you associated a milestone with this PR? Note: leave blank if unsure.
* [ ] Is this a breaking change to be called out in the release notes?
* [ ] Testing instructions included in associated issue OR is this a dependency/testcase change?
### Reviewer Checklist
@@ -25,5 +27,3 @@ Closes <!--- Insert Issue Number(s) this PR addresses. Start by typing # will op
* [ ] Changes appear not to be breaking changes?
* [ ] Appropriate automated tests included?
* [ ] Code style and in-line documentation are appropriate?
* [ ] 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)

View File

@@ -5,6 +5,7 @@ updates:
schedule:
interval: 'weekly'
open-pull-requests-limit: 10
rebase-strategy: 'disabled'
labels:
- 'pr:daveit'
- 'pr:e2e'
@@ -28,10 +29,13 @@ updates:
update-types: ['version-update:semver-patch']
- dependency-name: '@types/lodash'
update-types: ['version-update:semver-patch']
- dependency-name: 'marked'
update-types: ['version-update:semver-patch']
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'daily'
rebase-strategy: 'disabled'
labels:
- 'pr:daveit'
- 'type:maintenance'

View File

@@ -27,18 +27,18 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
config-file: ./.github/codeql/codeql-config.yml
languages: javascript
queries: security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3

View File

@@ -15,8 +15,8 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 'lts/hydrogen'
@@ -27,7 +27,7 @@ jobs:
key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm install --cache ~/.npm --no-audit --progress=false
- name: Login to DockerHub
@@ -36,8 +36,8 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- run: npx playwright@1.36.2 install
- run: npx playwright@1.39.0 install
- name: Start CouchDB Docker Container and Init with Setup Scripts
run: |

View File

@@ -20,11 +20,11 @@ jobs:
- ubuntu-latest
- windows-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 'lts/hydrogen'
- name: Cache NPM dependencies
uses: actions/cache@v3
with:
@@ -32,8 +32,8 @@ jobs:
key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npx playwright@1.36.2 install
- run: npx playwright@1.39.0 install
- run: npx playwright install chrome-beta
- run: npm install --cache ~/.npm --no-audit --progress=false
- run: npm run test:e2e:full -- --max-failures=40
@@ -65,4 +65,4 @@ jobs:
});
} catch (error) {
core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);
}
}

View File

@@ -11,8 +11,8 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/hydrogen
- run: npm install
@@ -26,8 +26,8 @@ jobs:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/hydrogen
registry-url: https://registry.npmjs.org/

View File

@@ -22,17 +22,17 @@ jobs:
- macos-latest
- windows-latest
node_version:
- lts/gallium
- lts/iron
- lts/hydrogen
architecture:
- x64
name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node_version }}
architecture: ${{ matrix.architecture }}

View File

@@ -3,17 +3,17 @@ name: PRCop
on:
pull_request:
types:
- labeled
- unlabeled
- opened
- reopened
- edited
- synchronize
- ready_for_review
- review_requested
- review_request_removed
- edited
pull_request_review_comment:
types:
- created
env:
LABELS: ${{ join( github.event.pull_request.labels.*.name, ' ' ) }}
jobs:
prcop:
runs-on: ubuntu-latest
@@ -24,3 +24,15 @@ jobs:
with:
config-file: '.github/workflows/prcop-config.json'
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
check-type-label:
name: Check type Label
runs-on: ubuntu-latest
steps:
- if: contains( env.LABELS, 'type:' ) == false
run: exit 1
check-milestone:
name: Check Milestone
runs-on: ubuntu-latest
steps:
- if: github.event.pull_request.milestone == null && contains( env.LABELS, 'no milestone' ) == false
run: exit 1

View File

@@ -69,16 +69,14 @@ const config = {
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'),
'plotly-basic': 'plotly.js-basic-dist-min',
'plotly-gl2d': 'plotly.js-gl2d-dist-min',
printj: 'printj/printj.mjs',
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'),
utils: path.join(projectRootDir, 'src/utils'),
vue: path.join(projectRootDir, 'node_modules/@vue/compat/dist/vue.esm-bundler.js'),
utils: path.join(projectRootDir, 'src/utils')
}
},
plugins: [
@@ -86,7 +84,9 @@ const config = {
__OPENMCT_VERSION__: `'${packageDefinition.version}'`,
__OPENMCT_BUILD_DATE__: `'${new Date()}'`,
__OPENMCT_REVISION__: `'${gitRevision}'`,
__OPENMCT_BUILD_BRANCH__: `'${gitBranch}'`
__OPENMCT_BUILD_BRANCH__: `'${gitBranch}'`,
__VUE_OPTIONS_API__: true, // enable/disable Options API support, default: true
__VUE_PROD_DEVTOOLS__: false // enable/disable devtools support in production, default: false
}),
new VueLoaderPlugin(),
new CopyWebpackPlugin({
@@ -115,7 +115,7 @@ const config = {
new webpack.BannerPlugin({
test: /.*Theme\.css$/,
raw: true,
banner: '@charset "UTF-8";',
banner: '@charset "UTF-8";'
})
],
module: {
@@ -142,10 +142,7 @@ const config = {
options: {
compilerOptions: {
hoistStatic: false,
whitespace: 'preserve',
compatConfig: {
MODE: 2
}
whitespace: 'preserve'
}
}
},

180
API.md
View File

@@ -94,6 +94,9 @@ well as assets such as html, css, and images necessary for the UI.
## Starting an Open MCT application
> [!WARNING]
> Open MCT provides a development server via `webpack-dev-server` (`npm start`). **This should be used for development purposes only and should never be deployed to a production environment**.
To start a minimally functional Open MCT application, it is necessary to
include the Open MCT distributable, enable some basic plugins, and bootstrap
the application. The tutorials walk through the process of getting Open MCT up
@@ -590,35 +593,108 @@ MinMax queries are issued by plots, and may be issued by other types as well. T
#### Telemetry Formats
Telemetry format objects define how to interpret and display telemetry data.
They have a simple structure:
They have a simple structure, provided here as a TypeScript interface:
- `key`: A `string` that uniquely identifies this formatter.
- `format`: A `function` that takes a raw telemetry value, and returns a
human-readable `string` representation of that value. It has one required
argument, and three optional arguments that provide context and can be used
for returning scaled representations of a value. An example of this is
representing time values in a scale such as the time conductor scale. There
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
useful representations of time given the provided limitations.
- `value`: The raw telemetry value in its native type.
- `minValue`: An **optional** argument specifying the minimum displayed
value.
- `maxValue`: An **optional** argument specifying the maximum displayed
value.
- `count`: An **optional** argument specifying the number of displayed
values.
- `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.
- `validate`: A `function` that takes a `string` representation of a telemetry
value, and returns a `boolean` value indicating whether the provided string
can be parsed.
```ts
interface Formatter {
key: string; // A string that uniquely identifies this formatter.
format: (
value: any, // The raw telemetry value in its native type.
minValue?: number, // An optional argument specifying the minimum displayed value.
maxValue?: number, // An optional argument specifying the maximum displayed value.
count?: number // An optional argument specifying the number of displayed values.
) => string; // Returns a human-readable string representation of the provided value.
parse: (
value: string | any // A string representation of a telemetry value or an already-parsed value.
) => any; // Returns the value in its native type. This function should be idempotent.
validate: (value: string) => boolean; // Takes a string representation of a telemetry value and returns a boolean indicating whether the provided string can be parsed.
}
```
##### Built-in Formats
Open MCT on its own defines a handful of built-in formats:
###### **Number Format (default):**
Applied to data with `format: 'number'`
```js
valueMetadata = {
format: 'number'
// ...
};
```
```ts
interface NumberFormatter extends Formatter {
parse: (x: any) => number;
format: (x: number) => string;
validate: (value: any) => boolean;
}
```
###### **String Format**:
Applied to data with `format: 'string'`
```js
valueMetadata = {
format: 'string'
// ...
};
```
```ts
interface StringFormatter extends Formatter {
parse: (value: any) => string;
format: (value: string) => string;
validate: (value: any) => boolean;
}
```
###### **Enum Format**:
Applied to data with `format: 'enum'`
```js
valueMetadata = {
format: 'enum',
enumerations: [
{
value: 1,
string: 'APPLE'
},
{
value: 2,
string: 'PEAR',
},
{
value: 3,
string: 'ORANGE'
}]
// ...
};
```
Creates a two-way mapping between enum string and value to be used in the `parse` and `format` methods.
Ex:
- `formatter.parse('APPLE') === 1;`
- `formatter.format(1) === 'APPLE';`
```ts
interface EnumFormatter extends Formatter {
parse: (value: string) => string;
format: (value: number) => string;
validate: (value: any) => boolean;
}
```
##### Registering Formats
Formats implement the following interface (provided here as TypeScript for simplicity):
Formats are registered with the Telemetry API using the `addFormat` function. eg.
``` javascript
```javascript
openmct.telemetry.addFormat({
key: 'number-to-string',
format: function (number) {
@@ -1228,3 +1304,61 @@ View provider Example:
}
}
```
## Visibility-Based Rendering in View Providers
To enhance performance and resource efficiency in OpenMCT, a visibility-based rendering feature has been added. This feature is designed to defer the execution of rendering logic for views that are not currently visible. It ensures that views are only updated when they are in the viewport, similar to how modern browsers handle rendering of inactive tabs but optimized for the OpenMCT tabbed display. It also works when views are scrolled outside the viewport (e.g., in a Display Layout).
### Overview
The show function is responsible for the rendering of a view. An [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) is used internally to determine whether the view is visible. This observer drives the visibility-based rendering feature, accessed via the `renderWhenVisible` function provided in the `viewOptions` parameter.
### Implementing Visibility-Based Rendering
The `renderWhenVisible` function is passed to the show function as part of the `viewOptions` object. This function can be used for all rendering logic that would otherwise be executed within a `requestAnimationFrame` call. When called, `renderWhenVisible` will either execute the provided function immediately (via `requestAnimationFrame`) if the view is currently visible, or defer its execution until the view becomes visible.
Additionally, `renderWhenVisible` returns a boolean value indicating whether the provided function was executed immediately (`true`) or deferred (`false`).
Monitoring of visibility begins after the first call to `renderWhenVisible` is made.
Heres the signature for the show function:
`show(element, isEditing, viewOptions)`
* `element` (HTMLElement) - The DOM element where the view should be rendered.
* `isEditing` (boolean) - Indicates whether the view is in editing mode.
* `viewOptions` (Object) - An object with configuration options for the view, including:
* `renderWhenVisible` (Function) - This function wraps the `requestAnimationFrame` and only triggers the provided render logic when the view is visible in the viewport.
### Example
An OpenMCT view provider might implement the show function as follows:
```js
// Define your view provider
const myViewProvider = {
// ... other properties and methods ...
show: function (element, isEditing, viewOptions) {
// Callback for rendering view content
const renderCallback = () => {
// Your view rendering logic goes here
};
// Use the renderWhenVisible function to ensure rendering only happens when view is visible
const wasRenderedImmediately = viewOptions.renderWhenVisible(renderCallback);
// Optionally handle the immediate rendering return value
if (wasRenderedImmediately) {
console.debug('🪞 Rendering triggered immediately as the view is visible.');
} else {
console.debug('🛑 Rendering has been deferred until the view becomes visible.');
}
}
};
```
Note that `renderWhenVisible` defers rendering while the view is not visible and caters to the latest execution call. This provides responsiveness for dynamic content while ensuring performance optimizations.
Ensure your view logic is prepared to handle potentially multiple deferrals if using this API, as only the last call to renderWhenVisible will be queued for execution upon the view becoming visible.

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) [![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) ![CodeQL](https://github.com/nasa/openmct/workflows/CodeQL/badge.svg)
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.
@@ -127,6 +127,8 @@ Each test suite generates a report in CircleCI. For a complete overview of testi
Our code coverage is generated during the runtime of our unit, e2e, and visual tests. The combination of those reports is published to [codecov.io](https://app.codecov.io/gh/nasa/openmct/)
For more on the specifics of our code coverage setup, [see](TESTING.md#code-coverage)
# Glossary
Certain terms are used throughout Open MCT with consistent meanings
@@ -182,3 +184,17 @@ You might still be using legacy API if your source code
### What should I do if I am using legacy API?
Please refer to [the modern Open MCT API](https://nasa.github.io/openmct/documentation/). Post any questions to the [Discussions section](https://github.com/nasa/openmct/discussions) of the Open MCT GitHub repository.
## Related Repos
> [!NOTE]
> Although Open MCT functions as a standalone project, it is primarily an extensible framework intended to be used as a dependency with users' own plugins and packaging. Furthermore, Open MCT is intended to be used with an HTTP server such as Apache or Nginx. A great example of hosting Open MCT with Apache is `openmct-quickstart` and can be found in the table below.
| Repository | Description |
| --- | --- |
| [openmct-tutorial](https://github.com/nasa/openmct-tutorial) | A great place for beginners to learn how to use and extend Open MCT. |
| [openmct-quickstart](https://github.com/scottbell/openmct-quickstart) | A working example of Open MCT integrated with Apache HTTP server, YAMCS telemetry, and Couch DB for persistence.
| [Open MCT YAMCS Plugin](https://github.com/akhenry/openmct-yamcs) | Plugin for integrating YAMCS telemetry and command server with Open MCT. |
| [openmct-performance](https://github.com/unlikelyzero/openmct-performance) | Resources for performance testing Open MCT. |
| [openmct-as-a-dependency](https://github.com/unlikelyzero/openmct-as-a-dependency) | An advanced guide for users on how to build, develop, and test Open MCT when it's used as a dependency. |

View File

@@ -37,14 +37,85 @@ Documentation located [here](./e2e/README.md)
## Code Coverage
* 100% statement coverage is achievable and desirable.
It's up to the individual developer as to whether they want to add line coverage in the form of a unit test or e2e test.
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.
Line Code Coverage is generated by our unit tests and e2e tests, then combined by ([Codecov.io Flags](https://docs.codecov.com/docs/flags)), and finally reported in GitHub PRs by Codecov.io's PR Bot. This workflow gives a comprehensive (if flawed) view of line coverage.
### Karma-istanbul
Line coverage is generated by our `karma-coverage-istanbul-reporter` package as defined in our `karma.conf.js` file:
```js
coverageIstanbulReporter: {
fixWebpackSourcePaths: true,
skipFilesWithNoCoverage: true,
dir: 'coverage/unit', //Sets coverage file to be consumed by codecov.io
reports: ['lcovonly']
},
```
Once the file is generated, it can be published to codecov with
```json
"cov:unit:publish": "codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit",
```
### e2e
The e2e line coverage is a bit more complex than the karma implementation. This is the general sequence of events:
1. Each e2e suite will start webpack with the ```npm run start:coverage``` command with config `webpack.coverage.js` and the `babel-plugin-istanbul` plugin to generate code coverage during e2e test execution using our custom [baseFixture](./baseFixtures.js).
1. During testcase execution, each e2e shard will generate its piece of the larger coverage suite. **This coverage file is not merged**. The raw coverage file is stored in a `.nyc_report` directory.
1. [nyc](https://github.com/istanbuljs/nyc) converts this directory into a `lcov` file with the following command `npm run cov:e2e:report`
1. Most of the tests are run in the '@stable' configuration and focus on chrome/ubuntu at a single resolution. This coverage is published to codecov with `npm run cov:e2e:stable:publish`.
1. The rest of our coverage only appears when run against `@unstable` tests, persistent datastore (couchdb), non-ubuntu machines, and non-chrome browsers with the `npm run cov:e2e:full:publish` flag. Since this happens about once a day, we have leveraged codecov.io's carryforward flag to report on lines covered outside of each commit on an individual PR.
This e2e coverage is combined with our unit test report to give a comprehensive (if flawed) view of line coverage.
### Limitations in our code coverage reporting
Our code coverage implementation has some known limitations:
- [Variability](https://github.com/nasa/openmct/issues/5811)
- [Accuracy](https://github.com/nasa/openmct/issues/7015)
- [Vue instrumentation gaps](https://github.com/nasa/openmct/issues/4973)
Our code coverage implementation has two known limitations:
- [Variability and accuracy](https://github.com/nasa/openmct/issues/5811)
- [Vue instrumentation](https://github.com/nasa/openmct/issues/4973)
## Troubleshooting CI
The following is an evolving guide to troubleshoot CI and PR issues.
### Github Checks failing
There are a few reasons that your GitHub PR could be failing beyond simple failed tests.
* Required Checks. We're leveraging required checks in GitHub so that we can quickly and precisely control what becomes and informational failure vs a hard requirement. The only way to determine the difference between a required vs information check is check for the `(Required)` emblem next to the step details in GitHub Checks.
* Not all required checks are run per commit. You may need to manually trigger addition GitHub checks with a `pr:<label>` label added to your PR.
### Flaky tests
There are two ways to know if a test on your branch is historically flaky:
1. `deploysentinel`'s PR comment bot to give an accurate and historical view of e2e flakiness. Check your PR for a view of the test failures and flakes (with link to the failing test). Note: only a 7 day window of flake is available.
2. (CircleCI's test insights feature)[https://circleci.com/blog/introducing-test-insights-with-flaky-test-detection/] collects historical data about the individual test results for both unit and e2e tests. Note: only a 14 day window of flake is available.
### Local=Pass and CI=Fail
Although rare, it is possible that your test can pass locally but fail in CI.
#### Busting Cache
In certain circumstances, the CircleCI cache can become stale. In order to bust the cache, we've implemented a runtime boolean parameter in Circle CI creatively name BUST_CACHE. To execute:
1. Navigate to the branch in Circle CI believed to have stale cache.
1. Click on the 'Trigger Pipeline' button.
1. Add Parameter -> Parameter Type = boolean , Name = BUST_CACHE ,Value = true
1. Click 'Trigger Pipeline'
#### Run tests in the same container as CI
In extreme cases, tests can fail due to the constraints of running within a container. To execute tests in exactly the same way as run in CircleCI.
```sh
// Replace {X.X.X} with the current Playwright version
// from our package.json or circleCI configuration file
docker run --rm --network host --cpus="2" -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v{X.X.X}-focal /bin/bash
npm install
```
At this point, you're running inside the same container and with 2 cpu cores. You can specify the unit tests:
```sh
npm run test
```
or e2e tests:
```sh
npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep <the testcase name>
```

View File

@@ -21,4 +21,8 @@ snapshot:
/* Embedded timestamp in notebooks */
.c-ne__embed__time{
opacity: 0 !important;
}
/* Time Conductor Start Time */
.c-compact-tc__setting-value{
opacity: 0 !important;
}

View File

@@ -21,4 +21,8 @@ snapshot:
/* Embedded timestamp in notebooks */
.c-ne__embed__time{
opacity: 0 !important;
}
/* Time Conductor Start Time */
.c-compact-tc__setting-value{
opacity: 0 !important;
}

View File

@@ -51,11 +51,13 @@ Next, you should walk through our implementation of Playwright in Open MCT:
## Types of e2e Testing
e2e testing describes the layer at which a test is performed without prescribing the assertions which are made. Generally, when writing an e2e test, we have three choices to make on an assertion strategy:
e2e testing describes the layer at which a test is performed without prescribing the assertions which are made. Generally, when writing an e2e test, we have five choices to make on an assertion strategy:
1. Functional - Verifies the functional correctness of the application. Sometimes interchanged with e2e or regression testing.
2. Visual - Verifies the "look and feel" of the application and can only detect _undesirable changes when compared to a previous baseline_.
3. Snapshot - Similar to Visual in that it captures the "look" of the application and can only detect _undesirable changes when compared to a previous baseline_. **Generally not preferred due to advanced setup necessary.**
4. Accessibility - Verifies that the application meets the accessibility standards defined by the [WCAG organization](https://www.w3.org/WAI/standards-guidelines/wcag/).
5. Performance - Verifies that application provides a performant experience. Like Snapshot testing, these tests are generally not recommended due to their difficulty in providing a consistent result.
When choosing between the different testing strategies, think only about the assertion that is made at the end of the series of test steps. "I want to verify that the Timer plugin functions correctly" vs "I want to verify that the Timer plugin does not look different than originally designed".
@@ -132,6 +134,35 @@ npm install
npm run test:e2e:updatesnapshots
```
## Automated Accessibility (a11y) Testing
Open MCT incorporates accessibility testing through two primary methods to ensure its compliance with accessibility standards:
1. **Usage of Playwright's Locator Strategy**: Open MCT utilizes Playwright's locator strategy, specifically the [page.getByRole('') function](https://playwright.dev/docs/api/class-framelocator#frame-locator-get-by-role), to ensure that web elements are accessible via assistive technologies. This approach focuses on the accessibility of elements rather than full adherence to a11y guidelines, which is covered in the second method.
2. **Enforcing a11y Guidelines with Playwright Axe Plugin**: To rigorously enforce a11y guideline compliance, Open MCT employs the [playwright axe plugin](https://playwright.dev/docs/accessibility-testing). This is achieved through the `scanForA11yViolations` function within the visual testing suite. This method not only benefits from the existing coverage of the visual tests but also targets specific a11y issues, such as `color-contrast` violations, which are particularly pertinent in the context of visual testing.
### a11y Standards (WCAG and Section 508)
Playwright axe supports a wide range of [WCAG Standards](https://playwright.dev/docs/accessibility-testing#scanning-for-wcag-violations) to test against. Open MCT is testing against the [Section 508](https://www.section508.gov/test/testing-overview/) accessibility guidelines with the intent to support higher standards over time. As of 2024, Section508 requirements now map completely to WCAG 2.0 AA. In the future, Section 508 requirements may map to WCAG 2.1 AA.
### Reading an a11y test failure
When an a11y test fails, the result must be interpreted in the html test report or the a11y report json artifact stored in the `/test-results/` folder. The json structure should be parsed for `"violations"` by `"id"` and identified `"target"`. Example provided for the 'color-contrast-enhanced' violation.
```json
"violations":
{
"id": "color-contrast-enhanced",
"impact": "serious",
"html": "<span class=\"label c-indicator__label\">0 Snapshots <button aria-label=\"Show Snapshots\">Show</button></span>",
"target": [
".s-status-off > .label.c-indicator__label"
],
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 6.51 (foreground color: #aaaaaa, background color: #262626, font size: 8.1pt (10.8px), font weight: normal). Expected contrast ratio of 7:1"
}
```
## Performance Testing
The open source performance tests function in three ways which match their naming and folder structure:
@@ -142,6 +173,8 @@ The open source performance tests function in three ways which match their namin
These tests are expected to become blocking and gating with assertions as we extend the capabilities of Playwright.
In addition to the explicit definition of performance tests, we also ensure that our test timeout timing is "tight" to catch performance regressions detectable by action timeouts. i.e. [Notebooks load much slower than they used to #6459](https://github.com/nasa/openmct/issues/6459)
## Test Architecture and CI
### Architecture
@@ -161,8 +194,8 @@ Our file structure follows the type of type of testing being excercised at the e
|`./tests/performance/` | Performance tests which should be run on every commit.|
|`./tests/performance/contract/` | A subset of performance tests which are designed to provide a contract between the open source tests which are run on every commit and the downstream tests which are run post merge and with other frameworks.|
|`./tests/performance/memory` | A subset of performance tests which are designed to test for memory leaks.|
|`./tests/visual/` | Visual tests.|
|`./tests/visual/component/` | Visual tests which are only run against a single component.|
|`./tests/visual-a11y/` | Visual tests and accessibility tests.|
|`./tests/visual-a11y/component/` | Visual and accessibility tests which are only run against a single component.|
|`./appActions.js` | Contains common methods which can be leveraged by test case authors to quickly move through the application when writing new tests.|
|`./baseFixture.js` | Contains base fixtures which only extend default `@playwright/test` functionality. The expectation is that these fixtures will be removed as the native Playwright API improves|
@@ -180,7 +213,7 @@ Open MCT is leveraging the [config file](https://playwright.dev/docs/test-config
|`./playwright-local.config.js` | Used when running locally|
|`./playwright-performance.config.js` | Used when running performance tests in CI or locally|
|`./playwright-performance-devmode.config.js` | Used when running performance tests in CI or locally|
|`./playwright-visual.config.js` | Used to run the visual tests in CI or locally|
|`./playwright-visual-a11y.config.js` | Used to run the visual and a11y tests in CI or locally|
#### Test Tags
@@ -191,9 +224,10 @@ Current list of test tags:
|Test Tag|Description|
|:-:|-|
|`@ipad` | Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no create button).|
|`@a11y` | Test case or test suite to execute playwright-axe accessibility checks and generate a11y reports.|
|`@gds` | Denotes a GDS Test Case used in the VIPER Mission.|
|`@addInit` | Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of `npm start`.|
|`@localStorage` | Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB).|
|`@localStorage` | Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB). See [note](#utilizing-localstorage)|
|`@snapshot` | Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of the playwright container.|
|`@unstable` | A new test or test which is known to be flaky.|
|`@2p` | Indicates that multiple users are involved, or multiple tabs/pages are used. Useful for testing multi-user interactivity.|
@@ -216,7 +250,7 @@ CircleCI
- Stable e2e tests against ubuntu and chrome
- Performance tests against ubuntu and chrome
- e2e tests are linted
- Visual tests are run in a single resolution on the default `espresso` theme
- Visual and a11y tests are run in a single resolution on the default `espresso` theme
#### 2. Per-Merge Testing
@@ -232,7 +266,7 @@ Nightly Testing in Circle CI
- Full e2e suite against ubuntu and chrome, firefox, and an MMOC resolution profile
- Performance tests against ubuntu and chrome
- CouchDB suite
- Visual Tests are run in the full profile
- Visual and a11y Tests are run in the full profile
Github Actions / Workflow
@@ -352,6 +386,28 @@ By adhering to this principle, we can create tests that are both robust and refl
1. Avoid repeated setup to test a single assertion. Write longer tests with multiple soft assertions.
This ensures that your changes will be picked up with large refactors.
##### Utilizing LocalStorage
1. In order to save test runtime in the case of tests that require a decent amount of initial setup (such as in the case of testing complex displays), you may use [Playwright's `storageState` feature](https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state) to generate and load localStorage states.
1. To generate a localStorage state to be used in a test:
- Add an e2e test to our generateLocalStorageData suite which sets the initial state (creating/configuring objects, etc.), saving it in the `test-data` folder:
```js
// Save localStorage for future test execution
await context.storageState({
path: path.join(__dirname, '../../../e2e/test-data/display_layout_with_child_layouts.json')
});
```
- Load the state from file at the beginning of the desired test suite (within the `test.describe()`). (NOTE: the storage state will be used for each test in the suite, so you may need to create a new suite):
```js
const LOCALSTORAGE_PATH = path.resolve(
__dirname,
'../../../../test-data/display_layout_with_child_layouts.json'
);
test.use({
storageState: path.resolve(__dirname, LOCALSTORAGE_PATH)
});
```
### How to write a great test
- Avoid using css locators to find elements to the page. Use modern web accessible locators like `getByRole`
@@ -383,7 +439,7 @@ By adhering to this principle, we can create tests that are both robust and refl
5. **Hide the Tree and Inspector**: Generally, your test will not require comparisons involving the tree and inspector. These aspects are covered in component-specific tests (explained below). To exclude them from the comparison by default, navigate to the root of the main view with the tree and inspector hidden:
- `await page.goto('./#/browse/mine?hideTree=true&hideInspector=true')`
6. **Component-Specific Tests**: If you wish to focus on a particular component, use the `/visual/component/` folder and limit the scope of the comparison to that component. For instance:
6. **Component-Specific Tests**: If you wish to focus on a particular component, use the `/visual-a11y/component/` folder and limit the scope of the comparison to that component. For instance:
```js
await percySnapshot(page, `Tree Pane w/ single level expanded (theme: ${theme})`, {
scope: treePane
@@ -468,15 +524,7 @@ Our e2e code coverage is captured and combined with our unit test coverage. For
#### Generating 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:
```npm run cov:e2e:report```
At this point, the nyc linecov report can be published to [codecov.io](https://about.codecov.io/) with the following command:
```npm run cov:e2e:stable:publish``` for the stable suite running in ubuntu.
or
```npm run cov:e2e:full:publish``` for the full suite running against all available platforms.
Please read more about our code coverage [here](../TESTING.md#code-coverage)
## Other
@@ -526,10 +574,10 @@ A single e2e test in Open MCT is extended to run:
- How is Open MCT extending default Playwright functionality?
- What about Component Testing?
### Troubleshooting
### e2e Troubleshooting
Please follow the general guide troubleshooting in [the general troubleshooting doc](../TESTING.md#troubleshooting-ci)
- Why is my test failing on CI and not locally?
- How can I view the failing tests on CI?
- 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:
```lsof -n -i4TCP:8080 | awk '{print$2}' | tail -1 | xargs kill -9```

View File

@@ -81,7 +81,7 @@ async function createDomainObjectWithDefaults(
await page.goto(`${parentUrl}`);
//Click the Create button
await page.click('button:has-text("Create")');
await page.getByRole('button', { name: 'Create' }).click();
// Click the object specified by 'type'
await page.click(`li[role='menuitem']:text("${type}")`);
@@ -108,7 +108,7 @@ async function createDomainObjectWithDefaults(
// Click OK button and wait for Navigate event
await Promise.all([
page.waitForLoadState(),
page.click('[aria-label="Save"]'),
await page.getByRole('button', { name: 'Save' }).click(),
// Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
@@ -120,8 +120,8 @@ async function createDomainObjectWithDefaults(
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();
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
}
return {
@@ -182,7 +182,7 @@ async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
await page.goto(`${parentUrl}`);
// Click the Create button
await page.click('button:has-text("Create")');
await page.getByRole('button', { name: 'Create' }).click();
// Click 'Plan' menu option
await page.click(`li:text("Plan")`);
@@ -231,7 +231,7 @@ async function createExampleTelemetryObject(page, parent = 'mine') {
await page.goto(`${parentUrl}`);
await page.locator('button:has-text("Create")').click();
await page.getByRole('button', { name: 'Create' }).click();
await page.locator('li:has-text("Sine Wave Generator")').click();

97
e2e/avpFixtures.js Normal file
View File

@@ -0,0 +1,97 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/**
* avpFixtures.js
*
* @file This module provides custom fixtures specifically tailored for Accessibility, Visual, and Performance (AVP) tests.
* These fixtures extend the base functionality of the Playwright fixtures and appActions, and are designed to be
* generalized across all plugins. They offer functionalities like scanning for accessibility violations, integrating
* with axe-core, and more.
*
* IMPORTANT NOTE: This fixture file is not intended to be extended further by other fixtures. If you find yourself
* needing to do so, please consult the documentation and consider creating a specialized fixture or modifying the
* existing ones.
*/
const fs = require('fs');
const path = require('path');
const { test, expect } = require('./pluginFixtures');
const AxeBuilder = require('@axe-core/playwright').default;
// Constants for repeated values
const TEST_RESULTS_DIR = './test-results';
/**
* Scans for accessibility violations on a page and writes a report to disk if violations are found.
* Automatically asserts that no violations should be present.
*
* @typedef {object} GenerateReportOptions
* @property {string} [reportName] - The name for the report file.
*
* @param {import('playwright').Page} page - The page object from Playwright.
* @param {string} testCaseName - The name of the test case.
* @param {GenerateReportOptions} [options={}] - The options for the report generation.
*
* @returns {Promise<object|null>} Returns the accessibility scan results if violations are found,
* otherwise returns null.
*/
/* eslint-disable no-undef */
exports.scanForA11yViolations = async function (page, testCaseName, options = {}) {
const builder = new AxeBuilder({ page });
builder.withTags(['wcag2aa']);
// https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md
builder.disableRules(['color-contrast']);
const accessibilityScanResults = await builder.analyze();
// Assert that no violations should be present
expect(
accessibilityScanResults.violations,
`Accessibility violations found in test case: ${testCaseName}`
).toEqual([]);
// Check if there are any violations
if (accessibilityScanResults.violations.length > 0) {
let reportName = options.reportName || testCaseName;
let sanitizedReportName = reportName.replace(/\//g, '_');
const reportPath = path.join(TEST_RESULTS_DIR, `${sanitizedReportName}.json`);
try {
if (!fs.existsSync(TEST_RESULTS_DIR)) {
fs.mkdirSync(TEST_RESULTS_DIR);
}
fs.writeFileSync(reportPath, JSON.stringify(accessibilityScanResults, null, 2));
console.log(`Accessibility report with violations saved successfully as ${reportPath}`);
return accessibilityScanResults;
} catch (err) {
console.error(`Error writing the accessibility report to file ${reportPath}:`, err);
throw err;
}
} else {
console.log('No accessibility violations found, no report generated.');
return null;
}
};
exports.expect = expect;
exports.test = test;

View File

@@ -0,0 +1,30 @@
/*****************************************************************************
* 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.ExampleDataVisualizationSourcePlugin());
openmct.install(
openmct.plugins.InspectorDataVisualization({ type: 'exampleDataVisualizationSource' })
);
});

View File

@@ -81,6 +81,30 @@ function activitiesWithinTimeBounds(start1, end1, start2, end2) {
);
}
/**
* Asserts that the swim lanes / groups in the plan view matches the order of
* groups in the plan data.
* @param {import('@playwright/test').Page} page the page
* @param {object} plan The raw plan json to assert against
* @param {string} objectUrl The URL of the object to assert against (plan or gantt chart)
*/
export async function assertPlanOrderedSwimLanes(page, plan, objectUrl) {
// Switch to the plan view
await page.goto(`${objectUrl}?view=plan.view`);
const planGroups = await page
.locator('.c-plan__contents > div > .c-swimlane__lane-label .c-object-label__name')
.all();
const groups = plan.Groups;
for (let i = 0; i < groups.length; i++) {
// Assert that the order of groups in the plan view matches the order of
// groups in the plan data
const groupName = await planGroups[i].innerText();
expect(groupName).toEqual(groups[i].name);
}
}
/**
* Navigate to the plan view, switch to fixed time mode,
* and set the bounds to span all activities.
@@ -110,3 +134,23 @@ export async function setDraftStatusForPlan(page, plan) {
await window.openmct.status.set(planObject.uuid, 'draft');
}, plan);
}
export async function addPlanGetInterceptor(page) {
await page.waitForLoadState('load');
await page.evaluate(async () => {
await window.openmct.objects.addGetInterceptor({
appliesTo: (identifier, domainObject) => {
return domainObject && domainObject.type === 'plan';
},
invoke: (identifier, object) => {
if (object) {
object.sourceMap = {
orderedGroups: 'Groups'
};
}
return object;
}
});
});
}

189
e2e/helper/plotTagsUtils.js Normal file
View File

@@ -0,0 +1,189 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { expect } from '../pluginFixtures';
const { waitForPlotsToRender } = require('../appActions');
/**
* Given a canvas and a set of points, tags the points on the canvas.
* @param {import('@playwright/test').Page} page
* @param {HTMLCanvasElement} canvas a telemetry item with a plot
* @param {Number} xEnd a telemetry item with a plot
* @param {Number} yEnd a telemetry item with a plot
* @returns {Promise}
*/
export async function createTags({ page, canvas, xEnd = 700, yEnd = 520 }) {
await canvas.hover({ trial: true });
//Alt+Shift Drag Start to select some points to tag
await page.keyboard.down('Alt');
await page.keyboard.down('Shift');
await canvas.dragTo(canvas, {
sourcePosition: {
x: 1,
y: 1
},
targetPosition: {
x: xEnd,
y: yEnd
}
});
//Alt Drag End
await page.keyboard.up('Alt');
await page.keyboard.up('Shift');
//Wait for canvas to stabilize.
await canvas.hover({ trial: true });
// add some tags
await page.getByText('Annotations').click();
await page.getByRole('button', { name: /Add Tag/ }).click();
await page.getByPlaceholder('Type to select tag').click();
await page.getByText('Driving').click();
await page.getByRole('button', { name: /Add Tag/ }).click();
await page.getByPlaceholder('Type to select tag').click();
await page.getByText('Science').click();
}
/**
* Given a telemetry item (e.g., a Sine Wave Generator) with a plot, tests that the plot can be tagged.
* @param {import('@playwright/test').Page} page
* @param {import('../../../../appActions').CreatedObjectInfo} telemetryItem a telemetry item with a plot
* @returns {Promise}
*/
export async function testTelemetryItem(page, telemetryItem) {
// Check that telemetry item also received the tag
await page.goto(telemetryItem.url);
await expect(page.getByText('No tags to display for this item')).toBeVisible();
const canvas = page.locator('canvas').nth(1);
//Wait for canvas to stabilize.
await waitForPlotsToRender(page);
await expect(canvas).toBeInViewport();
await canvas.hover({ trial: true });
// click on the tagged plot point
await canvas.click({
position: {
x: 100,
y: 100
}
});
await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Driving')).toBeHidden();
}
/**
* Given a page, tests that tags are searchable, deletable, and persist across reloads.
* @param {import('@playwright/test').Page} page
* @returns {Promise}
*/
export async function basicTagsTests(page) {
// Search for Driving
await page.getByRole('searchbox', { name: 'Search Input' }).click();
// Clicking elsewhere should cause annotation selection to be cleared
await expect(page.getByText('No tags to display for this item')).toBeVisible();
//
await page.getByRole('searchbox', { name: 'Search Input' }).fill('driv');
// Always click on the first Sine Wave result
await page
.getByLabel('Search Result')
.getByText(/Sine Wave/)
.first()
.click();
// Delete Driving Tag
await page.hover('[aria-label="Tag"]:has-text("Driving")');
await page.locator('[aria-label="Remove tag Driving"]').click();
// Search for Science Tag
await page.getByRole('searchbox', { name: 'Search Input' }).click();
await page.getByRole('searchbox', { name: 'Search Input' }).fill('sc');
//Expect Science Tag to be present and and Driving Tags to be deleted
await expect(page.getByLabel('Search Result').first()).toContainText('Science');
await expect(page.getByLabel('Search Result').first()).not.toContainText('Driving');
// Search for Driving Tag and expect nothing found
await page.getByRole('searchbox', { name: 'Search Input' }).click();
await page.getByRole('searchbox', { name: 'Search Input' }).fill('driv');
await expect(page.getByText('No results found')).toBeVisible();
await page.reload({ waitUntil: 'domcontentloaded' });
await waitForPlotsToRender(page);
//Navigate to the Inspector and check that all tags have been removed
await expect(page.getByRole('tab', { name: 'Annotations' })).not.toHaveClass(/is-current/);
await page.getByRole('tab', { name: 'Annotations' }).click();
await expect(page.getByRole('tab', { name: 'Annotations' })).toHaveClass(/is-current/);
await expect(page.getByText('No tags to display for this item')).toBeVisible();
const canvas = page.locator('canvas').nth(1);
// click on the tagged plot point
await canvas.click({
position: {
x: 100,
y: 100
}
});
//Expect Science to be visible but Driving to be hidden
await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Driving')).toBeHidden();
//Click elsewhere
await page.locator('body').click();
//Click on tagged plot point again
await canvas.click({
position: {
x: 100,
y: 100
}
});
// Add Driving Tag again
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();
//Science and Driving Tags should be visible
await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Driving')).toBeVisible();
// Delete Driving Tag again
await page.hover('[aria-label="Tag"]:has-text("Driving")');
await page.locator('[aria-label="Remove tag Driving"]').click();
//Science Tag should be visible and Driving Tag should be hidden
await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Driving')).toBeHidden();
}

View File

@@ -17,7 +17,7 @@ const config = {
command: 'npm run start:coverage',
url: 'http://localhost:8080/#',
timeout: 200 * 1000,
reuseExistingServer: false
reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.
},
maxFailures: MAX_FAILURES, //Limits failures to 5 to reduce CI Waste
workers: NUM_WORKERS, //Limit to 2 for CircleCI Agent

View File

@@ -5,7 +5,7 @@
/** @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/visual',
testDir: 'tests/visual-a11y',
testMatch: '**/*.visual.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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,54 @@
{
"Groups": [
{
"name": "Group 1"
},
{
"name": "Group 2"
}
],
"Group 2": [
{
"name": "Past event 3",
"start": 1660493208000,
"end": 1660503981000,
"type": "Group 2",
"color": "orange",
"textColor": "white"
},
{
"name": "Past event 4",
"start": 1660579608000,
"end": 1660624108000,
"type": "Group 2",
"color": "orange",
"textColor": "white"
},
{
"name": "Past event 5",
"start": 1660666008000,
"end": 1660681529000,
"type": "Group 2",
"color": "orange",
"textColor": "white"
}
],
"Group 1": [
{
"name": "Past event 1",
"start": 1660320408000,
"end": 1660343797000,
"type": "Group 1",
"color": "orange",
"textColor": "white"
},
{
"name": "Past event 2",
"start": 1660406808000,
"end": 1660429160000,
"type": "Group 1",
"color": "orange",
"textColor": "white"
}
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -55,6 +55,67 @@ test.describe('Generate Visual Test Data @localStorage @generatedata', () => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Generate display layout with 2 child display layouts', async ({ page, context }) => {
// Create Display Layout
const parent = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Parent Display Layout'
});
const child1 = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Child Layout 1',
parent: parent.uuid
});
const child2 = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Child Layout 2',
parent: parent.uuid
});
await page.goto(parent.url);
await page.getByLabel('Edit').click();
await page.getByLabel(`${child2.name} Layout Grid`).hover();
await page.getByLabel('Move Sub-object Frame').nth(1).click();
await page.getByLabel('X:').fill('30');
await page.getByLabel(`${child1.name} Layout Grid`).hover();
await page.getByLabel('Move Sub-object Frame').first().click();
await page.getByLabel('Y:').fill('30');
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
//Save localStorage for future test execution
await context.storageState({
path: path.join(__dirname, '../../../e2e/test-data/display_layout_with_child_layouts.json')
});
});
test('Generate flexible layout with 2 child display layouts', async ({ page, context }) => {
// Create Display Layout
const parent = await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout',
name: 'Parent Flexible Layout'
});
await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Child Layout 1',
parent: parent.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Child Layout 2',
parent: parent.uuid
});
await page.goto(parent.url);
//Save localStorage for future test execution
await context.storageState({
path: path.join(__dirname, '../../../e2e/test-data/flexible_layout_with_child_layouts.json')
});
});
// TODO: Visual test for the generated object here
// - Move to using appActions to create the overlay plot
// and embedded standard telemetry object

View File

@@ -0,0 +1,59 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
Verify that the "Clear Data" menu action performs as expected for various object types.
*/
const { test, expect } = require('../../pluginFixtures.js');
const { createDomainObjectWithDefaults } = require('../../appActions.js');
const backgroundImageSelector = '.c-imagery__main-image__background-image';
test.describe('Clear Data Action', () => {
test.beforeEach(async ({ page }) => {
// Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create a default 'Example Imagery' object
const exampleImagery = await createDomainObjectWithDefaults(page, { type: 'Example Imagery' });
// Verify that the created object is focused
await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name);
await page.locator('.c-imagery__main-image__bg').hover({ trial: true });
await expect(page.locator(backgroundImageSelector)).toBeVisible();
});
test('works as expected with Example Imagery', async ({ page }) => {
await expect(await page.locator('.c-thumb__image').count()).toBeGreaterThan(0);
// Click the "Clear Data" menu action
await page.getByTitle('More options').click();
const clearDataMenuItem = page.getByRole('menuitem', {
name: 'Clear Data'
});
await expect(clearDataMenuItem).toBeEnabled();
await clearDataMenuItem.click();
// Verify that the background image is no longer visible
await expect(page.locator(backgroundImageSelector)).toBeHidden();
await expect(await page.locator('.c-thumb__image').count()).toBe(0);
});
});

View File

@@ -149,7 +149,7 @@ test.describe('Move & link item tests', () => {
// 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();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Create New Folder Basic Domain Object
let folder = 'Test Folder';

View File

@@ -109,8 +109,7 @@ test.describe('Notification Overlay', () => {
// 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"]');
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Verify that Notification List is NOT open
expect(await page.locator('div[role="dialog"]').isVisible()).toBe(false);

View File

@@ -21,8 +21,13 @@
*****************************************************************************/
const { test } = require('../../../pluginFixtures');
const { createPlanFromJSON } = require('../../../appActions');
const { addPlanGetInterceptor } = require('../../../helper/planningUtils.js');
const testPlan1 = require('../../../test-data/examplePlans/ExamplePlan_Small1.json');
const { assertPlanActivities } = require('../../../helper/planningUtils');
const testPlanWithOrderedLanes = require('../../../test-data/examplePlans/ExamplePlanWithOrderedLanes.json');
const {
assertPlanActivities,
assertPlanOrderedSwimLanes
} = require('../../../helper/planningUtils');
test.describe('Plan', () => {
let plan;
@@ -36,4 +41,14 @@ test.describe('Plan', () => {
test('Displays all plan events', async ({ page }) => {
await assertPlanActivities(page, testPlan1, plan.url);
});
test('Displays plans with ordered swim lanes configuration', async ({ page }) => {
// Add configuration for swim lanes
await addPlanGetInterceptor(page);
// Create the plan
const planWithSwimLanes = await createPlanFromJSON(page, {
json: testPlanWithOrderedLanes
});
await assertPlanOrderedSwimLanes(page, testPlanWithOrderedLanes, planWithSwimLanes.url);
});
});

View File

@@ -19,7 +19,7 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* global __dirname */
/*
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets. Note: this
suite is sharing state between tests which is considered an anti-pattern. Implementing in this way to
@@ -31,6 +31,7 @@ const {
createDomainObjectWithDefaults,
createExampleTelemetryObject
} = require('../../../../appActions');
const path = require('path');
let conditionSetUrl;
let getConditionSetIdentifierFromUrl;
@@ -48,7 +49,9 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
await Promise.all([page.waitForNavigation(), page.click('button:has-text("OK")')]);
//Save localStorage for future test execution
await context.storageState({ path: './e2e/test-data/recycled_local_storage.json' });
await context.storageState({
path: path.resolve(__dirname, '../../../../test-data/recycled_local_storage.json')
});
//Set object identifier from url
conditionSetUrl = page.url();
@@ -59,7 +62,9 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
});
//Load localStorage for subsequent tests
test.use({ storageState: './e2e/test-data/recycled_local_storage.json' });
test.use({
storageState: path.resolve(__dirname, '../../../../test-data/recycled_local_storage.json')
});
//Begin suite of tests again localStorage
test('Condition set object properties persist in main view and inspector @localStorage', async ({
@@ -117,7 +122,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
.nth(1)
.click();
// Click Save and Finish Editing Option
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
//Verify Main section reflects updated Name Property
await expect

View File

@@ -19,8 +19,9 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* global __dirname */
const { test, expect } = require('../../../../pluginFixtures');
const path = require('path');
const {
createDomainObjectWithDefaults,
setStartOffset,
@@ -29,6 +30,88 @@ const {
setIndependentTimeConductorBounds
} = require('../../../../appActions');
const LOCALSTORAGE_PATH = path.resolve(
__dirname,
'../../../../test-data/display_layout_with_child_layouts.json'
);
const TINY_IMAGE_BASE64 =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII';
test.describe('Display Layout Toolbar Actions @localStorage', () => {
const PARENT_DISPLAY_LAYOUT_NAME = 'Parent Display Layout';
const CHILD_DISPLAY_LAYOUT_NAME1 = 'Child Layout 1';
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await setRealTimeMode(page);
await page
.locator('a')
.filter({ hasText: 'Parent Display Layout Display Layout' })
.first()
.click();
await page.getByLabel('Edit').click();
});
test.use({
storageState: path.resolve(__dirname, LOCALSTORAGE_PATH)
});
test('can add/remove Text element to a single layout', async ({ page }) => {
const layoutObject = 'Text';
await test.step(`Add and remove ${layoutObject} from the parent's layout`, async () => {
await addAndRemoveDrawingObjectAndAssert(page, layoutObject, PARENT_DISPLAY_LAYOUT_NAME);
});
await test.step(`Add and remove ${layoutObject} from the child's layout`, async () => {
await addAndRemoveDrawingObjectAndAssert(page, layoutObject, CHILD_DISPLAY_LAYOUT_NAME1);
});
});
test('can add/remove Image to a single layout', async ({ page }) => {
const layoutObject = 'Image';
await test.step("Add and remove image element from the parent's layout", async () => {
expect(await page.getByLabel(`Move ${layoutObject} Frame`).count()).toBe(0);
await addLayoutObject(page, PARENT_DISPLAY_LAYOUT_NAME, layoutObject);
expect(await page.getByLabel(`Move ${layoutObject} Frame`).count()).toBe(1);
await removeLayoutObject(page, layoutObject);
expect(await page.getByLabel(`Move ${layoutObject} Frame`).count()).toBe(0);
});
await test.step("Add and remove image from the child's layout", async () => {
await addLayoutObject(page, CHILD_DISPLAY_LAYOUT_NAME1, layoutObject);
expect(await page.getByLabel(`Move ${layoutObject} Frame`).count()).toBe(1);
await removeLayoutObject(page, layoutObject);
expect(await page.getByLabel(`Move ${layoutObject} Frame`).count()).toBe(0);
});
});
test(`can add/remove Box to a single layout`, async ({ page }) => {
const layoutObject = 'Box';
await test.step(`Add and remove ${layoutObject} from the parent's layout`, async () => {
await addAndRemoveDrawingObjectAndAssert(page, layoutObject, PARENT_DISPLAY_LAYOUT_NAME);
});
await test.step(`Add and remove ${layoutObject} from the child's layout`, async () => {
await addAndRemoveDrawingObjectAndAssert(page, layoutObject, CHILD_DISPLAY_LAYOUT_NAME1);
});
});
test(`can add/remove Line to a single layout`, async ({ page }) => {
const layoutObject = 'Line';
await test.step(`Add and remove ${layoutObject} from the parent's layout`, async () => {
await addAndRemoveDrawingObjectAndAssert(page, layoutObject, PARENT_DISPLAY_LAYOUT_NAME);
});
await test.step(`Add and remove ${layoutObject} from the child's layout`, async () => {
await addAndRemoveDrawingObjectAndAssert(page, layoutObject, CHILD_DISPLAY_LAYOUT_NAME1);
});
});
test(`can add/remove Ellipse to a single layout`, async ({ page }) => {
const layoutObject = 'Ellipse';
await test.step(`Add and remove ${layoutObject} from the parent's layout`, async () => {
await addAndRemoveDrawingObjectAndAssert(page, layoutObject, PARENT_DISPLAY_LAYOUT_NAME);
});
await test.step(`Add and remove ${layoutObject} from the child's layout`, async () => {
await addAndRemoveDrawingObjectAndAssert(page, layoutObject, CHILD_DISPLAY_LAYOUT_NAME1);
});
});
test.fixme('Can switch view types of a single SWG in a layout', async ({ page }) => {});
test.fixme('Can merge multiple plots in a layout', async ({ page }) => {});
test.fixme('Can adjust stack order of a single object in a layout', async ({ page }) => {});
test.fixme('Can duplicate a single object in a layout', async ({ page }) => {});
});
test.describe('Display Layout', () => {
/** @type {import('../../../../appActions').CreatedObjectInfo} */
let sineWaveObject;
@@ -41,6 +124,7 @@ test.describe('Display Layout', () => {
type: 'Sine Wave Generator'
});
});
test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in real time', async ({
page
}) => {
@@ -64,7 +148,7 @@ test.describe('Display Layout', () => {
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();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Subscribe to the Sine Wave Generator data
// On getting data, check if the value found in the Display Layout is the most recent value
@@ -102,7 +186,7 @@ test.describe('Display Layout', () => {
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();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Subscribe to the Sine Wave Generator data
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
@@ -144,7 +228,7 @@ test.describe('Display Layout', () => {
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();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);
@@ -186,7 +270,7 @@ test.describe('Display Layout', () => {
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();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);
@@ -242,7 +326,7 @@ test.describe('Display Layout', () => {
await page.locator('div[title="Resize object width"] > input').fill('70');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
const startDate = '2021-12-30 01:01:00.000Z';
const endDate = '2021-12-30 01:11:00.000Z';
@@ -304,7 +388,7 @@ test.describe('Display Layout', () => {
await page.getByText('Overlay Plot').click();
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Time to inspect some network traffic
let networkRequests = [];
@@ -339,6 +423,59 @@ test.describe('Display Layout', () => {
});
});
async function addAndRemoveDrawingObjectAndAssert(page, layoutObject, DISPLAY_LAYOUT_NAME) {
expect(await page.getByLabel(layoutObject, { exact: true }).count()).toBe(0);
await addLayoutObject(page, DISPLAY_LAYOUT_NAME, layoutObject);
expect(
await page
.getByLabel(layoutObject, {
exact: true
})
.count()
).toBe(1);
await removeLayoutObject(page, layoutObject);
expect(await page.getByLabel(layoutObject, { exact: true }).count()).toBe(0);
}
/**
* Remove the first matching layout object from the layout
* @param {import('@playwright/test').Page} page
* @param {'Box' | 'Ellipse' | 'Line' | 'Text' | 'Image'} layoutObject
*/
async function removeLayoutObject(page, layoutObject) {
await page
.getByLabel(`Move ${layoutObject} Frame`, { exact: true })
.or(page.getByLabel(layoutObject, { exact: true }))
.first()
// eslint-disable-next-line playwright/no-force-option
.click({ force: true });
await page.getByTitle('Delete the selected object').click();
await page.getByRole('button', { name: 'OK' }).click();
}
/**
* Add a layout object to the specified layout
* @param {import('@playwright/test').Page} page
* @param {string} layoutName
* @param {'Box' | 'Ellipse' | 'Line' | 'Text' | 'Image'} layoutObject
*/
async function addLayoutObject(page, layoutName, layoutObject) {
await page.getByLabel(`${layoutName} Layout`, { exact: true }).click();
await page.getByText('Add Drawing Object').click();
await page
.getByRole('menuitem', {
name: layoutObject
})
.click();
if (layoutObject === 'Text') {
await page.getByRole('textbox', { name: 'Text' }).fill('Hello, Universe!');
await page.getByText('OK').click();
} else if (layoutObject === 'Image') {
await page.getByLabel('Image URL').fill(TINY_IMAGE_BASE64);
await page.getByText('OK').click();
}
}
/**
* Util for subscribing to a telemetry object by object identifier
* Limitations: Currently only works to return telemetry once to the node scope

View File

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

View File

@@ -19,12 +19,19 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* global __dirname */
const { test, expect } = require('../../../../pluginFixtures');
const {
createDomainObjectWithDefaults,
setIndependentTimeConductorBounds
} = require('../../../../appActions');
const path = require('path');
const LOCALSTORAGE_PATH = path.resolve(
__dirname,
'../../../../test-data/flexible_layout_with_child_layouts.json'
);
test.describe('Flexible Layout', () => {
let sineWaveObject;
@@ -81,7 +88,7 @@ test.describe('Flexible Layout', () => {
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();
await page.getByRole('listitem', { name: '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');
@@ -167,7 +174,7 @@ test.describe('Flexible Layout', () => {
// 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();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1);
@@ -198,7 +205,7 @@ test.describe('Flexible Layout', () => {
// 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();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1);
@@ -239,7 +246,7 @@ test.describe('Flexible Layout', () => {
await exampleImageryTreeItem.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();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// flip on independent time conductor
await setIndependentTimeConductorBounds(
@@ -257,3 +264,53 @@ test.describe('Flexible Layout', () => {
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
});
});
test.describe('Flexible Layout Toolbar Actions @localStorage', () => {
test.use({
storageState: path.resolve(__dirname, LOCALSTORAGE_PATH)
});
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page
.locator('a')
.filter({ hasText: 'Parent Flexible Layout Flexible Layout' })
.first()
.click();
await page.getByLabel('Edit').click();
});
test('Add/Remove Container', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7234'
});
expect(await page.getByRole('group', { name: 'Container' }).count()).toEqual(2);
await page.getByRole('group', { name: 'Container' }).nth(1).click();
await page.getByTitle('Add Container').click();
expect(await page.getByRole('group', { name: 'Container' }).count()).toEqual(3);
await page.getByTitle('Remove Container').click();
await expect(page.getByRole('dialog')).toHaveText(
'This action will permanently delete this container from this Flexible Layout. Do you want to continue?'
);
await page.getByRole('button', { name: 'OK' }).click();
expect(await page.getByRole('group', { name: 'Container' }).count()).toEqual(2);
});
test('Remove Frame', async ({ page }) => {
expect(await page.getByRole('group', { name: 'Frame' }).count()).toEqual(2);
await page.getByRole('group', { name: 'Child Layout 1' }).click();
await page.getByTitle('Remove Frame').click();
await expect(page.getByRole('dialog')).toHaveText(
'This action will remove this frame from this Flexible Layout. Do you want to continue?'
);
await page.getByRole('button', { name: 'OK' }).click();
expect(await page.getByRole('group', { name: 'Frame' }).count()).toEqual(1);
});
test('Columns/Rows Layout Toggle', async ({ page }) => {
await page.getByRole('group', { name: 'Container' }).nth(1).click();
expect(await page.locator('.c-fl--rows').count()).toEqual(0);
await page.getByTitle('Columns layout').click();
expect(await page.locator('.c-fl--rows').count()).toEqual(1);
await page.getByTitle('Rows layout').click();
expect(await page.locator('.c-fl--rows').count()).toEqual(0);
});
});

View File

@@ -25,7 +25,10 @@
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const {
createDomainObjectWithDefaults,
createExampleTelemetryObject
} = require('../../../../appActions');
const uuid = require('uuid').v4;
test.describe('Gauge', () => {
@@ -53,7 +56,7 @@ test.describe('Gauge', () => {
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();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Create another sine wave generator within the gauge
const swg2 = await createDomainObjectWithDefaults(page, {
@@ -133,4 +136,50 @@ test.describe('Gauge', () => {
// TODO: Verify changes in the UI
});
test('Gauge does not display NaN when data not available', async ({ page }) => {
// Create a Gauge
const gauge = await createDomainObjectWithDefaults(page, {
type: 'Gauge'
});
// Create a Sine Wave Generator in the Gauge with a loading delay
const swgWith5sDelay = await createExampleTelemetryObject(page, gauge.uuid);
await page.goto(swgWith5sDelay.url);
await page.getByTitle('More options').click();
await page.getByRole('menuitem', { name: /Edit Properties.../ }).click();
//Edit Example Telemetry Object to include 5s loading Delay
await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000');
await page.getByRole('button', { name: 'Save' }).click();
// Wait until the URL is updated
await page.waitForURL(`**/${gauge.uuid}/*`);
// Nav to the Gauge
await page.goto(gauge.url);
const gaugeNoDataText = await page.locator('.js-dial-current-value tspan').textContent();
expect(gaugeNoDataText).toBe('--');
});
test('Gauge enforces composition policy', async ({ page }) => {
// Create a Gauge
await createDomainObjectWithDefaults(page, {
type: 'Gauge',
name: 'Unnamed Gauge'
});
// Try to create a Folder into the Gauge. Should be disallowed.
await page.getByRole('button', { name: /Create/ }).click();
await page.getByRole('menuitem', { name: /Folder/ }).click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
await page.getByLabel('Cancel').click();
// Try to create a Display Layout into the Gauge. Should be disallowed.
await page.getByRole('button', { name: /Create/ }).click();
await page.getByRole('menuitem', { name: /Display Layout/ }).click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
});
});

View File

@@ -247,6 +247,14 @@ test.describe('Example Imagery Object', () => {
await page.mouse.click(canvasCenterX - 50, canvasCenterY - 50);
await expect(page.getByText('Driving')).toBeVisible();
await expect(page.getByText('Science')).toBeVisible();
// add another tag and expect it to appear without changing selection
await page.getByRole('button', { name: /Add Tag/ }).click();
await page.getByPlaceholder('Type to select tag').click();
await page.getByText('Drilling').click();
await expect(page.getByText('Driving')).toBeVisible();
await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Drilling')).toBeVisible();
});
test('Can use + - buttons to zoom on the image @unstable', async ({ page }) => {

View File

@@ -0,0 +1,69 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* global __dirname */
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const path = require('path');
test.describe('Testing numeric data with inspector data visualization (i.e., data pivoting)', () => {
test.beforeEach(async ({ page }) => {
// eslint-disable-next-line no-undef
await page.addInitScript({
path: path.join(__dirname, '../../../../helper/', 'addInitDataVisualization.js')
});
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Can click on telemetry and see data in inspector', async ({ page }) => {
const exampleDataVisualizationSource = await createDomainObjectWithDefaults(page, {
type: 'Example Data Visualization Source'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'First Sine Wave Generator',
parent: exampleDataVisualizationSource.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Second Sine Wave Generator',
parent: exampleDataVisualizationSource.uuid
});
await page.goto(exampleDataVisualizationSource.url);
await page.getByRole('tab', { name: 'Data Visualization' }).click();
await page.getByRole('cell', { name: /First Sine Wave Generator/ }).click();
await expect(page.getByText('Numeric Data')).toBeVisible();
await expect(
page.locator('span.plot-series-name', { hasText: 'First Sine Wave Generator Hz' })
).toBeVisible();
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
await page.getByRole('cell', { name: /Second Sine Wave Generator/ }).click();
await expect(page.getByText('Numeric Data')).toBeVisible();
await expect(
page.locator('span.plot-series-name', { hasText: 'Second Sine Wave Generator Hz' })
).toBeVisible();
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
});
});

View File

@@ -25,21 +25,24 @@ const {
createDomainObjectWithDefaults,
setStartOffset,
setFixedTimeMode,
setRealTimeMode
setRealTimeMode,
openObjectTreeContextMenu
} = require('../../../../appActions');
test.describe('Testing LAD table configuration', () => {
let ladTable;
let sineWaveObject;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create LAD table
const ladTable = await createDomainObjectWithDefaults(page, {
ladTable = await createDomainObjectWithDefaults(page, {
type: 'LAD Table',
name: 'Test LAD Table'
});
// Create Sine Wave Generator
await createDomainObjectWithDefaults(page, {
sineWaveObject = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Test Sine Wave Generator',
parent: ladTable.uuid
@@ -50,24 +53,28 @@ test.describe('Testing LAD table configuration', () => {
test('in edit mode, LAD Tables provide ability to hide columns', async ({ page }) => {
// 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('role=treeitem[name=/Test Sine Wave Generator/]', '.c-lad-table-wrapper');
// select configuration tab in inspector
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
// make sure headers are visible initially
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
// hide timestamp column
await page.getByLabel('Timestamp').uncheck();
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
// hide units & type column
await page.getByLabel('Units').uncheck();
@@ -75,14 +82,35 @@ test.describe('Testing LAD table configuration', () => {
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
// hide WATCH column
await page.getByLabel('WATCH').uncheck();
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
// save and reload and verify they columns are still hidden
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.reload();
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
// Edit LAD table
await page.locator('[title="Edit"]').click();
@@ -93,33 +121,99 @@ test.describe('Testing LAD table configuration', () => {
await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
// save and reload and make sure only timestamp is still visible
// save and reload and make sure timestamp is still visible
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.reload();
await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
// Edit LAD table
await page.locator('[title="Edit"]').click();
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
// show units and type columns
// show units, type, and WATCH columns
await page.getByLabel('Units').check();
await page.getByLabel('Type').check();
await page.getByLabel('WATCH').check();
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
// save and reload and make sure all columns are still visible
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.reload();
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
});
test('When adding something without Units, do not show Units column', async ({ page }) => {
// Create Sine Wave Generator
await createDomainObjectWithDefaults(page, {
type: 'Event Message Generator',
parent: ladTable.uuid
});
await page.goto(ladTable.url);
// Edit LAD table
await page.getByLabel('Edit').click();
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
// make sure Sine Wave headers are visible initially too
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
// save and reload and verify they columns are still hidden
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Remove Sin Wave Generator
openObjectTreeContextMenu(page, sineWaveObject.url);
await page.getByRole('menuitem', { name: /Remove/ }).click();
await page.getByRole('button', { name: 'OK' }).click();
// Ensure Units & Limit columns are gone
// as Event Generator don't have them
await page.goto(ladTable.url);
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeHidden();
});
test("LAD Tables don't allow selection of rows but does show context click menus", async ({
@@ -171,7 +265,7 @@ test.describe('Testing LAD table @unstable', () => {
// 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();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Subscribe to the Sine Wave Generator data
// On getting data, check if the value found in the LAD table is the most recent value
@@ -199,7 +293,7 @@ test.describe('Testing LAD table @unstable', () => {
// 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();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Subscribe to the Sine Wave Generator data
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);

View File

@@ -32,12 +32,25 @@ const path = require('path');
const NOTEBOOK_NAME = 'Notebook';
test.describe('Notebook CRUD Operations', () => {
test.fixme('Can create a Notebook Object', async ({ page }) => {
test.beforeEach(async ({ page }) => {
//Navigate to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Can create a Notebook Object', async ({ page }) => {
//Create domain object
await createDomainObjectWithDefaults(page, {
type: NOTEBOOK_NAME
});
//Newly created notebook should have one Section and one page, 'Unnamed Section'/'Unnamed Page'
const notebookSectionNames = page.locator('.c-notebook__sections .c-list__item__name');
const notebookPageNames = page.locator('.c-notebook__pages .c-list__item__name');
await expect(notebookSectionNames).toBeHidden();
await expect(notebookPageNames).toBeHidden();
await expect(notebookSectionNames).toHaveText('Unnamed Section');
await expect(notebookPageNames).toHaveText('Unnamed Page');
});
test.fixme('Can update a Notebook Object', async ({ page }) => {});
test.fixme('Can view a perviously created Notebook Object', async ({ page }) => {});
test.fixme('Can view a previously created Notebook Object', async ({ page }) => {});
test.fixme('Can Delete a Notebook Object', async ({ page }) => {
// Other than non-persistable objects
});

View File

@@ -19,12 +19,13 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* global __dirname */
/*
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks.
*/
const fs = require('fs').promises;
const path = require('path');
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
@@ -176,7 +177,9 @@ test.describe('Snapshot image tests', () => {
});
test('Can drop an image onto a notebook and create a new entry', async ({ page }) => {
const imageData = await fs.readFile('src/images/favicons/favicon-96x96.png');
const imageData = await fs.readFile(
path.resolve(__dirname, '../../../../../src/images/favicons/favicon-96x96.png')
);
const imageArray = new Uint8Array(imageData);
const fileData = Array.from(imageArray);
@@ -201,14 +204,17 @@ test.describe('Snapshot image tests', () => {
// drop another image onto the entry
await page.dispatchEvent('.c-snapshots', 'drop', { dataTransfer: dropTransfer });
const secondThumbnail = page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).nth(1);
await secondThumbnail.waitFor({ state: 'attached' });
// expect two embedded images now
expect(await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).count()).toBe(2);
await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More options').click();
await page.getByRole('menuitem', { name: /Remove This Embed/ }).click();
await page.getByRole('button', { name: 'Ok', exact: true }).click();
// Ensure that the thumbnail is removed before we assert
await secondThumbnail.waitFor({ state: 'detached' });
// expect one embedded image now as we deleted the other
expect(await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).count()).toBe(1);

View File

@@ -224,4 +224,22 @@ test.describe('Tagging in Notebooks @addInit', () => {
// Verify the AutoComplete field is hidden
await expect(page.locator('[placeholder="Type to select tag"]')).toBeHidden();
});
test('Can start to add a tag, click away, and add a tag', async ({ page }) => {
await createNotebookEntryAndTags(page);
await page.getByRole('tab', { name: 'Annotations' }).click();
// Click on the body simulating a click outside the autocomplete)
await page.locator('body').click();
await page.locator(`[aria-label="Notebook Entry"]`).click();
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
// Click inside the tag search input
await page.locator('[placeholder="Type to select tag"]').click();
// Select the "Driving" tag
await page.locator('[aria-label="Autocomplete Options"] >> text=Drilling').click();
await expect(page.getByLabel('Notebook Entries').getByText('Drilling')).toBeVisible();
});
});

View File

@@ -68,7 +68,7 @@ test.describe('Autoscale', () => {
// save
await page.click('button[title="Save"]');
await Promise.all([
page.locator('li[title = "Save and Finish Editing"]').click(),
page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);

View File

@@ -214,7 +214,7 @@ async function saveOverlayPlot(page) {
.click();
await Promise.all([
page.locator('text=Save and Finish Editing').click(),
page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);

View File

@@ -105,7 +105,7 @@ test.describe('Overlay Plot', () => {
// Save (exit edit mode)
await page.locator('button[title="Save"]').click();
await page.locator('li[title="Save and Finish Editing"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await assertLimitLinesExistAndAreVisible(page);
@@ -127,7 +127,7 @@ test.describe('Overlay Plot', () => {
// Save (exit edit mode)
await page.locator('button[title="Save"]').click();
await page.locator('li[title="Save and Finish Editing"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await assertLimitLinesExistAndAreVisible(page);

View File

@@ -57,7 +57,7 @@ test.describe('Scatter Plot', () => {
await page.getByRole('tab', { name: 'Elements' }).click();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible();
await saveButton.click();
await page.locator('li[title="Save and Finish Editing"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Create another sine wave generator within the scatter plot
const swg2 = await createDomainObjectWithDefaults(page, {

View File

@@ -135,7 +135,7 @@ test.describe('Stacked Plot', () => {
// Save (exit edit mode)
await page.locator('button[title="Save"]').click();
await page.locator('li[title="Save and Finish Editing"]').click();
await page.getByRole('listitem', { name: '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}`);
@@ -243,7 +243,7 @@ test.describe('Stacked Plot', () => {
// Save (exit edit mode)
await page.locator('button[title="Save"]').click();
await page.locator('li[title="Save and Finish Editing"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await assertAggregateLegendIsVisible(page);

View File

@@ -25,6 +25,11 @@ Tests to verify plot tagging functionality.
*/
const { test, expect } = require('../../../../pluginFixtures');
const {
basicTagsTests,
createTags,
testTelemetryItem
} = require('../../../../helper/plotTagsUtils');
const {
createDomainObjectWithDefaults,
setRealTimeMode,
@@ -33,140 +38,6 @@ const {
} = require('../../../../appActions');
test.describe('Plot Tagging', () => {
/**
* Given a canvas and a set of points, tags the points on the canvas.
* @param {import('@playwright/test').Page} page
* @param {HTMLCanvasElement} canvas a telemetry item with a plot
* @param {Number} xEnd a telemetry item with a plot
* @param {Number} yEnd a telemetry item with a plot
* @returns {Promise}
*/
async function createTags({ page, canvas, xEnd = 700, yEnd = 520 }) {
await canvas.hover({ trial: true });
//Alt+Shift Drag Start to select some points to tag
await page.keyboard.down('Alt');
await page.keyboard.down('Shift');
await canvas.dragTo(canvas, {
sourcePosition: {
x: 1,
y: 1
},
targetPosition: {
x: xEnd,
y: yEnd
}
});
//Alt Drag End
await page.keyboard.up('Alt');
await page.keyboard.up('Shift');
//Wait for canvas to stabilize.
await canvas.hover({ trial: true });
// add some tags
await page.getByText('Annotations').click();
await page.getByRole('button', { name: /Add Tag/ }).click();
await page.getByPlaceholder('Type to select tag').click();
await page.getByText('Driving').click();
await page.getByRole('button', { name: /Add Tag/ }).click();
await page.getByPlaceholder('Type to select tag').click();
await page.getByText('Science').click();
}
/**
* Given a telemetry item (e.g., a Sine Wave Generator) with a plot, tests that the plot can be tagged.
* @param {import('@playwright/test').Page} page
* @param {import('../../../../appActions').CreatedObjectInfo} telemetryItem a telemetry item with a plot
* @returns {Promise}
*/
async function testTelemetryItem(page, telemetryItem) {
// Check that telemetry item also received the tag
await page.goto(telemetryItem.url);
await expect(page.getByText('No tags to display for this item')).toBeVisible();
const canvas = page.locator('canvas').nth(1);
//Wait for canvas to stabilize.
await waitForPlotsToRender(page);
await expect(canvas).toBeInViewport();
await canvas.hover({ trial: true });
// click on the tagged plot point
await canvas.click({
position: {
x: 100,
y: 100
}
});
await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Driving')).toBeHidden();
}
/**
* Given a page, tests that tags are searchable, deletable, and persist across reloads.
* @param {import('@playwright/test').Page} page
* @returns {Promise}
*/
async function basicTagsTests(page) {
// Search for Driving
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Clicking elsewhere should cause annotation selection to be cleared
await expect(page.getByText('No tags to display for this item')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('driv');
// click on the search result
await page
.getByRole('searchbox', { name: 'OpenMCT Search' })
.getByText(/Sine Wave/)
.first()
.click();
// Delete Driving
await page.hover('[aria-label="Tag"]:has-text("Driving")');
await page.locator('[aria-label="Remove tag Driving"]').click();
// Search for Science
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
await expect(page.locator('[aria-label="Search Result"]').nth(0)).toContainText('Science');
await expect(page.locator('[aria-label="Search Result"]').nth(0)).not.toContainText('Drilling');
// Search for Driving
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('driv');
await expect(page.getByText('No results found')).toBeVisible();
//Reload Page
await page.reload({ waitUntil: 'domcontentloaded' });
// wait for plots to load
await waitForPlotsToRender(page);
await expect(page.getByRole('tab', { name: 'Annotations' })).not.toHaveClass(/is-current/);
await page.getByRole('tab', { name: 'Annotations' }).click();
await expect(page.getByRole('tab', { name: 'Annotations' })).toHaveClass(/is-current/);
await expect(page.getByText('No tags to display for this item')).toBeVisible();
const canvas = page.locator('canvas').nth(1);
// click on the tagged plot point
await canvas.click({
position: {
x: 100,
y: 100
}
});
await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Driving')).toBeHidden();
}
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
@@ -221,19 +92,17 @@ test.describe('Plot Tagging', () => {
// set to real time mode
await setRealTimeMode(page);
// Search for Science
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
// click on the search result
await page
.getByRole('searchbox', { name: 'OpenMCT Search' })
.getByText('Alpha Sine Wave')
.first()
.click();
// wait for plots to load
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
// Search for Science Tag
await page.getByRole('searchbox', { name: 'Search Input' });
await page.getByRole('searchbox', { name: 'Search Input' }).fill('sc');
// Click on the search object result
await page.getByLabel('OpenMCT Search').getByText('Alpha Sine Wave').first().click();
await waitForPlotsToRender(page);
// expect plot to be paused
await expect(page.locator('[title="Resume displaying real-time data"]')).toBeVisible();
await expect(page.getByTitle('Resume displaying real-time data')).toBeVisible();
await setFixedTimeMode(page);
});

View File

@@ -0,0 +1,88 @@
/*****************************************************************************
* 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 { createDomainObjectWithDefaults } = require('../../../../appActions');
const { test, expect } = require('../../../../pluginFixtures');
test.describe('Tabs View', () => {
test('Renders tabbed elements', async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const tabsView = await createDomainObjectWithDefaults(page, {
type: 'Tabs View'
});
const table = await createDomainObjectWithDefaults(page, {
type: 'Telemetry Table',
parent: tabsView.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Event Message Generator',
parent: table.uuid
});
const notebook = await createDomainObjectWithDefaults(page, {
type: 'Notebook',
parent: tabsView.uuid
});
const sineWaveGenerator = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: tabsView.uuid
});
page.goto(tabsView.url);
// select first tab
await page.getByLabel(`${table.name} tab`).click();
// ensure table header visible
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
// no canvas (i.e., sine wave generator) in the document should be visible
await expect(page.locator('canvas')).toBeHidden();
// select second tab
await page.getByLabel(`${notebook.name} tab`).click();
// ensure notebook visible
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
// no canvas (i.e., sine wave generator) in the document should be visible
await expect(page.locator('canvas')).toBeHidden();
// select third tab
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
// expect sine wave generator visible
await expect(page.locator('.c-plot')).toBeVisible();
// expect two canvases (i.e., overlay & main canvas for sine wave generator) to be visible
await expect(page.locator('canvas')).toHaveCount(2);
await expect(page.locator('canvas').nth(0)).toBeVisible();
await expect(page.locator('canvas').nth(1)).toBeVisible();
// now try to select the first tab again
await page.getByLabel(`${table.name} tab`).click();
// ensure table header visible
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
// no canvas (i.e., sine wave generator) in the document should be visible
await expect(page.locator('canvas')).toBeHidden();
});
});

View File

@@ -78,4 +78,85 @@ test.describe('Telemetry Table', () => {
const endBoundMilliseconds = Date.parse(endDate);
expect(latestMilliseconds).toBeLessThanOrEqual(endBoundMilliseconds);
});
test('Supports filtering telemetry by regular text search', async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
await createDomainObjectWithDefaults(page, {
type: 'Event Message Generator',
parent: table.uuid
});
// focus the Telemetry Table
await page.goto(table.url);
await page.getByRole('searchbox', { name: 'message filter input' }).click();
await page.getByRole('searchbox', { name: 'message filter input' }).fill('Roger');
let cells = await page.getByRole('cell', { name: /Roger/ }).all();
// ensure we've got more than one cell
expect(cells.length).toBeGreaterThan(1);
// ensure the text content of each cell contains the search term
for (const cell of cells) {
const text = await cell.textContent();
expect(text).toContain('Roger');
}
await page.getByRole('searchbox', { name: 'message filter input' }).click();
await page.getByRole('searchbox', { name: 'message filter input' }).fill('Dodger');
cells = await page.getByRole('cell', { name: /Dodger/ }).all();
// ensure we've got more than one cell
expect(cells.length).toBe(0);
// ensure the text content of each cell contains the search term
for (const cell of cells) {
const text = await cell.textContent();
expect(text).not.toContain('Dodger');
}
// Click pause button
await page.click('button[title="Pause"]');
});
test('Supports filtering using Regex', async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
await createDomainObjectWithDefaults(page, {
type: 'Event Message Generator',
parent: table.uuid
});
// focus the Telemetry Table
page.goto(table.url);
await page.getByRole('searchbox', { name: 'message filter input' }).hover();
await page.getByLabel('Message filter header').getByRole('button', { name: '/R/' }).click();
await page.getByRole('searchbox', { name: 'message filter input' }).click();
await page.getByRole('searchbox', { name: 'message filter input' }).fill('/[Rr]oger/');
let cells = await page.getByRole('cell', { name: /Roger/ }).all();
// ensure we've got more than one cell
expect(cells.length).toBeGreaterThan(1);
// ensure the text content of each cell contains the search term
for (const cell of cells) {
const text = await cell.textContent();
expect(text).toContain('Roger');
}
await page.getByRole('searchbox', { name: 'message filter input' }).click();
await page.getByRole('searchbox', { name: 'message filter input' }).fill('/[Dd]oger/');
cells = await page.getByRole('cell', { name: /Dodger/ }).all();
// ensure we've got more than one cell
expect(cells.length).toBe(0);
// ensure the text content of each cell contains the search term
for (const cell of cells) {
const text = await cell.textContent();
expect(text).not.toContain('Dodger');
}
// Click pause button
await page.click('button[title="Pause"]');
});
});

View File

@@ -77,19 +77,19 @@ test.describe('Grand Search', () => {
// Click [aria-label="OpenMCT Search"] a >> nth=0
await page.locator('[aria-label="Search Result"] >> nth=0').click();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeInViewport();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('foo');
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toBeInViewport();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
await page
.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();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
// Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"]
@@ -198,6 +198,32 @@ test.describe('Grand Search', () => {
await expect(searchResultDropDown).toContainText('Clock A');
});
test('Slowly typing after search debounce will abort requests @couchdb', async ({ page }) => {
let requestWasAborted = false;
await createObjectsForSearch(page);
page.on('requestfailed', (request) => {
// check if the request was aborted
if (request.failure().errorText === 'net::ERR_ABORTED') {
requestWasAborted = true;
}
});
// Intercept and delay request
const delayInMs = 100;
await page.route('**', async (route, request) => {
await new Promise((resolve) => setTimeout(resolve, delayInMs));
route.continue();
});
// Slowly type after search delay
const searchInput = page.getByRole('searchbox', { name: 'Search Input' });
await searchInput.pressSequentially('Clock', { delay: 200 });
await expect(page.getByText('Clock B').first()).toBeVisible();
expect(requestWasAborted).toBe(true);
});
test('Validate multiple objects in search results return partial matches', async ({ page }) => {
test.info().annotations.push({
type: 'issue',

View File

@@ -89,20 +89,6 @@ test.describe('Verify tooltips', () => {
await expandEntireTree(page);
});
// LAD Tables - DONE
// Expanded collapsed plot legend - DONE
// Object Labels - DONE
// Display Layout headers - DONE
// Flexible Layout headers - DONE
// Tab View layout headers - DONE
// Search - DONE
// Gauge -
// Notebook Embed - DONE
// Telemetry Table -
// Timeline Objects
// Tree - DONE
// Recent Objects
test('display correct paths for LAD tables', async ({ page, openmctConfig }) => {
// Create LAD table
await createDomainObjectWithDefaults(page, {
@@ -117,7 +103,7 @@ test.describe('Verify tooltips', () => {
await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.c-lad-table-wrapper');
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-lad-table-wrapper');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.keyboard.down('Control');
@@ -147,7 +133,7 @@ test.describe('Verify tooltips', () => {
await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.gl-plot');
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.gl-plot');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.keyboard.down('Control');
@@ -214,7 +200,7 @@ test.describe('Verify tooltips', () => {
await page.locator('[title="Edit"]').click();
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.gl-plot');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Create Stacked Plot
await createDomainObjectWithDefaults(page, {
@@ -225,7 +211,7 @@ test.describe('Verify tooltips', () => {
await page.locator('[title="Edit"]').click();
await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.c-plot--stacked.holder');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Create Display Layout
await createDomainObjectWithDefaults(page, {
@@ -245,7 +231,7 @@ test.describe('Verify tooltips', () => {
targetPosition: { x: 500, y: 200 }
});
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.keyboard.down('Control');
@@ -254,13 +240,13 @@ test.describe('Verify tooltips', () => {
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe('My Items / Test Overlay Plot');
// await page.keyboard.up('Control');
// await page.locator('.c-plot-legend__view-control >> nth=0').click();
// await page.keyboard.down('Control');
// await page.locator('.plot-wrapper-expanded-legend .plot-series-name').first().hover();
// tooltipText = await page.locator('.c-tooltip').textContent();
// tooltipText = tooltipText.replace('\n', '').trim();
// expect(tooltipText).toBe(sineWaveObject1.path);
await page.keyboard.up('Control');
await page.locator('.c-plot-legend__view-control >> nth=0').click();
await page.keyboard.down('Control');
await page.locator('.plot-wrapper-expanded-legend .plot-series-name').first().hover();
tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject1.path);
await page.getByText('Test Stacked Plot').nth(2).hover();
tooltipText = await page.locator('.c-tooltip').textContent();
@@ -283,7 +269,7 @@ test.describe('Verify tooltips', () => {
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-fl__container >> nth=1');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.keyboard.down('Control');
await page.getByText('SWG 1').nth(2).hover();
@@ -307,7 +293,7 @@ test.describe('Verify tooltips', () => {
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-tabs-view__tabs-holder');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.keyboard.down('Control');
await page.getByText('SWG 1').nth(2).hover();
@@ -345,18 +331,18 @@ test.describe('Verify tooltips', () => {
expect(tooltipText).toBe(sineWaveObject3.path);
});
test('display path for source telemetry when hovering over gauge', ({ page }) => {
expect(true).toBe(true);
// await createDomainObjectWithDefaults(page, {
// type: 'Gauge',
// name: 'Test Gauge'
// });
// await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-gauge__wrapper');
// await page.keyboard.down('Control');
// await page.locator('.c-gauge__current-value-text-wrapper').hover();
// let tooltipText = await page.locator('.c-tooltip').textContent();
// tooltipText = tooltipText.replace('\n', '').trim();
// expect(tooltipText).toBe(sineWaveObject3.path);
test('display path for source telemetry when hovering over gauge', async ({ page }) => {
await createDomainObjectWithDefaults(page, {
type: 'Gauge',
name: 'Test Gauge'
});
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-gauge__wrapper');
await page.keyboard.down('Control');
// eslint-disable-next-line playwright/no-force-option
await page.locator('.c-gauge.c-dial').hover({ position: { x: 0, y: 0 }, force: true });
let tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject3.path);
});
test('display tooltip path for notebook embeds', async ({ page }) => {
@@ -373,26 +359,105 @@ test.describe('Verify tooltips', () => {
expect(tooltipText).toBe(sineWaveObject3.path);
});
// test('display tooltip path for telemetry table names', async ({ page }) => {
// await setEndOffset(page, { secs: '10' });
// await createDomainObjectWithDefaults(page, {
// type: 'Telemetry Table',
// name: 'Test Telemetry Table'
// });
test('display tooltip path for telemetry table names', async ({ page }) => {
// set endBound to 10 seconds after start bound
const url = await page.url();
const parsedUrl = new URL(url.replace('#', '!'));
const startBound = Number(parsedUrl.searchParams.get('tc.startBound'));
const tenSecondsInMilliseconds = 10 * 1000;
const endBound = startBound + tenSecondsInMilliseconds;
parsedUrl.searchParams.set('tc.endBound', endBound);
await page.goto(parsedUrl.href.replace('!', '#'));
// await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-telemetry-table');
// await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-telemetry-table');
await createDomainObjectWithDefaults(page, {
type: 'Telemetry Table',
name: 'Test Telemetry Table'
});
// await page.locator('button[title="Save"]').click();
// await page.locator('text=Save and Finish Editing').click();
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-telemetry-table');
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-telemetry-table');
// // .c-telemetry-table__body
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.keyboard.down('Control');
// await page.keyboard.down('Control');
await page.locator('.noselect > [title="SWG 3"]').first().hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject3.path);
// await page.locator('.noselect > [title="SWG 3"]').first().hover();
// let tooltipText = await page.locator('.c-tooltip').textContent();
// tooltipText = tooltipText.replace('\n', '').trim();
// expect(tooltipText).toBe(sineWaveObject3.path);
// });
await page.locator('.noselect > [title="SWG 1"]').first().hover();
tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject1.path);
});
test('display tooltip path for recently viewed items', async ({ page }) => {
// drag up Recently Viewed pane
await page
.locator('.l-pane.l-pane--vertical-handle-before', {
hasText: 'Recently Viewed'
})
.locator('.l-pane__handle')
.hover();
await page.mouse.down();
await page.mouse.move(0, 300);
await page.mouse.up();
await page.keyboard.down('Control');
await page.getByLabel('Recent Objects').getByText(sineWaveObject3.name).hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject3.path);
await page.getByLabel('Recent Objects').getByText(sineWaveObject2.name).hover();
tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject2.path);
await page.getByLabel('Recent Objects').getByText(sineWaveObject1.name).hover();
tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject1.path);
});
test('display tooltip path for time strips', async ({ page }) => {
// Create Time Strip
await createDomainObjectWithDefaults(page, {
type: 'Time Strip',
name: 'Test Time Strip'
});
// Edit Overlay Plot
await page.locator('[title="Edit"]').click();
await page.dragAndDrop(
`text=${sineWaveObject1.name}`,
'.c-object-view.is-object-type-time-strip'
);
await page.dragAndDrop(
`text=${sineWaveObject2.name}`,
'.c-object-view.is-object-type-time-strip'
);
await page.dragAndDrop(
`text=${sineWaveObject3.name}`,
'.c-object-view.is-object-type-time-strip'
);
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.keyboard.down('Control');
await page.getByText(sineWaveObject1.name).nth(2).hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject1.path);
await page.getByText(sineWaveObject2.name).nth(2).hover();
tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject2.path);
await page.getByText(sineWaveObject3.name).nth(2).hover();
tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject3.path);
});
});

View File

@@ -19,10 +19,15 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* global __dirname */
const { test, expect } = require('@playwright/test');
const path = require('path');
const memoryLeakFilePath = 'e2e/test-data/memory-leak-detection.json';
const memoryLeakFilePath = path.resolve(
__dirname,
'../../../../e2e/test-data/memory-leak-detection.json'
);
/**
* Executes tests to verify that views are not leaking memory on navigation away. This sort of
* memory leak is generally caused by a failure to clean up registered listeners.
@@ -40,54 +45,127 @@ const memoryLeakFilePath = 'e2e/test-data/memory-leak-detection.json';
*
*/
const NAV_LEAK_TIMEOUT = 10 * 1000; // 10s
test.describe('Navigation memory leak is not detected in', () => {
test.beforeEach(async ({ page }) => {
// Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.locator('a:has-text("My Items")').click({
button: 'right'
});
await page
.getByRole('treeitem', {
name: /My Items/
})
.click({
button: 'right'
});
await page.locator('text=Import from JSON').click();
await page
.getByRole('menuitem', {
name: /Import from JSON/
})
.click();
// Upload memory-leak-detection.json
await page.setInputFiles('#fileElem', memoryLeakFilePath);
await page.locator('text=OK').click();
await page
.getByRole('button', {
name: 'Save'
})
.click();
await expect(page.locator('a:has-text("Memory Leak Detection")')).toBeVisible();
});
test('gauge', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'gauge-single-1hz-swg');
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('plan', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'plan-generated');
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('time list', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'time-list');
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('scatter', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'scatter-plot-single-1hz-swg');
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('graph', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'graph-single-1hz-swg');
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('gantt chart', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'gantt-chart');
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('clock', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'clock');
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('timer', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'timer-far-future');
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('web page (nasa.gov)', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'web-page');
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('Complex Display Layout', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'complex-display-layout');
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('plot view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'overlay-plot-single-1hz-swg', {
timeout: NAV_LEAK_TIMEOUT
});
const result = await navigateToObjectAndDetectMemoryLeak(page, 'overlay-plot-single-1hz-swg');
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('stacked plot view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'stacked-plot-single-1hz-swg', {
timeout: NAV_LEAK_TIMEOUT
});
const result = await navigateToObjectAndDetectMemoryLeak(page, 'stacked-plot-single-1hz-swg');
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('LAD table view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-single-1hz-swg', {
timeout: NAV_LEAK_TIMEOUT
});
const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-single-1hz-swg');
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('LAD table set', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-set-single-1hz-swg', {
timeout: NAV_LEAK_TIMEOUT
});
const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-set-single-1hz-swg');
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
@@ -96,10 +174,7 @@ test.describe('Navigation memory leak is not detected in', () => {
test('telemetry table view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'telemetry-table-single-1hz-swg',
{
timeout: NAV_LEAK_TIMEOUT
}
'telemetry-table-single-1hz-swg'
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
@@ -110,10 +185,7 @@ test.describe('Navigation memory leak is not detected in', () => {
test('notebook view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'notebook-memory-leak-detection-test',
{
timeout: NAV_LEAK_TIMEOUT
}
'notebook-memory-leak-detection-test'
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
@@ -121,13 +193,7 @@ test.describe('Navigation memory leak is not detected in', () => {
});
test('display layout of a single SWG alphanumeric', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'display-layout-single-1hz-swg',
{
timeout: NAV_LEAK_TIMEOUT
}
);
const result = await navigateToObjectAndDetectMemoryLeak(page, 'display-layout-single-1hz-swg');
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
@@ -136,10 +202,7 @@ test.describe('Navigation memory leak is not detected in', () => {
test('display layout of a single SWG plot', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'display-layout-single-overlay-plot',
{
timeout: NAV_LEAK_TIMEOUT
}
'display-layout-single-overlay-plot'
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
@@ -150,10 +213,7 @@ test.describe('Navigation memory leak is not detected in', () => {
test('example imagery view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'example-imagery-memory-leak-test',
{
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
}
'example-imagery-memory-leak-test'
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
@@ -163,10 +223,7 @@ test.describe('Navigation memory leak is not detected in', () => {
test('display layout of example imagery views', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'display-layout-images-memory-leak-test',
{
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
}
'display-layout-images-memory-leak-test'
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
@@ -178,10 +235,7 @@ test.describe('Navigation memory leak is not detected in', () => {
}) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'display-layout-simple-telemetry',
{
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
}
'display-layout-simple-telemetry'
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
@@ -191,10 +245,7 @@ test.describe('Navigation memory leak is not detected in', () => {
test('flexible layout with plots of swgs', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'flexible-layout-plots-memory-leak-test',
{
timeout: NAV_LEAK_TIMEOUT
}
'flexible-layout-plots-memory-leak-test'
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
@@ -204,10 +255,7 @@ test.describe('Navigation memory leak is not detected in', () => {
test('flexible layout of example imagery views', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'flexible-layout-images-memory-leak-test',
{
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
}
'flexible-layout-images-memory-leak-test'
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
@@ -217,10 +265,7 @@ test.describe('Navigation memory leak is not detected in', () => {
test('tabbed view of display layouts and time strips', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'tab-view-simple-memory-leak-test',
{
timeout: NAV_LEAK_TIMEOUT * 6 * 2 // 2 min
}
'tab-view-simple-memory-leak-test'
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
@@ -230,10 +275,7 @@ test.describe('Navigation memory leak is not detected in', () => {
test('time strip view of telemetry', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'time-strip-telemetry-memory-leak-test',
{
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
}
'time-strip-telemetry-memory-leak-test'
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
@@ -247,15 +289,12 @@ test.describe('Navigation memory leak is not detected in', () => {
* @returns
*/
async function navigateToObjectAndDetectMemoryLeak(page, objectName) {
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.getByRole('searchbox', { name: 'Search Input' }).click();
// Fill Search input
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill(objectName);
await page.getByRole('searchbox', { name: 'Search Input' }).fill(objectName);
//Search Result Appears and is clicked
await Promise.all([
page.locator(`div.c-gsearch-result__title:has-text("${objectName}")`).first().click(),
page.waitForNavigation()
]);
await page.getByText(objectName, { exact: true }).click();
// Register a finalization listener on the root node for the view. This tends to be the last thing to be
// garbage collected since it has either direct or indirect references to all resources used by the view. Therefore it's a pretty good proxy
@@ -273,8 +312,7 @@ test.describe('Navigation memory leak is not detected in', () => {
});
// Nav back to folder
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
await page.waitForNavigation();
await page.goto('./#/browse/mine');
// This next code block blocks until the finalization listener is called and the gcPromise resolved. This means that the root node for the view has been garbage collected.
// In the event that the root node is not garbage collected, the gcPromise will never resolve and the test will time out.

View File

@@ -0,0 +1,100 @@
/*****************************************************************************
* 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 { createDomainObjectWithDefaults, waitForPlotsToRender } = require('../../appActions');
const { test, expect } = require('../../pluginFixtures');
test.describe('Tabs View', () => {
test('Renders tabbed elements nicely', async ({ page }) => {
// Code to hook into the requestAnimationFrame function and log each call
let animationCalls = [];
await page.exposeFunction('logCall', (callCount) => {
animationCalls.push(callCount);
});
await page.addInitScript(() => {
const oldRequestAnimationFrame = window.requestAnimationFrame;
let callCount = 0;
window.requestAnimationFrame = function (callback) {
// eslint-disable-next-line no-undef
logCall(callCount++);
return oldRequestAnimationFrame(callback);
};
});
await page.goto('./', { waitUntil: 'domcontentloaded' });
const tabsView = await createDomainObjectWithDefaults(page, {
type: 'Tabs View'
});
const table = await createDomainObjectWithDefaults(page, {
type: 'Telemetry Table',
parent: tabsView.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Event Message Generator',
parent: table.uuid
});
const notebook = await createDomainObjectWithDefaults(page, {
type: 'Notebook',
parent: tabsView.uuid
});
const sineWaveGenerator = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: tabsView.uuid
});
page.goto(tabsView.url);
// select first tab
await page.getByLabel(`${table.name} tab`).click();
// ensure table header visible
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
// select second tab
await page.getByLabel(`${notebook.name} tab`).click();
// expect notebook visible
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
// select third tab
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
// ensure sine wave generator visible
expect(await page.locator('.c-plot').isVisible()).toBe(true);
// now select notebook and clear animation calls
await page.getByLabel(`${notebook.name} tab`).click();
animationCalls = [];
// expect notebook visible
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
const notebookAnimationCalls = animationCalls.length;
// select sine wave generator and clear animation calls
animationCalls = [];
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
// ensure sine wave generator visible
await waitForPlotsToRender(page);
// we should be calling animation frames
const sineWaveAnimationCalls = animationCalls.length;
expect(sineWaveAnimationCalls).toBeGreaterThanOrEqual(notebookAnimationCalls);
});
});

View File

@@ -25,6 +25,7 @@ Tests to verify plot tagging performance.
*/
const { test, expect } = require('../../pluginFixtures');
const { basicTagsTests, createTags, testTelemetryItem } = require('../../helper/plotTagsUtils');
const {
createDomainObjectWithDefaults,
setRealTimeMode,
@@ -33,135 +34,6 @@ const {
} = require('../../appActions');
test.describe('Plot Tagging Performance', () => {
/**
* Given a canvas and a set of points, tags the points on the canvas.
* @param {import('@playwright/test').Page} page
* @param {HTMLCanvasElement} canvas a telemetry item with a plot
* @param {Number} xEnd a telemetry item with a plot
* @param {Number} yEnd a telemetry item with a plot
* @returns {Promise}
*/
async function createTags({ page, canvas, xEnd = 700, yEnd = 520 }) {
await canvas.hover({ trial: true });
//Alt+Shift Drag Start to select some points to tag
await page.keyboard.down('Alt');
await page.keyboard.down('Shift');
await canvas.dragTo(canvas, {
sourcePosition: {
x: 1,
y: 1
},
targetPosition: {
x: xEnd,
y: yEnd
}
});
//Alt Drag End
await page.keyboard.up('Alt');
await page.keyboard.up('Shift');
//Wait for canvas to stabilize.
await canvas.hover({ trial: true });
// add some tags
await page.getByText('Annotations').click();
await page.getByRole('button', { name: /Add Tag/ }).click();
await page.getByPlaceholder('Type to select tag').click();
await page.getByText('Driving').click();
await page.getByRole('button', { name: /Add Tag/ }).click();
await page.getByPlaceholder('Type to select tag').click();
await page.getByText('Science').click();
}
/**
* Given a telemetry item (e.g., a Sine Wave Generator) with a plot, tests that the plot can be tagged.
* @param {import('@playwright/test').Page} page
* @param {import('../../../../appActions').CreatedObjectInfo} telemetryItem a telemetry item with a plot
* @returns {Promise}
*/
async function testTelemetryItem(page, telemetryItem) {
// Check that telemetry item also received the tag
await page.goto(telemetryItem.url);
await expect(page.getByText('No tags to display for this item')).toBeVisible();
const canvas = page.locator('canvas').nth(1);
//Wait for canvas to stabilize.
await canvas.hover({ trial: true });
// click on the tagged plot point
await canvas.click({
position: {
x: 100,
y: 100
}
});
await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Driving')).toBeHidden();
}
/**
* Given a page, tests that tags are searchable, deletable, and persist across reloads.
* @param {import('@playwright/test').Page} page
* @returns {Promise}
*/
async function basicTagsTests(page) {
// Search for Driving
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Clicking elsewhere should cause annotation selection to be cleared
await expect(page.getByText('No tags to display for this item')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('driv');
// click on the search result
await page
.getByRole('searchbox', { name: 'OpenMCT Search' })
.getByText(/Sine Wave/)
.first()
.click();
// Delete Driving
await page.hover('[aria-label="Tag"]:has-text("Driving")');
await page.locator('[aria-label="Remove tag Driving"]').click();
// Search for Science
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
await expect(page.locator('[aria-label="Search Result"]').nth(0)).toContainText('Science');
await expect(page.locator('[aria-label="Search Result"]').nth(0)).not.toContainText('Drilling');
// Search for Driving
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('driv');
await expect(page.getByText('No results found')).toBeVisible();
//Reload Page
await page.reload({ waitUntil: 'domcontentloaded' });
// wait for plots to load
await waitForPlotsToRender(page);
await page.getByText('Annotations').click();
await expect(page.getByText('No tags to display for this item')).toBeVisible();
const canvas = page.locator('canvas').nth(1);
// click on the tagged plot point
await canvas.click({
position: {
x: 100,
y: 100
}
});
await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Driving')).toBeHidden();
}
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
@@ -212,18 +84,15 @@ test.describe('Plot Tagging Performance', () => {
await setRealTimeMode(page);
// Search for Science
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
await page.getByRole('searchbox', { name: 'Search Input' });
await page.getByRole('searchbox', { name: 'Search Input' }).fill('sc');
// click on the search result
await page
.getByRole('searchbox', { name: 'OpenMCT Search' })
.getByText('Alpha Sine Wave')
.first()
.click();
// wait for plots to load
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
await page.getByLabel('Search Result').getByText('Alpha Sine Wave').first().click();
await waitForPlotsToRender(page);
// expect plot to be paused
await expect(page.locator('[title="Resume displaying real-time data"]')).toBeVisible();
await expect(page.getByTitle('Resume displaying real-time data')).toBeVisible();
await setFixedTimeMode(page);
});

View File

@@ -0,0 +1,33 @@
/*****************************************************************************
* 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, scanForA11yViolations } = require('../../avpFixtures');
const VISUAL_URL = require('../../constants').VISUAL_URL;
test.describe('a11y - Default @a11y', () => {
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
});
test('main view @a11y', async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});
});

View File

@@ -24,7 +24,7 @@
Tests the branding associated with the default deployment. At least the about modal for now
*/
const { test, expect } = require('../../../pluginFixtures');
const { test, expect, scanForA11yViolations } = require('../../../avpFixtures');
const percySnapshot = require('@percy/playwright');
const VISUAL_URL = require('../../../constants').VISUAL_URL;
@@ -50,4 +50,7 @@ test.describe('Visual - Branding', () => {
// Take a snapshot of the About modal
await percySnapshot(page, `About (theme: '${theme}')`);
});
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});
});

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { test } = require('../../../pluginFixtures.js');
const { test, scanForA11yViolations } = require('../../../avpFixtures');
const { VISUAL_URL, MISSION_TIME } = require('../../../constants.js');
const percySnapshot = require('@percy/playwright');
@@ -53,4 +53,7 @@ test.describe('Visual - Controlled Clock', () => {
scope: inspectorPane
});
});
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});
});

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { test } = require('../../../pluginFixtures.js');
const { test, scanForA11yViolations } = require('../../../avpFixtures');
const {
expandTreePaneItemByName,
createDomainObjectWithDefaults
@@ -95,4 +95,7 @@ test.describe('Visual - Tree Pane', () => {
scope: treePane
});
});
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});
});

View File

@@ -26,12 +26,12 @@ are only meant to run against openmct's app.js started by `npm run start` within
`./e2e/playwright-visual.config.js` file.
*/
const { test, expect } = require('../../pluginFixtures');
const { test, expect, scanForA11yViolations } = require('../../avpFixtures');
const percySnapshot = require('@percy/playwright');
const { createDomainObjectWithDefaults } = require('../../appActions');
const { VISUAL_URL } = require('../../constants');
test.describe('Visual - Default', () => {
test.describe('Visual - Default @a11y', () => {
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
});
@@ -98,4 +98,8 @@ test.describe('Visual - Default', () => {
// Take a snapshot of the newly created Gauge object
await percySnapshot(page, `Default Gauge (theme: '${theme}')`);
});
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});
});

View File

@@ -0,0 +1,159 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/**
* Defines playwright locators that can be used in tests.
* @typedef {Object} LayoutLocators
* @property {Object<string, import('@playwright/test').Locator>} LayoutLocator
*/
const { test, scanForA11yViolations } = require('../../avpFixtures');
const { createDomainObjectWithDefaults } = require('../../appActions');
const VISUAL_URL = require('../../constants').VISUAL_URL;
const percySnapshot = require('@percy/playwright');
const snapshotScope = '.l-shell__pane-main .l-pane__contents';
test.describe('Visual - Display Layout - @a11y', () => {
test('Resize Marquee surrounds selection', async ({ page, theme }) => {
const baseline = await setupBaseline(page);
const { child1LayoutLocator, child1LayoutObjectLocator } = baseline;
await percySnapshot(page, `Resize nested layout selected (theme: '${theme}')`, {
scope: snapshotScope
});
await child1LayoutLocator.click();
await percySnapshot(page, `Resize new nested layout selected (theme: '${theme}')`, {
scope: snapshotScope
});
await child1LayoutObjectLocator.click();
await percySnapshot(page, `Resize Object in nested layout selected (theme: '${theme}')`, {
scope: snapshotScope
});
});
test('Parent layout of selection displays grid', async ({ page, theme }) => {
const baseline = await setupBaseline(page);
const { parentLayoutLocator, child1LayoutObjectLocator } = baseline;
await percySnapshot(page, `Parent nested layout selected (theme: '${theme}')`, {
scope: snapshotScope
});
await parentLayoutLocator.click();
await percySnapshot(page, `Parent outer layout selected (theme: '${theme}')`, {
scope: snapshotScope
});
await child1LayoutObjectLocator.click();
await percySnapshot(page, `Parent Object in nested layout selected (theme: '${theme}')`, {
scope: snapshotScope
});
});
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});
});
/**
* Sets up a complex layout with nested layouts and provides the playwright locators
* @param {import('@playwright/test').Page} page
* @returns {LayoutLocators} locators of baseline complex display to be used in tests
*/
async function setupBaseline(page) {
// Load Open MCT visual test baseline
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
// Open Tree
await page.getByRole('button', { name: 'Browse' }).click();
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const objectViewLocator = page.locator('.c-object-view');
const parentLayoutLocator = objectViewLocator.first();
const child1LayoutLocator = parentLayoutLocator.locator(objectViewLocator).first();
const child1LayoutObjectLocator = child1LayoutLocator.locator('.l-layout__frame');
const parentLayout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Parent Layout'
});
const child1Layout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Child 1 Layout'
});
const child2Layout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Child 2 Layout'
});
const swg1 = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'SWG 1'
});
const swg2 = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'SWG 2'
});
const child1LayoutTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(child1Layout.name)
});
const child2LayoutTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(child2Layout.name)
});
const swg1TreeItem = treePane.getByRole('treeitem', {
name: new RegExp(swg1.name)
});
const swg2TreeItem = treePane.getByRole('treeitem', {
name: new RegExp(swg2.name)
});
// Expand folder containing created objects
await page.goto(parentLayout.url);
await page.getByTitle('Show selected item in tree').click();
// Add swg1 to child1Layout
await page.goto(child1Layout.url);
await page.getByRole('button', { name: 'Edit' }).click();
await swg1TreeItem.dragTo(parentLayoutLocator, { targetPosition: { x: 0, y: 0 } });
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Add swg1 to child1Layout
await page.goto(child2Layout.url);
await page.getByRole('button', { name: 'Edit' }).click();
await swg2TreeItem.dragTo(parentLayoutLocator, { targetPosition: { x: 0, y: 0 } });
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Add child1Layout and child2Layout to parentLayout
await page.goto(parentLayout.url);
await page.getByRole('button', { name: 'Edit' }).click();
await child1LayoutTreeItem.dragTo(parentLayoutLocator, { targetPosition: { x: 350, y: 0 } });
await child2LayoutTreeItem.dragTo(parentLayoutLocator, { targetPosition: { x: 0, y: 0 } });
return {
parentLayoutLocator,
child1LayoutLocator,
child1LayoutObjectLocator
};
}

View File

@@ -21,12 +21,12 @@
*****************************************************************************/
/* global __dirname */
const path = require('path');
const { test } = require('../../pluginFixtures');
const { test, scanForA11yViolations } = require('../../avpFixtures');
const percySnapshot = require('@percy/playwright');
const utils = require('../../helper/faultUtils');
test.describe('Fault Management Visual Tests', () => {
test.describe('Fault Management Visual Tests - @a11y', () => {
test('icon test', async ({ page, theme }) => {
await page.addInitScript({
path: path.join(__dirname, '../../helper/', 'addInitFaultManagementPlugin.js')
@@ -87,4 +87,7 @@ test.describe('Fault Management Visual Tests', () => {
`Selected faults highlight the ability to Acknowledge or Shelve above the fault list (theme: '${theme}')`
);
});
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});
});

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { expect, test } = require('../../pluginFixtures');
const { test, expect, scanForA11yViolations } = require('../../avpFixtures');
const percySnapshot = require('@percy/playwright');
const { createDomainObjectWithDefaults } = require('../../appActions');
const VISUAL_URL = require('../../constants').VISUAL_URL;
@@ -74,4 +74,7 @@ test.describe('Visual - LAD Table', () => {
`LAD Table w/ Sine Wave Generator columns expanded (theme: ${theme})`
);
});
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});
});

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { test } = require('../../pluginFixtures');
const { test, scanForA11yViolations } = require('../../avpFixtures');
const percySnapshot = require('@percy/playwright');
const { expandTreePaneItemByName, createDomainObjectWithDefaults } = require('../../appActions');
const {
@@ -31,7 +31,8 @@ const { VISUAL_URL } = require('../../constants');
test.describe('Visual - Restricted Notebook', () => {
test.beforeEach(async ({ page }) => {
await startAndAddRestrictedNotebookObject(page);
const restrictedNotebook = await startAndAddRestrictedNotebookObject(page);
await page.goto(restrictedNotebook.url + '?hideTree=true&hideInspector=true');
});
test('Restricted Notebook is visually correct @addInit', async ({ page, theme }) => {
@@ -58,7 +59,7 @@ test.describe('Visual - Notebook', () => {
name: 'Dropped Overlay Plot'
});
//Open Tree
//Open Tree to perform drag
await page.getByRole('button', { name: 'Browse' }).click();
await expandTreePaneItemByName(page, myItemsFolderName);
@@ -97,4 +98,36 @@ test.describe('Visual - Notebook', () => {
// Take snapshot of the notebook with the AutoComplete field hidden and with the "Add Tag" button visible
await percySnapshot(page, `Notebook Annotation de-select blur (theme: '${theme}')`);
});
test('Visual check of entry hover and selection', async ({ page, theme }) => {
// Make two entries so we can test an unselected entry
await enterTextEntry(page, 'Entry 0');
await enterTextEntry(page, 'Entry 1');
// Hover the first entry
await page.getByText('Entry 0').hover();
// Take a snapshot
await percySnapshot(page, `Notebook Non-selected Entry Hover (theme: '${theme}')`);
// Click the first entry
await page.getByText('Entry 0').click();
// Take a snapshot
await percySnapshot(page, `Notebook Selected Entry Hover (theme: '${theme}')`);
// Hover the text entry area
await page.getByText('Entry 0').hover();
// Take a snapshot
await percySnapshot(page, `Notebook Selected Entry Text Area Hover (theme: '${theme}')`);
// Click the text entry area
await page.getByText('Entry 0').click();
// Take a snapshot
await percySnapshot(page, `Notebook Selected Entry Text Area Active (theme: '${theme}')`);
});
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});
});

View File

@@ -24,12 +24,12 @@
* This test is dedicated to test notification banner functionality and its accessibility attributes.
*/
const { test, expect } = require('../../pluginFixtures');
const { test, expect, scanForA11yViolations } = require('../../avpFixtures');
const percySnapshot = require('@percy/playwright');
const { createDomainObjectWithDefaults } = require('../../appActions');
const VISUAL_URL = require('../../constants').VISUAL_URL;
test.describe("Visual - Check Notification Info Banner of 'Save successful'", () => {
test.describe("Visual - Check Notification Info Banner of 'Save successful' @a11y", () => {
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
});
@@ -59,4 +59,7 @@ test.describe("Visual - Check Notification Info Banner of 'Save successful'", ()
expect(await page.locator('div[role="dialog"]').isVisible()).toBe(false);
await percySnapshot(page, `Notification banner dismissed (theme: '${theme}')`);
});
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});
});

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { test } = require('../../pluginFixtures');
const { test, scanForA11yViolations } = require('../../avpFixtures');
const {
setBoundsToSpanAllActivities,
setDraftStatusForPlan
@@ -32,7 +32,7 @@ const examplePlanSmall = require('../../test-data/examplePlans/ExamplePlan_Small
const snapshotScope = '.l-shell__pane-main .l-pane__contents';
test.describe('Visual - Planning', () => {
test.describe('Visual - Planning @a11y', () => {
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
});
@@ -97,4 +97,7 @@ test.describe('Visual - Planning', () => {
scope: snapshotScope
});
});
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});
});

View File

@@ -24,14 +24,14 @@
This test suite is dedicated to tests which verify search functionality.
*/
const { test, expect } = require('../../pluginFixtures');
const { test, expect, scanForA11yViolations } = require('../../avpFixtures');
const { createDomainObjectWithDefaults } = require('../../appActions');
const { VISUAL_URL } = require('../../constants');
const percySnapshot = require('@percy/playwright');
test.describe('Grand Search', () => {
let clock;
test.describe('Grand Search @a11y', () => {
let conditionWidget;
let displayLayout;
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
@@ -41,9 +41,9 @@ test.describe('Grand Search', () => {
name: 'Visual Test Display Layout'
});
clock = await createDomainObjectWithDefaults(page, {
type: 'Clock',
name: 'Visual Test Clock',
conditionWidget = await createDomainObjectWithDefaults(page, {
type: 'Condition Widget',
name: 'Visual Condition Widget',
parent: displayLayout.uuid
});
});
@@ -52,29 +52,27 @@ test.describe('Grand Search', () => {
page,
theme
}) => {
const searchInput = page.getByRole('searchbox', { name: 'Search Input' });
const searchResults = page.getByRole('searchbox', { name: 'OpenMCT Search' });
// Navigate to display layout
await page.goto(displayLayout.url);
// Search for the clock object
await searchInput.click();
await searchInput.fill(clock.name);
await expect(searchResults.getByText('Visual Test Clock')).toBeVisible();
// Search for the object
await page.getByRole('searchbox', { name: 'Search Input' }).click();
await page.getByRole('searchbox', { name: 'Search Input' }).fill(conditionWidget.name);
await expect(page.getByLabel('Search Result').getByText(conditionWidget.name)).toBeVisible();
//Searching for an object returns that object in the grandsearch
await percySnapshot(page, `Searching for Clock Object (theme: '${theme}')`);
await percySnapshot(page, `Searching for Object (theme: '${theme}')`);
// Enter Edit mode on the Display Layout
await page.getByRole('button', { name: 'Edit' }).click();
// Navigate to the clock object while in edit mode on the display layout
await searchInput.click();
await searchResults.getByText('Visual Test Clock').click();
// Navigate to the object while in edit mode on the display layout
await page.getByRole('searchbox', { name: 'Search Input' }).click();
await page.getByLabel('Search Result').getByText(conditionWidget.name).click();
await percySnapshot(
page,
`Preview for clock should display when editing enabled and search item clicked (theme: '${theme}')`
`Preview should display when editing enabled and search item clicked (theme: '${theme}')`
);
// Close the preview
@@ -88,17 +86,20 @@ test.describe('Grand Search', () => {
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Search for the clock object
await searchInput.click();
await searchInput.fill(clock.name);
await expect(searchResults.getByText('Visual Test Clock')).toBeVisible();
// Search for the object
await page.getByRole('searchbox', { name: 'Search Input' }).click();
await page.getByRole('searchbox', { name: 'Search Input' }).fill(conditionWidget.name);
await expect(page.getByLabel('Search Result').getByText(conditionWidget.name)).toBeVisible();
// Navigate to the clock object while not in edit mode on the display layout
await searchResults.getByText('Visual Test Clock').click();
// Navigate to the object while not in edit mode on the display layout
await page.getByLabel('Search Result').getByText(conditionWidget.name).click();
await percySnapshot(
page,
`Clicking on search results should navigate to them if not editing (theme: '${theme}')`
);
});
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});
});

View File

@@ -0,0 +1,75 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import mount from 'utils/mount';
import ExampleDataVisualizationSource from './components/ExampleDataVisualizationSource.vue';
export default function ExampleDataVisualizationSourceViewProvider(openmct) {
return {
key: 'exampleDataVisualizationSource',
name: 'Example Data Visualization Source',
cssClass: 'icon-telemetry',
canView: function (domainObject) {
return domainObject.type === 'exampleDataVisualizationSource';
},
canEdit: function (domainObject) {
if (domainObject.type === 'exampleDataVisualizationSource') {
return true;
}
},
view: function (domainObject) {
let _destroy = null;
return {
show: function (element, isEditing) {
const { destroy } = mount(
{
el: element,
components: {
ExampleDataVisualizationSource
},
provide: {
openmct,
domainObject
},
template: '<example-data-visualization-source></example-data-visualization-source>'
},
{
app: openmct.app,
element
}
);
_destroy = destroy;
},
destroy: function () {
if (_destroy) {
_destroy();
}
}
};
},
priority: function () {
return 1;
}
};
}

View File

@@ -0,0 +1,128 @@
<!--
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.
-->
<template>
<div class="c-table c-list-view c-list-view--selectable">
<table class="c-table__body">
<thead class="c-table__header">
<tr>
<th>Name</th>
<th>Type</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in items"
:key="item.keyString"
class="c-list-item js-folder-child"
@click="selectItem(item, $event)"
>
<td class="c-list-item__name">
<a ref="objectLink" class="c-object-label">
<div
class="c-object-label__type-icon c-list-item__name__type-icon"
:class="item.type.cssClass"
></div>
<div class="c-object-label__name c-list-item__name__name">{{ item.model.name }}</div>
</a>
</td>
<td class="c-list-item__type">
{{ item.type.name }}
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
export default {
inject: ['openmct', 'domainObject'],
data() {
return {
items: []
};
},
mounted() {
this.composition = this.openmct.composition.get(this.domainObject);
this.keystring = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.composition.on('add', this.addedTelemetry);
this.composition.on('remove', this.removedTelemetry);
this.composition.load();
},
unmounted() {
this.composition.off('add', this.addedTelemetry);
this.composition.off('remove', this.removedTelemetry);
},
methods: {
selectItem(item, event) {
event.stopPropagation();
const bounds = this.openmct.time.getBounds();
const selection = [
{
element: this.$el,
context: {
dataVisualization: {
telemetryKeys: [item.objectKeyString],
description: {
text: item.model.name,
icon: item.type.cssClass
},
dataRanges: [
{
bounds
}
],
loading: false
},
item: this.domainObject
}
}
];
this.openmct.selection.select(selection, false);
},
addedTelemetry(child) {
const type = this.openmct.types.get(child.type) || {
definition: {
cssClass: 'icon-object-unknown',
name: 'Unknown Type'
}
};
this.items.push({
model: child,
type: type.definition,
isAlias: this.keystring !== child.location,
objectPath: [child].concat(this.openmct.router.path),
objectKeyString: this.openmct.objects.makeKeyString(child.identifier)
});
},
removedTelemetry(identifier) {
this.items = this.items.filter((i) => {
return (
i.model.identifier.key !== identifier.key ||
i.model.identifier.namespace !== identifier.namespace
);
});
}
}
};
</script>

View File

@@ -0,0 +1,46 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ExampleDataVisualizationSourceViewProvider from './ExampleDataVisualizationSourceViewProvider';
export default function () {
return function install(openmct) {
openmct.objectViews.addProvider(new ExampleDataVisualizationSourceViewProvider(openmct));
openmct.types.addType('exampleDataVisualizationSource', {
name: 'Example Data Visualization Source',
creatable: true,
description: 'An example data visualization source to be used with an inspector.',
cssClass: 'icon-telemetry',
initialize(domainObject) {
domainObject.composition = [];
}
});
openmct.composition.addPolicy((parent, child) => {
if (parent.type === 'exampleDataVisualizationSource') {
return Object.prototype.hasOwnProperty.call(child, 'telemetry');
} else {
return true;
}
});
};
}

View File

@@ -29,7 +29,8 @@ define(['./WorkerInterface'], function (WorkerInterface) {
randomness: 0,
phase: 0,
loadDelay: 0,
infinityValues: false
infinityValues: false,
exceedFloat32: false
};
function GeneratorProvider(openmct, StalenessProvider) {
@@ -53,7 +54,8 @@ define(['./WorkerInterface'], function (WorkerInterface) {
'randomness',
'phase',
'loadDelay',
'infinityValues'
'infinityValues',
'exceedFloat32'
];
request = request || {};

View File

@@ -85,7 +85,8 @@
data.offset,
data.phase,
data.randomness,
data.infinityValues
data.infinityValues,
data.exceedFloat32
),
wavelengths: wavelengths(),
intensities: intensities(),
@@ -96,7 +97,8 @@
data.offset,
data.phase,
data.randomness,
data.infinityValues
data.infinityValues,
data.exceedFloat32
)
}
});
@@ -136,6 +138,7 @@
var randomness = request.randomness;
var loadDelay = Math.max(request.loadDelay, 0);
var infinityValues = request.infinityValues;
var exceedFloat32 = request.exceedFloat32;
var step = 1000 / dataRateInHz;
var nextStep = start - (start % step) + step;
@@ -146,10 +149,28 @@
data.push({
utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, period, amplitude, offset, phase, randomness, infinityValues),
sin: sin(
nextStep,
period,
amplitude,
offset,
phase,
randomness,
infinityValues,
exceedFloat32
),
wavelengths: wavelengths(),
intensities: intensities(),
cos: cos(nextStep, period, amplitude, offset, phase, randomness, infinityValues)
cos: cos(
nextStep,
period,
amplitude,
offset,
phase,
randomness,
infinityValues,
exceedFloat32
)
});
}
@@ -176,9 +197,26 @@
});
}
function cos(timestamp, period, amplitude, offset, phase, randomness, infinityValues) {
if (infinityValues && Math.random() > 0.5) {
function cos(
timestamp,
period,
amplitude,
offset,
phase,
randomness,
infinityValues,
exceedFloat32
) {
if (infinityValues && exceedFloat32) {
if (Math.random() > 0.5) {
return Number.POSITIVE_INFINITY;
} else if (Math.random() < 0.01) {
return getRandomFloat32OverflowValue();
}
} else if (infinityValues && Math.random() > 0.5) {
return Number.POSITIVE_INFINITY;
} else if (exceedFloat32 && Math.random() < 0.01) {
return getRandomFloat32OverflowValue();
}
return (
@@ -188,9 +226,26 @@
);
}
function sin(timestamp, period, amplitude, offset, phase, randomness, infinityValues) {
if (infinityValues && Math.random() > 0.5) {
function sin(
timestamp,
period,
amplitude,
offset,
phase,
randomness,
infinityValues,
exceedFloat32
) {
if (infinityValues && exceedFloat32) {
if (Math.random() > 0.5) {
return Number.POSITIVE_INFINITY;
} else if (Math.random() < 0.01) {
return getRandomFloat32OverflowValue();
}
} else if (infinityValues && Math.random() > 0.5) {
return Number.POSITIVE_INFINITY;
} else if (exceedFloat32 && Math.random() < 0.01) {
return getRandomFloat32OverflowValue();
}
return (
@@ -200,6 +255,13 @@
);
}
// Values exceeding float32 range (Positive: 3.4+38, Negative: -3.4+38)
function getRandomFloat32OverflowValue() {
const sign = Math.random() > 0.5 ? 1 : -1;
return sign * 3.4e39;
}
function wavelengths() {
let values = [];
while (values.length < 5) {

View File

@@ -122,6 +122,13 @@ export default function (openmct) {
key: 'infinityValues',
property: ['telemetry', 'infinityValues']
},
{
name: 'Exceed Float32 Limits',
control: 'toggleSwitch',
cssClass: 'l-input',
key: 'exceedFloat32',
property: ['telemetry', 'exceedFloat32']
},
{
name: 'Provide Staleness Updates',
control: 'toggleSwitch',
@@ -140,6 +147,7 @@ export default function (openmct) {
randomness: 0,
loadDelay: 0,
infinityValues: false,
exceedFloat32: false,
staleness: false
};
}

View File

@@ -23,10 +23,8 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, shrink-to-fit=no"
/>
<!-- Modified viewport meta tag to improve accessibility -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<title>Open MCT</title>
<script src="dist/openmct.js"></script>
@@ -91,157 +89,157 @@
width: 100px;
}
</style>
<script defer>
const THIRTY_SECONDS = 30 * 1000;
const ONE_MINUTE = THIRTY_SECONDS * 2;
const FIVE_MINUTES = ONE_MINUTE * 5;
const FIFTEEN_MINUTES = FIVE_MINUTES * 3;
const THIRTY_MINUTES = FIFTEEN_MINUTES * 2;
const ONE_HOUR = THIRTY_MINUTES * 2;
const TWO_HOURS = ONE_HOUR * 2;
const ONE_DAY = ONE_HOUR * 24;
openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.example.Generator());
openmct.install(openmct.plugins.example.EventGeneratorPlugin());
openmct.install(openmct.plugins.example.ExampleImagery());
openmct.install(openmct.plugins.example.ExampleTags());
openmct.install(openmct.plugins.Espresso());
openmct.install(openmct.plugins.MyItems());
openmct.install(
openmct.plugins.PlanLayout({
creatable: true
})
);
openmct.install(openmct.plugins.Timeline());
openmct.install(openmct.plugins.Hyperlink());
openmct.install(openmct.plugins.UTCTimeSystem());
openmct.install(
openmct.plugins.AutoflowView({
type: 'telemetry.panel'
})
);
openmct.install(
openmct.plugins.DisplayLayout({
showAsView: ['summary-widget', 'example.imagery']
})
);
openmct.install(
openmct.plugins.Conductor({
menuOptions: [
{
name: 'Fixed',
timeSystem: 'utc',
bounds: {
start: Date.now() - THIRTY_MINUTES,
end: Date.now()
},
// commonly used bounds can be stored in history
// bounds (start and end) can accept either a milliseconds number
// or a callback function returning a milliseconds number
// a function is useful for invoking Date.now() at exact moment of preset selection
presets: [
{
label: 'Last Day',
bounds: {
start: () => Date.now() - ONE_DAY,
end: () => Date.now()
}
},
{
label: 'Last 2 hours',
bounds: {
start: () => Date.now() - TWO_HOURS,
end: () => Date.now()
}
},
{
label: 'Last hour',
bounds: {
start: () => Date.now() - ONE_HOUR,
end: () => Date.now()
}
}
],
// maximum recent bounds to retain in conductor history
records: 10
// maximum duration between start and end bounds
// for utc-based time systems this is in milliseconds
// limit: ONE_DAY
},
{
name: 'Realtime',
timeSystem: 'utc',
clock: 'local',
clockOffsets: {
start: -THIRTY_MINUTES,
end: THIRTY_SECONDS
},
presets: [
{
label: '1 Hour',
bounds: {
start: -ONE_HOUR,
end: THIRTY_SECONDS
}
},
{
label: '30 Minutes',
bounds: {
start: -THIRTY_MINUTES,
end: THIRTY_SECONDS
}
},
{
label: '15 Minutes',
bounds: {
start: -FIFTEEN_MINUTES,
end: THIRTY_SECONDS
}
},
{
label: '5 Minutes',
bounds: {
start: -FIVE_MINUTES,
end: THIRTY_SECONDS
}
},
{
label: '1 Minute',
bounds: {
start: -ONE_MINUTE,
end: THIRTY_SECONDS
}
}
]
}
]
})
);
openmct.install(openmct.plugins.SummaryWidget());
openmct.install(openmct.plugins.Notebook());
openmct.install(openmct.plugins.LADTable());
openmct.install(openmct.plugins.Filters(['table', 'telemetry.plot.overlay']));
openmct.install(openmct.plugins.ObjectMigration());
openmct.install(
openmct.plugins.ClearData(
['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked', 'example.imagery'],
{ indicator: true }
)
);
openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
openmct.install(openmct.plugins.Timer());
openmct.install(openmct.plugins.Timelist());
openmct.install(openmct.plugins.BarChart());
openmct.install(openmct.plugins.ScatterPlot());
document.addEventListener('DOMContentLoaded', function () {
openmct.start();
});
</script>
</head>
<body>
<div id="app"></div>
</body>
<script>
const THIRTY_SECONDS = 30 * 1000;
const ONE_MINUTE = THIRTY_SECONDS * 2;
const FIVE_MINUTES = ONE_MINUTE * 5;
const FIFTEEN_MINUTES = FIVE_MINUTES * 3;
const THIRTY_MINUTES = FIFTEEN_MINUTES * 2;
const ONE_HOUR = THIRTY_MINUTES * 2;
const TWO_HOURS = ONE_HOUR * 2;
const ONE_DAY = ONE_HOUR * 24;
openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.example.Generator());
openmct.install(openmct.plugins.example.EventGeneratorPlugin());
openmct.install(openmct.plugins.example.ExampleImagery());
openmct.install(openmct.plugins.example.ExampleTags());
openmct.install(openmct.plugins.Espresso());
openmct.install(openmct.plugins.MyItems());
openmct.install(
openmct.plugins.PlanLayout({
creatable: true
})
);
openmct.install(openmct.plugins.Timeline());
openmct.install(openmct.plugins.Hyperlink());
openmct.install(openmct.plugins.UTCTimeSystem());
openmct.install(
openmct.plugins.AutoflowView({
type: 'telemetry.panel'
})
);
openmct.install(
openmct.plugins.DisplayLayout({
showAsView: ['summary-widget', 'example.imagery']
})
);
openmct.install(
openmct.plugins.Conductor({
menuOptions: [
{
name: 'Fixed',
timeSystem: 'utc',
bounds: {
start: Date.now() - THIRTY_MINUTES,
end: Date.now()
},
// commonly used bounds can be stored in history
// bounds (start and end) can accept either a milliseconds number
// or a callback function returning a milliseconds number
// a function is useful for invoking Date.now() at exact moment of preset selection
presets: [
{
label: 'Last Day',
bounds: {
start: () => Date.now() - ONE_DAY,
end: () => Date.now()
}
},
{
label: 'Last 2 hours',
bounds: {
start: () => Date.now() - TWO_HOURS,
end: () => Date.now()
}
},
{
label: 'Last hour',
bounds: {
start: () => Date.now() - ONE_HOUR,
end: () => Date.now()
}
}
],
// maximum recent bounds to retain in conductor history
records: 10
// maximum duration between start and end bounds
// for utc-based time systems this is in milliseconds
// limit: ONE_DAY
},
{
name: 'Realtime',
timeSystem: 'utc',
clock: 'local',
clockOffsets: {
start: -THIRTY_MINUTES,
end: THIRTY_SECONDS
},
presets: [
{
label: '1 Hour',
bounds: {
start: -ONE_HOUR,
end: THIRTY_SECONDS
}
},
{
label: '30 Minutes',
bounds: {
start: -THIRTY_MINUTES,
end: THIRTY_SECONDS
}
},
{
label: '15 Minutes',
bounds: {
start: -FIFTEEN_MINUTES,
end: THIRTY_SECONDS
}
},
{
label: '5 Minutes',
bounds: {
start: -FIVE_MINUTES,
end: THIRTY_SECONDS
}
},
{
label: '1 Minute',
bounds: {
start: -ONE_MINUTE,
end: THIRTY_SECONDS
}
}
]
}
]
})
);
openmct.install(openmct.plugins.SummaryWidget());
openmct.install(openmct.plugins.Notebook());
openmct.install(openmct.plugins.LADTable());
openmct.install(openmct.plugins.Filters(['table', 'telemetry.plot.overlay']));
openmct.install(openmct.plugins.ObjectMigration());
openmct.install(
openmct.plugins.ClearData(
['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked', 'example.imagery'],
{ indicator: true }
)
);
openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
openmct.install(openmct.plugins.Timer());
openmct.install(openmct.plugins.Timelist());
openmct.install(openmct.plugins.BarChart());
openmct.install(openmct.plugins.ScatterPlot());
document.addEventListener('DOMContentLoaded', function () {
openmct.start();
});
</script>
</html>

View File

@@ -1,45 +1,50 @@
{
"name": "openmct",
"version": "3.1.0-next",
"version": "3.3.0-next",
"description": "The Open MCT core platform",
"main": "dist/openmct.js",
"devDependencies": {
"@axe-core/playwright": "4.8.2",
"@babel/eslint-parser": "7.22.5",
"@braintree/sanitize-url": "6.0.2",
"@braintree/sanitize-url": "6.0.4",
"@deploysentinel/playwright": "0.3.4",
"@percy/cli": "1.26.0",
"@percy/cli": "1.27.4",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.36.2",
"@playwright/test": "1.39.0",
"@types/d3-axis": "3.0.6",
"@types/d3-scale": "4.0.8",
"@types/d3-selection": "3.0.10",
"@types/eventemitter3": "1.2.0",
"@types/jasmine": "4.3.4",
"@types/jasmine": "5.1.2",
"@types/lodash": "4.14.192",
"@vue/compat": "3.3.4",
"@vue/compiler-sfc": "3.3.4",
"@vue/compiler-sfc": "3.3.10",
"babel-loader": "9.1.0",
"babel-plugin-istanbul": "6.1.1",
"codecov": "3.8.3",
"comma-separated-values": "3.6.4",
"copy-webpack-plugin": "11.0.0",
"cspell": "7.3.6",
"cspell": "7.3.8",
"css-loader": "6.8.1",
"d3-axis": "3.0.0",
"d3-scale": "3.3.0",
"d3-scale": "4.0.2",
"d3-selection": "3.0.0",
"eslint": "8.48.0",
"eslint": "8.54.0",
"eslint-config-prettier": "9.0.0",
"eslint-plugin-compat": "4.2.0",
"eslint-plugin-no-unsanitized": "4.0.2",
"eslint-plugin-playwright": "0.12.0",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-simple-import-sort": "10.0.0",
"eslint-plugin-unicorn": "44.0.2",
"eslint-plugin-vue": "9.15.0",
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
"eslint-plugin-unicorn": "49.0.0",
"eslint-plugin-vue": "9.18.1",
"eslint-plugin-you-dont-need-lodash-underscore": "6.13.0",
"eventemitter3": "1.2.0",
"file-saver": "2.0.5",
"flatbush": "4.2.0",
"git-rev-sync": "3.0.2",
"html2canvas": "1.4.1",
"imports-loader": "4.0.1",
"jasmine-core": "5.0.0",
"jasmine-core": "5.1.1",
"karma": "6.4.2",
"karma-chrome-launcher": "3.2.0",
"karma-cli": "2.0.0",
@@ -52,34 +57,35 @@
"karma-webpack": "5.0.0",
"location-bar": "3.0.1",
"lodash": "4.17.21",
"marked": "9.0.3",
"marked": "11.1.0",
"mini-css-extract-plugin": "2.7.6",
"moment": "2.29.4",
"moment-duration-format": "2.3.2",
"moment-timezone": "0.5.41",
"npm-run-all2": "6.0.6",
"npm-run-all2": "6.1.1",
"nyc": "15.1.0",
"painterro": "1.2.78",
"plotly.js-basic-dist": "2.20.0",
"plotly.js-gl2d-dist": "2.20.0",
"painterro": "1.2.87",
"plotly.js-basic-dist-min": "2.20.0",
"plotly.js-gl2d-dist-min": "2.20.0",
"prettier": "2.8.7",
"printj": "1.3.1",
"resolve-url-loader": "5.0.0",
"sanitize-html": "2.11.0",
"sass": "1.63.4",
"sass": "1.68.0",
"sass-loader": "13.3.2",
"sinon": "15.1.0",
"sinon": "17.0.0",
"style-loader": "3.3.3",
"terser-webpack-plugin": "5.3.9",
"tiny-emitter": "2.1.0",
"typescript": "5.2.2",
"uuid": "9.0.0",
"vue": "3.3.4",
"vue-eslint-parser": "9.3.1",
"uuid": "9.0.1",
"vue": "3.3.8",
"vue-eslint-parser": "9.3.2",
"vue-loader": "16.8.3",
"webpack": "5.88.0",
"webpack": "5.89.0",
"webpack-cli": "5.1.1",
"webpack-dev-server": "4.15.1",
"webpack-merge": "5.9.0"
"webpack-merge": "5.10.0"
},
"scripts": {
"clean": "rm -rf ./dist ./node_modules ./package-lock.json ./coverage ./html-test-results ./test-results ./.nyc_output ",
@@ -99,14 +105,15 @@
"test": "karma start",
"test:debug": "KARMA_DEBUG=true karma start",
"test:e2e": "npx playwright test",
"test:e2e:a11y": "npx playwright test --config=e2e/playwright-visual-a11y.config.js --project=chrome --grep @a11y",
"test:e2e:couchdb": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @couchdb --workers=1",
"test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert \"@unstable|@couchdb|@generatedata\"",
"test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable",
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
"test:e2e:generatedata": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @generatedata",
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots",
"test:e2e:visual:ci": "percy exec --config ./e2e/.percy.ci.yml --partial -- npx playwright test --config=e2e/playwright-visual.config.js --project=chrome --grep-invert @unstable",
"test:e2e:visual:full": "percy exec --config ./e2e/.percy.nightly.yml -- npx playwright test --config=e2e/playwright-visual.config.js --grep-invert @unstable",
"test:e2e:visual:ci": "percy exec --config ./e2e/.percy.ci.yml --partial -- npx playwright test --config=e2e/playwright-visual-a11y.config.js --project=chrome --grep-invert @unstable",
"test:e2e:visual:full": "percy exec --config ./e2e/.percy.nightly.yml -- npx playwright test --config=e2e/playwright-visual-a11y.config.js --grep-invert @unstable",
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js --grep-invert @couchdb",
"test:e2e:watch": "npx playwright test --ui --config=e2e/playwright-ci.config.js",
"test:perf:contract": "npx playwright test --config=e2e/playwright-performance-dev.config.js",
@@ -123,10 +130,10 @@
"homepage": "https://nasa.github.io/openmct",
"repository": {
"type": "git",
"url": "https://github.com/nasa/openmct.git"
"url": "git+https://github.com/nasa/openmct.git"
},
"engines": {
"node": ">=16.19.1 <20"
"node": ">=18.14.2 <22"
},
"browserslist": [
"Firefox ESR",

View File

@@ -366,15 +366,19 @@ export default class AnnotationAPI extends EventEmitter {
return tagsAddedToResults;
}
async #addTargetModelsToResults(results) {
async #addTargetModelsToResults(results, abortSignal) {
const modelAddedToResults = await Promise.all(
results.map(async (result) => {
const targetModels = await Promise.all(
result.targets.map(async (target) => {
const targetID = target.keyString;
const targetModel = await this.openmct.objects.get(targetID);
const targetModel = await this.openmct.objects.get(targetID, abortSignal);
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
const originalPathObjects = await this.openmct.objects.getOriginalPath(targetKeyString);
const originalPathObjects = await this.openmct.objects.getOriginalPath(
targetKeyString,
[],
abortSignal
);
return {
originalPath: originalPathObjects,
@@ -442,7 +446,7 @@ export default class AnnotationAPI extends EventEmitter {
* @param {Object} [abortController] An optional abort method to stop the query
* @returns {Promise} returns a model of matching tags with their target domain objects attached
*/
async searchForTags(query, abortController) {
async searchForTags(query, abortSignal) {
const matchingTagKeys = this.#getMatchingTags(query);
if (!matchingTagKeys.length) {
return [];
@@ -452,7 +456,7 @@ export default class AnnotationAPI extends EventEmitter {
await Promise.all(
this.openmct.objects.search(
matchingTagKeys,
abortController,
abortSignal,
this.openmct.objects.SEARCH_TYPES.TAGS
)
)
@@ -465,7 +469,10 @@ export default class AnnotationAPI extends EventEmitter {
combinedSameTargets,
matchingTagKeys
);
const appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults);
const appliedTargetsModels = await this.#addTargetModelsToResults(
appliedTagSearchResults,
abortSignal
);
const resultsWithValidPath = appliedTargetsModels.filter((result) => {
return this.openmct.objects.isReachable(result.targetModels?.[0]?.originalPath);
});

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import Vue from 'vue';
import { nextTick } from 'vue';
import { createMouseEvent, createOpenMct, resetApplicationState } from '../../utils/testing';
import Menu from './menu';
@@ -186,7 +186,7 @@ describe('The Menu API', () => {
superMenuItem.dispatchEvent(mouseOverEvent);
const itemDescription = document.querySelector('.l-item-description__description');
Vue.nextTick(() => {
nextTick(() => {
expect(menuElement).not.toBeNull();
expect(itemDescription.innerText).toEqual(actionsArray[0].description);

View File

@@ -71,11 +71,6 @@ class Menu extends EventEmitter {
},
provide: {
options: this.options
},
// TODO: Remove this exception upon full migration to Vue 3
// https://v3-migration.vuejs.org/breaking-changes/render-function-api.html#render-function-argument
compatConfig: {
RENDER_FUNCTION: false
}
});
@@ -98,11 +93,6 @@ class Menu extends EventEmitter {
},
provide: {
options: this.options
},
// TODO: Remove this exception upon full migration to Vue 3
// https://v3-migration.vuejs.org/breaking-changes/render-function-api.html#render-function-argument
compatConfig: {
RENDER_FUNCTION: false
}
});

View File

@@ -221,7 +221,7 @@ export default class ObjectAPI {
const provider = this.getProvider(identifier);
if (!provider) {
throw new Error(`No Provider Matched for keyString "${this.makeKeyString(identifier)}}"`);
throw new Error(`No Provider Matched for keyString "${this.makeKeyString(identifier)}"`);
}
if (!provider.get) {
@@ -232,6 +232,10 @@ export default class ObjectAPI {
.get(identifier, abortSignal)
.then((domainObject) => {
delete this.cache[keystring];
if (!domainObject && abortSignal.aborted) {
// we've aborted the request
return;
}
domainObject = this.applyGetInterceptors(identifier, domainObject);
if (this.supportsMutation(identifier)) {
@@ -786,16 +790,20 @@ export default class ObjectAPI {
* Given an identifier, constructs the original path by walking up its parents
* @param {module:openmct.ObjectAPI~Identifier} identifier
* @param {Array<module:openmct.DomainObject>} path an array of path objects
* @param {AbortSignal} abortSignal (optional) signal to abort fetch requests
* @returns {Promise<Array<module:openmct.DomainObject>>} a promise containing an array of domain objects
*/
async getOriginalPath(identifier, path = []) {
const domainObject = await this.get(identifier);
async getOriginalPath(identifier, path = [], abortSignal = null) {
const domainObject = await this.get(identifier, abortSignal);
if (!domainObject) {
return [];
}
path.push(domainObject);
const { location } = domainObject;
if (location && !this.#pathContainsDomainObject(location, path)) {
// if we have a location, and we don't already have this in our constructed path,
// then keep walking up the path
return this.getOriginalPath(utils.parseKeyString(location), path);
return this.getOriginalPath(utils.parseKeyString(location), path, abortSignal);
} else {
return path;
}

View File

@@ -20,39 +20,25 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(['lodash', 'printj'], function (_, printj) {
// TODO: needs reference to formatService;
function TelemetryValueFormatter(valueMetadata, formatMap) {
import _ from 'lodash';
import { sprintf } from 'printj';
// TODO: needs reference to formatService;
export default class TelemetryValueFormatter {
constructor(valueMetadata, formatMap) {
this.valueMetadata = valueMetadata;
this.formatMap = formatMap;
this.valueMetadataFormat = this.getNonArrayValue(valueMetadata.format);
const numberFormatter = {
parse: function (x) {
return Number(x);
},
format: function (x) {
return x;
},
validate: function (x) {
return true;
}
parse: (x) => Number(x),
format: (x) => x,
validate: (x) => true
};
this.valueMetadata = valueMetadata;
function getNonArrayValue(value) {
//metadata format could have array formats ex. string[]/number[]
const arrayRegex = /\[\]$/g;
if (value && value.match(arrayRegex)) {
return value.replace(arrayRegex, '');
}
return value;
}
let valueMetadataFormat = getNonArrayValue(valueMetadata.format);
//Is there an existing formatter for the format specified? If not, default to number format
this.formatter = formatMap.get(valueMetadataFormat) || numberFormatter;
if (valueMetadataFormat === 'enum') {
// Is there an existing formatter for the format specified? If not, default to number format
this.formatter = formatMap.get(this.valueMetadataFormat) || numberFormatter;
if (this.valueMetadataFormat === 'enum') {
this.formatter = {};
this.enumerations = valueMetadata.enumerations.reduce(
function (vm, e) {
@@ -66,14 +52,14 @@ define(['lodash', 'printj'], function (_, printj) {
byString: {}
}
);
this.formatter.format = function (value) {
this.formatter.format = (value) => {
if (Object.prototype.hasOwnProperty.call(this.enumerations.byValue, value)) {
return this.enumerations.byValue[value];
}
return value;
}.bind(this);
this.formatter.parse = function (string) {
};
this.formatter.parse = (string) => {
if (typeof string === 'string') {
if (Object.prototype.hasOwnProperty.call(this.enumerations.byString, string)) {
return this.enumerations.byString[string];
@@ -81,19 +67,19 @@ define(['lodash', 'printj'], function (_, printj) {
}
return Number(string);
}.bind(this);
};
}
// Check for formatString support once instead of per format call.
if (valueMetadata.formatString) {
const baseFormat = this.formatter.format;
const formatString = getNonArrayValue(valueMetadata.formatString);
const formatString = this.getNonArrayValue(valueMetadata.formatString);
this.formatter.format = function (value) {
return printj.sprintf(formatString, baseFormat.call(this, value));
return sprintf(formatString, baseFormat.call(this, value));
};
}
if (valueMetadataFormat === 'string') {
if (this.valueMetadataFormat === 'string') {
this.formatter.parse = function (value) {
if (value === undefined) {
return '';
@@ -116,7 +102,17 @@ define(['lodash', 'printj'], function (_, printj) {
}
}
TelemetryValueFormatter.prototype.parse = function (datum) {
getNonArrayValue(value) {
//metadata format could have array formats ex. string[]/number[]
const arrayRegex = /\[\]$/g;
if (value && value.match(arrayRegex)) {
return value.replace(arrayRegex, '');
}
return value;
}
parse(datum) {
const isDatumArray = Array.isArray(datum);
if (_.isObject(datum)) {
const objectDatum = isDatumArray ? datum : datum[this.valueMetadata.source];
@@ -130,9 +126,9 @@ define(['lodash', 'printj'], function (_, printj) {
}
return this.formatter.parse(datum);
};
}
TelemetryValueFormatter.prototype.format = function (datum) {
format(datum) {
const isDatumArray = Array.isArray(datum);
if (_.isObject(datum)) {
const objectDatum = isDatumArray ? datum : datum[this.valueMetadata.source];
@@ -146,7 +142,5 @@ define(['lodash', 'printj'], function (_, printj) {
}
return this.formatter.format(datum);
};
return TelemetryValueFormatter;
});
}
}

View File

@@ -34,7 +34,7 @@ export default class LADTableView {
this._destroy = null;
}
show(element) {
show(element, isEditing, { renderWhenVisible }) {
let ladTableConfiguration = new LADTableConfiguration(this.domainObject, this.openmct);
const { vNode, destroy } = mount(
@@ -46,7 +46,8 @@ export default class LADTableView {
provide: {
openmct: this.openmct,
currentView: this,
ladTableConfiguration
ladTableConfiguration,
renderWhenVisible
},
data: () => {
return {

View File

@@ -22,6 +22,7 @@
<template>
<tr
ref="tableRow"
class="js-lad-table__body__row c-table__selectable-row"
@click="clickedRow"
@contextmenu.prevent="showContextMenu"
@@ -40,6 +41,9 @@
{{ unit }}
</td>
<td v-if="showType" class="js-type-data">{{ typeLabel }}</td>
<td v-for="limit in formattedLimitValues" :key="limit.key" class="js-limit-data">
{{ limit.value }}
</td>
</tr>
</template>
@@ -54,7 +58,7 @@ import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
export default {
mixins: [tooltipHelpers],
inject: ['openmct', 'currentView'],
inject: ['openmct', 'currentView', 'renderWhenVisible'],
props: {
domainObject: {
type: Object,
@@ -77,6 +81,19 @@ export default {
configuration: {
type: Object,
required: true
},
limitDefinition: {
type: Object,
default() {
return {};
}
},
limitColumnNames: {
// for ordering
type: Array,
default() {
return [];
}
}
},
emits: ['row-context-click'],
@@ -85,6 +102,7 @@ export default {
datum: undefined,
timestamp: undefined,
timestampKey: undefined,
valueKey: null,
composition: [],
unit: ''
};
@@ -97,6 +115,26 @@ export default {
return this.formats[this.valueKey].format(this.datum);
},
formattedLimitValues() {
if (!this.valueKey) {
return [];
}
return this.limitColumnNames.map((column) => {
if (this.limitDefinition?.[column.key]) {
const highValue = this.limitDefinition[column.key].high[this.valueKey];
const lowValue = this.limitDefinition[column.key].low[this.valueKey];
return {
key: column.key,
value: `${lowValue}${highValue}`
};
} else {
return {
key: column.key,
value: BLANK_VALUE
};
}
});
},
typeLabel() {
if (this.isAggregate) {
return 'Aggregate';
@@ -203,8 +241,7 @@ export default {
methods: {
updateView() {
if (!this.updatingView) {
this.updatingView = true;
requestAnimationFrame(() => {
this.updatingView = this.renderWhenVisible(() => {
this.timestamp = this.getParsedTimestamp(this.latestDatum);
this.datum = this.latestDatum;
this.updatingView = false;

View File

@@ -30,6 +30,9 @@
<th>Value</th>
<th v-if="hasUnits">Units</th>
<th v-if="showType">Type</th>
<th v-for="limitColumn in limitColumnNames" :key="limitColumn.key">
{{ limitColumn.label }}
</th>
</tr>
</thead>
<tbody>
@@ -37,6 +40,8 @@
v-for="ladRow in items"
:key="ladRow.key"
:domain-object="ladRow.domainObject"
:limit-definition="ladRow.limitDefinition"
:limit-column-names="limitColumnNames"
:path-to-table="objectPath"
:has-units="hasUnits"
:is-stale="staleObjects.includes(ladRow.key)"
@@ -89,6 +94,23 @@ export default {
return itemsWithUnits.length !== 0 && !this.configuration?.hiddenColumns?.units;
},
limitColumnNames() {
const limitDefinitions = [];
this.items.forEach((item) => {
if (item.limitDefinition) {
const limits = Object.keys(item.limitDefinition);
limits.forEach((limit) => {
const limitAlreadyAdded = limitDefinitions.some((limitDef) => limitDef.key === limit);
const limitHidden = this.configuration?.hiddenColumns?.[limit];
if (!limitAlreadyAdded && !limitHidden) {
limitDefinitions.push({ label: `Limit ${limit}`, key: limit });
}
});
}
});
return limitDefinitions;
},
showTimestamp() {
return !this.configuration?.hiddenColumns?.timestamp;
},
@@ -149,10 +171,11 @@ export default {
this.composition.off('reorder', this.reorder);
},
methods: {
addItem(domainObject) {
async addItem(domainObject) {
let item = {};
item.domainObject = domainObject;
item.key = this.openmct.objects.makeKeyString(domainObject.identifier);
item.limitDefinition = await this.openmct.telemetry.limitDefinition(domainObject).limits();
this.items.push(item);
this.subscribeToStaleness(domainObject);

View File

@@ -58,23 +58,37 @@ export default {
const ladTableConfiguration = new LADTableConfiguration(domainObject, this.openmct);
return {
headers: {
timestamp: 'Timestamp',
units: 'Units',
type: 'Type'
},
ladTableConfiguration,
isEditing: this.openmct.editor.isEditing(),
configuration: ladTableConfiguration.getConfiguration(),
items: [],
ladTableObjects: [],
ladTelemetryObjects: {}
ladTableObjects: []
};
},
computed: {
headers() {
const fullHeaders = {
timestamp: 'Timestamp',
type: 'Type'
};
// check hasUnits and limitColumnName and add then to fullHeaders
this.items.forEach((item) => {
if (item.hasUnits) {
fullHeaders.units = 'Units';
}
if (item.limitDefinition) {
const limits = Object.keys(item.limitDefinition);
limits.forEach((limit) => {
fullHeaders[limit] = limit;
});
}
});
return fullHeaders;
}
},
mounted() {
this.openmct.editor.on('isEditing', this.toggleEdit);
this.composition = this.openmct.composition.get(this.ladTableConfiguration.domainObject);
this.shouldShowUnitsCheckbox();
if (this.ladTableConfiguration.domainObject.type === 'LadTable') {
this.composition.on('add', this.addItem);
@@ -104,22 +118,25 @@ export default {
}
},
methods: {
addItem(domainObject) {
async addItem(domainObject) {
const item = {};
item.domainObject = domainObject;
item.key = this.openmct.objects.makeKeyString(domainObject.identifier);
item.limitDefinition = await this.openmct.telemetry.limitDefinition(domainObject).limits();
const metadata = this.openmct.telemetry.getMetadata(domainObject);
const valueMetadatas = metadata ? metadata.valueMetadatas : [];
const metadataWithUnits = valueMetadatas.filter((metadatum) => metadatum.unit);
item.hasUnits = metadataWithUnits.length > 0;
this.items.push(item);
this.shouldShowUnitsCheckbox();
},
removeItem(identifier) {
const keystring = this.openmct.objects.makeKeyString(identifier);
const index = this.items.findIndex((item) => keystring === item.key);
this.items.splice(index, 1);
this.shouldShowUnitsCheckbox();
},
addLadTable(domainObject) {
let ladTable = {};
@@ -130,20 +147,12 @@ export default {
this.ladTableObjects.push(ladTable);
const composition = this.openmct.composition.get(ladTable.domainObject);
const addCallback = this.addTelemetryObject(ladTable);
const removeCallback = this.removeTelemetryObject(ladTable);
composition.on('add', addCallback);
composition.on('remove', removeCallback);
composition.load();
this.compositions.push({
composition,
addCallback,
removeCallback
composition
});
this.shouldShowUnitsCheckbox();
},
removeLadTable(identifier) {
const index = this.ladTableObjects.findIndex(
@@ -153,36 +162,6 @@ export default {
delete this.ladTelemetryObjects[ladTable.key];
this.ladTableObjects.splice(index, 1);
this.shouldShowUnitsCheckbox();
},
addTelemetryObject(ladTable) {
return (domainObject) => {
const telemetryObject = {};
telemetryObject.key = this.openmct.objects.makeKeyString(domainObject.identifier);
telemetryObject.domainObject = domainObject;
const telemetryObjects = this.ladTelemetryObjects[ladTable.key];
telemetryObjects.push(telemetryObject);
this.ladTelemetryObjects[ladTable.key] = telemetryObjects;
this.shouldShowUnitsCheckbox();
};
},
removeTelemetryObject(ladTable) {
return (identifier) => {
const keystring = this.openmct.objects.makeKeyString(identifier);
const telemetryObjects = this.ladTelemetryObjects[ladTable.key];
const index = telemetryObjects.findIndex(
(telemetryObject) => keystring === telemetryObject.key
);
telemetryObjects.splice(index, 1);
this.ladTelemetryObjects[ladTable.key] = telemetryObjects;
this.shouldShowUnitsCheckbox();
};
},
combineKeys(ladKey, telemetryObjectKey) {
return `${ladKey}-${telemetryObjectKey}`;
@@ -195,44 +174,6 @@ export default {
},
toggleEdit(isEditing) {
this.isEditing = isEditing;
},
shouldShowUnitsCheckbox() {
let showUnitsCheckbox = false;
if (this.ladTableConfiguration?.domainObject) {
if (this.ladTableConfiguration.domainObject.type === 'LadTable') {
const itemsWithUnits = this.items.filter((item) => {
return this.metadataHasUnits(item.domainObject);
});
showUnitsCheckbox = itemsWithUnits.length !== 0;
} else {
const ladTables = Object.values(this.ladTelemetryObjects);
for (const ladTable of ladTables) {
for (const telemetryObject of ladTable) {
if (this.metadataHasUnits(telemetryObject.domainObject)) {
showUnitsCheckbox = true;
}
}
}
}
}
if (showUnitsCheckbox && this.headers.units === undefined) {
this.headers.units = 'Units';
}
if (!showUnitsCheckbox && this.headers?.units) {
delete this.headers.units;
}
},
metadataHasUnits(domainObject) {
const metadata = this.openmct.telemetry.getMetadata(domainObject);
const valueMetadatas = metadata ? metadata.valueMetadatas : [];
const metadataWithUnits = valueMetadatas.filter((metadatum) => metadatum.unit);
return metadataWithUnits.length > 0;
}
}
};

View File

@@ -192,11 +192,7 @@ export default {
reorderLadTables(reorderPlan) {
let oldComposition = this.ladTableObjects.slice();
reorderPlan.forEach((reorderEvent) => {
this.$set(
this.ladTableObjects,
reorderEvent.newIndex,
oldComposition[reorderEvent.oldIndex]
);
this.ladTableObjects[reorderEvent.newIndex] = oldComposition[reorderEvent.oldIndex];
});
},
addTelemetryObject(ladTable) {

View File

@@ -24,10 +24,11 @@ import {
getLatestTelemetry,
getMockObjects,
getMockTelemetry,
renderWhenVisible,
resetApplicationState,
spyOnBuiltins
} from 'utils/testing';
import Vue from 'vue';
import { nextTick } from 'vue';
import LadPlugin from './plugin.js';
@@ -225,7 +226,7 @@ describe('The LAD Table', () => {
(viewProvider) => viewProvider.key === ladTableKey
);
ladTableView = ladTableViewProvider.view(mockObj.ladTable, [mockObj.ladTable]);
ladTableView.show(child, true);
ladTableView.show(child, true, { renderWhenVisible });
await Promise.all([
telemetryRequestPromise,
@@ -233,7 +234,7 @@ describe('The LAD Table', () => {
anotherTelemetryObjectPromise,
aggregateTelemetryObjectResolve
]);
await Vue.nextTick();
await nextTick();
});
it('should show one row per object in the composition', () => {
@@ -244,7 +245,7 @@ describe('The LAD Table', () => {
it('should show the most recent datum from the telemetry producing object', async () => {
const latestDatum = getLatestTelemetry(mockTelemetry, { timeFormat });
const expectedDate = utcTimeFormat(latestDatum[timeFormat]);
await Vue.nextTick();
await nextTick();
const latestDate = parent.querySelector(TABLE_BODY_FIRST_ROW_SECOND_DATA).innerText;
expect(latestDate).toBe(expectedDate);
const dataType = parent
@@ -254,7 +255,7 @@ describe('The LAD Table', () => {
});
it('should show aggregate telemetry type with blank data', async () => {
await Vue.nextTick();
await nextTick();
const latestData = parent
.querySelectorAll(TABLE_BODY_ROWS)[1]
.querySelectorAll('td')[2].innerText;
@@ -282,7 +283,7 @@ describe('The LAD Table', () => {
const mostRecentTelemetry = getLatestTelemetry(mockTelemetry, { timeFormat });
const rangeValue = mostRecentTelemetry[range];
const domainValue = utcTimeFormat(mostRecentTelemetry[domain]);
await Vue.nextTick();
await nextTick();
const actualDomainValue = parent.querySelector(TABLE_BODY_FIRST_ROW_SECOND_DATA).innerText;
const actualRangeValue = parent.querySelector(TABLE_BODY_FIRST_ROW_THIRD_DATA).innerText;
expect(actualRangeValue).toBe(rangeValue);
@@ -424,7 +425,7 @@ describe('The LAD Table Set', () => {
ladTableSetView = ladTableSetViewProvider.view(mockObj.ladTableSet, [mockObj.ladTableSet]);
ladTableSetView.show(child);
return Vue.nextTick();
return nextTick();
});
it('should show one row per lad table object in the composition', () => {

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { createOpenMct, resetApplicationState, spyOnBuiltins } from 'utils/testing';
import Vue from 'vue';
import { nextTick } from 'vue';
import AutoflowTabularConstants from './AutoflowTabularConstants';
import AutoflowTabularPlugin from './AutoflowTabularPlugin';
@@ -175,7 +175,7 @@ xdescribe('AutoflowTabularPlugin', () => {
view = provider.view(testObject, [testObject]);
view.show(testContainer);
return Vue.nextTick();
return nextTick();
});
afterEach(() => {

View File

@@ -99,6 +99,7 @@ export default {
},
beforeUnmount() {
if (this.plotResizeObserver) {
this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
this.plotResizeObserver.disconnect();
clearTimeout(this.resizeTimer);
}
@@ -106,6 +107,8 @@ export default {
if (this.removeBarColorListener) {
this.removeBarColorListener();
}
Plotly.purge(this.$refs.plot);
},
methods: {
getAxisMinMax(axis) {

View File

@@ -6,7 +6,7 @@ import BarGraphOptions from './BarGraphOptions.vue';
export default function BarGraphInspectorViewProvider(openmct) {
return {
key: BAR_GRAPH_INSPECTOR_KEY,
name: 'Bar Graph Configuration',
name: 'Config',
canView: function (selection) {
if (selection.length === 0 || selection[0].length === 0) {
return false;

View File

@@ -26,7 +26,7 @@
<li>
<series-options
v-for="series in plotSeries"
:key="series.key"
:key="series.keyString"
:item="series"
:color-palette="colorPalette"
/>

View File

@@ -23,9 +23,9 @@
// import BarGraph from './BarGraphPlot.vue';
import EventEmitter from 'EventEmitter';
import { createOpenMct, resetApplicationState } from 'utils/testing';
import Vue from 'vue';
import { nextTick } from 'vue';
import { BAR_GRAPH_KEY, BAR_GRAPH_VIEW } from './BarGraphConstants';
import { BAR_GRAPH_INSPECTOR_KEY, BAR_GRAPH_KEY, BAR_GRAPH_VIEW } from './BarGraphConstants';
import BarGraphPlugin from './plugin';
describe('the plugin', function () {
@@ -153,7 +153,7 @@ describe('the plugin', function () {
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
await Vue.nextTick();
await nextTick();
});
it('provides a bar graph view', () => {
@@ -217,7 +217,6 @@ describe('the plugin', function () {
'someNamespace:~OpenMCT~outer.test-object.foo.bar'
].name
).toEqual('A Dotful Object');
barGraphView.destroy();
});
});
@@ -254,7 +253,7 @@ describe('the plugin', function () {
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
await Vue.nextTick();
await nextTick();
});
it('Renders spectral plots', async () => {
@@ -305,12 +304,11 @@ describe('the plugin', function () {
barGraphView.show(child, true);
mockComposition.emit('add', dotFullTelemetryObject);
await Vue.nextTick();
await Vue.nextTick();
await nextTick();
await nextTick();
const plotElement = element.querySelector('.cartesianlayer .scatterlayer .trace .lines');
expect(plotElement).not.toBeNull();
barGraphView.destroy();
});
});
@@ -578,12 +576,10 @@ describe('the plugin', function () {
child.append(viewContainer);
const applicableViews = openmct.inspectorViews.get(selection);
plotInspectorView = applicableViews.filter(
(view) => view.name === 'Bar Graph Configuration'
)[0];
plotInspectorView = applicableViews.filter((view) => view.key === BAR_GRAPH_INSPECTOR_KEY)[0];
plotInspectorView.show(viewContainer);
await Vue.nextTick();
await nextTick();
optionsElement = element.querySelector('.c-bar-graph-options');
});

View File

@@ -131,6 +131,8 @@ export default {
if (this.unobserveColorChanges) {
this.unobserveColorChanges();
}
Plotly.purge(this.$refs.plot);
},
methods: {
getUnderlayPlotData() {

View File

@@ -22,7 +22,7 @@
import EventEmitter from 'EventEmitter';
import { createOpenMct, resetApplicationState } from 'utils/testing';
import Vue from 'vue';
import { nextTick } from 'vue';
import ScatterPlotPlugin from './plugin';
import { SCATTER_PLOT_KEY, SCATTER_PLOT_VIEW } from './scatterPlotConstants';
@@ -178,7 +178,7 @@ describe('the plugin', function () {
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
await Vue.nextTick();
await nextTick();
});
it('provides a scatter plot view', () => {
@@ -406,7 +406,7 @@ describe('the plugin', function () {
plotInspectorView = applicableViews[0];
plotInspectorView.show(viewContainer);
await Vue.nextTick();
await nextTick();
optionsElement = element.querySelector('.c-scatter-plot-options');
});

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
import { createMouseEvent, createOpenMct, resetApplicationState } from 'utils/testing';
import Vue from 'vue';
import { nextTick } from 'vue';
import ClearDataPlugin from './plugin.js';
@@ -213,7 +213,7 @@ describe('The Clear Data Plugin:', () => {
});
it('renders its major elements', async () => {
await Vue.nextTick();
await nextTick();
const indicatorClass = appHolder.querySelector('.c-indicator');
const iconClass = appHolder.querySelector('.icon-clear-data');
const indicatorLabel = appHolder.querySelector('.c-indicator__label');

View File

@@ -21,7 +21,10 @@
-->
<template>
<div class="c-indicator t-indicator-clock icon-clock no-minify c-indicator--not-clickable">
<div
class="c-indicator t-indicator-clock icon-clock no-minify c-indicator--not-clickable"
role="complementary"
>
<span class="label c-indicator__label">
{{ timeTextValue }}
</span>

View File

@@ -22,7 +22,7 @@
import EventEmitter from 'EventEmitter';
import { createOpenMct, resetApplicationState } from 'utils/testing';
import Vue from 'vue';
import { nextTick } from 'vue';
import clockPlugin from './plugin';
@@ -106,7 +106,7 @@ describe('Clock plugin:', () => {
clockView = clockViewProvider.view(mutableClockObject);
clockView.show(child);
await Vue.nextTick();
await nextTick();
await new Promise((resolve) => requestAnimationFrame(resolve));
});
@@ -232,7 +232,7 @@ describe('Clock plugin:', () => {
it('contains text', async () => {
await setupClock(true);
await Vue.nextTick();
await nextTick();
await new Promise((resolve) => requestAnimationFrame(resolve));
clockIndicator = openmct.indicators.indicatorObjects.find(

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