Compare commits

...

62 Commits

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

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

* Remove log statements

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

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

* Remove excess padding when there is no left y axis

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

* Fix now marker visibility

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

* refactor: improve readability and conciseness

* docs: improve jsdocs

* refactor: move jsdoc typedefs to bottom of file

* refactor: axis to use vue reactivity

* fix: return alignment as an object of refs

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

* Fix now marker offset

* Add new visual test for time strips

* update with animation stabilization

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

* change injected property from path to objectPath

* Fix spelling

* Remove unused arguments to function call

---------

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

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

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

* add tab navigation to inspector tabs

* add accessibility metadata to this component

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

* clean up test

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

* a11y: aria label for plotOptionsItem

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

* fixed tests

* address comment

* reverted to match previous commit

---------

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

* chore: remove valid words

* fix(cspell): resolve linting errors

* fix(naming): change filename

* fix(cspell): resolve linting errors

* fix(spelling): remove wrong word

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

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

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

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

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

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

* bump versions

* doesn't do anything

* does this do anything

* undo only

* update the hardcoded urls

* add ignore CI logs push

* downgrade to test

* update the package lock

* Add some more noise reduction

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

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

* Fix all a11y failing tests

* drive-by: missed clean

* test: attempt to fix parallelization

* split the test 3 ways

* lint

---------

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

* Enable tests and fix some failing ones

* revert accidental changes

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

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

* Fix all failing tests

* test: disable a11y checks for planning suite

* lint:fix

* fix pathing issues

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

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

* test: fix file name and path

* fix(#7317): scott bell prophecy

* jk no credit for u

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

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

---------

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

* Add test for synchronized time conductor via plots

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

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

* revert

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

* consistent naming convention

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

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

* adding back vars that were removed

---------

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

---------

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

* clarifying comment

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

* adding descriptive comments

* adding in ascending check as well

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

* lint

---------

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

* add edit and save domain object helper functions

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

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

* rename const

* move creation code out of generateLocalStorage since it is immutable

* remove function abstractions

* remove @localStorage test label

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

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

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

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

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

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

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

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

* remove unneccesary aria text

* remove unneccessary aria text

* use recommended playwright locators

* lint fix

* remove unneccesary steps now that child created directly in parent

---------

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

* update visual tests

* Changes to gauge, layout borders, and background

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

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

* changes to menu

* Fix to make theme easy to run

* Fix tab colors and add glass background to menus

* make highlightd corners longer

* Initial changes to font styles

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

* Initial changes to alphanumerics in layouts

* Updated variables

* update plugin.js file

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

* renaming theme plugin and rename branch

* fix button colors to be more readable

* change background image

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

* more fixes

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

* remove blur from meter gauges

* Add comment about this theme being in beta mode

* Delete draft .scss file that is no longer needed

* Fix major accessibility issues

* Fix PR review comments

*  fix: Correct import file name for DarkMatter theme.

* Fix other theme code that was failing e2e tests

* Revert index.html

* Fix linting error

* Fix for failing percy test regarding padding

* Fix for failing percy test regarding padding part 2

* Fix for failing percy test regarding padding part 3

* Remove mixin that may be causing percy issue

* Another fix to resolve percy issue

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

* Fix gradient clipping in inspector

* Restructure all constants-.scss files

* Change bg image to be square and NASA official picture

* Final fixes to darkmatter variable layouts

* Address PR comments

* Change darkmatter to darkmatterTheme

---------

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

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

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

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

* fix selector in test

* fix more bad selectors

* fix changed title

---------

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

* improves performance a lot

* fix test

* remove old function

* fix legit bug from test - well done tests

* add constant, better comments

---------

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

* test: update unit test

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

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

* split image and text paste handling
better event handling

* change back source maps

* image takes precedence over text

* break up notebook entry functions for re-use

* create hotkeys utils
add clipboard functions

* add notebook paste test

* add test for pasting to selected but not editing entry

* link tests to issue

* jsdoc addition

* jsdocs

* no need to import then export

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

* fix changed path

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2024-04-16 13:54:40 -07:00
349be42275 add draggable true to tree items 2024-03-13 16:16:56 -07:00
6edb3b2dc4 Update plot aria 2024-03-13 16:16:43 -07:00
444c9ff33e update notebook snapshot drop area 2024-03-13 16:16:20 -07:00
b7b7bc2d41 update gauge component 2024-03-13 16:16:06 -07:00
6e1272dfe9 start to factor out bad locators 2024-03-13 16:15:34 -07:00
d8df0e15e8 driveby 2024-03-13 16:15:17 -07:00
a393bfb87a update component tests to match linting rules 2024-03-12 11:24:45 -07:00
8933baf103 change ruleset 2024-03-12 11:24:14 -07:00
82326dc658 update package 2024-03-12 11:24:02 -07:00
df2d9ab133 Merge branch 'master' of https://github.com/nasa/openmct into eslint_update 2024-03-11 18:44:06 -07:00
2897ca65b3 first pass of lint fixes 2024-02-26 08:59:20 -08:00
0590b50d59 update eslint package 2024-02-26 04:28:24 -08:00
288 changed files with 6853 additions and 4289 deletions

View File

@ -5,7 +5,7 @@ orbs:
executors:
pw-focal-development:
docker:
- image: mcr.microsoft.com/playwright:v1.42.1-focal
- image: mcr.microsoft.com/playwright:v1.45.2-focal
environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
@ -159,7 +159,7 @@ jobs:
steps:
- build_and_install:
node-version: lts/hydrogen
- run: npx playwright@1.42.1 install #Necessary for bare ubuntu machine
- run: npx playwright@1.45.2 install #Necessary for bare ubuntu machine
- run: |
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
@ -230,7 +230,7 @@ jobs:
steps:
- build_and_install:
node-version: lts/iron
- run: npm run test:e2e:visual:<<parameters.suite>>
- run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:visual:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
- store_test_results:
path: test-results/results.xml
- store_artifacts:

View File

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

View File

@ -39,6 +39,7 @@ const config = {
'vue/no-deprecated-events-api': 'warn',
'vue/no-v-for-template-key': 'off',
'vue/no-v-for-template-key-on-child': 'error',
'vue/component-name-in-template-casing': ['error', 'PascalCase'],
'prettier/prettier': 'error',
'you-dont-need-lodash-underscore/omit': 'off',
'you-dont-need-lodash-underscore/throttle': 'off',
@ -154,6 +155,7 @@ const config = {
'error',
{
cases: {
camelCase: true,
pascalCase: true
},
ignore: ['^.*\\.(js|cjs|mjs)$']

View File

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

View File

@ -37,7 +37,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- run: npx playwright@1.42.1 install
- run: npx playwright@1.45.2 install
- name: Start CouchDB Docker Container and Init with Setup Scripts
run: |

View File

@ -30,7 +30,7 @@ jobs:
restore-keys: |
${{ runner.os }}-node-
- run: npx playwright@1.42.1 install
- run: npx playwright@1.45.2 install
- run: npm ci --no-audit --progress=false
- name: Run E2E Tests (Repeated 10 Times)

View File

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

View File

@ -33,7 +33,7 @@ jobs:
restore-keys: |
${{ runner.os }}-node-
- run: npx playwright@1.42.1 install
- run: npx playwright@1.45.2 install
- run: npx playwright install chrome-beta
- run: npm ci --no-audit --progress=false
- run: npm run test:e2e:full -- --max-failures=40

View File

@ -22,3 +22,6 @@
!index.html
!openmct.js
!SECURITY.md
# Dont include the example html
dist/index.html

View File

@ -19,7 +19,7 @@ import { merge } from 'webpack-merge';
let gitRevision = 'error-retrieving-revision';
let gitBranch = 'error-retrieving-branch';
const packageDefinition = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url)));
const { version } = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url)));
try {
gitRevision = execSync('git rev-parse HEAD').toString().trim();
@ -49,7 +49,8 @@ const config = {
couchDBChangesFeed: './src/plugins/persistence/couch/CouchChangesFeed.js',
inMemorySearchWorker: './src/api/objects/InMemorySearchWorker.js',
espressoTheme: './src/plugins/themes/espresso-theme.scss',
snowTheme: './src/plugins/themes/snow-theme.scss'
snowTheme: './src/plugins/themes/snow-theme.scss',
darkmatterTheme: './src/plugins/themes/darkmatter-theme.scss'
},
output: {
globalObject: 'this',
@ -84,7 +85,7 @@ const config = {
},
plugins: [
new webpack.DefinePlugin({
__OPENMCT_VERSION__: `'${packageDefinition.version}'`,
__OPENMCT_VERSION__: `'${version}'`,
__OPENMCT_BUILD_DATE__: `'${new Date()}'`,
__OPENMCT_REVISION__: `'${gitRevision}'`,
__OPENMCT_BUILD_BRANCH__: `'${gitBranch}'`,

View File

@ -6,7 +6,7 @@ information to pull requests.
import config from './webpack.dev.mjs';
config.devtool = 'source-map';
config.devtool = 'inline-source-map';
config.devServer.hot = false;
config.module.rules.push({

View File

@ -39,7 +39,7 @@ export default merge(common, {
return shouldWrite;
}
},
watchFiles: ['**/*.css'],
watchFiles: ['src/**/*.css', 'example/**/*.css'],
static: {
directory: fileURLToPath(new URL('../dist', import.meta.url)),
publicPath: '/dist',

8
API.md
View File

@ -713,7 +713,7 @@ openmct.telemetry.addFormat({
A single telemetry point is considered a Datum, and is represented by a standard
javascript object. Realtime subscriptions (obtained via **subscribe**) will
invoke the supplied callback once for each telemetry datum recieved. Telemetry
invoke the supplied callback once for each telemetry datum received. Telemetry
requests (obtained via **request**) will return a promise for an array of
telemetry datums.
@ -738,7 +738,7 @@ section.
Limit evaluators allow a telemetry integrator to define which limits exist for a
telemetry endpoint and how limits should be applied to telemetry from a given domain object.
A limit evaluator can implement the `evalute` method which is used to define how limits
A limit evaluator can implement the `evaluate` method which is used to define how limits
should be applied to telemetry and the `getLimits` method which is used to specify
what the limit values are for different limit levels.
@ -1180,7 +1180,7 @@ An example time conductor configuration is provided below. It sets up some
default options for the [UTCTimeSystem](https://github.com/nasa/openmct/blob/master/src/plugins/utcTimeSystem/UTCTimeSystem.js)
and [LocalTimeSystem](https://github.com/nasa/openmct/blob/master/src/plugins/localTimeSystem/LocalTimeSystem.js),
in both fixed mode, and for the [LocalClock](https://github.com/nasa/openmct/blob/master/src/plugins/utcTimeSystem/LocalClock.js)
source. In this configutation, the local clock supports both the UTCTimeSystem
source. In this configuration, the local clock supports both the UTCTimeSystem
and LocalTimeSystem. Configuration for fixed bounds mode is specified by omitting
a clock key.
@ -1190,7 +1190,7 @@ const ONE_MINUTE = 60 * 1000;
openmct.install(openmct.plugins.Conductor({
menuOptions: [
// 'Fixed' bounds mode configuation for the UTCTimeSystem
// 'Fixed' bounds mode configuration for the UTCTimeSystem
{
timeSystem: 'utc',
bounds: {start: Date.now() - 30 * ONE_MINUTE, end: Date.now()},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -181,7 +181,7 @@ In addition to the explicit definition of performance tests, we also ensure that
### File Structure
Our file structure follows the type of type of testing being excercised at the e2e layer and files containing test suites which matcher application behavior or our `src` and `example` layout. This area is not well refined as we figure out what works best for closed source and downstream projects. This may change altogether if we move `e2e` to it's own npm package.
Our file structure follows the type of type of testing being exercised at the e2e layer and files containing test suites which matcher application behavior or our `src` and `example` layout. This area is not well refined as we figure out what works best for closed source and downstream projects. This may change altogether if we move `e2e` to it's own npm package.
|File Path|Description|
|:-:|-|
@ -236,7 +236,7 @@ Current list of test tags:
### Continuous Integration
The cheapest time to catch a bug is pre-merge. Unfortuantely, this is the most expensive time to run all of the tests since each merge event can consist of hundreds of commits. For this reason, we're selective in _what we run_ as much as _when we run it_.
The cheapest time to catch a bug is pre-merge. Unfortunately, this is the most expensive time to run all of the tests since each merge event can consist of hundreds of commits. For this reason, we're selective in _what we run_ as much as _when we run it_.
We leverage CircleCI to run tests against each commit and inject the Test Reports which are generated by Playwright so that they team can keep track of flaky and [historical test trends](https://app.circleci.com/insights/github/nasa/openmct/workflows/overall-circleci-commit-status/tests?branch=master&reporting-window=last-30-days)
@ -281,7 +281,7 @@ Playwright has native support for semi-intelligent sharding. Read about it [here
We will be adjusting the parallelization of the Per-Commit tests to keep below the 5 minute total runtime threshold.
In addition to the Parallelization of Test Runners (Sharding), we're also running two concurrent threads on every Shard. This is the functional limit of what CircelCI Agents can support from a memory and CPU resource constraint.
In addition to the Parallelization of Test Runners (Sharding), we're also running two concurrent threads on every Shard. This is the functional limit of what CircleCI Agents can support from a memory and CPU resource constraint.
So for every commit, Playwright is effectively running 4 x 2 concurrent browsercontexts to keep the overall runtime to a miminum.
@ -380,8 +380,7 @@ By adhering to this principle, we can create tests that are both robust and refl
1. Avoid creating locator aliases. This likely means that you're compensating for a bad locator. Improve the application instead.
1. Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });` instead of `{ waitUntil: 'networkidle' }`. Tests run against deployments with websockets often have issues with the networkidle detection.
#### How to make tests faster and more resilient
#### How to make tests faster and more resilient to application changes
1. Avoid app interaction when possible. The best way of doing this is to navigate directly by URL:
```js
@ -396,6 +395,16 @@ By adhering to this principle, we can create tests that are both robust and refl
- Initial navigation should _almost_ always use the `{ waitUntil: 'domcontentloaded' }` option.
1. Avoid repeated setup to test a single assertion. Write longer tests with multiple soft assertions.
This ensures that your changes will be picked up with large refactors.
1. Use [user-facing locators](https://playwright.dev/docs/best-practices#use-locators) (Now a eslint rule!)
```js
page.getByRole('button', { name: 'Create' } )
```
Instead of
```js
page.locator('.c-create-button')
```
Note: `page.locator()` can be used in performance tests as xk6-browser does not yet support the new `page.getBy` pattern and css lookups can be [1.5x faster](https://serpapi.com/blog/css-selectors-faster-than-getbyrole-playwright/)
##### Utilizing LocalStorage

View File

@ -78,13 +78,13 @@ async function createDomainObjectWithDefaults(
// Navigate to the parent object. This is necessary to create the object
// in the correct location, such as a folder, layout, or plot.
await page.goto(`${parentUrl}`);
await page.goto(parentUrl);
//Click the Create button
await page.getByRole('button', { name: 'Create' }).click();
// Click the Create button
await page.getByRole('button', { name: 'Create', exact: true }).click();
// Click the object specified by 'type'
await page.click(`li[role='menuitem']:text("${type}")`);
// Click the object specified by 'type'-- case insensitive
await page.getByRole('menuitem', { name: new RegExp(`^${type}$`, 'i') }).click();
// Modify the name input field of the domain object to accept 'name'
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
@ -275,6 +275,17 @@ async function navigateToObjectWithFixedTimeBounds(page, url, start, end) {
);
}
/**
* Navigates directly to a given object url, in real-time mode.
* @param {import('@playwright/test').Page} page
* @param {string} url The url to the domainObject
*/
async function navigateToObjectWithRealTime(page, url, start = '1800000', end = '30000') {
await page.goto(
`${url}?tc.mode=local&tc.startDelta=${start}&tc.endDelta=${end}&tc.timeSystem=utc`
);
}
/**
* Open the given `domainObject`'s context menu from the object tree.
* Expands the path to the object and scrolls to it if necessary.
@ -342,7 +353,7 @@ async function getFocusedObjectUuid(page) {
* @returns {Promise<string>} the url of the object
*/
async function getHashUrlToDomainObject(page, identifier) {
await page.waitForLoadState('load');
await page.waitForLoadState('domcontentloaded');
const hashUrl = await page.evaluate(async (objectIdentifier) => {
const path = await window.openmct.objects.getOriginalPath(objectIdentifier);
let url =
@ -581,9 +592,6 @@ async function waitForPlotsToRender(page) {
* @return {Promise<PlotPixel[]>}
*/
async function getCanvasPixels(page, canvasSelector) {
const getTelemValuePromise = new Promise((resolve) =>
page.exposeFunction('getCanvasValue', resolve)
);
const canvasHandle = await page.evaluateHandle(
(canvas) => document.querySelector(canvas),
canvasSelector
@ -594,7 +602,7 @@ async function getCanvasPixels(page, canvasSelector) {
);
await waitForPlotsToRender(page);
await page.evaluate(
return page.evaluate(
([canvas, ctx]) => {
// The document canvas is where the plot points and lines are drawn.
// The only way to access the canvas is using document (using page.evaluate)
@ -622,12 +630,10 @@ async function getCanvasPixels(page, canvasSelector) {
i = i + 4;
}
window.getCanvasValue(plotPixels);
return plotPixels;
},
[canvasHandle, canvasContextHandle]
);
return getTelemValuePromise;
}
/**
@ -656,6 +662,7 @@ export {
getFocusedObjectUuid,
getHashUrlToDomainObject,
navigateToObjectWithFixedTimeBounds,
navigateToObjectWithRealTime,
openObjectTreeContextMenu,
renameObjectFromContextMenu,
setEndOffset,

View File

@ -34,7 +34,7 @@
*/
import AxeBuilder from '@axe-core/playwright';
import fs from 'fs';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
@ -87,6 +87,27 @@ const extendedTest = test.extend({
}
});
/**
* Writes the accessibility report to the specified path.
*
* @param {string} reportPath - The path to write the report to.
* @param {Object} accessibilityScanResults - The results of the accessibility scan.
* @returns {Promise<Object>} The accessibility scan results.
* @throws Will throw an error if writing the report fails.
*/
async function writeAccessibilityReport(reportPath, accessibilityScanResults) {
try {
await fs.mkdir(path.dirname(reportPath), { recursive: true });
const data = JSON.stringify(accessibilityScanResults, null, 2);
await fs.writeFile(reportPath, data);
console.log(`Accessibility report with violations saved successfully as ${reportPath}`);
return accessibilityScanResults;
} catch (err) {
console.error(`Error writing the accessibility report to file ${reportPath}:`, err);
throw err;
}
}
/**
* Scans for accessibility violations on a page and writes a report to disk if violations are found.
* Automatically asserts that no violations should be present.
@ -104,25 +125,29 @@ export async function scanForA11yViolations(page, testCaseName, options = {}) {
const accessibilityScanResults = await builder.analyze();
// Assert that no violations should be present
expect(
accessibilityScanResults.violations,
`Accessibility violations found in test case: ${testCaseName}`
).toEqual([]);
expect
.soft(
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`);
const reportName = options.reportName || testCaseName;
const sanitizedReportName = reportName.replace(/\//g, '_');
const reportPath = path.join(
TEST_RESULTS_DIR,
'a11y-json-reports',
`${sanitizedReportName}.json`
);
try {
if (!fs.existsSync(TEST_RESULTS_DIR)) {
fs.mkdirSync(TEST_RESULTS_DIR);
}
await page.screenshot({
path: path.join(TEST_RESULTS_DIR, 'a11y-screenshots', `${sanitizedReportName}.png`)
});
fs.writeFileSync(reportPath, JSON.stringify(accessibilityScanResults, null, 2));
console.log(`Accessibility report with violations saved successfully as ${reportPath}`);
return accessibilityScanResults;
return await writeAccessibilityReport(reportPath, accessibilityScanResults);
} catch (err) {
console.error(`Error writing the accessibility report to file ${reportPath}:`, err);
throw err;

View File

@ -212,23 +212,6 @@ const extendedTest = test.extend({
.not.toEqual('error')
);
}
},
/**
* Extends the base browser class to enable CDP connection definition in playwright.config.js. Once
* that RFE is implemented, this function can be removed.
* @see {@link https://github.com/microsoft/playwright/issues/8379 Github RFE}
*/
browser: async ({ playwright, browser }, use, workerInfo) => {
// Use browserless if configured
if (workerInfo.project.name.match(/browserless/)) {
const vBrowser = await playwright.chromium.connectOverCDP({
endpointURL: 'ws://localhost:3003'
});
await use(vBrowser);
} else {
// Use Local Browser for testing.
await use(browser);
}
}
});

View File

@ -0,0 +1,47 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
const isMac = process.platform === 'darwin';
const modifier = isMac ? 'Meta' : 'Control';
/**
* @param {import('@playwright/test').Page} page
*/
async function selectAll(page) {
await page.keyboard.press(`${modifier}+KeyA`);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function copy(page) {
await page.keyboard.press(`${modifier}+KeyC`);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function paste(page) {
await page.keyboard.press(`${modifier}+KeyV`);
}
export { copy, paste, selectAll };

View File

@ -0,0 +1,23 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
export * from './clipboard.js';

View File

@ -28,16 +28,28 @@ import { fileURLToPath } from 'url';
/**
* @param {import('@playwright/test').Page} page
* @param {string} text
*/
async function enterTextEntry(page, text) {
// Click the 'Add Notebook Entry' area
await page.locator(NOTEBOOK_DROP_AREA).click();
// enter text
await page.getByLabel('Notebook Entry Input').last().fill(text);
await addNotebookEntry(page);
await enterTextInLastEntry(page, text);
await commitEntry(page);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function addNotebookEntry(page) {
await page.locator(NOTEBOOK_DROP_AREA).click();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function enterTextInLastEntry(page, text) {
await page.getByLabel('Notebook Entry Input').last().fill(text);
}
/**
* @param {import('@playwright/test').Page} page
*/
@ -140,10 +152,13 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
}
export {
addNotebookEntry,
commitEntry,
createNotebookAndEntry,
createNotebookEntryAndTags,
dragAndDropEmbed,
enterTextEntry,
enterTextInLastEntry,
lockPage,
startAndAddRestrictedNotebookObject
};

View File

@ -0,0 +1,29 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
// This should be used to install the Darkmatter theme for Open MCT.
// e.g.
// await page.addInitScript({ path: path.join(__dirname, 'useDarkmatterTheme.js') });
document.addEventListener('DOMContentLoaded', () => {
const openmct = window.openmct;
openmct.install(openmct.plugins.DarkmatterTheme());
});

View File

@ -10,7 +10,6 @@
}
},
"scripts": {
"pretest:visual": "npm install",
"test": "npx playwright test",
"test:visual": "percy exec"
},
@ -18,10 +17,13 @@
"@types/sinonjs__fake-timers": "8.1.5",
"@percy/cli": "1.27.4",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.42.1",
"@playwright/test": "1.45.2",
"@axe-core/playwright": "4.8.5",
"sinon": "17.0.0"
},
"author": "NASA Ames Research Center",
"author": {
"name": "National Aeronautics and Space Administration",
"url": "https://www.nasa.gov"
},
"license": "Apache-2.0"
}

View File

@ -12,7 +12,7 @@ const config = {
retries: 2, //Retries 2 times for a total of 3 runs. When running sharded and with max-failures=5, this should ensure that flake is managed without failing the full suite
testDir: 'tests',
grepInvert: /@mobile/, //Ignore mobile tests
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-performance.config.js
timeout: 60 * 1000,
webServer: {
command: 'npm run start:coverage',

View File

@ -10,7 +10,7 @@ import { fileURLToPath } from 'url';
const config = {
retries: 1, //Retries 2 times for a total of 3 runs. When running sharded and with max-failures=5, this should ensure that flake is managed without failing the full suite
testDir: 'tests',
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-performance.config.js
timeout: 30 * 1000,
webServer: {
command: 'npm run start:coverage',

View File

@ -13,7 +13,7 @@ const config = {
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
url: 'http://localhost:8080/#',
timeout: 200 * 1000,
reuseExistingServer: !process.env.CI
reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.
},
use: {
baseURL: 'http://localhost:8080/',
@ -36,6 +36,13 @@ const config = {
browserName: 'chromium',
theme: 'snow'
}
},
{
name: 'darkmatter-theme', //Runs the same visual tests but with darkmatter-theme
use: {
browserName: 'chromium',
theme: 'darkmatter'
}
}
],
reporter: [

View File

@ -127,6 +127,11 @@ const extendedTest = test.extend({
await page.addInitScript({
path: fileURLToPath(new URL('./helper/useSnowTheme.js', import.meta.url))
});
} else if (theme === 'darkmatter') {
//inject darkmatter theme
await page.addInitScript({
path: fileURLToPath(new URL('./helper/useDarkmatterTheme.js', import.meta.url))
});
}
// Attach info about the currently running test and its project.

File diff suppressed because one or more lines are too long

View File

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

View File

@ -41,7 +41,7 @@ test.describe('CouchDB Status Indicator with mocked responses @couchdb', () => {
//Go to baseURL
await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', {
waitUntil: 'networkidle'
waitUntil: 'domcontentloaded'
});
await expect(page.locator('div:has-text("CouchDB is connected")').nth(3)).toBeVisible();
});
@ -56,7 +56,7 @@ test.describe('CouchDB Status Indicator with mocked responses @couchdb', () => {
//Go to baseURL
await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', {
waitUntil: 'networkidle'
waitUntil: 'domcontentloaded'
});
await expect(page.locator('div:has-text("CouchDB is offline")').nth(3)).toBeVisible();
});
@ -71,7 +71,7 @@ test.describe('CouchDB Status Indicator with mocked responses @couchdb', () => {
//Go to baseURL
await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', {
waitUntil: 'networkidle'
waitUntil: 'domcontentloaded'
});
await expect(page.locator('div:has-text("CouchDB connectivity unknown")').nth(3)).toBeVisible();
});

View File

@ -188,8 +188,8 @@ test.describe('Persistence operations @couchdb', () => {
// Both pages: Go to baseURL
await Promise.all([
page.goto('./', { waitUntil: 'networkidle' }),
page2.goto('./', { waitUntil: 'networkidle' })
page.goto('./', { waitUntil: 'domcontentloaded' }),
page2.goto('./', { waitUntil: 'domcontentloaded' })
]);
//Slow down the test a bit

View File

@ -68,39 +68,36 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage @2p', () =>
});
//Begin suite of tests again localStorage
test.fixme(
'Condition set object properties persist in main view and inspector @localStorage',
async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7421'
});
//Navigate to baseURL with injected localStorage
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
test('Condition set object properties persist in main view and inspector after reload @localStorage', async ({
page
}) => {
//Navigate to baseURL with injected localStorage
await page.goto(conditionSetUrl, { waitUntil: 'domcontentloaded' });
//Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
await expect
.soft(page.locator('.l-browse-bar__object-name'))
.toContainText('Unnamed Condition Set');
//Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
await expect.soft(page.getByRole('main')).toContainText('Unnamed Condition Set');
//Assertions on loaded Condition Set in Inspector
expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
//Assertions on loaded Condition Set in Inspector
await expect(
page.getByLabel('Title inspector properties').getByLabel('inspector property value')
).toContainText('Unnamed Condition Set');
//Reload Page
await Promise.all([page.reload(), page.waitForLoadState('networkidle')]);
//Reload Page
await page.reload({ waitUntil: 'domcontentloaded' });
//Re-verify after reload
await expect.soft(page.getByRole('main')).toContainText('Unnamed Condition Set');
//Assertions on loaded Condition Set in Inspector
await expect(
page.getByLabel('Title inspector properties').getByLabel('inspector property value')
).toContainText('Unnamed Condition Set');
});
//Re-verify after reload
await expect
.soft(page.locator('.l-browse-bar__object-name'))
.toContainText('Unnamed Condition Set');
//Assertions on loaded Condition Set in Inspector
expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
}
);
test('condition set object can be modified on @localStorage', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
await page.goto(conditionSetUrl, { waitUntil: 'domcontentloaded' });
//Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
await expect
@ -151,7 +148,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage @2p', () =>
expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
//Reload Page
await Promise.all([page.reload(), page.waitForLoadState('networkidle')]);
await Promise.all([page.reload(), page.waitForLoadState('domcontentloaded')]);
//Verify Main section reflects updated Name Property
await expect
@ -213,265 +210,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage @2p', () =>
//Feature?
//Domain Object is still available by direct URL after delete
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
await page.goto(conditionSetUrl, { waitUntil: 'domcontentloaded' });
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
});
});
test.describe('Basic Condition Set Use', () => {
let conditionSet;
test.beforeEach(async ({ page }) => {
// Open a browser, navigate to the main page, and wait until all network events to resolve
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create a new condition set
conditionSet = await createDomainObjectWithDefaults(page, {
type: 'Condition Set',
name: 'Test Condition Set'
});
});
test('Creating a condition defaults the condition name to "Unnamed Condition"', async ({
page
}) => {
await page.goto(conditionSet.url);
// Change the object to edit mode
await page.getByLabel('Edit Object').click();
// Click Add Condition button
await page.locator('#addCondition').click();
// Check that the new Unnamed Condition section appears
const numOfUnnamedConditions = await page
.locator('.c-condition__name', { hasText: 'Unnamed Condition' })
.count();
expect(numOfUnnamedConditions).toEqual(1);
});
test('ConditionSet should display appropriate view options', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5924'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Alpha Sine Wave Generator'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Beta Sine Wave Generator'
});
await page.goto(conditionSet.url);
// Change the object to edit mode
await page.getByLabel('Edit Object').click();
// Expand the 'My Items' folder in the left tree
page.click('button[title="Show selected item in tree"]');
// Add the Alpha & Beta Sine Wave Generator to the Condition Set and save changes
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const alphaGeneratorTreeItem = treePane.getByRole('treeitem', {
name: 'Alpha Sine Wave Generator'
});
const betaGeneratorTreeItem = treePane.getByRole('treeitem', {
name: 'Beta Sine Wave Generator'
});
const conditionCollection = page.locator('#conditionCollection');
await alphaGeneratorTreeItem.dragTo(conditionCollection);
await betaGeneratorTreeItem.dragTo(conditionCollection);
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.getByLabel('Open the View Switcher Menu').click();
await expect(page.getByRole('menuitem', { name: /Lad Table/ })).toBeHidden();
await expect(page.getByRole('menuitem', { name: /Conditions View/ })).toBeVisible();
await expect(page.getByRole('menuitem', { name: /Plot/ })).toBeVisible();
await expect(page.getByRole('menuitem', { name: /Telemetry Table/ })).toBeVisible();
await page.getByLabel('Plot').click();
await expect(
page.getByLabel('Plot Legend Collapsed').getByText('Test Condition Set')
).toBeVisible();
await page.getByLabel('Open the View Switcher Menu').click();
await page.getByLabel('Telemetry Table').click();
await expect(page.getByRole('searchbox', { name: 'output filter input' })).toBeVisible();
await page.getByLabel('Open the View Switcher Menu').click();
await page.getByLabel('Conditions View').click();
await expect(page.getByText('Current Output')).toBeVisible();
});
test('ConditionSet has correct outputs when telemetry is and is not available', async ({
page
}) => {
const exampleTelemetry = await createExampleTelemetryObject(page);
await page.getByLabel('Show selected item in tree').click();
await page.goto(conditionSet.url);
// Change the object to edit mode
await page.getByLabel('Edit Object').click();
// Create two conditions
await page.locator('#addCondition').click();
await page.locator('#addCondition').click();
await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition');
await page.locator('#conditionCollection').getByRole('textbox').nth(1).fill('Second Condition');
// Add Telemetry to ConditionSet
const sineWaveGeneratorTreeItem = page
.getByRole('tree', {
name: 'Main Tree'
})
.getByRole('treeitem', {
name: exampleTelemetry.name
});
const conditionCollection = page.locator('#conditionCollection');
await sineWaveGeneratorTreeItem.dragTo(conditionCollection);
// Modify First Criterion
const firstCriterionTelemetry = page.locator(
'[aria-label="Criterion Telemetry Selection"] >> nth=0'
);
firstCriterionTelemetry.selectOption({ label: exampleTelemetry.name });
const firstCriterionMetadata = page.locator(
'[aria-label="Criterion Metadata Selection"] >> nth=0'
);
firstCriterionMetadata.selectOption({ label: 'Sine' });
const firstCriterionComparison = page.locator(
'[aria-label="Criterion Comparison Selection"] >> nth=0'
);
firstCriterionComparison.selectOption({ label: 'is greater than or equal to' });
const firstCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=0');
await firstCriterionInput.fill('0');
// Modify First Criterion
const secondCriterionTelemetry = page.locator(
'[aria-label="Criterion Telemetry Selection"] >> nth=1'
);
secondCriterionTelemetry.selectOption({ label: exampleTelemetry.name });
const secondCriterionMetadata = page.locator(
'[aria-label="Criterion Metadata Selection"] >> nth=1'
);
secondCriterionMetadata.selectOption({ label: 'Sine' });
const secondCriterionComparison = page.locator(
'[aria-label="Criterion Comparison Selection"] >> nth=1'
);
secondCriterionComparison.selectOption({ label: 'is less than' });
const secondCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=1');
await secondCriterionInput.fill('0');
// Save ConditionSet
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Validate that the condition set is evaluating and outputting
// the correct value when the underlying telemetry subscription is active.
let outputValue = page.locator('[aria-label="Current Output Value"]');
await expect(outputValue).toHaveText('false');
await page.goto(exampleTelemetry.url);
// Edit SWG to add 8 second loading delay to simulate the case
// where telemetry is not available.
await page.getByTitle('More actions').click();
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
await page.getByRole('spinbutton', { name: 'Loading Delay (ms)' }).fill('8000');
await page.getByLabel('Save').click();
// Expect that the output value is blank or '---' if the
// underlying telemetry subscription is not active.
await page.goto(conditionSet.url);
await expect(outputValue).toHaveText('---');
});
test('ConditionSet has correct outputs when test data is enabled', async ({ page }) => {
const exampleTelemetry = await createExampleTelemetryObject(page);
await page.getByLabel('Show selected item in tree').click();
await page.goto(conditionSet.url);
// Change the object to edit mode
await page.getByLabel('Edit Object').click();
// Create two conditions
await page.locator('#addCondition').click();
await page.locator('#addCondition').click();
await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition');
await page.locator('#conditionCollection').getByRole('textbox').nth(1).fill('Second Condition');
// Add Telemetry to ConditionSet
const sineWaveGeneratorTreeItem = page
.getByRole('tree', {
name: 'Main Tree'
})
.getByRole('treeitem', {
name: exampleTelemetry.name
});
const conditionCollection = page.locator('#conditionCollection');
await sineWaveGeneratorTreeItem.dragTo(conditionCollection);
// Modify First Criterion
const firstCriterionTelemetry = page.locator(
'[aria-label="Criterion Telemetry Selection"] >> nth=0'
);
firstCriterionTelemetry.selectOption({ label: exampleTelemetry.name });
const firstCriterionMetadata = page.locator(
'[aria-label="Criterion Metadata Selection"] >> nth=0'
);
firstCriterionMetadata.selectOption({ label: 'Sine' });
const firstCriterionComparison = page.locator(
'[aria-label="Criterion Comparison Selection"] >> nth=0'
);
firstCriterionComparison.selectOption({ label: 'is greater than or equal to' });
const firstCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=0');
await firstCriterionInput.fill('0');
// Modify Second Criterion
const secondCriterionTelemetry = page.locator(
'[aria-label="Criterion Telemetry Selection"] >> nth=1'
);
await secondCriterionTelemetry.selectOption({ label: exampleTelemetry.name });
const secondCriterionMetadata = page.locator(
'[aria-label="Criterion Metadata Selection"] >> nth=1'
);
await secondCriterionMetadata.selectOption({ label: 'Sine' });
const secondCriterionComparison = page.locator(
'[aria-label="Criterion Comparison Selection"] >> nth=1'
);
await secondCriterionComparison.selectOption({ label: 'is less than' });
const secondCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=1');
await secondCriterionInput.fill('0');
// Enable test data
await page.getByLabel('Apply Test Data').nth(1).click();
const testDataTelemetry = page.locator('[aria-label="Test Data Telemetry Selection"] >> nth=0');
await testDataTelemetry.selectOption({ label: exampleTelemetry.name });
const testDataMetadata = page.locator('[aria-label="Test Data Metadata Selection"] >> nth=0');
await testDataMetadata.selectOption({ label: 'Sine' });
const testInput = page.locator('[aria-label="Test Data Input"] >> nth=0');
await testInput.fill('0');
// Validate that the condition set is evaluating and outputting
// the correct value when the underlying telemetry subscription is active.
let outputValue = page.locator('[aria-label="Current Output Value"]');
await expect(outputValue).toHaveText('false');
await page.goto(exampleTelemetry.url);
});
test.fixme('Ensure condition sets work with telemetry like operator status', ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7484'
});
});
});

View File

@ -0,0 +1,368 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets. Note: this
suite is sharing state between tests which is considered an anti-pattern. Implementing in this way to
demonstrate some playwright for test developers. This pattern should not be re-used in other CRUD suites.
*/
import {
createDomainObjectWithDefaults,
createExampleTelemetryObject
} from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Basic Condition Set Use', () => {
let conditionSet;
test.beforeEach(async ({ page }) => {
// Open a browser, navigate to the main page, and wait until all network events to resolve
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create a new condition set
conditionSet = await createDomainObjectWithDefaults(page, {
type: 'Condition Set',
name: 'Test Condition Set'
});
});
test('Creating a condition defaults the condition name to "Unnamed Condition"', async ({
page
}) => {
await page.goto(conditionSet.url);
// Change the object to edit mode
await page.getByLabel('Edit Object').click();
// Click Add Condition button
await page.locator('#addCondition').click();
// Check that the new Unnamed Condition section appears
const numOfUnnamedConditions = await page
.locator('.c-condition__name', { hasText: 'Unnamed Condition' })
.count();
expect(numOfUnnamedConditions).toEqual(1);
});
test('ConditionSet should display appropriate view options', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5924'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Alpha Sine Wave Generator'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Beta Sine Wave Generator'
});
await page.goto(conditionSet.url);
// Change the object to edit mode
await page.getByLabel('Edit Object').click();
// Expand the 'My Items' folder in the left tree
page.click('button[title="Show selected item in tree"]');
// Add the Alpha & Beta Sine Wave Generator to the Condition Set and save changes
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const alphaGeneratorTreeItem = treePane.getByRole('treeitem', {
name: 'Alpha Sine Wave Generator'
});
const betaGeneratorTreeItem = treePane.getByRole('treeitem', {
name: 'Beta Sine Wave Generator'
});
const conditionCollection = page.locator('#conditionCollection');
await alphaGeneratorTreeItem.dragTo(conditionCollection);
await betaGeneratorTreeItem.dragTo(conditionCollection);
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.getByLabel('Open the View Switcher Menu').click();
await expect(page.getByRole('menuitem', { name: /Lad Table/ })).toBeHidden();
await expect(page.getByRole('menuitem', { name: /Conditions View/ })).toBeVisible();
await expect(page.getByRole('menuitem', { name: /Plot/ })).toBeVisible();
await expect(page.getByRole('menuitem', { name: /Telemetry Table/ })).toBeVisible();
await page.getByLabel('Plot').click();
await expect(
page.getByLabel('Plot Legend Collapsed').getByText('Test Condition Set')
).toBeVisible();
await page.getByLabel('Open the View Switcher Menu').click();
await page.getByLabel('Telemetry Table').click();
await expect(page.getByRole('searchbox', { name: 'output filter input' })).toBeVisible();
await page.getByLabel('Open the View Switcher Menu').click();
await page.getByLabel('Conditions View').click();
await expect(page.getByText('Current Output')).toBeVisible();
});
test('ConditionSet has correct outputs when telemetry is and is not available', async ({
page
}) => {
const exampleTelemetry = await createExampleTelemetryObject(page);
await page.getByLabel('Show selected item in tree').click();
await page.goto(conditionSet.url);
// Change the object to edit mode
await page.getByLabel('Edit Object').click();
// Create two conditions
await page.locator('#addCondition').click();
await page.locator('#addCondition').click();
await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition');
await page.locator('#conditionCollection').getByRole('textbox').nth(1).fill('Second Condition');
// Add Telemetry to ConditionSet
const sineWaveGeneratorTreeItem = page
.getByRole('tree', {
name: 'Main Tree'
})
.getByRole('treeitem', {
name: exampleTelemetry.name
});
const conditionCollection = page.locator('#conditionCollection');
await sineWaveGeneratorTreeItem.dragTo(conditionCollection);
// Modify First Criterion
const firstCriterionTelemetry = page.locator(
'[aria-label="Criterion Telemetry Selection"] >> nth=0'
);
firstCriterionTelemetry.selectOption({ label: exampleTelemetry.name });
const firstCriterionMetadata = page.locator(
'[aria-label="Criterion Metadata Selection"] >> nth=0'
);
firstCriterionMetadata.selectOption({ label: 'Sine' });
const firstCriterionComparison = page.locator(
'[aria-label="Criterion Comparison Selection"] >> nth=0'
);
firstCriterionComparison.selectOption({ label: 'is greater than or equal to' });
const firstCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=0');
await firstCriterionInput.fill('0');
// Modify First Criterion
const secondCriterionTelemetry = page.locator(
'[aria-label="Criterion Telemetry Selection"] >> nth=1'
);
secondCriterionTelemetry.selectOption({ label: exampleTelemetry.name });
const secondCriterionMetadata = page.locator(
'[aria-label="Criterion Metadata Selection"] >> nth=1'
);
secondCriterionMetadata.selectOption({ label: 'Sine' });
const secondCriterionComparison = page.locator(
'[aria-label="Criterion Comparison Selection"] >> nth=1'
);
secondCriterionComparison.selectOption({ label: 'is less than' });
const secondCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=1');
await secondCriterionInput.fill('0');
// Save ConditionSet
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Validate that the condition set is evaluating and outputting
// the correct value when the underlying telemetry subscription is active.
let outputValue = page.getByLabel('Current Output Value');
await expect(outputValue).toHaveText('false');
await page.goto(exampleTelemetry.url);
// Edit SWG to add 8 second loading delay to simulate the case
// where telemetry is not available.
await page.getByTitle('More actions').click();
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
await page.getByRole('spinbutton', { name: 'Loading Delay (ms)' }).fill('8000');
await page.getByLabel('Save').click();
// Expect that the output value is blank or '---' if the
// underlying telemetry subscription is not active.
await page.goto(conditionSet.url);
await expect(outputValue).toHaveText('---');
});
test('ConditionSet has correct outputs when test data is enabled', async ({ page }) => {
const exampleTelemetry = await createExampleTelemetryObject(page);
await page.getByLabel('Show selected item in tree').click();
await page.goto(conditionSet.url);
// Change the object to edit mode
await page.getByLabel('Edit Object').click();
// Create two conditions
await page.locator('#addCondition').click();
await page.locator('#addCondition').click();
await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition');
await page.locator('#conditionCollection').getByRole('textbox').nth(1).fill('Second Condition');
// Add Telemetry to ConditionSet
const sineWaveGeneratorTreeItem = page
.getByRole('tree', {
name: 'Main Tree'
})
.getByRole('treeitem', {
name: exampleTelemetry.name
});
const conditionCollection = page.locator('#conditionCollection');
await sineWaveGeneratorTreeItem.dragTo(conditionCollection);
// Modify First Criterion
const firstCriterionTelemetry = page.locator(
'[aria-label="Criterion Telemetry Selection"] >> nth=0'
);
firstCriterionTelemetry.selectOption({ label: exampleTelemetry.name });
const firstCriterionMetadata = page.locator(
'[aria-label="Criterion Metadata Selection"] >> nth=0'
);
firstCriterionMetadata.selectOption({ label: 'Sine' });
const firstCriterionComparison = page.locator(
'[aria-label="Criterion Comparison Selection"] >> nth=0'
);
firstCriterionComparison.selectOption({ label: 'is greater than or equal to' });
const firstCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=0');
await firstCriterionInput.fill('0');
// Modify Second Criterion
const secondCriterionTelemetry = page.locator(
'[aria-label="Criterion Telemetry Selection"] >> nth=1'
);
await secondCriterionTelemetry.selectOption({ label: exampleTelemetry.name });
const secondCriterionMetadata = page.locator(
'[aria-label="Criterion Metadata Selection"] >> nth=1'
);
await secondCriterionMetadata.selectOption({ label: 'Sine' });
const secondCriterionComparison = page.locator(
'[aria-label="Criterion Comparison Selection"] >> nth=1'
);
await secondCriterionComparison.selectOption({ label: 'is less than' });
const secondCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=1');
await secondCriterionInput.fill('0');
// Enable test data
await page.getByLabel('Apply Test Data').nth(1).click();
const testDataTelemetry = page.locator('[aria-label="Test Data Telemetry Selection"] >> nth=0');
await testDataTelemetry.selectOption({ label: exampleTelemetry.name });
const testDataMetadata = page.locator('[aria-label="Test Data Metadata Selection"] >> nth=0');
await testDataMetadata.selectOption({ label: 'Sine' });
const testInput = page.locator('[aria-label="Test Data Input"] >> nth=0');
await testInput.fill('0');
// Validate that the condition set is evaluating and outputting
// the correct value when the underlying telemetry subscription is active.
let outputValue = page.getByLabel('Current Output Value');
await expect(outputValue).toHaveText('false');
await page.goto(exampleTelemetry.url);
});
test.fixme('Ensure condition sets work with telemetry like operator status', ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7484'
});
});
});
test.describe('Condition Set Composition', () => {
let conditionSet;
let exampleTelemetry;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create Condition Set
conditionSet = await createDomainObjectWithDefaults(page, {
type: 'Condition Set'
});
// Create Telemetry Object as child to Condition Set
exampleTelemetry = await createExampleTelemetryObject(page, conditionSet.uuid);
// Edit Condition Set
await page.goto(conditionSet.url);
await page.getByRole('button', { name: 'Edit Object' }).click();
// Add Condition to Condition Set
await page.getByRole('button', { name: 'Add Condition' }).click();
// Enter Condition Output
await page.getByLabel('Condition Name Input').first().fill('Negative');
await page.getByLabel('Condition Output Type').first().selectOption({ value: 'string' });
await page.getByLabel('Condition Output String').first().fill('Negative');
// Condition Trigger default is okay so no change needed to form
// Enter Condition Criterion
await page.getByLabel('Criterion Telemetry Selection').first().selectOption({ value: 'all' });
await page.getByLabel('Criterion Metadata Selection').first().selectOption({ value: 'sin' });
await page
.locator('select[aria-label="Criterion Comparison Selection"]')
.first()
.selectOption({ value: 'lessThan' });
await page.getByLabel('Criterion Input').first().fill('0');
// Save the Condition Set
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
});
test('You can remove telemetry from a condition set with existing conditions', async ({
page
}) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7710'
});
await page.getByLabel('Expand My Items folder').click();
await page.getByLabel(`Expand ${conditionSet.name} conditionSet`).click();
await page
.getByLabel(`Navigate to ${exampleTelemetry.name}`, { exact: false })
.click({ button: 'right' });
await page
.getByLabel(`${exampleTelemetry.name} Context Menu`)
.getByRole('menuitem', { name: 'Remove' })
.click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
await page
.getByLabel(`Navigate to ${conditionSet.name} conditionSet Object`, { exact: true })
.click();
await page.getByRole('button', { name: 'Edit Object' }).click();
await page.getByRole('tab', { name: 'Elements' }).click();
expect(
await page
.getByRole('tabpanel', { name: 'Inspector Views' })
.getByRole('listitem', { name: exampleTelemetry.name })
.count()
).toEqual(0);
});
});

View File

@ -519,7 +519,7 @@ test.describe('Display Layout', () => {
await page.reload();
// wait for annotations requests to be batched and requested
await page.waitForLoadState('networkidle');
await page.waitForLoadState('domcontentloaded');
// Network requests for the composite telemetry with multiple items should be:
// 1. a single batched request for annotations
expect(networkRequests.length).toBe(1);
@ -531,7 +531,7 @@ test.describe('Display Layout', () => {
await page.reload();
// wait for annotations to not load (if we have any, we've got a problem)
await page.waitForLoadState('networkidle');
await page.waitForLoadState('domcontentloaded');
// In real time mode, we don't fetch annotations at all
expect(networkRequests.length).toBe(0);

View File

@ -78,8 +78,8 @@ test.describe('Flexible Layout', () => {
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
// Add the Sine Wave Generator and Clock to the Flexible Layout
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await clockTreeItem.dragTo(page.locator('.c-fl__container.is-empty'));
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());
await clockTreeItem.dragTo(page.locator('.c-fl-container.is-empty'));
// Check that panes can be dragged while Flexible Layout is in Edit mode
let dragWrapper = page
.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper')
@ -105,8 +105,8 @@ test.describe('Flexible Layout', () => {
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
// Add the Sine Wave Generator and Clock to the Flexible Layout
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await clockTreeItem.dragTo(page.locator('.c-fl__container.is-empty'));
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());
await clockTreeItem.dragTo(page.locator('.c-fl-container.is-empty'));
// Click on the first frame to select it
await page.locator('.c-fl-container__frame').first().click();
@ -122,7 +122,7 @@ test.describe('Flexible Layout', () => {
expect(await page.locator('.c-fl--rows').count()).toEqual(0);
// Change the layout to rows orientation
await page.getByTitle('Columns layout').click();
await page.getByTitle('Switch to rows layout').click();
// Assert the layout is in rows orientation
expect(await page.locator('.c-fl--rows').count()).toBeGreaterThan(0);
@ -171,7 +171,7 @@ test.describe('Flexible Layout', () => {
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
// Add the Sine Wave Generator to the Flexible Layout and save changes
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
@ -202,7 +202,7 @@ test.describe('Flexible Layout', () => {
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// Add the Sine Wave Generator to the Flexible Layout and save changes
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
@ -242,7 +242,7 @@ test.describe('Flexible Layout', () => {
name: new RegExp(exampleImageryObject.name)
});
// Add the Sine Wave Generator to the Flexible Layout and save changes
await exampleImageryTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await exampleImageryTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
@ -309,9 +309,9 @@ test.describe('Flexible Layout Toolbar Actions @localStorage', () => {
await page.getByRole('columnheader', { name: 'Container Handle 1' }).click();
const flexRows = page.getByLabel('Flexible Layout Row');
expect(await flexRows.count()).toEqual(0);
await page.getByTitle('Columns layout').click();
await page.getByTitle('Switch to rows layout').click();
expect(await flexRows.count()).toEqual(1);
await page.getByTitle('Rows layout').click();
await page.getByTitle('Switch to columns layout').click();
expect(await flexRows.count()).toEqual(0);
});
});

View File

@ -136,14 +136,11 @@ test.describe('Gauge', () => {
// TODO: Verify changes in the UI
});
test.fixme('Gauge does not display NaN when data not available', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7421'
});
test('Gauge does not display NaN when data not available', async ({ page }) => {
// Create a Gauge
const gauge = await createDomainObjectWithDefaults(page, {
type: 'Gauge'
type: 'Gauge',
name: 'Gauge with no data'
});
// Create a Sine Wave Generator in the Gauge with a loading delay
@ -154,7 +151,7 @@ test.describe('Gauge', () => {
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.getByLabel('Loading Delay (ms)', { exact: true }).fill('5000');
await page.getByRole('button', { name: 'Save' }).click();
@ -162,9 +159,13 @@ test.describe('Gauge', () => {
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('--');
await page.goto(gauge.url, { waitUntil: 'domcontentloaded' });
// Check that the value is not displayed
//TODO https://github.com/nasa/openmct/issues/7790 update this locator
await expect(page.getByTitle('Value is currently out of')).toHaveAttribute(
'aria-valuenow',
'--'
);
});
test('Gauge enforces composition policy', async ({ page }) => {

View File

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

View File

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

View File

@ -27,6 +27,7 @@ This test suite is dedicated to tests which verify the basic operations surround
import { fileURLToPath } from 'url';
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
import { copy, paste, selectAll } from '../../../../helper/hotkeys/hotkeys.js';
import * as nbUtils from '../../../../helper/notebookUtils.js';
import { expect, streamToString, test } from '../../../../pluginFixtures.js';
@ -546,4 +547,53 @@ test.describe('Notebook entry tests', () => {
);
await expect(secondLineOfBlockquoteText).toBeVisible();
});
/**
* Paste into notebook entry tests
*/
test('Can paste text into a notebook entry', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7686'
});
const TEST_TEXT = 'This is a test';
const iterations = 20;
const EXPECTED_TEXT = TEST_TEXT.repeat(iterations);
await page.goto(notebookObject.url);
await nbUtils.addNotebookEntry(page);
await nbUtils.enterTextInLastEntry(page, TEST_TEXT);
await selectAll(page);
await copy(page);
for (let i = 0; i < iterations; i++) {
await paste(page);
}
await nbUtils.commitEntry(page);
await expect(page.locator(`text="${EXPECTED_TEXT}"`)).toBeVisible();
});
test('Prevents pasting text into selected notebook entry if not editing', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7686'
});
const TEST_TEXT = 'This is a test';
await page.goto(notebookObject.url);
await nbUtils.addNotebookEntry(page);
await nbUtils.enterTextInLastEntry(page, TEST_TEXT);
await selectAll(page);
await copy(page);
await paste(page);
await nbUtils.commitEntry(page);
// This should not paste text into the entry
await paste(page);
await expect(await page.locator(`text="${TEST_TEXT.repeat(1)}"`).count()).toEqual(1);
await expect(await page.locator(`text="${TEST_TEXT.repeat(2)}"`).count()).toEqual(0);
});
});

View File

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

View File

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

View File

@ -69,6 +69,7 @@ test.describe('Handle missing object for plots', () => {
}, jsonData);
//Reloads page and clicks on stacked plot
await Promise.all([page.reload(), page.waitForLoadState('domcontentloaded')]);
await page.reload({ waitUntil: 'domcontentloaded' });
await page.goto(stackedPlot.url);
@ -81,3 +82,84 @@ test.describe('Handle missing object for plots', () => {
expect(warningReceived).toBe(true);
});
});
/**
* This is used the create a stacked plot object
* @private
*/
async function makeStackedPlot(page, myItemsFolderName) {
// fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
await page.goto('./', { waitUntil: 'domcontentloaded' });
// create stacked plot
await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Stacked Plot")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
// save the stacked plot
await saveStackedPlot(page);
// create a sinewave generator
await createSineWaveGenerator(page);
// click on stacked plot
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Stacked Plot').first().click()
]);
// create a second sinewave generator
await createSineWaveGenerator(page);
// click on stacked plot
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Stacked Plot').first().click()
]);
}
/**
* This is used to save a stacked plot object
* @private
*/
async function saveStackedPlot(page) {
// save stacked plot
await page
.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button')
.nth(1)
.click();
await Promise.all([
page.locator('text=Save and Finish Editing').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
}
/**
* This is used to create a sine wave generator object
* @private
*/
async function createSineWaveGenerator(page) {
//Create sine wave generator
await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
}

View File

@ -91,7 +91,7 @@ test.describe('Overlay Plot', () => {
// Assert that the legend is collapsed by default
await expect(page.getByLabel('Plot Legend Collapsed')).toBeVisible();
await expect(page.getByLabel('Plot Legend Expanded')).toBeHidden();
await expect(page.getByLabel('Expand by Default')).toHaveText('No');
await expect(page.getByLabel('Expand by Default')).toHaveText(/No/);
expect(await page.getByLabel('Plot Legend Item').count()).toBe(3);
@ -106,7 +106,7 @@ test.describe('Overlay Plot', () => {
await expect(page.getByRole('cell', { name: 'Name' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Value' })).toBeVisible();
await expect(page.getByLabel('Expand by Default')).toHaveText('Yes');
await expect(page.getByLabel('Expand by Default')).toHaveText(/Yes/);
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(3);
// Assert that the legend is expanded on page load
@ -116,7 +116,7 @@ test.describe('Overlay Plot', () => {
await expect(page.getByRole('cell', { name: 'Name' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Value' })).toBeVisible();
await expect(page.getByLabel('Expand by Default')).toHaveText('Yes');
await expect(page.getByLabel('Expand by Default')).toHaveText(/Yes/);
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(3);
});
@ -144,15 +144,8 @@ test.describe('Overlay Plot', () => {
// Expand the "Sine Wave Generator" plot series options and enable limit lines
await page.getByRole('tab', { name: 'Config' }).click();
await page
.getByRole('list', { name: 'Plot Series Properties' })
.locator('span')
.first()
.click();
await page
.getByRole('list', { name: 'Plot Series Properties' })
.locator('[title="Display limit lines"]~div input')
.check();
await page.getByLabel('Expand Sine Wave Generator:').click();
await page.getByLabel('Limit lines').check();
await assertLimitLinesExistAndAreVisible(page);
@ -215,21 +208,13 @@ test.describe('Overlay Plot', () => {
// Expand the "Sine Wave Generator" plot series options and enable limit lines
await page.getByRole('tab', { name: 'Config' }).click();
await page
.getByRole('list', { name: 'Plot Series Properties' })
.locator('span')
.first()
.click();
await page
.getByRole('list', { name: 'Plot Series Properties' })
.getByRole('checkbox', { name: 'Limit lines' })
.check();
await page.getByLabel('Expand Sine Wave Generator:').click();
await page.getByLabel('Limit lines').check();
await assertLimitLinesExistAndAreVisible(page);
// Save (exit edit mode)
await page.locator('button[title="Save"]').click();
await page.locator('li[title="Save and Finish Editing"]').click();
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
const initialCoords = await assertLimitLinesExistAndAreVisible(page);
// Resize the chart container by showing the snapshot pane.
@ -324,32 +309,26 @@ test.describe('Overlay Plot', () => {
expect(yAxis3Group.getByRole('listitem').nth(0).getByText(swgB.name)).toBeTruthy();
});
test.fixme(
'Clicking on an item in the elements pool brings up the plot preview with data points',
async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7421'
});
test('Clicking on an item in the elements pool brings up the plot preview with data points', async ({
page
}) => {
const swgA = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
const swgA = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url);
// Wait for plot series data to load and be drawn
await waitForPlotsToRender(page);
await page.getByLabel('Edit Object').click();
await page.goto(overlayPlot.url);
// Wait for plot series data to load and be drawn
await waitForPlotsToRender(page);
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'Elements' }).click();
await page.getByRole('tab', { name: 'Elements' }).click();
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click();
const plotPixels = await getCanvasPixels(page, '.js-overlay canvas');
const plotPixelSize = plotPixels.length;
expect(plotPixelSize).toBeGreaterThan(0);
}
);
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click();
const plotPixels = await getCanvasPixels(page, '.js-overlay canvas');
const plotPixelSize = plotPixels.length;
expect(plotPixelSize).toBeGreaterThan(0);
});
test('Can remove an item via the elements pool action menu', async ({ page }) => {
const swgA = await createDomainObjectWithDefaults(page, {

View File

@ -0,0 +1,118 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
* This test suite is dedicated to testing the rendering and interaction of plots.
*
*/
import {
createDomainObjectWithDefaults,
getCanvasPixels,
setEndOffset,
setRealTimeMode,
setStartOffset
} from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Plot Controls', () => {
let overlayPlot;
test.beforeEach(async ({ page }) => {
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'domcontentloaded' });
overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
// Create an overlay plot with a sine wave generator
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await page.goto(`${overlayPlot.url}`);
});
test("Plots don't purge data when paused", async ({ page }) => {
// Set realtime mode with 2 second window
const startOffset = {
startMins: '00',
startSecs: '01'
};
const endOffset = {
endMins: '00',
endSecs: '01'
};
// Switch to real-time mode
await setRealTimeMode(page);
// Set start time offset
await setStartOffset(page, startOffset);
// Set end time offset
await setEndOffset(page, endOffset);
// Edit the overlay plot and turn off auto scale, setting the min and max to -1 and 1
// enter edit mode
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'Config' }).click();
// turn off autoscale
await page.getByRole('checkbox', { name: 'Auto scale' }).uncheck();
await page.getByLabel('Y Axis 1 Minimum value').fill('-1');
await page.getByLabel('Y Axis 1 Maximum value').fill('1');
// save
await page.click('button[title="Save"]');
await Promise.all([
page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
// hover over plot for plot controls
await page.getByLabel('Plot Canvas').hover();
// click on pause control
await page.getByTitle('Pause incoming real-time data').click();
// expect plot to be paused
await expect(page.getByTitle('Resume displaying real-time data')).toBeVisible();
// Wait for 2 seconds to stabilize plot data - future timestamp
// eslint-disable-next-line
await page.waitForTimeout(2000);
// Capture the # of plot points
const plotPixels = await getCanvasPixels(page, 'canvas');
const plotPixelSizeAtPause = plotPixels.length;
// Wait 2 seconds
// eslint-disable-next-line
await page.waitForTimeout(2000);
// Capture the # of plot points
const plotPixelsAfterWait = await getCanvasPixels(page, 'canvas');
const plotPixelSizeAfterWait = plotPixelsAfterWait.length;
// Expect before and after plot points to match
await expect(plotPixelSizeAtPause).toEqual(plotPixelSizeAfterWait);
});
});

View File

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

View File

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

View File

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

View File

@ -30,7 +30,7 @@ import { expect, test } from '../../baseFixtures.js';
test.describe('Renaming objects', () => {
test.beforeEach(async ({ page }) => {
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('When renaming objects, the browse bar and various components all update', async ({

View File

@ -0,0 +1,64 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { createDomainObjectWithDefaults, navigateToObjectWithRealTime } from '../../appActions.js';
import { expect, test } from '../../pluginFixtures.js';
test.describe('Staleness', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Does not show staleness after navigating from a stale object', async ({ page }) => {
const objectViewSelector = '.c-object-view';
const isStaleClass = 'is-stale';
const staleSWG = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'SWG'
});
// edit properties and enable staleness updates
await page.getByLabel('More actions').click();
await page.getByLabel('Edit properties...').click();
await page.getByLabel('Provide Staleness Updates', { exact: true }).click();
await page.getByLabel('Save').click();
const folder = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Folder 1'
});
// Navigate to the stale object
await navigateToObjectWithRealTime(page, staleSWG.url);
// Assert that staleness is shown
await expect(page.locator(`${objectViewSelector} .${isStaleClass}`)).toBeAttached({
timeout: 30 * 1000 // Give 30 seconds for the staleness to be updated
});
// Immediately navigate to the folder
await page.goto(folder.url);
// Verify that staleness is not shown
await expect(page.locator(`${objectViewSelector} .${isStaleClass}`)).not.toBeAttached();
});
});

View File

@ -21,16 +21,7 @@
*****************************************************************************/
/*
This test suite is dedicated to tests which can quickly verify that any openmct installation is
operable and that any type of testing can proceed.
Ideally, smoke tests should make zero assumptions about how and where they are run. This makes them
more resilient to change and therefor a better indicator of failure. Smoke tests will also run quickly
as they cover a very "thin surface" of functionality.
When deciding between authoring new smoke tests or functional tests, ask yourself "would I feel
comfortable running this test during a live mission?" Avoid creating or deleting Domain Objects.
Make no assumptions about the order that elements appear in the DOM.
This suite is dedicated to tests which verify that tooltips are displayed correctly.
*/
import { createDomainObjectWithDefaults, expandEntireTree } from '../../appActions.js';
@ -48,7 +39,7 @@ test.describe('Verify tooltips', () => {
const swg2Path = 'My Items / Folder Foo / Folder Bar / SWG 2';
const swg3Path = 'My Items / Folder Foo / Folder Bar / Folder Baz / SWG 3';
test.beforeEach(async ({ page, openmctConfig }) => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
folder1 = await createDomainObjectWithDefaults(page, {
@ -89,7 +80,7 @@ test.describe('Verify tooltips', () => {
await expandEntireTree(page);
});
test('display correct paths for LAD tables', async ({ page, openmctConfig }) => {
test('display correct paths for LAD tables', async ({ page }) => {
// Create LAD table
await createDomainObjectWithDefaults(page, {
type: 'LAD Table',
@ -98,25 +89,32 @@ test.describe('Verify tooltips', () => {
// Edit LAD table
await page.getByLabel('Edit Object').click();
// Add the Sine Wave Generator to the LAD table and save changes
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-lad-table-wrapper');
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();
// Add the Sine Wave Generator to the LAD table and save changes.
//TODO Follow up with https://github.com/nasa/openmct/issues/7773
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '#lad-table-drop-area');
await page.dragAndDrop(`text=${sineWaveObject2.name}`, '#lad-table-drop-area');
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '#lad-table-drop-area');
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.keyboard.down('Control');
//Hover on something else
await page.getByRole('button', { name: 'Create' }).hover();
//Hover over the first
await page.getByLabel('lad name').getByText(sineWaveObject1.name).hover();
await expect(page.getByRole('tooltip', { name: sineWaveObject1.path })).toBeVisible();
async function getToolTip(object) {
await page.locator('.c-create-button').hover();
await page.getByLabel('lad name').getByText(object.name).hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
return tooltipText.replace('\n', '').trim();
}
//Hover on something else
await page.getByRole('button', { name: 'Create' }).hover();
//Hover over second object
await page.getByLabel('lad name').getByText(sineWaveObject2.name).hover();
await expect(page.getByRole('tooltip', { name: sineWaveObject2.path })).toBeVisible();
expect(await getToolTip(sineWaveObject1)).toBe(sineWaveObject1.path);
expect(await getToolTip(sineWaveObject2)).toBe(sineWaveObject2.path);
expect(await getToolTip(sineWaveObject3)).toBe(sineWaveObject3.path);
//Hover on something else
await page.getByRole('button', { name: 'Create' }).hover();
//Hover over third object
await page.getByLabel('lad name').getByText(sineWaveObject3.name).hover();
await expect(page.getByRole('tooltip', { name: sineWaveObject3.path })).toBeVisible();
});
test('display correct paths for expanded and collapsed plot legend items', async ({ page }) => {
@ -128,66 +126,74 @@ test.describe('Verify tooltips', () => {
// Edit Overlay Plot
await page.getByLabel('Edit Object').click();
// Add the Sine Wave Generator to the LAD table and save changes
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.gl-plot');
await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.gl-plot');
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.gl-plot');
await page.locator('button[title="Save"]').click();
// Add the Sine Wave Generators to the and save changes
await page
.getByLabel('Preview SWG 1 generator Object')
.dragTo(page.getByLabel('Plot Container Style Target'));
await page
.getByLabel('Preview SWG 2 generator Object')
.dragTo(page.getByLabel('Plot Container Style Target'));
await page
.getByLabel('Preview SWG 3 generator Object')
.dragTo(page.getByLabel('Plot Container Style Target'));
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
//Hover over Collapsed Plot Legend Components with the Control Key pressed
await page.keyboard.down('Control');
async function getCollapsedLegendToolTip(object) {
await page.locator('.c-create-button').hover();
await page
.locator('.plot-series-name', { has: page.locator(`text="${object.name} Hz"`) })
.hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
return tooltipText.replace('\n', '').trim();
}
async function getExpandedLegendToolTip(object) {
await page.locator('.c-create-button').hover();
await page
.locator('.plot-series-name', { has: page.locator(`text="${object.name}"`) })
.hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
return tooltipText.replace('\n', '').trim();
}
expect(await getCollapsedLegendToolTip(sineWaveObject1)).toBe(sineWaveObject1.path);
expect(await getCollapsedLegendToolTip(sineWaveObject2)).toBe(sineWaveObject2.path);
expect(await getCollapsedLegendToolTip(sineWaveObject3)).toBe(sineWaveObject3.path);
//Hover over first object
await page.getByText('SWG 1 Hz').hover();
await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);
//Hover over another object to clear
await page.getByRole('button', { name: 'create' }).hover();
//Hover over second object
await page.getByText('SWG 2 Hz').hover();
await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject2.path);
//Hover over another object to clear
await page.getByRole('button', { name: 'create' }).hover();
//Hover over third object
await page.getByText('SWG 3 Hz').hover();
await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);
//Release the Control Key
await page.keyboard.up('Control');
//Expand the legend
await page.locator('.gl-plot-legend__view-control.c-disclosure-triangle').click();
//Hover over Expanded Plot Legend Components with the Control Key pressed
await page.keyboard.down('Control');
expect(await getExpandedLegendToolTip(sineWaveObject1)).toBe(sineWaveObject1.path);
expect(await getExpandedLegendToolTip(sineWaveObject2)).toBe(sineWaveObject2.path);
expect(await getExpandedLegendToolTip(sineWaveObject3)).toBe(sineWaveObject3.path);
await page.getByLabel('Plot Legend Expanded').getByText('SWG 1').hover();
await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);
//Hover over another object to clear
await page.getByRole('button', { name: 'create' }).hover();
//Hover over second object
await page.getByLabel('Plot Legend Expanded').getByText('SWG 2').hover();
await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject2.path);
//Hover over another object to clear
await page.getByRole('button', { name: 'create' }).hover();
//Hover over third object
await page.getByLabel('Plot Legend Expanded').getByText('SWG 3').hover();
await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);
});
test('display correct paths when hovering over object labels', async ({ page }) => {
async function getObjectLabelTooltip(object) {
await page
.locator('.c-tree__item__name.c-object-label__name', {
has: page.locator(`text="${object.name}"`)
})
.click();
await page.keyboard.down('Control');
await page
.locator('.l-browse-bar__object-name.c-object-label__name', {
has: page.locator(`text="${object.name}"`)
})
.hover();
const tooltipText = await page.locator('.c-tooltip').textContent();
await page.keyboard.up('Control');
return tooltipText.replace('\n', '').trim();
}
//Navigate to SWG 1 in Tree
await page.getByLabel('Navigate to SWG 1 generator').click();
expect(await getObjectLabelTooltip(sineWaveObject1)).toBe(sineWaveObject1.path);
expect(await getObjectLabelTooltip(sineWaveObject3)).toBe(sineWaveObject3.path);
//Expect tooltip to be the path of SWG 1
await page.keyboard.down('Control');
await page.getByRole('main').getByText('SWG 1', { exact: true }).hover();
await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);
await page.keyboard.up('Control');
//Navigate to SWG 3 in Tree
await page.getByLabel('Navigate to SWG 3 generator').click();
//Expect tooltip to be the path of SWG 3
await page.keyboard.down('Control');
await page.getByRole('main').getByText('SWG 3', { exact: true }).hover();
await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);
});
test('display correct paths when hovering over display layout pane headers', async ({ page }) => {
@ -198,8 +204,11 @@ test.describe('Verify tooltips', () => {
});
// Edit Overlay Plot
await page.getByLabel('Edit Object').click();
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.gl-plot');
await page.locator('button[title="Save"]').click();
await page
.getByLabel('Preview SWG 1 generator Object')
.dragTo(page.getByLabel('Plot Container Style Target'));
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Create Stacked Plot
@ -209,8 +218,9 @@ test.describe('Verify tooltips', () => {
});
// Edit Stacked Plot
await page.getByLabel('Edit Object').click();
await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.c-plot--stacked.holder');
await page.locator('button[title="Save"]').click();
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Create Display Layout
@ -221,66 +231,77 @@ test.describe('Verify tooltips', () => {
// Edit Display Layout
await page.getByLabel('Edit Object').click();
await page.dragAndDrop("text='Test Overlay Plot'", '.l-layout__grid-holder', {
targetPosition: { x: 0, y: 0 }
});
await page.dragAndDrop("text='Test Stacked Plot'", '.l-layout__grid-holder', {
targetPosition: { x: 0, y: 250 }
});
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.l-layout__grid-holder', {
targetPosition: { x: 500, y: 200 }
});
await page.locator('button[title="Save"]').click();
await page
.getByLabel('Preview Test Overlay Plot')
.dragTo(page.locator('#display-layout-drop-area'), {
targetPosition: { x: 0, y: 0 }
});
//Add Display Layout below Overlay Plot
await page
.getByLabel('Preview Test Stacked Plot')
.dragTo(page.locator('#display-layout-drop-area'), {
targetPosition: { x: 0, y: 250 }
});
//Drag the SWG3 Object to the Display off to the right
await page
.getByLabel('Preview SWG 3 generator Object')
.dragTo(page.locator('#display-layout-drop-area'), {
targetPosition: { x: 500, y: 200 }
});
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
//Hover over Overlay Plot with the Control Key pressed
await page.keyboard.down('Control');
await page.getByText('Test Overlay Plot').nth(2).hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe('My Items / Test Overlay Plot');
//Hover Overlay Plot
await page.getByTitle('Test Overlay Plot').hover();
await expect(page.getByRole('tooltip')).toHaveText('My Items / Test Overlay Plot');
await page.keyboard.up('Control');
await page.locator('.c-plot-legend__view-control >> nth=0').click();
//Expand the Overlay Plot Legend and hover over the first legend item
await page.getByLabel('Expand Test Overlay Plot Legend').click();
await page.keyboard.down('Control');
await page.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.getByLabel('Plot Legend Item for Test').getByText('SWG').hover();
await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);
await page.getByText('Test Stacked Plot').nth(2).hover();
tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe('My Items / Test Stacked Plot');
//Hover over Stacked Plot Title
await page.getByTitle('Test Stacked Plot').hover();
await expect(page.getByRole('tooltip')).toHaveText('My Items / Test Stacked Plot');
await page.getByText('SWG 3').nth(2).hover();
tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(sineWaveObject3.path).toBe(tooltipText);
//Hover over SWG3 Object
await page.getByLabel('Alpha-numeric telemetry name for SWG').hover();
await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);
});
test('display correct paths when hovering over flexible object labels', async ({ page }) => {
//Create Flexible Layout
await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout',
name: 'Test Flexible Layout'
});
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-fl__container >> nth=0');
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-fl__container >> nth=1');
//Add SWG1 and SWG3 to Flexible Layout
await page.getByLabel('Navigate to SWG 1 generator').dragTo(page.getByRole('row').nth(0));
await page
.getByLabel('Preview SWG 3 generator Object')
.dragTo(page.getByLabel('Container Handle 2'));
await page.locator('button[title="Save"]').click();
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
//Hover over SWG1 Object
await page.keyboard.down('Control');
await page.getByText('SWG 1').nth(2).hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject1.path);
await page.getByTitle('SWG 1').hover();
await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);
await page.getByText('SWG 3').nth(2).hover();
tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject3.path);
//Hover over SWG3 Object
await page.getByTitle('SWG 3').hover();
await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);
});
test('display correct paths when hovering over tab view labels', async ({ page }) => {
@ -289,46 +310,40 @@ test.describe('Verify tooltips', () => {
name: 'Test Tabs View'
});
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-tabs-view__tabs-holder');
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-tabs-view__tabs-holder');
//Add SWG1 and SWG3 to Flexible Layout
await page
.getByLabel('Navigate to SWG 1 generator')
.dragTo(page.getByText('Drag objects here to add them'));
await page.getByLabel('Preview SWG 3 generator Object').dragTo(page.getByLabel('SWG 1 tab'));
await page.locator('button[title="Save"]').click();
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.keyboard.down('Control');
await page.getByText('SWG 1').nth(2).hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject1.path);
await page.getByLabel('SWG 1 tab').getByText('SWG').hover();
await page.getByText('SWG 3').nth(2).hover();
tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject3.path);
await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);
await page.getByLabel('SWG 3 tab').getByText('SWG').hover();
await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);
});
test('display correct paths when hovering tree items', async ({ page }) => {
await page.keyboard.down('Control');
await page.getByText('SWG 1').nth(0).hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject1.path);
await page.getByText('SWG 1').first().hover();
await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);
await page.getByText('SWG 3').nth(0).hover();
tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject3.path);
await page.getByText('SWG 3').first().hover();
await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);
});
test('display correct paths when hovering search items', async ({ page }) => {
await page.getByRole('searchbox', { name: 'Search Input' }).click();
await page.fill('.c-search__input', 'SWG 3');
await page.getByRole('searchbox', { name: 'Search Input' }).fill('SWG 3');
await page.keyboard.down('Control');
await page.locator('.c-gsearch-result__title').hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject3.path);
await page.getByLabel('Object Results').getByText('SWG').hover();
await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);
});
test('display path for source telemetry when hovering over gauge', async ({ page }) => {
@ -336,13 +351,11 @@ test.describe('Verify tooltips', () => {
type: 'Gauge',
name: 'Test Gauge'
});
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-gauge__wrapper');
await page.getByLabel('Navigate to SWG 3 generator').dragTo(page.getByRole('meter'));
await page.keyboard.down('Control');
// 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);
await page.getByRole('meter').hover({ position: { x: 0, y: 0 } });
await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);
});
test('display tooltip path for notebook embeds', async ({ page }) => {
@ -351,27 +364,23 @@ test.describe('Verify tooltips', () => {
name: 'Test Notebook'
});
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-notebook__drag-area');
await page
.getByLabel('Navigate to SWG 3 generator')
.dragTo(page.getByLabel('To start a new entry, click'));
await page.keyboard.down('Control');
await page.locator('.c-ne__embed').hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject3.path);
await page.getByLabel('SWG 3 Notebook Embed').hover();
await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);
});
test.fixme('display tooltip path for telemetry table names', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7421'
});
// set endBound to 10 seconds after start bound
const url = await page.url();
const parsedUrl = new URL(url.replace('#', '!'));
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('!', '#'));
test('display tooltip path for telemetry table names', async ({ page }) => {
// set endBound to 10 seconds after start bound to ensure that the telemetry doesn't change
// const url = 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 createDomainObjectWithDefaults(page, {
type: 'Telemetry Table',
@ -381,48 +390,35 @@ test.describe('Verify tooltips', () => {
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-telemetry-table');
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-telemetry-table');
await page.locator('button[title="Save"]').click();
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.keyboard.down('Control');
//Hover over SWG3 in Telemetry Table
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 expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);
//Hover over SWG1 in Telemetry Table
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);
await expect(page.getByRole('tooltip')).toHaveText(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.getByLabel('Resize Recently Viewed Pane').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 expect(page.getByRole('tooltip')).toHaveText(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 expect(page.getByRole('tooltip')).toHaveText(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);
await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject1.path);
});
test('display tooltip path for time strips', async ({ page }) => {
@ -445,23 +441,17 @@ test.describe('Verify tooltips', () => {
`text=${sineWaveObject3.name}`,
'.c-object-view.is-object-type-time-strip'
);
await page.locator('button[title="Save"]').click();
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.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 expect(page.getByRole('tooltip')).toHaveText(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 expect(page.getByRole('tooltip')).toHaveText(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);
await expect(page.getByRole('tooltip')).toHaveText(sineWaveObject3.path);
});
});

View File

@ -48,7 +48,9 @@ test.describe('Main Tree', () => {
});
await expandTreePaneItemByName(page, folder.name);
await assertTreeItemIsVisible(page, clock.name);
await expect(
page.getByRole('tree', { name: 'Main Tree' }).getByRole('treeitem', { name: clock.name })
).toBeVisible();
});
test('Creating a child object on one tab and expanding its parent on the other shows the correct composition @2p', async ({
@ -65,8 +67,8 @@ test.describe('Main Tree', () => {
// Both pages: Go to baseURL
await Promise.all([
page.goto('./', { waitUntil: 'networkidle' }),
page2.goto('./', { waitUntil: 'networkidle' })
page.goto('./', { waitUntil: 'domcontentloaded' }),
page2.goto('./', { waitUntil: 'domcontentloaded' })
]);
const page1Folder = await createDomainObjectWithDefaults(page, {
@ -74,7 +76,11 @@ test.describe('Main Tree', () => {
});
await expandTreePaneItemByName(page2, myItemsFolderName);
await assertTreeItemIsVisible(page2, page1Folder.name);
await expect(
page2
.getByRole('tree', { name: 'Main Tree' })
.getByRole('treeitem', { name: page1Folder.name })
).toBeVisible();
});
test('Creating a child object on one tab and expanding its parent on the other shows the correct composition @couchdb @2p', async ({
@ -91,8 +97,8 @@ test.describe('Main Tree', () => {
// Both pages: Go to baseURL
await Promise.all([
page.goto('./', { waitUntil: 'networkidle' }),
page2.goto('./', { waitUntil: 'networkidle' })
page.goto('./', { waitUntil: 'domcontentloaded' }),
page2.goto('./', { waitUntil: 'domcontentloaded' })
]);
const page1Folder = await createDomainObjectWithDefaults(page, {
@ -100,9 +106,13 @@ test.describe('Main Tree', () => {
});
await expandTreePaneItemByName(page2, myItemsFolderName);
await assertTreeItemIsVisible(page2, page1Folder.name);
await expect(
page2
.getByRole('tree', { name: 'Main Tree' })
.getByRole('treeitem', { name: page1Folder.name })
).toBeVisible();
});
// eslint-disable-next-line playwright/expect-expect
test('Renaming an object reorders the tree @unstable', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
@ -221,17 +231,6 @@ async function getAndAssertTreeItems(page, expected) {
expect(allTexts).toEqual(expected);
}
async function assertTreeItemIsVisible(page, name) {
const mainTree = page.getByRole('tree', {
name: 'Main Tree'
});
const treeItem = mainTree.getByRole('treeitem', {
name
});
await expect(treeItem).toBeVisible();
}
/**
* @param {import('@playwright/test').Page} page
* @param {string} name

View File

@ -82,14 +82,14 @@ test.describe('Smoke tests for @mobile', () => {
await page.getByTitle('Collapse Browse Pane').click();
await expect(page.getByRole('main').getByText('Parent Display Layout')).toBeVisible();
//Verify both objects are in view
await expect(await page.getByLabel('Child Layout 1 Layout')).toBeVisible();
await expect(await page.getByLabel('Child Layout 2 Layout')).toBeVisible();
await expect(page.getByLabel('Child Layout 1 Layout')).toBeVisible();
await expect(page.getByLabel('Child Layout 2 Layout')).toBeVisible();
//Remove First Object to bring up confirmation dialog
await page.getByLabel('View menu items').nth(1).click();
await page.getByLabel('Remove').click();
await page.getByRole('button', { name: 'OK' }).click();
//Verify that the object is removed
await expect(await page.getByLabel('Child Layout 1 Layout')).toBeVisible();
await expect(page.getByLabel('Child Layout 1 Layout')).toBeVisible();
expect(await page.getByLabel('Child Layout 2 Layout').count()).toBe(0);
});
});

View File

@ -39,7 +39,7 @@ const filePath = 'test-data/PerformanceDisplayLayout.json';
test.describe('Performance tests', () => {
test.beforeEach(async ({ page, browser }, testInfo) => {
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Click a:has-text("My Items")
await page.locator('a:has-text("My Items")').click({
@ -129,12 +129,12 @@ test.describe('Performance tests', () => {
]);
//Time to Example Imagery Frame loads within Display Layout
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' });
await page.locator('.c-imagery__main-image__bg').waitFor({ state: 'visible' });
//Time to Example Imagery object loads
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' });
await page.locator('.c-imagery__main-image__background-image').waitFor({ state: 'visible' });
//Get background-image url from background-image css prop
const backgroundImage = await page.locator('.c-imagery__main-image__background-image');
const backgroundImage = page.locator('.c-imagery__main-image__background-image');
let backgroundImageUrl = await backgroundImage.evaluate((el) => {
return window
.getComputedStyle(el)
@ -156,15 +156,15 @@ test.describe('Performance tests', () => {
await page.evaluate(() => window.performance.mark('viewLarge.start.test')); //This is a mark only to compare evaluate timing
//Time to Imagery Rendered in Large Frame
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' });
await page.locator('.c-imagery__main-image__bg').waitFor({ state: 'visible' });
await page.evaluate(() => window.performance.mark('background-image-frame'));
//Time to Example Imagery object loads
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' });
await page.locator('.c-imagery__main-image__background-image').waitFor({ state: 'visible' });
await page.evaluate(() => window.performance.mark('background-image-visible'));
// Get Current number of images in thumbstrip
await page.waitForSelector('.c-imagery__thumb');
await page.locator('.c-imagery__thumb').waitFor({ state: 'visible' });
const thumbCount = await page.locator('.c-imagery__thumb').count();
console.log('number of thumbs rendered ' + thumbCount);
await page.locator('.c-imagery__thumb').last().click();

View File

@ -38,7 +38,7 @@ const notebookFilePath = 'test-data/PerformanceNotebook.json';
test.describe('Performance tests', () => {
test.beforeEach(async ({ page, browser }, testInfo) => {
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Click a:has-text("My Items")
await page.locator('a:has-text("My Items")').click({
@ -110,20 +110,19 @@ test.describe('Performance tests', () => {
await page.evaluate(() => window.performance.mark('search-entered'));
//Search Result Appears and is clicked
await Promise.all([
page.waitForNavigation(),
page.locator('a:has-text("Performance Notebook")').first().click(),
page.evaluate(() => window.performance.mark('click-search-result'))
]);
await page.waitForSelector('.c-tree__item c-tree-and-search__loading loading', {
state: 'hidden'
});
await page
.locator('.c-tree__item c-tree-and-search__loading loading')
.waitFor({ state: 'hidden' });
await page.evaluate(() => window.performance.mark('search-spinner-gone'));
await page.waitForSelector('.l-browse-bar__object-name', { state: 'visible' });
await page.locator('.l-browse-bar__object-name').waitFor({ state: 'visible' });
await page.evaluate(() => window.performance.mark('object-title-appears'));
await page.waitForSelector('.c-notebook__entry >> nth=0', { state: 'visible' });
await page.locator('.c-notebook__entry >> nth=0').waitFor({ state: 'visible' });
await page.evaluate(() => window.performance.mark('notebook-entry-appears'));
// Click Add new Notebook Entry
@ -139,9 +138,9 @@ test.describe('Performance tests', () => {
await page.evaluate(() => window.performance.mark('notebook-search-start'));
await page.locator('.c-notebook__search >> input').fill('Existing Entry');
await page.evaluate(() => window.performance.mark('notebook-search-filled'));
await page.waitForSelector('text=Search Results (3)', { state: 'visible' });
await page.locator('text=Search Results (3)').waitFor({ state: 'visible' });
await page.evaluate(() => window.performance.mark('notebook-search-processed'));
await page.waitForSelector('.c-notebook__entry >> nth=2', { state: 'visible' });
await page.locator('.c-notebook__entry >> nth=2').waitFor({ state: 'visible' });
await page.evaluate(() => window.performance.mark('notebook-search-processed'));
//Clear Search
@ -154,7 +153,7 @@ test.describe('Performance tests', () => {
await page.locator('div.c-ne__time-and-content').last().hover();
await page.locator('button[title="Delete this entry"]').last().click();
await page.locator('button:has-text("Ok")').click();
await page.waitForSelector('.c-notebook__entry >> nth=3', { state: 'detached' });
await page.locator('.c-notebook__entry >> nth=3').waitFor({ state: 'detached' });
await page.evaluate(() => window.performance.mark('new-notebook-entry-deleted'));
//await client.send('HeapProfiler.enable');

View File

@ -228,9 +228,7 @@ test.describe('Navigation memory leak is not detected in', () => {
expect(result).toBe(true);
});
test('display layout with plots of swgs, alphanumerics, and condition sets, ', async ({
page
}) => {
test('display layout with plots of swgs, alphanumerics, and condition sets', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'display-layout-simple-telemetry'

View File

@ -78,7 +78,7 @@ test.describe('Tabs View', () => {
await page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true }).click();
// ensure sine wave generator visible
expect(await page.locator('.c-plot').isVisible()).toBe(true);
await expect(page.locator('.c-plot')).toBeVisible();
// now select notebook and clear animation calls
await page.getByLabel(`${notebook.name} tab`, { exact: true }).click();

View File

@ -84,7 +84,7 @@ test.describe('Plot Tagging Performance', () => {
await setRealTimeMode(page);
// Search for Science
await page.getByRole('searchbox', { name: 'Search Input' });
await page.getByRole('searchbox', { name: 'Search Input' }).click();
await page.getByRole('searchbox', { name: 'Search Input' }).fill('sc');
// click on the search result

View File

@ -20,15 +20,14 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { test } from '../../avpFixtures.js';
import { scanForA11yViolations, test } from '../../avpFixtures.js';
import { VISUAL_FIXED_URL } from '../../constants.js';
test.describe('a11y - Default', () => {
test.describe('a11y - Default @a11y', () => {
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
});
test('main view', async ({ page }, testInfo) => {
//Skipping for https://github.com/nasa/openmct/issues/7421
//await scanForA11yViolations(page, testInfo.title);
await scanForA11yViolations(page, testInfo.title);
});
});

View File

@ -27,7 +27,7 @@ Tests the branding associated with the default deployment. At least the about mo
import percySnapshot from '@percy/playwright';
import { fileURLToPath } from 'url';
import { expect, test } from '../../../avpFixtures.js';
import { expect, scanForA11yViolations, test } from '../../../avpFixtures.js';
import { VISUAL_FIXED_URL } from '../../../constants.js';
//Declare the component scope of the visual test for Percy
@ -69,14 +69,26 @@ test.describe('Visual - Header @a11y', () => {
});
test('show snapshot button', async ({ page, theme }) => {
test.slow(true, 'We have to wait for the snapshot indicator to stop flashing');
await page.getByLabel('Open the Notebook Snapshot Menu').click();
await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();
await expect(page.getByLabel('Show Snapshots')).toBeVisible();
/**
* We have to wait for the snapshot indicator to stop flashing. This happens
* for a really long time (15 seconds 😳).
* TODO: Either reduce the length of the animation, convert this to a
* Playwright snapshot test (and disable animations), or augment the `waitForAnimations`
* fixture to adjust the timeout.
*/
await expect(page.locator('.has-new-snapshot')).not.toBeAttached({
timeout: 30 * 1000
});
await percySnapshot(page, `Notebook Snapshot Show button (theme: '${theme}')`, {
scope: header
});
await expect(page.getByLabel('Show Snapshots')).toBeVisible();
});
});
@ -99,7 +111,6 @@ test.describe('Mission Header @a11y', () => {
});
});
});
// Skipping for https://github.com/nasa/openmct/issues/7421
// test.afterEach(async ({ page }, testInfo) => {
// await scanForA11yViolations(page, testInfo.title);
// });
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});

View File

@ -22,7 +22,7 @@
import percySnapshot from '@percy/playwright';
import { test } from '../../../avpFixtures.js';
import { scanForA11yViolations, test } from '../../../avpFixtures.js';
import { MISSION_TIME, VISUAL_FIXED_URL } from '../../../constants.js';
//Declare the scope of the visual test
@ -55,7 +55,6 @@ test.describe('Visual - Inspector @ally @clock', () => {
});
});
});
// Skipping for https://github.com/nasa/openmct/issues/7421
// test.afterEach(async ({ page }, testInfo) => {
// await scanForA11yViolations(page, testInfo.title);
// });
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});

View File

@ -48,7 +48,13 @@ test.describe('Visual - Time Conductor', () => {
// await scanForA11yViolations(page, testInfo.title);
// });
test('Visual - Time Conductor (Fixed time) @clock @snapshot', async ({ page }) => {
/**
* FIXME: This test fails sporadically due to layout shift during initial load.
* The layout shift seems to be caused by loading Open MCT's icons, which are not preloaded
* and load after the initial DOM content has loaded.
* @see https://github.com/nasa/openmct/issues/7775
*/
test.fixme('Visual - Time Conductor (Fixed time) @clock @snapshot', async ({ page }) => {
// Navigate to a specific view that uses the Time Conductor in Fixed Time mode with inspect and browse panes collapsed
await page.goto(
`./#/browse/mine?tc.mode=fixed&tc.startBound=${MISSION_TIME_FIXED_START}&tc.endBound=${MISSION_TIME_FIXED_END}&tc.timeSystem=utc&view=grid&hideInspector=true&hideTree=true`,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -110,6 +110,7 @@ test.describe('Visual - Display Layout @clock', () => {
});
await page.getByLabel('Expand Inspect Pane').click();
await page.getByLabel('Resize Inspect Pane').dragTo(page.getByLabel('X:'));
await page.getByRole('tab', { name: 'Elements' }).click();
await percySnapshot(page, `Toolbar does not overflow into inspector (theme: '${theme}')`);
});
});

View File

@ -19,7 +19,6 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import percySnapshot from '@percy/playwright';
import { createDomainObjectWithDefaults, setRealTimeMode } from '../../appActions.js';
@ -45,17 +44,19 @@ test.describe('Visual - Example Imagery', () => {
parent: parentLayout.uuid
});
// Modify Example Imagery to create a really stable Example Imagery
// Modify Example Imagery to create a really stable image which will never let us down
await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' });
await page.getByRole('button', { name: 'More actions' }).click();
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
await page
.locator('#imageLocation-textarea')
.fill(
'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg'
'https://raw.githubusercontent.com/nasa/openmct/554f77c42fec81cf0f63e62b278012cb08d82af9/e2e/test-data/rick.jpg,https://raw.githubusercontent.com/nasa/openmct/554f77c42fec81cf0f63e62b278012cb08d82af9/e2e/test-data/rick.jpg'
);
await page.getByRole('button', { name: 'Save' }).click();
await page.reload({ waitUntil: 'domcontentloaded' });
//Hide the Browse and Inspect panes to make the image more stable
await page.getByTitle('Collapse Browse Pane').click();
await page.getByTitle('Collapse Inspect Pane').click();
});

View File

@ -23,7 +23,7 @@
import percySnapshot from '@percy/playwright';
import { createDomainObjectWithDefaults, expandTreePaneItemByName } from '../../appActions.js';
import { expect, test } from '../../avpFixtures.js';
import { expect, scanForA11yViolations, test } from '../../avpFixtures.js';
import { VISUAL_FIXED_URL } from '../../constants.js';
import { enterTextEntry, startAndAddRestrictedNotebookObject } from '../../helper/notebookUtils.js';
@ -163,8 +163,7 @@ test.describe('Visual - Notebook @a11y', () => {
// Take a snapshot
await percySnapshot(page, `Notebook Selected Entry Text Area Active (theme: '${theme}')`);
});
// Skipping for https://github.com/nasa/openmct/issues/7421
// test.afterEach(async ({ page }, testInfo) => {
// await scanForA11yViolations(page, testInfo.title);
// });
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});
});

View File

@ -24,91 +24,15 @@ import percySnapshot from '@percy/playwright';
import fs from 'fs';
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appActions.js';
import { test } from '../../avpFixtures.js';
import { scanForA11yViolations, test } from '../../avpFixtures.js';
import { VISUAL_FIXED_URL } from '../../constants.js';
import {
createTimelistWithPlanAndSetActivityInProgress,
getFirstActivity,
setBoundsToSpanAllActivities,
setDraftStatusForPlan
} from '../../helper/planningUtils.js';
const examplePlanSmall1 = JSON.parse(
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url))
);
import { setBoundsToSpanAllActivities, setDraftStatusForPlan } from '../../helper/planningUtils.js';
const examplePlanSmall2 = JSON.parse(
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url))
);
test.describe('Visual - Timelist progress bar @clock', () => {
const firstActivity = getFirstActivity(examplePlanSmall1);
test.use({
clockOptions: {
now: firstActivity.end + 10000,
shouldAdvanceTime: true
}
});
test.beforeEach(async ({ page }) => {
await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);
await page.getByLabel('Click to collapse items').click();
});
test('progress pie is full', async ({ page, theme }) => {
// Progress pie is completely full and doesn't update if now is greater than the end time
await percySnapshot(page, `Time List with Activity in Progress (theme: ${theme})`);
});
});
test.describe('Visual - Planning', () => {
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
});
test('Plan View', async ({ page, theme }) => {
const plan = await createPlanFromJSON(page, {
name: 'Plan Visual Test',
json: examplePlanSmall2
});
await setBoundsToSpanAllActivities(page, examplePlanSmall2, plan.url);
await percySnapshot(page, `Plan View (theme: ${theme})`);
});
test('Resize Plan View @2p', async ({ browser, theme }) => {
// need to set viewport to null to allow for resizing
const newContext = await browser.newContext({
viewport: null
});
const newPage = await newContext.newPage();
await newPage.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
const plan = await createPlanFromJSON(newPage, {
name: 'Plan Visual Test',
json: examplePlanSmall2
});
await setBoundsToSpanAllActivities(newPage, examplePlanSmall2, plan.url);
// resize the window
await newPage.setViewportSize({ width: 800, height: 600 });
await percySnapshot(newPage, `Plan View resized (theme: ${theme})`);
});
test('Plan View w/ draft status', async ({ page, theme }) => {
const plan = await createPlanFromJSON(page, {
name: 'Plan Visual Test (Draft)',
json: examplePlanSmall2
});
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
await setDraftStatusForPlan(page, plan);
await setBoundsToSpanAllActivities(page, examplePlanSmall2, plan.url);
await percySnapshot(page, `Plan View w/ draft status (theme: ${theme})`);
});
});
test.describe('Visual - Gantt Chart', () => {
test.describe('Visual - Gantt Chart @a11y', () => {
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
});
@ -179,7 +103,6 @@ test.describe('Visual - Gantt Chart', () => {
});
});
// Skipping for https://github.com/nasa/openmct/issues/7421
// test.afterEach(async ({ page }, testInfo) => {
// await scanForA11yViolations(page, testInfo.title);
// });
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});

View File

@ -0,0 +1,59 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import percySnapshot from '@percy/playwright';
import fs from 'fs';
import { scanForA11yViolations, test } from '../../avpFixtures.js';
import {
createTimelistWithPlanAndSetActivityInProgress,
getFirstActivity
} from '../../helper/planningUtils.js';
const examplePlanSmall1 = JSON.parse(
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url))
);
test.describe('Visual - Timelist progress bar @clock @a11y', () => {
const firstActivity = getFirstActivity(examplePlanSmall1);
test.use({
clockOptions: {
now: firstActivity.end + 10000,
shouldAdvanceTime: true
}
});
test.beforeEach(async ({ page }) => {
await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);
await page.getByLabel('Click to collapse items').click();
});
test('progress pie is full', async ({ page, theme }) => {
// Progress pie is completely full and doesn't update if now is greater than the end time
await percySnapshot(page, `Time List with Activity in Progress (theme: ${theme})`);
});
});
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});

View File

@ -0,0 +1,69 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import percySnapshot from '@percy/playwright';
import fs from 'fs';
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appActions.js';
import { scanForA11yViolations, test } from '../../avpFixtures.js';
import { waitForAnimations } from '../../baseFixtures.js';
import { VISUAL_FIXED_URL } from '../../constants.js';
import { setBoundsToSpanAllActivities } from '../../helper/planningUtils.js';
const examplePlanSmall2 = JSON.parse(
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url))
);
test.describe('Visual - Time Strip @a11y', () => {
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
});
test('Time Strip View', async ({ page, theme }) => {
const timeStrip = await createDomainObjectWithDefaults(page, {
type: 'Time Strip',
name: 'Time Strip Visual Test'
});
await createPlanFromJSON(page, {
json: examplePlanSmall2,
parent: timeStrip.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: timeStrip.uuid
});
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
//This will indirectly modify the url such that the SWG is not rendered
await setBoundsToSpanAllActivities(page, examplePlanSmall2, timeStrip.url);
//TODO Find a way to set the "now" activity line
//This will stabilize the state of the test and allow the SWG to render as empty
await waitForAnimations(page.getByLabel('Plot Canvas'));
await percySnapshot(page, `Time Strip View (theme: ${theme}) - With SWG and Plan`);
});
});
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});

View File

@ -0,0 +1,113 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import percySnapshot from '@percy/playwright';
import fs from 'fs';
import { createPlanFromJSON } from '../../appActions.js';
import { scanForA11yViolations, test } from '../../avpFixtures.js';
import { VISUAL_FIXED_URL } from '../../constants.js';
import {
createTimelistWithPlanAndSetActivityInProgress,
getFirstActivity,
setBoundsToSpanAllActivities,
setDraftStatusForPlan
} from '../../helper/planningUtils.js';
const examplePlanSmall1 = JSON.parse(
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url))
);
const examplePlanSmall2 = JSON.parse(
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url))
);
test.describe('Visual - Timelist progress bar @clock @a11y', () => {
const firstActivity = getFirstActivity(examplePlanSmall1);
test.use({
clockOptions: {
now: firstActivity.end + 10000,
shouldAdvanceTime: true
}
});
test.beforeEach(async ({ page }) => {
await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);
await page.getByLabel('Click to collapse items').click();
});
test('progress pie is full', async ({ page, theme }) => {
// Progress pie is completely full and doesn't update if now is greater than the end time
await percySnapshot(page, `Time List with Activity in Progress (theme: ${theme})`);
});
});
test.describe('Visual - Plan View @a11y', () => {
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
});
test('Plan View', async ({ page, theme }) => {
const plan = await createPlanFromJSON(page, {
name: 'Plan Visual Test',
json: examplePlanSmall2
});
await setBoundsToSpanAllActivities(page, examplePlanSmall2, plan.url);
await percySnapshot(page, `Plan View (theme: ${theme})`);
});
test('Resize Plan View @2p', async ({ browser, theme }) => {
// need to set viewport to null to allow for resizing
const newContext = await browser.newContext({
viewport: null
});
const newPage = await newContext.newPage();
await newPage.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
const plan = await createPlanFromJSON(newPage, {
name: 'Plan Visual Test',
json: examplePlanSmall2
});
await setBoundsToSpanAllActivities(newPage, examplePlanSmall2, plan.url);
// resize the window
await newPage.setViewportSize({ width: 800, height: 600 });
await percySnapshot(newPage, `Plan View resized (theme: ${theme})`);
});
test('Plan View w/ draft status', async ({ page, theme }) => {
const plan = await createPlanFromJSON(page, {
name: 'Plan Visual Test (Draft)',
json: examplePlanSmall2
});
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
await setDraftStatusForPlan(page, plan);
await setBoundsToSpanAllActivities(page, examplePlanSmall2, plan.url);
await percySnapshot(page, `Plan View w/ draft status (theme: ${theme})`);
});
});
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});

View File

@ -26,7 +26,6 @@ export default class SinewaveLimitProvider extends EventEmitter {
#openmct;
#observingStaleness;
#watchingTheClock;
#isRealTime;
constructor(openmct) {
super();
@ -34,7 +33,6 @@ export default class SinewaveLimitProvider extends EventEmitter {
this.#openmct = openmct;
this.#observingStaleness = {};
this.#watchingTheClock = false;
this.#isRealTime = undefined;
}
supportsStaleness(domainObject) {
@ -61,10 +59,7 @@ export default class SinewaveLimitProvider extends EventEmitter {
subscribeToStaleness(domainObject, callback) {
const id = this.#getObjectKeyString(domainObject);
if (this.#isRealTime === undefined) {
this.#updateRealTime(this.#openmct.time.getMode());
}
this.#realTimeCheck();
this.#handleClockUpdate();
if (this.#observerExists(id)) {
@ -92,17 +87,15 @@ export default class SinewaveLimitProvider extends EventEmitter {
if (observers && !this.#watchingTheClock) {
this.#watchingTheClock = true;
this.#openmct.time.on('modeChanged', this.#updateRealTime, this);
this.#openmct.time.on('modeChanged', this.#realTimeCheck, this);
} else if (!observers && this.#watchingTheClock) {
this.#watchingTheClock = false;
this.#openmct.time.off('modeChanged', this.#updateRealTime, this);
this.#openmct.time.off('modeChanged', this.#realTimeCheck, this);
}
}
#updateRealTime(mode) {
this.#isRealTime = mode !== 'fixed';
if (!this.#isRealTime) {
#realTimeCheck() {
if (!this.#openmct.time.isRealTime()) {
Object.keys(this.#observingStaleness).forEach((id) => {
this.#updateStaleness(id, false);
});
@ -140,7 +133,7 @@ export default class SinewaveLimitProvider extends EventEmitter {
}
#providingStaleness(domainObject) {
return domainObject.telemetry?.staleness === true && this.#isRealTime;
return domainObject.telemetry?.staleness === true && this.#openmct.time.isRealTime();
}
#getObjectKeyString(object) {

872
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -40,13 +40,13 @@
"eslint-config-prettier": "9.1.0",
"eslint-plugin-compat": "4.2.0",
"eslint-plugin-no-unsanitized": "4.0.2",
"eslint-plugin-playwright": "0.12.0",
"eslint-plugin-playwright": "1.5.2",
"eslint-plugin-prettier": "5.1.3",
"eslint-plugin-simple-import-sort": "10.0.0",
"eslint-plugin-unicorn": "49.0.0",
"eslint-plugin-vue": "9.22.0",
"eslint-plugin-you-dont-need-lodash-underscore": "6.13.0",
"eventemitter3": "1.2.0",
"eventemitter3": "5.0.1",
"file-saver": "2.0.5",
"flatbush": "4.2.0",
"git-rev-sync": "3.0.2",
@ -87,7 +87,7 @@
"tiny-emitter": "2.1.0",
"typescript": "5.3.3",
"uuid": "9.0.1",
"vue": "3.4.19",
"vue": "3.4.24",
"vue-eslint-parser": "9.4.2",
"vue-loader": "16.8.3",
"webpack": "5.90.3",
@ -96,7 +96,7 @@
"webpack-merge": "5.10.0"
},
"scripts": {
"clean": "rm -rf ./dist ./node_modules ./coverage ./html-test-results ./test-results ./.nyc_output",
"clean": "rm -rf ./dist ./node_modules ./coverage ./html-test-results ./e2e/test-results ./.nyc_output ./e2e/.nyc_output",
"start": "npx webpack serve --config ./.webpack/webpack.dev.mjs",
"start:prod": "npx webpack serve --config ./.webpack/webpack.prod.mjs",
"start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.mjs",
@ -161,4 +161,4 @@
"keywords": [
"nasa"
]
}
}

View File

@ -79,7 +79,6 @@ import Browse from './ui/router/Browse.js';
export class MCT extends EventEmitter {
constructor() {
super();
EventEmitter.call(this);
this.buildInfo = {
version: __OPENMCT_VERSION__,
@ -371,6 +370,5 @@ export class MCT extends EventEmitter {
destroy() {
window.removeEventListener('beforeunload', this.destroy);
this.emit('destroy');
this.router.destroy();
}
}

View File

@ -21,7 +21,7 @@
-->
<template>
<mct-tree
<MctTree
:is-selector-tree="true"
:initial-selection="model.parent"
@tree-item-selection="handleItemSelection"

View File

@ -22,7 +22,7 @@ class Dialog extends Overlay {
super({
element: vNode.el,
size: 'fit',
dismissable: false,
dismissible: false,
...options
});

View File

@ -15,7 +15,7 @@ class Overlay extends EventEmitter {
constructor({
buttons,
autoHide = true,
dismissable = true,
dismissible = true,
element,
onDestroy,
onDismiss,
@ -27,7 +27,7 @@ class Overlay extends EventEmitter {
this.container.classList.add('l-overlay-wrapper', cssClasses[size]);
this.autoHide = autoHide;
this.dismissable = dismissable !== false;
this.dismissible = dismissible !== false;
const { destroy } = mount(
{
@ -38,7 +38,7 @@ class Overlay extends EventEmitter {
dismiss: this.notifyAndDismiss.bind(this),
element,
buttons,
dismissable: this.dismissable
dismissible: this.dismissible
},
template: '<overlay-component></overlay-component>'
},

View File

@ -76,7 +76,7 @@ class OverlayAPI {
*/
dismissLastOverlay() {
let lastOverlay = this.activeOverlays[this.activeOverlays.length - 1];
if (lastOverlay && lastOverlay.dismissable) {
if (lastOverlay && lastOverlay.dismissible) {
lastOverlay.notifyAndDismiss();
}
}
@ -89,7 +89,7 @@ class OverlayAPI {
* @property {'large'|'small'|'fit'} size The preferred size of the overlay.
* @property {Array<{label: string, callback: Function}>} [buttons] Optional array of button objects, each with 'label' and 'callback' properties.
* @property {Function} onDestroy Callback to be called when the overlay is destroyed.
* @property {boolean} [dismissable=true] Whether the overlay can be dismissed by pressing 'esc' or clicking outside of it. Defaults to true.
* @property {boolean} [dismissible=true] Whether the overlay can be dismissed by pressing 'esc' or clicking outside of it. Defaults to true.
*
* @param {OverlayOptions} options - The configuration options for the overlay.
* @returns {Overlay} An instance of the Overlay class.

View File

@ -40,7 +40,7 @@ class ProgressDialog extends Overlay {
super({
element: vNode.el,
size: 'fit',
dismissable: false,
dismissible: false,
...options
});

View File

@ -55,7 +55,7 @@ class Selection extends Overlay {
super({
element: component.$el,
size: 'fit',
dismissable: false,
dismissible: false,
onChange,
currentSelection,
...options

View File

@ -24,7 +24,7 @@
<div class="c-overlay__blocker" @click="destroy"></div>
<div class="c-overlay__outer">
<button
v-if="dismissable"
v-if="dismissible"
aria-label="Close"
class="c-click-icon c-overlay__close-button icon-x"
@click.stop="destroy"
@ -56,7 +56,7 @@
<script>
export default {
inject: ['dismiss', 'element', 'buttons', 'dismissable'],
inject: ['dismiss', 'element', 'buttons', 'dismissible'],
emits: ['destroy'],
data() {
return {
@ -73,7 +73,7 @@ export default {
},
methods: {
destroy() {
if (this.dismissable) {
if (this.dismissible) {
this.dismiss();
}
},

View File

@ -1033,14 +1033,14 @@ export default class TelemetryAPI {
*/
/**
* Provides telemetry staleness data. To subscribe to telemetry stalenes,
* Provides telemetry staleness data. To subscribe to telemetry staleness,
* new StalenessProvider implementations should be
* [registered]{@link module:openmct.TelemetryAPI#addProvider}.
*
* @interface StalenessProvider
* @property {function} supportsStaleness receieves a domainObject and
* @property {function} supportsStaleness receives a domainObject and
* returns a boolean to indicate it will provide staleness
* @property {function} subscribeToStaleness receieves a domainObject to
* @property {function} subscribeToStaleness receives a domainObject to
* be subscribed to and a callback to invoke with a StalenessResponseObject
* @property {function} isStale an asynchronous method called with a domainObject
* and an options object which currently has an abort signal, ex.

View File

@ -47,7 +47,7 @@ describe('Telemetry API', () => {
telemetryAPI = new TelemetryAPI(openmct);
});
describe('telemetry providers', () => {
describe('Telemetry providers', () => {
let telemetryProvider;
let domainObject;
@ -706,7 +706,7 @@ describe('Telemetry API', () => {
});
});
describe('Telemetery', () => {
describe('telemetry', () => {
let openmct;
let telemetryProvider;
let telemetryAPI;

View File

@ -22,6 +22,10 @@
import TimeContext from './TimeContext.js';
/**
* @typedef {import('./TimeAPI').TimeConductorBounds} TimeConductorBounds
*/
/**
* The GlobalContext handles getting and setting time of the openmct application in general.
* Views will use this context unless they specify an alternate/independent time context
@ -38,12 +42,10 @@ class GlobalTimeContext extends TimeContext {
* Get or set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @param {TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
* @returns {TimeConductorBounds}
* @override
*/
bounds(newBounds) {
if (arguments.length > 0) {
@ -61,9 +63,9 @@ class GlobalTimeContext extends TimeContext {
/**
* Update bounds based on provided time and current offsets
* @private
* @param {number} timestamp A time from which bounds will be calculated
* using current offsets.
* @override
*/
tick(timestamp) {
super.tick.call(this, ...arguments);
@ -81,11 +83,8 @@ class GlobalTimeContext extends TimeContext {
* be manipulated by the user from the time conductor or from other views.
* The time of interest can effectively be unset by assigning a value of
* 'undefined'.
* @fires module:openmct.TimeAPI~timeOfInterest
* @param newTOI
* @returns {number} the current time of interest
* @memberof module:openmct.TimeAPI#
* @method timeOfInterest
*/
timeOfInterest(newTOI) {
if (arguments.length > 0) {
@ -93,8 +92,7 @@ class GlobalTimeContext extends TimeContext {
/**
* The Time of Interest has moved.
* @event timeOfInterest
* @memberof module:openmct.TimeAPI~
* @property {number} Current time of interest
* @property {number} timeOfInterest time of interest
*/
this.emit('timeOfInterest', this.toi);
}

View File

@ -23,19 +23,36 @@
import { MODES, REALTIME_MODE_KEY, TIME_CONTEXT_EVENTS } from './constants.js';
import TimeContext from './TimeContext.js';
/**
* @typedef {import('./TimeAPI.js').default} TimeAPI
* @typedef {import('./GlobalTimeContext.js').default} GlobalTimeContext
* @typedef {import('./TimeAPI.js').TimeSystem} TimeSystem
* @typedef {import('./TimeContext.js').Mode} Mode
* @typedef {import('./TimeContext.js').TimeConductorBounds} TimeConductorBounds
* @typedef {import('./TimeAPI.js').ClockOffsets} ClockOffsets
*/
/**
* The IndependentTimeContext handles getting and setting time of the openmct application in general.
* Views will use the GlobalTimeContext unless they specify an alternate/independent time context here.
*/
class IndependentTimeContext extends TimeContext {
/**
* @param {import('openmct').OpenMCT} openmct - The Open MCT application instance.
* @param {TimeAPI & GlobalTimeContext} globalTimeContext - The global time context.
* @param {import('openmct').ObjectPath} objectPath - The path of objects.
*/
constructor(openmct, globalTimeContext, objectPath) {
super();
/** @type {any} */
this.openmct = openmct;
/** @type {Function[]} */
this.unlisteners = [];
/** @type {TimeAPI & GlobalTimeContext | undefined} */
this.globalTimeContext = globalTimeContext;
// We always start with the global time context.
// This upstream context will be undefined when an independent time context is added later.
/** @type {TimeAPI & GlobalTimeContext | undefined} */
this.upstreamTimeContext = this.globalTimeContext;
/** @type {Array<any>} */
this.objectPath = objectPath;
this.refreshContext = this.refreshContext.bind(this);
this.resetContext = this.resetContext.bind(this);
@ -47,6 +64,10 @@ class IndependentTimeContext extends TimeContext {
this.globalTimeContext.on('removeOwnContext', this.removeIndependentContext);
}
/**
* @deprecated
* @override
*/
bounds() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.bounds(...arguments);
@ -55,6 +76,9 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
* @override
*/
getBounds() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.getBounds();
@ -63,6 +87,9 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
* @override
*/
setBounds() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setBounds(...arguments);
@ -71,6 +98,9 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
* @override
*/
tick() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.tick(...arguments);
@ -79,6 +109,9 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
* @override
*/
clockOffsets() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.clockOffsets(...arguments);
@ -87,6 +120,9 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
* @override
*/
getClockOffsets() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.getClockOffsets();
@ -95,6 +131,9 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
* @override
*/
setClockOffsets() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setClockOffsets(...arguments);
@ -103,12 +142,24 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
*
* @param {number} newTOI
* @returns {number}
*/
timeOfInterest(newTOI) {
return this.globalTimeContext.timeOfInterest(...arguments);
}
/**
*
* @param {TimeSystem | string} timeSystemOrKey
* @param {TimeConductorBounds} bounds
* @returns {TimeSystem}
* @override
*/
timeSystem(timeSystemOrKey, bounds) {
return this.globalTimeContext.timeSystem(...arguments);
return this.globalTimeContext.setTimeSystem(...arguments);
}
/**
@ -116,6 +167,7 @@ class IndependentTimeContext extends TimeContext {
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method getTimeSystem
* @override
*/
getTimeSystem() {
return this.globalTimeContext.getTimeSystem();
@ -246,6 +298,7 @@ class IndependentTimeContext extends TimeContext {
/**
* Get the current mode.
* @return {Mode} the current mode;
* @override
*/
getMode() {
if (this.upstreamTimeContext) {
@ -259,9 +312,8 @@ class IndependentTimeContext extends TimeContext {
* Set the mode to either fixed or realtime.
*
* @param {Mode} mode The mode to activate
* @param {TimeBounds | ClockOffsets} offsetsOrBounds A time window of a fixed width
* @fires module:openmct.TimeAPI~clock
* @return {Mode} the currently active mode;
* @param {TimeConductorBounds | ClockOffsets} offsetsOrBounds A time window of a fixed width
* @return {Mode | undefined} the currently active mode;
*/
setMode(mode, offsetsOrBounds) {
if (!mode) {
@ -299,6 +351,10 @@ class IndependentTimeContext extends TimeContext {
return this.mode;
}
/**
* @returns {boolean}
* @override
*/
isRealTime() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.isRealTime(...arguments);
@ -307,6 +363,10 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
* @returns {number}
* @override
*/
now() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.now(...arguments);
@ -343,6 +403,9 @@ class IndependentTimeContext extends TimeContext {
this.unlisteners = [];
}
/**
* Reset the time context to the global time context
*/
resetContext() {
if (this.upstreamTimeContext) {
this.stopFollowingTimeContext();
@ -352,6 +415,7 @@ class IndependentTimeContext extends TimeContext {
/**
* Refresh the time context, following any upstream time contexts as necessary
* @param {string} [viewKey] The key of the view to refresh
*/
refreshContext(viewKey) {
const key = this.openmct.objects.makeKeyString(this.objectPath[0].identifier);
@ -366,14 +430,21 @@ class IndependentTimeContext extends TimeContext {
this.followTimeContext();
// Emit bounds so that views that are changing context get the upstream bounds
this.emit('bounds', this.bounds());
this.emit('bounds', this.getBounds());
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());
}
/**
* @returns {boolean} True if this time context has an independent context, false otherwise
*/
hasOwnContext() {
return this.upstreamTimeContext === undefined;
}
/**
* Get the upstream time context of this time context
* @returns {TimeAPI & GlobalTimeContext | undefined} The upstream time context
*/
getUpstreamContext() {
// If a view has an independent context, don't return an upstream context
// Be aware that when a new independent time context is created, we assign the global context as default

View File

@ -25,6 +25,41 @@ import IndependentTimeContext from '@/api/time/IndependentTimeContext';
import GlobalTimeContext from './GlobalTimeContext.js';
/**
* @typedef {import('./TimeContext.js').default} TimeContext
*/
/**
* @typedef {import('./TimeContext.js').TimeConductorBounds} TimeConductorBounds
*/
/**
* @typedef {import('./TimeContext.js').ClockOffsets} ClockOffsets
*/
/**
* A TimeSystem provides meaning to the values returned by the TimeAPI. Open
* MCT supports multiple different types of time values, although all are
* intrinsically represented by numbers, the meaning of those numbers can
* differ depending on context.
*
* A default time system is provided by Open MCT in the form of the {@link UTCTimeSystem},
* which represents integer values as ms in the Unix epoch. An example of
* another time system might be "sols" for a Martian mission. TimeSystems do
* not address the issue of converting between time systems.
*
* @typedef {Object} TimeSystem
* @property {string} key A unique identifier
* @property {string} name A human-readable descriptor
* @property {string} [cssClass] Specify a css class defining an icon for
* this time system. This will be visible next to the time system in the
* menu in the Time Conductor
* @property {string} timeFormat The key of a format to use when displaying
* discrete timestamps from this time system
* @property {string} [durationFormat] The key of a format to use when
* displaying a duration or relative span of time in this time system.
*/
/**
* The public API for setting and querying the temporal state of the
* application. The concept of time is integral to Open MCT, and at least
@ -41,8 +76,8 @@ import GlobalTimeContext from './GlobalTimeContext.js';
* fired when properties of the time conductor change, which are documented
* below.
*
* @interface
* @memberof module:openmct
* @class
* @extends {GlobalTimeContext}
*/
class TimeAPI extends GlobalTimeContext {
constructor(openmct) {
@ -51,33 +86,9 @@ class TimeAPI extends GlobalTimeContext {
this.independentContexts = new Map();
}
/**
* A TimeSystem provides meaning to the values returned by the TimeAPI. Open
* MCT supports multiple different types of time values, although all are
* intrinsically represented by numbers, the meaning of those numbers can
* differ depending on context.
*
* A default time system is provided by Open MCT in the form of the {@link UTCTimeSystem},
* which represents integer values as ms in the Unix epoch. An example of
* another time system might be "sols" for a Martian mission. TimeSystems do
* not address the issue of converting between time systems.
*
* @typedef {Object} TimeSystem
* @property {string} key A unique identifier
* @property {string} name A human-readable descriptor
* @property {string} [cssClass] Specify a css class defining an icon for
* this time system. This will be visible next to the time system in the
* menu in the Time Conductor
* @property {string} timeFormat The key of a format to use when displaying
* discrete timestamps from this time system
* @property {string} [durationFormat] The key of a format to use when
* displaying a duration or relative span of time in this time system.
*/
/**
* Register a new time system. Once registered it can activated using
* {@link TimeAPI.timeSystem}, and can be referenced via its key in [Time Conductor configuration](@link https://github.com/nasa/openmct/blob/master/API.md#time-conductor).
* @memberof module:openmct.TimeAPI#
* @param {TimeSystem} timeSystem A time system object.
*/
addTimeSystem(timeSystem) {
@ -109,7 +120,6 @@ class TimeAPI extends GlobalTimeContext {
/**
* Register a new Clock.
* @memberof module:openmct.TimeAPI#
* @param {Clock} clock
*/
addClock(clock) {
@ -117,9 +127,7 @@ class TimeAPI extends GlobalTimeContext {
}
/**
* @memberof module:openmct.TimeAPI#
* @returns {Clock[]}
* @memberof module:openmct.TimeAPI#
*/
getAllClocks() {
return Array.from(this.clocks.values());
@ -128,11 +136,9 @@ class TimeAPI extends GlobalTimeContext {
/**
* Get or set an independent time context which follows the TimeAPI timeSystem,
* but with different offsets for a given domain object
* @param {key | string} key The identifier key of the domain object these offsets are set for
* @param {ClockOffsets | TimeBounds} value This maintains a sliding time window of a fixed width that automatically updates
* @param {string} key The identifier key of the domain object these offsets are set for
* @param {ClockOffsets | TimeConductorBounds} value This maintains a sliding time window of a fixed width that automatically updates
* @param {key | string} clockKey the real time clock key currently in use
* @memberof module:openmct.TimeAPI#
* @method addIndependentTimeContext
*/
addIndependentContext(key, value, clockKey) {
let timeContext = this.getIndependentContext(key);
@ -159,9 +165,8 @@ class TimeAPI extends GlobalTimeContext {
/**
* Get the independent time context which follows the TimeAPI timeSystem,
* but with different offsets.
* @param {key | string} key The identifier key of the domain object these offsets
* @memberof module:openmct.TimeAPI#
* @method getIndependentTimeContext
* @param {string} key The identifier key of the domain object these offsets
* @returns {IndependentTimeContext} The independent time context
*/
getIndependentContext(key) {
return this.independentContexts.get(key);
@ -170,9 +175,8 @@ class TimeAPI extends GlobalTimeContext {
/**
* Get the a timeContext for a view based on it's objectPath. If there is any object in the objectPath with an independent time context, it will be returned.
* Otherwise, the global time context will be returned.
* @param { Array } objectPath The view's objectPath
* @memberof module:openmct.TimeAPI#
* @method getContextForView
* @param {Array} objectPath The view's objectPath
* @returns {TimeContext | GlobalTimeContext} The time context
*/
getContextForView(objectPath) {
if (!objectPath || !Array.isArray(objectPath)) {

View File

@ -57,7 +57,7 @@ describe('The Time API', function () {
expect(api.timeOfInterest()).toBe(toi);
});
it('Allows setting of valid bounds', function () {
it('[Legacy TimeAPI]: Allows setting of valid bounds', function () {
bounds = {
start: 0,
end: 1
@ -67,7 +67,17 @@ describe('The Time API', function () {
expect(api.bounds()).toEqual(bounds);
});
it('Disallows setting of invalid bounds', function () {
it('Allows setting of valid bounds', function () {
bounds = {
start: 0,
end: 1
};
expect(api.getBounds()).not.toBe(bounds);
expect(api.setBounds.bind(api, bounds)).not.toThrow();
expect(api.getBounds()).toEqual(bounds);
});
it('[Legacy TimeAPI]: Disallows setting of invalid bounds', function () {
bounds = {
start: 1,
end: 0
@ -82,7 +92,22 @@ describe('The Time API', function () {
expect(api.bounds()).not.toEqual(bounds);
});
it('Allows setting of previously registered time system with bounds', function () {
it('Disallows setting of invalid bounds', function () {
bounds = {
start: 1,
end: 0
};
expect(api.getBounds()).not.toEqual(bounds);
expect(api.setBounds.bind(api, bounds)).toThrow();
expect(api.getBounds()).not.toEqual(bounds);
bounds = { start: 1 };
expect(api.getBounds()).not.toEqual(bounds);
expect(api.setBounds.bind(api, bounds)).toThrow();
expect(api.getBounds()).not.toEqual(bounds);
});
it('[Legacy TimeAPI]: Allows setting of previously registered time system with bounds', function () {
api.addTimeSystem(timeSystem);
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
@ -91,7 +116,16 @@ describe('The Time API', function () {
expect(api.timeSystem()).toEqual(timeSystem);
});
it('Disallows setting of time system without bounds', function () {
it('Allows setting of previously registered time system with bounds', function () {
api.addTimeSystem(timeSystem);
expect(api.getTimeSystem()).not.toBe(timeSystem);
expect(function () {
api.setTimeSystem(timeSystem, bounds);
}).not.toThrow();
expect(api.getTimeSystem()).toEqual(timeSystem);
});
it('[Legacy TimeAPI]: Disallows setting of time system without bounds', function () {
api.addTimeSystem(timeSystem);
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
@ -100,6 +134,32 @@ describe('The Time API', function () {
expect(api.timeSystem()).not.toBe(timeSystem);
});
it('Allows setting of time system without bounds', function () {
api.addTimeSystem(timeSystem);
expect(api.getTimeSystem()).not.toBe(timeSystem);
expect(function () {
api.setTimeSystem(timeSystemKey);
}).not.toThrow();
expect(api.getTimeSystem()).not.toBe(timeSystem);
});
it('Disallows setting of invalid time system', function () {
expect(function () {
api.setTimeSystem();
}).toThrow();
expect(function () {
api.setTimeSystem('invalidTimeSystemKey');
}).toThrow();
expect(function () {
api.setTimeSystem({
key: 'invalidTimeSystemKey'
});
}).toThrow();
expect(function () {
api.setTimeSystem(42);
}).toThrow();
});
it('allows setting of timesystem without bounds with clock', function () {
api.addTimeSystem(timeSystem);
api.addClock(clock);
@ -114,7 +174,7 @@ describe('The Time API', function () {
expect(api.timeSystem()).toEqual(timeSystem);
});
it('Emits an event when time system changes', function () {
it('Emits a legacy event when time system changes', function () {
api.addTimeSystem(timeSystem);
expect(eventListener).not.toHaveBeenCalled();
api.on('timeSystem', eventListener);
@ -122,6 +182,14 @@ describe('The Time API', function () {
expect(eventListener).toHaveBeenCalledWith(timeSystem);
});
it('Emits an event when time system changes', function () {
api.addTimeSystem(timeSystem);
expect(eventListener).not.toHaveBeenCalled();
api.on('timeSystemChanged', eventListener);
api.timeSystem(timeSystemKey, bounds);
expect(eventListener).toHaveBeenCalledWith(timeSystem);
});
it('Emits an event when time of interest changes', function () {
expect(eventListener).not.toHaveBeenCalled();
api.on('timeOfInterest', eventListener);
@ -129,13 +197,20 @@ describe('The Time API', function () {
expect(eventListener).toHaveBeenCalledWith(toi);
});
it('Emits an event when bounds change', function () {
it('Emits a legacy event when bounds change', function () {
expect(eventListener).not.toHaveBeenCalled();
api.on('bounds', eventListener);
api.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds, false);
});
it('Emits an event when bounds change', function () {
expect(eventListener).not.toHaveBeenCalled();
api.on('boundsChanged', eventListener);
api.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds, false);
});
it('If bounds are set and TOI lies inside them, do not change TOI', function () {
api.timeOfInterest(6);
api.bounds({
@ -154,13 +229,39 @@ describe('The Time API', function () {
expect(api.timeOfInterest()).toBeUndefined();
});
it('Maintains delta during tick', function () {});
it('Maintains delta during tick', function () {
const initialBounds = { start: 100, end: 200 };
api.bounds(initialBounds);
const mockTickSource = jasmine.createSpyObj('mockTickSource', ['on', 'off', 'currentValue']);
mockTickSource.key = 'mct';
mockTickSource.currentValue.and.returnValue(150);
api.addClock(mockTickSource);
api.clock('mct', { start: 0, end: 100 });
it('Allows registered time system to be activated', function () {});
// Simulate a tick event
const tickCallback = mockTickSource.on.calls.mostRecent().args[1];
tickCallback(150);
const newBounds = api.bounds();
expect(newBounds.end - newBounds.start).toEqual(initialBounds.end - initialBounds.start);
});
it('Allows registered time system to be activated', function () {
api.addClock(clock);
api.clock(clockKey, { start: 0, end: 100 });
api.addTimeSystem(timeSystem);
api.timeSystem(timeSystemKey);
expect(api.timeSystem().key).toEqual(timeSystemKey);
});
it('Allows a registered tick source to be activated', function () {
const mockTickSource = jasmine.createSpyObj('mockTickSource', ['on', 'off', 'currentValue']);
mockTickSource.key = 'mockTickSource';
mockTickSource.currentValue.and.returnValue(50);
api.addClock(mockTickSource);
api.clock(mockTickSource.key, { start: 0, end: 100 });
expect(mockTickSource.on).toHaveBeenCalledWith('tick', jasmine.any(Function));
});
describe(' when enabling a tick source', function () {
@ -184,7 +285,7 @@ describe('The Time API', function () {
api.addClock(anotherMockTickSource);
});
it('sets bounds based on current value', function () {
it('[Legacy TimeAPI]: sets bounds based on current value', function () {
api.clock('mts', mockOffsets);
expect(api.bounds()).toEqual({
start: 10,
@ -192,23 +293,46 @@ describe('The Time API', function () {
});
});
it('a new tick listener is registered', function () {
it('does not set bounds based on current value', function () {
api.setClock('mts');
expect(api.getBounds()).toEqual({});
});
it('does not set invalid clock', function () {
expect(function () {
api.setClock();
}).toThrow();
expect(function () {
api.setClock({});
}).toThrow();
expect(function () {
api.setClock('invalidClockKey');
}).toThrow();
});
it('[Legacy TimeAPI]: a new tick listener is registered', function () {
api.clock('mts', mockOffsets);
expect(mockTickSource.on).toHaveBeenCalledWith('tick', jasmine.any(Function));
});
it('a new tick listener is registered', function () {
api.setClock('mts', mockOffsets);
expect(mockTickSource.on).toHaveBeenCalledWith('tick', jasmine.any(Function));
});
it('listener of existing tick source is reregistered', function () {
api.clock('mts', mockOffsets);
api.clock('amts', mockOffsets);
expect(mockTickSource.off).toHaveBeenCalledWith('tick', jasmine.any(Function));
});
xit('Allows the active clock to be set and unset', function () {
it('[Legacy TimeAPI]: Allows the active clock to be set and unset', function () {
expect(api.clock()).toBeUndefined();
api.clock('mts', mockOffsets);
expect(api.clock()).toBeDefined();
// api.stopClock();
// expect(api.clock()).toBeUndefined();
// Unset the clock
api.stopClock();
expect(api.clock()).toBeUndefined();
});
it('Provides a default time context', () => {

View File

@ -20,26 +20,89 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import EventEmitter from 'eventemitter3';
import { FIXED_MODE_KEY, MODES, REALTIME_MODE_KEY, TIME_CONTEXT_EVENTS } from './constants.js';
/**
* @typedef {import('../../utils/clock/DefaultClock.js').default} Clock
*/
/**
* @typedef {import('./TimeAPI.js').TimeSystem} TimeSystem
*/
/**
* @typedef {Object} TimeConductorBounds
* @property {number} start The start time displayed by the time conductor
* in ms since epoch. Epoch determined by currently active time system
* @property {number} end The end time displayed by the time conductor in ms
* since epoch.
*/
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {Object} ClockOffsets
* @property {number} start A time span relative to the current value of the
* ticking clock, from which start bounds will be calculated. This value must
* be < 0. When a clock is active, bounds will be calculated automatically
* based on the value provided by the clock, and the defined clock offsets.
* @property {number} end A time span relative to the current value of the
* ticking clock, from which end bounds will be calculated. This value must
* be >= 0.
*/
/**
* @typedef {Object} ValidationResult
* @property {boolean} valid Result of the validation - true or false.
* @property {string} message An error message if valid is false.
*/
/**
* @typedef {'fixed' | 'realtime'} Mode The time conductor mode.
*/
/**
* @class TimeContext
* @extends EventEmitter
*/
class TimeContext extends EventEmitter {
constructor() {
super();
//The Time System
/**
* The time systems available to the TimeAPI.
* @type {Map<string, TimeSystem>}
*/
this.timeSystems = new Map();
/**
* The currently applied time system.
* @type {TimeSystem | undefined}
*/
this.system = undefined;
/**
* The clocks available to the TimeAPI.
* @type {Map<string, import('../../utils/clock/DefaultClock.js').default>}
*/
this.clocks = new Map();
/**
* The current bounds of the time conductor.
* @type {TimeConductorBounds}
*/
this.boundsVal = {
start: undefined,
end: undefined
};
/**
* The currently active clock.
* @type {Clock | undefined}
*/
this.activeClock = undefined;
this.offsets = undefined;
this.mode = undefined;
@ -51,11 +114,9 @@ class TimeContext extends EventEmitter {
/**
* Get or set the time system of the TimeAPI.
* @param {TimeSystem | string} timeSystemOrKey
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
* @fires module:openmct.TimeAPI~timeSystem
* @param {TimeConductorBounds} bounds
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method timeSystem
* @deprecated This method is deprecated. Use "getTimeSystem" and "setTimeSystem" instead.
*/
timeSystem(timeSystemOrKey, bounds) {
this.#warnMethodDeprecated('"timeSystem"', '"getTimeSystem" and "setTimeSystem"');
@ -101,11 +162,8 @@ class TimeContext extends EventEmitter {
* The time system used by the time
* conductor has changed. A change in Time System will always be
* followed by a bounds event specifying new query bounds.
*
* @event module:openmct.TimeAPI~timeSystem
* @property {TimeSystem} The value of the currently applied
* Time System
* */
* @type {TimeSystem}
*/
const system = this.#copy(this.system);
this.emit('timeSystem', system);
this.emit(TIME_CONTEXT_EVENTS.timeSystemChanged, system);
@ -118,21 +176,11 @@ class TimeContext extends EventEmitter {
return this.system;
}
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {Object} ValidationResult
* @property {boolean} valid Result of the validation - true or false.
* @property {string} message An error message if valid is false.
*/
/**
* Validate the given bounds. This can be used for pre-validation of bounds,
* for example by views validating user inputs.
* @param {TimeBounds} bounds The start and end time of the conductor.
* @param {TimeConductorBounds} bounds The start and end time of the conductor.
* @returns {ValidationResult} A validation error, or true if valid
* @memberof module:openmct.TimeAPI#
* @method validateBounds
*/
validateBounds(bounds) {
if (
@ -162,12 +210,10 @@ class TimeContext extends EventEmitter {
* Get or set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @param {TimeConductorBounds} [newBounds] The new bounds to set. If not provided, current bounds will be returned.
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
* @returns {TimeConductorBounds} The current bounds of the time conductor.
* @deprecated This method is deprecated. Use "getBounds" and "setBounds" instead.
*/
bounds(newBounds) {
this.#warnMethodDeprecated('"bounds"', '"getBounds" and "setBounds"');
@ -183,7 +229,6 @@ class TimeContext extends EventEmitter {
/**
* The start time, end time, or both have been updated.
* @event bounds
* @memberof module:openmct.TimeAPI~
* @property {TimeConductorBounds} bounds The newly updated bounds
* @property {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (ie. was an automatic update), false otherwise.
@ -200,9 +245,7 @@ class TimeContext extends EventEmitter {
* Validate the given offsets. This can be used for pre-validation of
* offsets, for example by views validating user inputs.
* @param {ClockOffsets} offsets The start and end offsets from a 'now' value.
* @returns { ValidationResult } A validation error, and true/false if valid or not
* @memberof module:openmct.TimeAPI#
* @method validateOffsets
* @returns {ValidationResult} A validation error, and true/false if valid or not
*/
validateOffsets(offsets) {
if (
@ -228,34 +271,13 @@ class TimeContext extends EventEmitter {
};
}
/**
* @typedef {Object} TimeBounds
* @property {number} start The start time displayed by the time conductor
* in ms since epoch. Epoch determined by currently active time system
* @property {number} end The end time displayed by the time conductor in ms
* since epoch.
* @memberof module:openmct.TimeAPI~
*/
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {Object} ClockOffsets
* @property {number} start A time span relative to the current value of the
* ticking clock, from which start bounds will be calculated. This value must
* be < 0. When a clock is active, bounds will be calculated automatically
* based on the value provided by the clock, and the defined clock offsets.
* @property {number} end A time span relative to the current value of the
* ticking clock, from which end bounds will be calculated. This value must
* be >= 0.
*/
/**
* Get or set the currently applied clock offsets. If no parameter is provided,
* the current value will be returned. If provided, the new value will be
* used as the new clock offsets.
* @param {ClockOffsets} offsets
* @returns {ClockOffsets}
* @param {ClockOffsets} [offsets] The new clock offsets to set. If not provided, current offsets will be returned.
* @returns {ClockOffsets} The current clock offsets.
* @deprecated This method is deprecated. Use "getClockOffsets" and "setClockOffsets" instead.
*/
clockOffsets(offsets) {
this.#warnMethodDeprecated('"clockOffsets"', '"getClockOffsets" and "setClockOffsets"');
@ -293,6 +315,7 @@ class TimeContext extends EventEmitter {
* Stop following the currently active clock. This will
* revert all views to showing a static time frame defined by the current
* bounds.
* @deprecated This method is deprecated.
*/
stopClock() {
this.#warnMethodDeprecated('"stopClock"');
@ -304,12 +327,14 @@ class TimeContext extends EventEmitter {
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided.
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {string|Clock} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds. This maintains a sliding time window of a fixed
* width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
* (Legacy) Emits a "clock" event with the new clock.
* Emits a "clockChanged" event with the new clock.
* @return {Clock|undefined} the currently active clock; undefined if in fixed mode
* @deprecated This method is deprecated. Use "getClock" and "setClock" instead.
*/
clock(keyOrClock, offsets) {
this.#warnMethodDeprecated('"clock"', '"getClock" and "setClock"');
@ -339,7 +364,6 @@ class TimeContext extends EventEmitter {
/**
* The active clock has changed.
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
@ -361,7 +385,7 @@ class TimeContext extends EventEmitter {
}
/**
* Update bounds based on provided time and current offsets
* Update bounds based on provided time and current offsets.
* @param {number} timestamp A time from which bounds will be calculated
* using current offsets.
*/
@ -385,8 +409,6 @@ class TimeContext extends EventEmitter {
/**
* Get the timestamp of the current clock
* @returns {number} current timestamp of current clock regardless of mode
* @memberof module:openmct.TimeAPI#
* @method now
*/
now() {
@ -396,8 +418,6 @@ class TimeContext extends EventEmitter {
/**
* Get the time system of the TimeAPI.
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method getTimeSystem
*/
getTimeSystem() {
return this.system;
@ -405,12 +425,9 @@ class TimeContext extends EventEmitter {
/**
* Set the time system of the TimeAPI.
* @param {TimeSystem | string} timeSystemOrKey
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
* @fires module:openmct.TimeAPI~timeSystem
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method setTimeSystem
* Emits a "timeSystem" event with the new time system.
* @param {TimeSystem | string} timeSystemOrKey The time system to set, or its key
* @param {TimeConductorBounds} [bounds] Optional bounds to set
*/
setTimeSystem(timeSystemOrKey, bounds) {
if (timeSystemOrKey === undefined) {
@ -441,7 +458,6 @@ class TimeContext extends EventEmitter {
* conductor has changed. A change in Time System will always be
* followed by a bounds event specifying new query bounds.
*
* @event module:openmct.TimeAPI~timeSystem
* @property {TimeSystem} The value of the currently applied
* Time System
* */
@ -456,9 +472,7 @@ class TimeContext extends EventEmitter {
/**
* Get the start and end time of the time conductor. Basic validation
* of bounds is performed.
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
* @returns {TimeConductorBounds} The current bounds of the time conductor.
*/
getBounds() {
//Return a copy to prevent direct mutation of time conductor bounds.
@ -469,12 +483,8 @@ class TimeContext extends EventEmitter {
* Set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
* @param {TimeConductorBounds} newBounds The new bounds to set.
* @throws {Error} Validation error if bounds are invalid
*/
setBounds(newBounds) {
const validationResult = this.validateBounds(newBounds);
@ -487,7 +497,6 @@ class TimeContext extends EventEmitter {
/**
* The start time, end time, or both have been updated.
* @event bounds
* @memberof module:openmct.TimeAPI~
* @property {TimeConductorBounds} bounds The newly updated bounds
* @property {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (i.e. was an automatic update), false otherwise.
@ -498,7 +507,7 @@ class TimeContext extends EventEmitter {
/**
* Get the active clock.
* @return {Clock} the currently active clock;
* @return {Clock|undefined} the currently active clock; undefined if in fixed mode.
*/
getClock() {
return this.activeClock;
@ -509,9 +518,7 @@ class TimeContext extends EventEmitter {
* and the currently ticking will begin.
* Offsets from 'now', if provided, will be used to set realtime mode offsets
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
* @param {string|Clock} keyOrClock The clock to activate, or its key
*/
setClock(keyOrClock) {
let clock;
@ -540,7 +547,7 @@ class TimeContext extends EventEmitter {
* The active clock has changed.
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* @property {TimeContext} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);
@ -549,7 +556,7 @@ class TimeContext extends EventEmitter {
/**
* Get the current mode.
* @return {Mode} the current mode;
* @return {Mode} the current mode
*/
getMode() {
return this.mode;
@ -559,9 +566,9 @@ class TimeContext extends EventEmitter {
* Set the mode to either fixed or realtime.
*
* @param {Mode} mode The mode to activate
* @param {TimeBounds | ClockOffsets} offsetsOrBounds A time window of a fixed width
* @param {TimeConductorBounds|ClockOffsets} offsetsOrBounds A time window of a fixed width
* @fires module:openmct.TimeAPI~clock
* @return {Mode} the currently active mode;
* @return {Mode | undefined} the currently active mode
*/
setMode(mode, offsetsOrBounds) {
if (!mode) {
@ -577,7 +584,6 @@ class TimeContext extends EventEmitter {
/**
* The active mode has changed.
* @event modeChanged
* @memberof module:openmct.TimeAPI~
* @property {Mode} mode The newly activated mode
*/
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.mode));
@ -610,18 +616,15 @@ class TimeContext extends EventEmitter {
/**
* Get the currently applied clock offsets.
* @returns {ClockOffsets}
* @returns {ClockOffsets} The current clock offsets.
*/
getClockOffsets() {
return this.offsets;
}
/**
* Set the currently applied clock offsets. If no parameter is provided,
* the current value will be returned. If provided, the new value will be
* used as the new clock offsets.
* @param {ClockOffsets} offsets
* @returns {ClockOffsets}
* Set the currently applied clock offsets.
* @param {ClockOffsets} offsets The new clock offsets to set.
*/
setClockOffsets(offsets) {
const validationResult = this.validateOffsets(offsets);
@ -642,13 +645,20 @@ class TimeContext extends EventEmitter {
/**
* Event that is triggered when clock offsets change.
* @event clockOffsets
* @memberof module:openmct.TimeAPI~
* @property {ClockOffsets} clockOffsets The newly activated clock
* offsets.
*/
this.emit(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.#copy(offsets));
}
/**
* Prints a warning to the console when a deprecated method is used. Limits
* the number of times a warning is printed per unique method and newMethod
* combination.
* @param {string} method the deprecated method
* @param {string} [newMethod] the new method to use instead
* @returns
*/
#warnMethodDeprecated(method, newMethod) {
const MAX_CALLS = 1; // Only warn once per unique method and newMethod combination
@ -673,6 +683,11 @@ class TimeContext extends EventEmitter {
console.warn(message);
}
/**
* Deep copy an object.
* @param {object} object The object to copy
* @returns {object} The copied object
*/
#copy(object) {
return JSON.parse(JSON.stringify(object));
}

View File

@ -20,7 +20,13 @@ this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div ref="tooltip-wrapper" class="c-menu c-tooltip-wrapper" :style="toolTipLocationStyle">
<div
ref="tooltip-wrapper"
class="c-menu c-tooltip-wrapper"
:style="toolTipLocationStyle"
role="tooltip"
aria-live="polite"
>
<div class="c-tooltip">
{{ toolTipText }}
</div>

View File

@ -22,7 +22,7 @@
import { ACTIVE_ROLE_LOCAL_STORAGE_KEY } from './constants.js';
class StoragePersistance {
class StoragePersistence {
getActiveRole() {
return localStorage.getItem(ACTIVE_ROLE_LOCAL_STORAGE_KEY);
}
@ -34,4 +34,4 @@ class StoragePersistance {
}
}
export default new StoragePersistance();
export default new StoragePersistence();

View File

@ -24,7 +24,7 @@ import EventEmitter from 'EventEmitter';
import { MULTIPLE_PROVIDER_ERROR, NO_PROVIDER_ERROR } from './constants.js';
import StatusAPI from './StatusAPI.js';
import StoragePersistance from './StoragePersistance.js';
import StoragePersistence from './StoragePersistence.js';
import User from './User.js';
class UserAPI extends EventEmitter {
@ -115,7 +115,7 @@ class UserAPI extends EventEmitter {
}
// get from session storage
const sessionStorageValue = StoragePersistance.getActiveRole();
const sessionStorageValue = StoragePersistence.getActiveRole();
return sessionStorageValue;
}
@ -126,9 +126,9 @@ class UserAPI extends EventEmitter {
*/
setActiveRole(role) {
if (!role) {
StoragePersistance.clearActiveRole();
StoragePersistence.clearActiveRole();
} else {
StoragePersistance.setActiveRole(role);
StoragePersistence.setActiveRole(role);
}
this.emit('roleChanged', role);
}

View File

@ -30,6 +30,7 @@
>
<td
ref="tableCell"
scope="row"
aria-label="lad name"
class="js-first-data"
@mouseover.ctrl="showToolTip"
@ -57,14 +58,21 @@
</template>
<script>
const CONTEXT_MENU_ACTIONS = ['viewDatumAction', 'viewHistoricalData', 'remove'];
const BLANK_VALUE = '---';
import { objectPathToUrl } from '/src/tools/url.js';
import PreviewAction from '@/ui/preview/PreviewAction.js';
import { REMOVE_ACTION_KEY } from '@/plugins/remove/RemoveAction.js';
import { VIEW_DATUM_ACTION_KEY } from '@/plugins/viewDatumAction/ViewDatumAction.js';
import { PREVIEW_ACTION_KEY } from '@/ui/preview/PreviewAction.js';
import { VIEW_HISTORICAL_DATA_ACTION_KEY } from '@/ui/preview/ViewHistoricalDataAction.js';
import tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';
const BLANK_VALUE = '---';
const CONTEXT_MENU_ACTIONS = [
VIEW_DATUM_ACTION_KEY,
VIEW_HISTORICAL_DATA_ACTION_KEY,
REMOVE_ACTION_KEY
];
export default {
mixins: [tooltipHelpers],
inject: ['openmct', 'currentView', 'renderWhenVisible'],
@ -212,7 +220,7 @@ export default {
this.openmct.time.on('timeSystem', this.updateTimeSystem);
this.timestampKey = this.openmct.time.timeSystem().key;
this.timestampKey = this.openmct.time.getTimeSystem().key;
this.valueMetadata = undefined;
@ -236,14 +244,12 @@ export default {
this.setUnit();
}
this.previewAction = new PreviewAction(this.openmct);
this.previewAction.on('isVisible', this.togglePreviewState);
this.previewAction = this.openmct.actions.getAction(PREVIEW_ACTION_KEY);
},
unmounted() {
this.openmct.time.off('timeSystem', this.updateTimeSystem);
this.telemetryCollection.off('add', this.setLatestValues);
this.telemetryCollection.off('clear', this.resetValues);
this.previewAction.off('isVisible', this.togglePreviewState);
this.telemetryCollection.destroy();
},

View File

@ -21,22 +21,26 @@
-->
<template>
<div class="c-lad-table-wrapper u-style-receiver js-style-receiver" :class="staleClass">
<table aria-label="lad table" class="c-table c-lad-table" :class="applyLayoutClass">
<div
id="lad-table-drop-area"
class="c-lad-table-wrapper u-style-receiver js-style-receiver"
:class="staleClass"
>
<table class="c-table c-lad-table" :class="applyLayoutClass">
<thead>
<tr>
<th>Name</th>
<th v-if="showTimestamp">Timestamp</th>
<th>Value</th>
<th v-if="hasUnits">Units</th>
<th v-if="showType">Type</th>
<th v-for="limitColumn in limitColumnNames" :key="limitColumn.key">
<th scope="col">Name</th>
<th v-if="showTimestamp" scope="col">Timestamp</th>
<th scope="col">Value</th>
<th v-if="hasUnits" scope="col">Units</th>
<th v-if="showType" scope="col">Type</th>
<th v-for="limitColumn in limitColumnNames" :key="limitColumn.key" scope="col">
{{ limitColumn.label }}
</th>
</tr>
</thead>
<tbody>
<lad-row
<LadRow
v-for="ladRow in items"
:key="ladRow.key"
:domain-object="ladRow.domainObject"

View File

@ -39,7 +39,7 @@
{{ ladTable.domainObject.name }}
</td>
</tr>
<lad-row
<LadRow
v-for="ladRow in ladTelemetryObjects[ladTable.key]"
:key="combineKeys(ladTable.key, ladRow.key)"
:domain-object="ladRow.domainObject"

View File

@ -61,7 +61,7 @@ export default class URLTimeSettingsSynchronizer {
TIME_EVENTS.forEach((event) => {
this.openmct.time.on(event, this.setUrlFromTimeApi);
});
this.openmct.time.on('bounds', this.updateBounds);
this.openmct.time.on('boundsChanged', this.updateBounds);
}
destroy() {
@ -73,7 +73,7 @@ export default class URLTimeSettingsSynchronizer {
TIME_EVENTS.forEach((event) => {
this.openmct.time.off(event, this.setUrlFromTimeApi);
});
this.openmct.time.off('bounds', this.updateBounds);
this.openmct.time.off('boundsChanged', this.updateBounds);
}
updateTimeSettings() {

View File

@ -115,11 +115,11 @@ export default {
this.followTimeContext();
},
followTimeContext() {
this.timeContext.on('bounds', this.refreshData);
this.timeContext.on('boundsChanged', this.refreshData);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('bounds', this.refreshData);
this.timeContext.off('boundsChanged', this.refreshData);
}
},
addToComposition(telemetryObject) {
@ -253,7 +253,7 @@ export default {
};
},
getOptions() {
const { start, end } = this.timeContext.bounds();
const { start, end } = this.timeContext.getBounds();
return {
end,
@ -372,13 +372,13 @@ export default {
this.setTrace(key, telemetryObject.name, axisMetadata, xValues, yValues);
},
isDataInTimeRange(datum, key, telemetryObject) {
const timeSystemKey = this.timeContext.timeSystem().key;
const timeSystemKey = this.timeContext.getTimeSystem().key;
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
let metadataValue = metadata.value(timeSystemKey) || { key: timeSystemKey };
let currentTimestamp = this.parse(key, metadataValue.key, datum);
return currentTimestamp && this.timeContext.bounds().end >= currentTimestamp;
return currentTimestamp && this.timeContext.getBounds().end >= currentTimestamp;
},
format(telemetryObjectKey, metadataKey, data) {
const formats = this.telemetryObjectFormats[telemetryObjectKey];

View File

@ -24,7 +24,7 @@
<ul class="c-tree">
<h2 title="Display properties for this object">Bar Graph Series</h2>
<li>
<series-options
<SeriesOptions
v-for="series in plotSeries"
:key="series.keyString"
:item="series"

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