Compare commits

..

120 Commits

Author SHA1 Message Date
Jesse Mazzella
fdfca3ac1a Merge branch 'master' into vue3-compat-migration 2023-07-19 09:00:27 -07:00
Jesse Mazzella
62db57b758 fix: reactify yaxis config 2023-07-18 15:54:05 -07:00
Jesse Mazzella
8d2068ff3b fix: alphanumericFormat 2023-07-18 15:23:14 -07:00
Jesse Mazzella
aa8f60f672 fix: restore nextTick() 2023-07-18 15:14:11 -07:00
Jesse Mazzella
5095e87c2d chore: revert commented code in unit tests 2023-07-18 14:37:28 -07:00
Jesse Mazzella
c2749e4081 refactor: run lint:fix 2023-07-18 14:34:40 -07:00
Jesse Mazzella
67fd649416 refactor: simplify methods w/ optional chaining 2023-07-18 14:32:50 -07:00
Jesse Mazzella
c8379538b7 refactor: change variable name 2023-07-18 14:32:32 -07:00
Jesse Mazzella
826599ff57 fix: make static 2023-07-18 14:24:07 -07:00
Jesse Mazzella
75072e3c0d fix: make spellcheck static 2023-07-18 14:19:34 -07:00
Jesse Mazzella
6a1b1dbb54 fix: don't make roleChannel reactive 2023-07-18 14:14:18 -07:00
Jesse Mazzella
4fc8233c36 fix: Operator Status popup use componentInstance 2023-07-18 14:13:44 -07:00
Jesse Mazzella
fe2ae0f89f fix: respond to code review comments 2023-07-18 13:56:09 -07:00
Jesse Mazzella
8a6bbcae22 fix: modify this.selection without losing reactivity
- NOTE: There's still an issue with the tree and modifying attributes from EditProperties dialog or the BrowseBar. WRT the BrowseBar, modifying a name triggers the Location to update in the Properties Inspector Pane, which in turn triggers a `get()` and a `$refresh`, which is picked up by Proxy and changes the name back before we can save.

- The tree is sporadically picking up treeItem changes and it seems random, but could be a conflict or race condition between Mutables and Proxys of Mutables (?)
2023-07-17 15:10:24 -07:00
Jesse Mazzella
7b112365f3 Revert "fix: prevent Properties re-render from blocking object name change"
This reverts commit 8f11b0f1a2.
2023-07-17 14:55:42 -07:00
Jesse Mazzella
53fb7d7c30 fix: beforeDestroy() --> beforeUnmount() 2023-07-17 14:27:09 -07:00
John Hill
4f1a58b446 Merge branch 'master' into vue3-compat-migration 2023-07-17 14:22:05 -07:00
Jesse Mazzella
9d3709beb6 refactor: run lint:fix 2023-07-17 13:21:30 -07:00
Jesse Mazzella
0185d5fc2a fix(RestrictedNotebook): Multiple reactivity fixes
- Restore lock button reactivity

- Restore "Delete Section" reactivity without needing an observer
2023-07-17 13:21:01 -07:00
Jesse Mazzella
da63de2431 fix: observe sections so we can delete 2023-07-17 11:49:00 -07:00
Jesse Mazzella
29a492cb51 fix: ensure dependencies on computed properties remain reactive (Proxy) 2023-07-17 10:47:57 -07:00
Jesse Mazzella
637c2a5c35 refactor: use optional chaining 2023-07-17 10:47:24 -07:00
Jesse Mazzella
eca17ae3f5 fix(e2e): fix forms test selector 2023-07-17 10:20:55 -07:00
Jesse Mazzella
66738ebcde fix: viewLargeAction for Imagery 2023-07-17 10:00:56 -07:00
Jesse Mazzella
9447c609e6 fix: undo accidental find and replace 2023-07-15 19:12:33 -07:00
Jesse Mazzella
8f11b0f1a2 fix: prevent Properties re-render from blocking object name change 2023-07-15 11:27:04 -07:00
Jesse Mazzella
12085d673a Revert "fix: fixes issue causing Inspector to block object name updates"
This reverts commit 34111c98f7.
2023-07-15 09:12:22 -07:00
Jesse Mazzella
344993ae2f fix: ImageryView controls update correctly on TimeContext change
- When `timeContext` is retrieved from the Open MCT API and assigned to the data value, it becomes reactified (Proxy). Since it is wrapped in a Proxy, listeners don't trigger properly when events are emitted from the non-reactified API. Solution (for now) is to remove `this.timeContext` as a data (reactified) value, remove the `isFixed` and `canTrackDuration` computed values (which depend on changes to `this.timeContext` being detected), and manually update these when our non-reactified `timeContext` changes.
2023-07-15 08:59:41 -07:00
Jesse Mazzella
0b55361e54 style: fix tree pane width snapback issue 2023-07-15 08:03:08 -07:00
Jesse Mazzella
fae18bd1cd Merge branch 'master' into vue3-compat-migration 2023-07-15 07:45:43 -07:00
Jesse Mazzella
888c3fda6a style: fix AboutDialog 2023-07-14 17:04:35 -07:00
Jesse Mazzella
8c808d9bb8 style: fix Preview 2023-07-14 16:20:31 -07:00
Jesse Mazzella
4452b98097 style: a ton of css fixes
- Vue 3 appends itself on the given element on mount instead of replacing the mounting element, so we can get rid of a bunch of extra divs we didn't need.
- Styling fixes for ViewLarge, Preview, FormRows
- Fix sizing for Menu and SuperMenus
- Fix sizing for Forms and Locators
- Fix the Styles InspectorView horizontal pane issue
2023-07-14 16:18:20 -07:00
Jesse Mazzella
c62a314015 refactor: lint:fix 2023-07-14 14:11:41 -07:00
Jesse Mazzella
08a3cd7ade fix: Vue() --> mount() 2023-07-14 14:08:14 -07:00
Jesse Mazzella
a70859daf7 Merge branch 'master' into vue3-compat-migration 2023-07-14 14:03:47 -07:00
Jesse Mazzella
db2bdb878b fix: remove console.debug 2023-07-14 13:28:50 -07:00
Jesse Mazzella
d1f3c22d47 fix: never reactify the composition collection 2023-07-14 13:09:41 -07:00
Jesse Mazzella
315b39a5ea Revert "fix: remove private methods as they conflict with Vue3 proxy system"
This reverts commit 8cfa812241.
2023-07-14 12:59:59 -07:00
Jesse Mazzella
fc9889630b fix: toRaw before structuredClone of notebook entries 2023-07-14 12:57:25 -07:00
Jesse Mazzella
3a9cc42107 fix: update tooltips for vue 3 2023-07-14 10:13:13 -07:00
Jesse Mazzella
6c49bbaf70 fix: add observer for notebook section changes so it can detect section deletion 2023-07-14 08:14:03 -07:00
Scott Bell
9eaaa8199a can add sections 2023-07-14 16:21:50 +02:00
Andrew Henry
068ba69cde Changed 'callback' to 'onItemClicked' for popup menu items 2023-07-14 07:13:54 -07:00
Scott Bell
b2ff80f2c0 fix sections persisting on notebook load, and fixed router error 2023-07-14 15:25:41 +02:00
Jesse Mazzella
1f4408eb16 Merge branch 'master' into vue3-compat-migration 2023-07-13 21:39:32 -07:00
Jesse Mazzella
f269f33edd refactor: fix eslint warnings without losing progress 2023-07-13 15:52:45 -07:00
Jesse Mazzella
3a83f2a584 run lint:fix, adjust eslint rule for vue 3 2023-07-13 14:41:10 -07:00
Jesse Mazzella
1e19485f6d Merge branch 'master' into vue3-compat-migration 2023-07-13 13:39:42 -07:00
Jesse Mazzella
dfb4b53cd3 fix: remove debug code 2023-07-13 13:22:35 -07:00
Jesse Mazzella
a7ad89592a fix: add fallback if no child of document.body exists 2023-07-13 13:22:14 -07:00
Jesse Mazzella
a5734e80f5 fix: follow mutation events for timer configuration 2023-07-13 10:15:01 -07:00
Jesse Mazzella
6ac980c500 fix: follow mutation events for clock configuration changes 2023-07-13 10:11:54 -07:00
Jesse Mazzella
810c67a27b fix: ensure component is mounted before starting open mct 2023-07-13 10:02:04 -07:00
Jesse Mazzella
09f58ef85c fix: define _destroy 2023-07-13 09:20:46 -07:00
Jesse Mazzella
fd37d88ac4 fix: use public vue methods to get the layout ref 2023-07-13 08:35:57 -07:00
Andrew Henry
23906c7649 Remove dependency on Vue reactivity to detect changes from Toolbar API. Use mutation events instead 2023-07-12 14:51:14 -07:00
Andrew Henry
7d53ffa30f Do not make a copy of the domain object. This causes issues with Vue reactivity 2023-07-12 14:50:41 -07:00
Andrew Henry
a63c9457dd Mod the mutation API to emit event for array property when any member changes 2023-07-12 14:49:35 -07:00
Jesse Mazzella
679fe76412 fix: remove unnecessary element append 2023-07-11 21:16:51 -07:00
Jesse Mazzella
089a19c843 fix: vue warning 2023-07-11 17:11:26 -07:00
Jesse Mazzella
bb26169b88 fix: call me the memory leak destoyer 😎
- fixed some typos causing potential memory leaks
2023-07-11 14:11:09 -07:00
Jesse Mazzella
7b9958e094 fix: pass objectPath as a prop to Timer 2023-07-11 14:05:16 -07:00
Jesse Mazzella
28917443cf fix(StackedPlotItem): mounting no longer replaces the element
- viewContainer not needed
2023-07-11 13:46:58 -07:00
Jesse Mazzella
7b4e4af2bd fix: add deep: true on array watcher 2023-07-11 13:46:18 -07:00
Jesse Mazzella
34111c98f7 fix: fixes issue causing Inspector to block object name updates 2023-07-11 09:35:29 -07:00
Jesse Mazzella
d56ad6d286 fix: revert tiny-emitter changes for now 2023-07-11 09:35:09 -07:00
Scott Bell
ee454029b9 fix plan acitvity 2023-07-10 22:19:29 +02:00
Scott Bell
96af897cfd to raw the whole tagging process 2023-07-10 17:15:59 +02:00
Scott Bell
9489ee4fc4 Merge branch 'master' into vue3-compat-migration 2023-07-10 15:15:16 +02:00
Jesse Mazzella
93f3093d85 fix: mounted app no longer replaces HTMLElement 2023-07-09 16:16:14 -07:00
Jesse Mazzella
690fd4285a style: display: none no longer needed here 2023-07-09 11:05:58 -07:00
Jesse Mazzella
103ad6c995 fix: remove unused import 2023-07-09 11:05:40 -07:00
Jesse Mazzella
ffaedc97ed fix: use regular assignment instead of $set 2023-07-09 08:53:34 -07:00
Jesse Mazzella
88382797c3 fix: make notification indicator reactive again 2023-07-08 17:12:03 -07:00
Jesse Mazzella
4994433347 fix: revert emitter changes for MctPlot 2023-07-08 16:35:30 -07:00
Jesse Mazzella
c1263c3823 fix: do not convert the publicAPI to a Proxy 2023-07-08 12:04:11 -07:00
Jesse Mazzella
70b3db0f84 fix: GH code scan warnings 2023-07-08 11:42:20 -07:00
Jesse Mazzella
f20aa8c6d9 fix: typo 2023-07-08 11:39:31 -07:00
Jesse Mazzella
7db0d5191d fix: replace some $set with regular assignment 2023-07-08 10:41:14 -07:00
Jesse Mazzella
710fd2e1dc fix: use mount pattern to create Vue components 2023-07-08 10:40:14 -07:00
Jesse Mazzella
bb3a367f1d fix: expose the main Vue app on openmct 2023-07-08 10:21:28 -07:00
Jesse Mazzella
4bacbc03c1 fix: use correct distfile for webpack 2023-07-06 13:53:48 -07:00
Jesse Mazzella
fd6cb38596 fix: beforeUnmount and array handler 2023-07-06 13:53:34 -07:00
Scott Bell
41a540ab49 old object was a proxy and causing further object propagations with Object.assign 2023-07-06 16:05:48 +02:00
Jesse Mazzella
fa4bdbcdba fix: unmount AboutDialog onDestroy 2023-07-05 14:49:48 -07:00
Jesse Mazzella
fc170fd2fe fix(WIP): new Vue() -> Vue.createApp() 2023-07-05 12:08:03 -07:00
Jesse Mazzella
eefd3ac322 fix: upgrade Menu and SuperMenu to Vue 3
- fixes positioning of Menus

- ensure that Menu and SuperMenu are unmounted properly

- extract common logic for popup positioning into a mixin
2023-07-03 16:01:18 -07:00
Jesse Mazzella
3338e594f1 refactor: PopupMenu to use MenuAPI 2023-07-03 10:28:54 -07:00
Scott Bell
43f10befac fix infinite loop issue 2023-07-03 14:09:50 +02:00
Jesse Mazzella
c994d05c78 fix(TTable): horizontal header scroll, etc
- remove unnecessary `nextTick()`s

- `const` -> `let`
2023-06-30 10:40:16 -07:00
Jesse Mazzella
44cbbe9791 Revert "chore: pin versions"
This reverts commit 120e907486.
2023-06-29 15:53:45 -07:00
Jesse Mazzella
d8f0b6e3a8 Merge branch 'master' into vue3-compat-migration 2023-06-29 15:19:41 -07:00
Jesse Mazzella
45bd00bea6 chore: run lint:fix 2023-06-29 15:15:59 -07:00
Jesse Mazzella
120e907486 chore: pin versions 2023-06-29 14:26:21 -07:00
Jesse Mazzella
fd848972b4 fix: misc fixes 2023-06-29 14:08:50 -07:00
Jesse Mazzella
ac1e1ef54c fix: restore call $destroy on Objects 2023-06-29 09:50:17 -07:00
Jesse Mazzella
22796377c7 fix: use $refs and replace $on, $off, $once with tiny-emitter 2023-06-23 14:50:44 -07:00
Jesse Mazzella
55ebfcdf7c fix: use deep: true option for watched arrays 2023-06-23 14:48:48 -07:00
Jesse Mazzella
68ac6b6fc2 fix: warning for boolean coercion 2023-06-23 14:48:23 -07:00
Jesse Mazzella
59bde3151d fix: use regular js assignment isntead of $set 2023-06-23 14:46:18 -07:00
Jesse Mazzella
90459c3d41 fix: TelemetryTable migration
- Replace `.$el` with refs
- Use `toRaw()` to unwrap reactive Proxy for comparison
2023-06-21 14:51:32 -07:00
Jesse Mazzella
8cfa812241 fix: remove private methods as they conflict with Vue3 proxy system 2023-06-20 15:47:33 -07:00
Jesse Mazzella
609b3c2b1b fix: inspectorViews selected tab 2023-06-20 15:47:06 -07:00
Jesse Mazzella
12a0a6f7b2 fix: click handler for ImageryView 2023-06-20 15:46:36 -07:00
Jesse Mazzella
5438711228 chore: update array watchers 2023-06-15 16:49:48 -07:00
Jesse Mazzella
9f66843ed6 chore: do not convert conductor to proxy, ever 2023-06-15 16:47:58 -07:00
Jesse Mazzella
bc61b32930 chore: REVISIT: not sure this is the right way to do this but makes the objectView work 2023-06-15 16:47:41 -07:00
Jesse Mazzella
8fce444885 chore: destroy all $destroy()s 2023-06-15 16:47:01 -07:00
Jesse Mazzella
41d280934e chore: add div for mount 2023-06-15 16:42:35 -07:00
Jesse Mazzella
afce9344f4 chore: update webpack 2023-06-15 16:41:23 -07:00
Jesse Mazzella
432295606d chore destroyed: --> unmounted: 2023-06-15 11:01:41 -07:00
Jesse Mazzella
5bdfa04eb4 chore: destroyed() --> unmounted() 2023-06-15 10:47:02 -07:00
Jesse Mazzella
e7d9e37fc7 chore: use createApp in MCT.js 2023-06-15 10:45:47 -07:00
Jesse Mazzella
2896520d37 chore: beforeDestroy() --> beforeUnmount() 2023-06-14 11:41:52 -07:00
Jesse Mazzella
6610793a8d chore: fix warnings
- move `v-bind` to first property
- remove `.native()` method
2023-06-14 11:41:31 -07:00
Jesse Mazzella
e3dffc5c12 chore: update vue alias 2023-06-14 11:37:06 -07:00
Jesse Mazzella
e323d4f441 chore: add dependencies, modify webpack 2023-06-14 09:25:30 -07:00
Jesse Mazzella
3f7d071347 fix: more deprecated slot syntax 2023-06-13 15:40:19 -07:00
Jesse Mazzella
5169676eb0 fix: update usage of named slots 2023-06-13 15:36:29 -07:00
261 changed files with 2581 additions and 5988 deletions

View File

@@ -2,7 +2,7 @@ version: 2.1
executors:
pw-focal-development:
docker:
- image: mcr.microsoft.com/playwright:v1.36.2-focal
- image: mcr.microsoft.com/playwright:v1.32.3-focal
environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
@@ -162,7 +162,7 @@ jobs:
steps:
- build_and_install:
node-version: <<parameters.node-version>>
- run: npx playwright@1.36.2 install #Necessary for bare ubuntu machine
- run: npx playwright@1.32.3 install #Necessary for bare ubuntu machine
- run: |
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
@@ -197,9 +197,7 @@ jobs:
steps:
- build_and_install:
node-version: <<parameters.node-version>>
- run: npm run test:perf:memory
- run: npm run test:perf:localhost
- run: npm run test:perf:contract
- run: npm run test:perf
- store_test_results:
path: test-results/results.xml
- store_artifacts:
@@ -215,13 +213,11 @@ jobs:
parameters:
node-version:
type: string
suite:
type: string # ci or full
executor: pw-focal-development
steps:
- build_and_install:
node-version: <<parameters.node-version>>
- run: npm run test:e2e:visual:<<parameters.suite>>
- run: npm run test:e2e:visual
- store_test_results:
path: test-results/results.xml
- store_artifacts:
@@ -246,12 +242,6 @@ workflows:
name: e2e-stable
node-version: lts/hydrogen
suite: stable
- perf-test:
node-version: lts/hydrogen
- visual-test:
name: visual-test-ci
suite: ci
node-version: lts/hydrogen
the-nightly: #These jobs do not run on PRs, but against master at night
jobs:
@@ -270,8 +260,6 @@ workflows:
- perf-test:
node-version: lts/hydrogen
- visual-test:
name: visual-test-nightly
suite: full
node-version: lts/hydrogen
- e2e-couchdb:
node-version: lts/hydrogen

View File

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

View File

@@ -13,7 +13,7 @@ module.exports = {
extends: [
'eslint:recommended',
'plugin:compat/recommended',
'plugin:vue/vue3-recommended',
'plugin:vue/recommended',
'plugin:you-dont-need-lodash-underscore/compatible',
'plugin:prettier/recommended'
],
@@ -28,8 +28,6 @@ module.exports = {
}
},
rules: {
'vue/no-deprecated-dollar-listeners-api': 'warn',
'vue/no-deprecated-events-api': 'warn',
'vue/no-v-for-template-key': 'off',
'vue/no-v-for-template-key-on-child': 'error',
'prettier/prettier': 'error',

20
.github/release.yml vendored
View File

@@ -1,20 +0,0 @@
changelog:
categories:
- title: 🏕 Features
labels:
- type:feature
- title: 🎉 Enhancements
labels:
- type:enhancement
exclude:
labels:
- type:feature
- title: 🐛 Bug Fixes
labels:
- type:bug
- title: 🔧 Maintenance
labels:
- type:maintenance
- title: 👒 Dependencies
labels:
- dependencies

View File

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

View File

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

View File

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

View File

@@ -33,16 +33,6 @@ const projectRootDir = path.resolve(__dirname, '..');
/** @type {import('webpack').Configuration} */
const config = {
context: projectRootDir,
devServer: {
client: {
progress: true,
overlay: {
// Disable overlay for runtime errors.
// See: https://github.com/webpack/webpack-dev-server/issues/4771
runtimeErrors: false
}
}
},
entry: {
openmct: './openmct.js',
generatorWorker: './example/generator/generatorWorker.js',
@@ -110,12 +100,6 @@ const config = {
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[name].css'
}),
// Add a UTF-8 BOM to CSS output to avoid random mojibake
new webpack.BannerPlugin({
test: /.*Theme\.css$/,
raw: true,
banner: '@charset "UTF-8";',
})
],
module: {
@@ -141,7 +125,6 @@ const config = {
loader: 'vue-loader',
options: {
compilerOptions: {
hoistStatic: false,
whitespace: 'preserve',
compatConfig: {
MODE: 2

View File

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

View File

@@ -1,17 +0,0 @@
version: 2
snapshot:
widths: [1024]
min-height: 1440 # px
percyCSS: |
.t-indicator-clock > .label {
opacity: 0 !important;
}
.c-input--datetime {
opacity: 0 !important;
}
div.c-conductor-axis.c-conductor__ticks > svg {
opacity: 0 !important;
}
div.c-inspector__properties.c-inspect-properties > ul > li:nth-child(3) {
display: none !important;
}

View File

@@ -1,17 +0,0 @@
version: 2
snapshot:
widths: [1024, 2000]
min-height: 1440 # px
percyCSS: |
.t-indicator-clock > .label {
opacity: 0 !important;
}
.c-input--datetime {
opacity: 0 !important;
}
div.c-conductor-axis.c-conductor__ticks > svg {
opacity: 0 !important;
}
div.c-inspector__properties.c-inspect-properties > ul > li:nth-child(3) {
display: none !important;
}

6
e2e/.percy.yml Normal file
View File

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

View File

@@ -72,30 +72,19 @@ Visual Testing is an essential part of our e2e strategy as it ensures that the a
For a better understanding of the visual issues which affect Open MCT, please see our bug tracker with the `label:visual` filter applied [here](https://github.com/nasa/openmct/issues?q=label%3Abug%3Avisual+)
To read about how to write a good visual test, please see [How to write a great Visual Test](#how-to-write-a-great-visual-test).
`npm run test:e2e:visual` commands will run all of the visual tests against a local instance of Open MCT. If no `PERCY_TOKEN` API key is found in the terminal or command line environment variables, no visual comparisons will be made.
`npm run test:e2e:visual` will run all of the visual tests against a local instance of Open MCT. If no `PERCY_TOKEN` API key is found in the terminal or command line environment variables, no visual comparisons will be made.
- `npm run test:e2e:visual:ci` will run against every commit and PR.
- `npm run test:e2e:visual:full` will run every night with additional comparisons made for Larger Displays and with the `snow` theme.
#### Percy.io
To make this possible, we're leveraging a 3rd party service, [Percy](https://percy.io/). This service maintains a copy of all changes, users, scm-metadata, and baselines to verify that the application looks and feels the same _unless approved by a Open MCT developer_. To request a Percy API token, please reach out to the Open MCT Dev team on GitHub. For more information, please see the official [Percy documentation](https://docs.percy.io/docs/visual-testing-basics).
To make this possible, we're leveraging a 3rd party service, [Percy](https://percy.io/). This service maintains a copy of all changes, users, scm-metadata, and baselines to verify that the application looks and feels the same _unless approved by a Open MCT developer_. To request a Percy API token, please reach out to the Open MCT Dev team on GitHub. For more information, please see the official [Percy documentation](https://docs.percy.io/docs/visual-testing-basics)
At present, we are using percy with two configuration files: `./e2e/.percy.nightly.yml` and `./e2e/.percy.ci.yml`. This is mainly to reduce the number of snapshots.
### (Advanced) Snapshot Testing
### Advanced: Snapshot Testing (Not Recommended)
Snapshot testing is very similar to visual testing but allows us to be more precise in detecting change without relying on a 3rd party service. Unfortuantely, this precision requires advanced test setup and teardown and so we're using this pattern as a last resort.
While snapshot testing offers a precise way to detect changes in your application without relying on third-party services like Percy.io, we've found that it doesn't offer any advantages over visual testing in our use-cases. Therefore, snapshot testing is **not recommended** for further implementation.
#### CI vs Manual Checks
Snapshot tests can be reliably executed in Continuous Integration (CI) environments but lack the manual oversight provided by visual testing platforms like Percy.io. This means they may miss issues that a human reviewer could catch during manual checks.
#### Example
A single visual test assertion in Percy.io can be executed across 10 different browser and resolution combinations without additional setup, providing comprehensive testing with minimal configuration. In contrast, a snapshot test is restricted to a single OS and browser resolution, requiring more effort to achieve the same level of coverage.
#### Further Reading
For those interested in the mechanics of snapshot testing with Playwright, you can refer to the [Playwright Snapshots Documentation](https://playwright.dev/docs/test-snapshots). However, keep in mind that we do not recommend using this approach.
To give an example, if a _single_ visual test assertion for an Overlay plot is run through multiple DOM rendering engines at various viewports to see how the Plot looks. If that same test were run as a snapshot test, it could only be executed against a single browser, on a single platform (ubuntu docker container).
Read more about [Playwright Snapshots](https://playwright.dev/docs/test-snapshots)
#### Open MCT's implementation
@@ -134,17 +123,17 @@ npm run test:e2e:updatesnapshots
## Performance Testing
The open source performance tests function in three ways which match their naming and folder structure:
The open source performance tests function mostly as a contract for the locator logic, functionality, and assumptions will work in our downstream, closed source test suites.
`./e2e/tests/performance` - The tests at the root of this folder path detect functional changes which are mostly apparent with large performance regressions like [this](https://github.com/nasa/openmct/issues/6879). These tests run against openmct webpack in `production-mode` with the `npm run test:perf:localhost` script.
`./e2e/tests/performance/contract/` - These tests serve as [contracts](https://martinfowler.com/bliki/ContractTest.html) for the locator logic, functionality, and assumptions will work in our downstream, closed source test suites. These tests run against openmct webpack in `dev-mode` with the `npm run test:perf:contract` script.
`./e2e/tests/performance/memory/` - These tests execute memory leak detection checks in various ways. This is expected to evolve as we move to the `memlab` project. These tests run against openmct webpack in `production-mode` with the `npm run test:perf:memory` script.
They're found under `./e2e/tests/performance` and are to be executed with the following npm script:
`npm run test:perf`
These tests are expected to become blocking and gating with assertions as we extend the capabilities of Playwright.
## Test Architecture and CI
### Architecture
### Architecture (TODO)
### File Structure
@@ -158,11 +147,8 @@ Our file structure follows the type of type of testing being excercised at the e
|`./tests/functional/example/` | Tests which specifically verify the example plugins (e.g.: Sine Wave Generator).|
|`./tests/functional/plugins/` | Tests which loosely test each plugin. This folder is the most likely to change. Note: some `@snapshot` tests are still contained within this structure.|
|`./tests/framework/` | Tests which verify that our testing framework's functionality and assumptions will continue to work based on further refactoring or Playwright version changes (e.g.: verifying custom fixtures and appActions).|
|`./tests/performance/` | Performance tests which should be run on every commit.|
|`./tests/performance/contract/` | A subset of performance tests which are designed to provide a contract between the open source tests which are run on every commit and the downstream tests which are run post merge and with other frameworks.|
|`./tests/performance/memory` | A subset of performance tests which are designed to test for memory leaks.|
|`./tests/performance/` | Performance tests.|
|`./tests/visual/` | Visual tests.|
|`./tests/visual/component/` | Visual tests which are only run against a single component.|
|`./appActions.js` | Contains common methods which can be leveraged by test case authors to quickly move through the application when writing new tests.|
|`./baseFixture.js` | Contains base fixtures which only extend default `@playwright/test` functionality. The expectation is that these fixtures will be removed as the native Playwright API improves|
@@ -179,7 +165,6 @@ Open MCT is leveraging the [config file](https://playwright.dev/docs/test-config
|`./playwright-ci.config.js` | Used when running in CI or to debug CI issues locally|
|`./playwright-local.config.js` | Used when running locally|
|`./playwright-performance.config.js` | Used when running performance tests in CI or locally|
|`./playwright-performance-devmode.config.js` | Used when running performance tests in CI or locally|
|`./playwright-visual.config.js` | Used to run the visual tests in CI or locally|
#### Test Tags
@@ -197,7 +182,6 @@ Current list of test tags:
|`@snapshot` | Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of the playwright container.|
|`@unstable` | A new test or test which is known to be flaky.|
|`@2p` | Indicates that multiple users are involved, or multiple tabs/pages are used. Useful for testing multi-user interactivity.|
|`@generatedata` | Indicates that a test is used to generate testdata or test the generated test data. Usually to be associated with localstorage, but this may grow over time.|
### Continuous Integration
@@ -216,7 +200,6 @@ CircleCI
- Stable e2e tests against ubuntu and chrome
- Performance tests against ubuntu and chrome
- e2e tests are linted
- Visual tests are run in a single resolution on the default `espresso` theme
#### 2. Per-Merge Testing
@@ -224,19 +207,18 @@ Github Actions / Workflow
- Full suite against all browsers/projects. Triggered with Github Label Event 'pr:e2e'
- CouchDB Tests. Triggered on PR Create and again with Github Label Event 'pr:e2e:couchdb'
- Visual Tests. Triggered with Github Label Event 'pr:visual'
#### 3. Scheduled / Batch Testing
Nightly Testing in Circle CI
- Full e2e suite against ubuntu and chrome, firefox, and an MMOC resolution profile
- Full e2e suite against ubuntu and chrome
- Performance tests against ubuntu and chrome
- CouchDB suite
- Visual Tests are run in the full profile
Github Actions / Workflow
- None at the moment
- Visual Test baseline generation.
#### Parallelism and Fast Feedback
@@ -268,7 +250,7 @@ A testcase and testsuite are to be unmarked as @unstable when:
#### **What's supported:**
We are leveraging the `browserslist` project to declare our supported list of browsers. We support macOS, Windows, and ubuntu 20+.
We are leveraging the `browserslist` project to declare our supported list of browsers.
#### **Where it's tested:**
@@ -282,17 +264,11 @@ We also have the need to execute our e2e tests across this published list of bro
- A stable version of Chromium from the official chromium channels. This is always at least 1 version ahead of desktop chrome.
- `playwright-chrome`
- The stable channel of Chrome from the official chrome channels. This is always 2 versions behind chromium.
- `playwright-firefox`
- Firefox Latest Stable. Modified slightly by the playwright team to support a CDP Shim.
In terms of operating system testing, we're only limited by what the CI providers are able to support. The bulk of our testing is performed on the official playwright container which is based on ubuntu. Github Actions allows us to use `windows-latest` and `mac-latest` and is run as needed.
#### **Mobile**
We have the Mission-need to support iPad. To run our iPad suite, please see our `playwright-*.config.js` with the 'iPad' project.
In general, our test suite is not designed to run against mobile devices as the mobile experience is a focused version of the application. Core functionality is missing (chiefly the 'Create' button) and so this will likely turn into a separate suite.
#### **Skipping or executing tests based on browser, os, and/os browser version:**
Conditionally skipping tests based on browser (**RECOMMENDED**):
@@ -319,27 +295,14 @@ Skipping based on browser version (Rarely used): <https://github.com/microsoft/p
## Test Design, Best Practices, and Tips & Tricks
### Test Design
### Test Design (TODO)
#### Test as the User
- How to make tests robust to function in other contexts (VISTA, VIPER, etc.)
- Leverage the use of `appActions.js` methods such as `createDomainObjectWithDefaults()`
- How to make tests faster and more resilient
- When possible, navigate directly by URL:
In general, strive to test only through the UI as a user would. As stated in the [Playwright Best Practices](https://playwright.dev/docs/best-practices#test-user-visible-behavior):
> "Automated tests should verify that the application code works for the end users, and avoid relying on implementation details such as things which users will not typically use, see, or even know about such as the name of a function, whether something is an array, or the CSS class of some element. The end user will see or interact with what is rendered on the page, so your test should typically only see/interact with the same rendered output."
By adhering to this principle, we can create tests that are both robust and reflective of actual user experiences.
#### How to make tests robust to function in other contexts (VISTA, COUCHDB, YAMCS, VIPER, etc.)
1. Leverage the use of `appActions.js` methods such as `createDomainObjectWithDefaults()`. This ensures that your tests will create unique instances of objects for your test to interact with.
1. Do not assert on the order or structure of objects available unless you created them yourself. These tests may be used against a persistent datastore like couchdb with many objects in the tree.
1. Do not search for your created objects. Open MCT does not performance uniqueness checks so it's possible that your tests will break when run twice.
1. Avoid creating locator aliases. This likely means that you're compensating for a bad locator. Improve the application instead.
1. Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });` instead of `{ waitUntil: 'networkidle' }`. Tests run against deployments with websockets often have issues with the networkidle detection.
#### How to make tests faster and more resilient
1. Avoid app interaction when possible. The best way of doing this is to navigate directly by URL:
```js
```javascript
// You can capture the CreatedObjectInfo returned from this appAction:
const clock = await createDomainObjectWithDefaults(page, { type: 'Clock' });
@@ -347,14 +310,12 @@ By adhering to this principle, we can create tests that are both robust and refl
await page.goto(clock.url);
```
1. Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });`
- Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });`
- 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.
- Avoid repeated setup to test to test a single assertion. Write longer tests with multiple soft assertions.
### How to write a great test
### How to write a great test (WIP)
- Avoid using css locators to find elements to the page. Use modern web accessible locators like `getByRole`
- Use our [App Actions](./appActions.js) for performing common actions whenever applicable.
- Use `waitForPlotsToRender()` before asserting against anything that is dependent upon plot series data being loaded and drawn.
- If you create an object outside of using the `createDomainObjectWithDefaults` App Action, make sure to fill in the 'Notes' section of your object with `page.testNotes`:
@@ -367,29 +328,7 @@ By adhering to this principle, we can create tests that are both robust and refl
await notesInput.fill(testNotes);
```
#### How to Write a Great Visual Test
1. **Look for the Unknown Unknowns**: Avoid asserting on specific differences in the visual diff. Visual tests are most effective for identifying unknown unknowns.
2. **Get the App into Interesting States**: Prioritize getting Open MCT into unusual layouts or behaviors before capturing a visual snapshot. For instance, you could open a dropdown menu.
3. **Expect the Unexpected**: Use functional expect statements only to verify assumptions about the state between steps. A great visual test doesn't fail during the test itself, but rather when changes are reviewed in Percy.io.
4. **Control Variability**: Account for variations inherent in working with time-based telemetry and clocks.
- Utilize `percyCSS` to ignore time-based elements. For more details, consult our [percyCSS file](./.percy.ci.yml).
- Use Open MCT's fixed-time mode.
- Employ the `createExampleTelemetryObject` appAction to source telemetry and specify a `name` to avoid autogenerated names.
5. **Hide the Tree and Inspector**: Generally, your test will not require comparisons involving the tree and inspector. These aspects are covered in component-specific tests (explained below). To exclude them from the comparison by default, navigate to the root of the main view with the tree and inspector hidden:
- `await page.goto('./#/browse/mine?hideTree=true&hideInspector=true')`
6. **Component-Specific Tests**: If you wish to focus on a particular component, use the `/visual/component/` folder and limit the scope of the comparison to that component. For instance:
```js
await percySnapshot(page, `Tree Pane w/ single level expanded (theme: ${theme})`, {
scope: treePane
});
```
- Note: The `scope` variable can be any valid CSS selector.
#### How to write a great visual test (TODO)
#### How to write a great network test
@@ -406,35 +345,12 @@ For now, our best practices exist as self-tested, living documentation in our [e
For best practices with regards to mocking network responses, see our [couchdb.e2e.spec.js](./tests/functional/couchdb.e2e.spec.js) file.
### Tips & Tricks
### Tips & Tricks (TODO)
The following contains a list of tips and tricks which don't exactly fit into a FAQ or Best Practices doc.
- (Advanced) Overriding the Browser's Clock
It is possible to override the browser's clock in order to control time-based elements. Since this can cause unwanted behavior (i.e. Tree not rendering), only use this sparingly. To do this, use the `overrideClock` fixture as such:
```js
const { test, expect } = require('../../pluginFixtures.js');
test.describe('foo test suite', () => {
// All subsequent tests in this suite will override the clock
test.use({
clockOptions: {
now: 1732413600000, // A timestamp given as milliseconds since the epoch
shouldAdvanceTime: true // Should the clock tick?
}
});
test('bar test', async ({ page }) => {
// ...
});
});
```
More info and options for `overrideClock` can be found in [baseFixtures.js](baseFixtures.js)
- Working with multiple pages
There are instances where multiple browser pages will needed to verify multi-page or multi-tab application behavior. Make sure to use the `@2p` annotation as well as name each page appropriately: i.e. `page1` and `page2` or `tab1` and `tab2` depending on the intended use case. Generally pages should be used unless testing `sharedWorker` code, specifically.
There are instances where multiple browser pages will need to be opened to verify multi-page or multi-tab application behavior.
### Reporting

View File

@@ -35,7 +35,6 @@
* @property {string} type the type of domain object to create (e.g.: "Sine Wave Generator").
* @property {string} [name] the desired name of the created domain object.
* @property {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the Identifier or uuid of the parent object.
* @property {Object<string, string>} [customParameters] any additional parameters to be passed to the domain object's form. E.g. '[aria-label="Data Rate (hz)"]': {'0.1'}
*/
/**
@@ -66,10 +65,7 @@ const { expect } = require('@playwright/test');
* @param {CreateObjectOptions} options
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
*/
async function createDomainObjectWithDefaults(
page,
{ type, name, parent = 'mine', customParameters = {} }
) {
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) {
if (!name) {
name = `${type}:${genUuid()}`;
}
@@ -78,7 +74,7 @@ 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}?hideTree=true`);
//Click the Create button
await page.click('button:has-text("Create")');
@@ -98,13 +94,6 @@ async function createDomainObjectWithDefaults(
await notesInput.fill(page.testNotes);
}
// If there are any further parameters, fill them in
for (const [key, value] of Object.entries(customParameters)) {
const input = page.locator(`form[name="mctForm"] ${key}`);
await input.fill('');
await input.fill(value);
}
// Click OK button and wait for Navigate event
await Promise.all([
page.waitForLoadState(),
@@ -179,7 +168,7 @@ async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
// Navigate to the parent object. This is necessary to create the object
// in the correct location, such as a folder, layout, or plot.
await page.goto(`${parentUrl}`);
await page.goto(`${parentUrl}?hideTree=true`);
// Click the Create button
await page.click('button:has-text("Create")');
@@ -188,7 +177,7 @@ async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
await page.click(`li:text("Plan")`);
// Modify the name input field of the domain object to accept 'name'
const nameInput = page.getByLabel('Title', { exact: true });
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
await nameInput.fill('');
await nameInput.fill(name);
@@ -219,64 +208,6 @@ async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
};
}
/**
* Create a standardized Telemetry Object (Sine Wave Generator) for use in visual tests
* and tests against plotting telemetry (e.g. logPlot tests).
* @param {import('@playwright/test').Page} page
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the uuid or identifier of the parent object. Defaults to 'mine'
* @returns {Promise<CreatedObjectInfo>} An object containing information about the telemetry object.
*/
async function createExampleTelemetryObject(page, parent = 'mine') {
const parentUrl = await getHashUrlToDomainObject(page, parent);
// TODO: Make this field even more accessible
const name = 'VIPER Rover Heading';
const nameInputLocator = page.getByRole('dialog').locator('input[type="text"]');
await page.goto(`${parentUrl}`);
await page.locator('button:has-text("Create")').click();
await page.locator('li:has-text("Sine Wave Generator")').click();
await nameInputLocator.fill(name);
// Fill out the fields with default values
await page.getByRole('spinbutton', { name: 'Period' }).fill('10');
await page.getByRole('spinbutton', { name: 'Amplitude' }).fill('1');
await page.getByRole('spinbutton', { name: 'Offset' }).fill('0');
await page.getByRole('spinbutton', { name: 'Data Rate (hz)' }).fill('1');
await page.getByRole('spinbutton', { name: 'Phase (radians)' }).fill('0');
await page.getByRole('spinbutton', { name: 'Randomness' }).fill('0');
await page.getByRole('spinbutton', { name: 'Loading Delay (ms)' }).fill('0');
await page.getByRole('button', { name: 'Save' }).click();
// Wait until the URL is updated
await page.waitForURL(`**/${parent}/*`);
const uuid = await getFocusedObjectUuid(page);
const url = await getHashUrlToDomainObject(page, uuid);
return {
name,
uuid,
url
};
}
/**
* Navigates directly to a given object url, in fixed time mode, with the given start and end bounds.
* @param {import('@playwright/test').Page} page
* @param {string} url The url to the domainObject
* @param {string | number} start The starting time bound in milliseconds since epoch
* @param {string | number} end The ending time bound in milliseconds since epoch
*/
async function navigateToObjectWithFixedTimeBounds(page, url, start, end) {
await page.goto(
`${url}?tc.mode=fixed&tc.timeSystem=utc&tc.startBound=${start}&tc.endBound=${end}`
);
}
/**
* Open the given `domainObject`'s context menu from the object tree.
* Expands the path to the object and scrolls to it if necessary.
@@ -340,13 +271,13 @@ async function getFocusedObjectUuid(page) {
* URLs returned will be of the form `'./browse/#/mine/<uuid0>/<uuid1>/...'`
*
* @param {import('@playwright/test').Page} page
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} identifier the uuid or identifier of the object to get the url for
* @param {string} uuid the uuid of the object to get the url for
* @returns {Promise<string>} the url of the object
*/
async function getHashUrlToDomainObject(page, identifier) {
await page.waitForLoadState('load');
const hashUrl = await page.evaluate(async (objectIdentifier) => {
const path = await window.openmct.objects.getOriginalPath(objectIdentifier);
async function getHashUrlToDomainObject(page, uuid) {
await page.waitForLoadState('load'); //Add some determinism
const hashUrl = await page.evaluate(async (objectUuid) => {
const path = await window.openmct.objects.getOriginalPath(objectUuid);
let url =
'./#/browse/' +
[...path]
@@ -360,7 +291,7 @@ async function getHashUrlToDomainObject(page, identifier) {
}
return url;
}, identifier);
}, uuid);
return hashUrl;
}
@@ -369,7 +300,6 @@ async function getHashUrlToDomainObject(page, identifier) {
* Utilizes the OpenMCT API to detect if the UI is in Edit mode.
* @private
* @param {import('@playwright/test').Page} page
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} identifier
* @return {Promise<boolean>} true if the Open MCT is in Edit Mode
*/
async function _isInEditMode(page, identifier) {
@@ -384,13 +314,15 @@ async function _isInEditMode(page, identifier) {
*/
async function setTimeConductorMode(page, isFixedTimespan = true) {
// Click 'mode' button
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
await page.getByRole('button', { name: 'Time Conductor Mode Menu' }).click();
const timeConductorMode = await page.locator('.c-compact-tc');
await timeConductorMode.click();
await timeConductorMode.locator('.js-mode-button').click();
// Switch time conductor mode
if (isFixedTimespan) {
await page.getByRole('menuitem', { name: /Fixed Timespan/ }).click();
await page.locator('data-testid=conductor-modeOption-fixed').click();
} else {
await page.getByRole('menuitem', { name: /Real-Time/ }).click();
await page.locator('data-testid=conductor-modeOption-realtime').click();
}
}
@@ -412,12 +344,9 @@ async function setRealTimeMode(page) {
/**
* @typedef {Object} OffsetValues
* @property {string | undefined} startHours
* @property {string | undefined} startMins
* @property {string | undefined} startSecs
* @property {string | undefined} endHours
* @property {string | undefined} endMins
* @property {string | undefined} endSecs
* @property {string | undefined} hours
* @property {string | undefined} mins
* @property {string | undefined} secs
*/
/**
@@ -426,32 +355,19 @@ async function setRealTimeMode(page) {
* @param {OffsetValues} offset
* @param {import('@playwright/test').Locator} offsetButton
*/
async function setTimeConductorOffset(
page,
{ startHours, startMins, startSecs, endHours, endMins, endSecs }
) {
if (startHours) {
await page.getByRole('spinbutton', { name: 'Start offset hours' }).fill(startHours);
async function setTimeConductorOffset(page, { hours, mins, secs }) {
// await offsetButton.click();
if (hours) {
await page.fill('.pr-time-input__hrs', hours);
}
if (startMins) {
await page.getByRole('spinbutton', { name: 'Start offset minutes' }).fill(startMins);
if (mins) {
await page.fill('.pr-time-input__mins', mins);
}
if (startSecs) {
await page.getByRole('spinbutton', { name: 'Start offset seconds' }).fill(startSecs);
}
if (endHours) {
await page.getByRole('spinbutton', { name: 'End offset hours' }).fill(endHours);
}
if (endMins) {
await page.getByRole('spinbutton', { name: 'End offset minutes' }).fill(endMins);
}
if (endSecs) {
await page.getByRole('spinbutton', { name: 'End offset seconds' }).fill(endSecs);
if (secs) {
await page.fill('.pr-time-input__secs', secs);
}
// Click the check button
@@ -465,7 +381,8 @@ async function setTimeConductorOffset(
*/
async function setStartOffset(page, offset) {
// Click 'mode' button
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
const timeConductorMode = await page.locator('.c-compact-tc');
await timeConductorMode.click();
await setTimeConductorOffset(page, offset);
}
@@ -476,74 +393,11 @@ async function setStartOffset(page, offset) {
*/
async function setEndOffset(page, offset) {
// Click 'mode' button
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
const timeConductorMode = await page.locator('.c-compact-tc');
await timeConductorMode.click();
await setTimeConductorOffset(page, offset);
}
/**
* Set the time conductor bounds in fixed time mode
*
* NOTE: Unless explicitly testing the Time Conductor itself, it is advised to instead
* navigate directly to the object with the desired time bounds using `navigateToObjectWithFixedTimeBounds()`.
* @param {import('@playwright/test').Page} page
* @param {string} startDate
* @param {string} endDate
*/
async function setTimeConductorBounds(page, startDate, endDate) {
// Bring up the time conductor popup
expect(await page.locator('.l-shell__time-conductor.c-compact-tc').count()).toBe(1);
await page.click('.l-shell__time-conductor.c-compact-tc');
await setTimeBounds(page, startDate, endDate);
await page.keyboard.press('Enter');
}
/**
* Set the independent time conductor bounds in fixed time mode
* @param {import('@playwright/test').Page} page
* @param {string} startDate
* @param {string} endDate
*/
async function setIndependentTimeConductorBounds(page, startDate, endDate) {
// Activate Independent Time Conductor in Fixed Time Mode
await page.getByRole('switch').click();
// Bring up the time conductor popup
await page.click('.c-conductor-holder--compact .c-compact-tc');
await expect(page.locator('.itc-popout')).toBeInViewport();
await setTimeBounds(page, startDate, endDate);
await page.keyboard.press('Enter');
}
/**
* Set the bounds of the visible conductor in fixed time mode
* @param {import('@playwright/test').Page} page
* @param {string} startDate
* @param {string} endDate
*/
async function setTimeBounds(page, startDate, endDate) {
if (startDate) {
// Fill start time
await page
.getByRole('textbox', { name: 'Start date' })
.fill(startDate.toString().substring(0, 10));
await page
.getByRole('textbox', { name: 'Start time' })
.fill(startDate.toString().substring(11, 19));
}
if (endDate) {
// Fill end time
await page.getByRole('textbox', { name: 'End date' }).fill(endDate.toString().substring(0, 10));
await page
.getByRole('textbox', { name: 'End time' })
.fill(endDate.toString().substring(11, 19));
}
}
/**
* Selects an inspector tab based on the provided tab name
*
@@ -640,25 +494,9 @@ async function getCanvasPixels(page, canvasSelector) {
return getTelemValuePromise;
}
/**
* @param {import('@playwright/test').Page} page
* @param {string} myItemsFolderName
* @param {string} url
* @param {string} newName
*/
async function renameObjectFromContextMenu(page, url, newName) {
await openObjectTreeContextMenu(page, url);
await page.click('li:text("Edit Properties")');
const nameInput = page.getByLabel('Title', { exact: true });
await nameInput.fill('');
await nameInput.fill(newName);
await page.click('[aria-label="Save"]');
}
// eslint-disable-next-line no-undef
module.exports = {
createDomainObjectWithDefaults,
createExampleTelemetryObject,
createNotification,
createPlanFromJSON,
expandEntireTree,
@@ -666,15 +504,11 @@ module.exports = {
getCanvasPixels,
getHashUrlToDomainObject,
getFocusedObjectUuid,
navigateToObjectWithFixedTimeBounds,
openObjectTreeContextMenu,
setFixedTimeMode,
setRealTimeMode,
setStartOffset,
setEndOffset,
setTimeConductorBounds,
setIndependentTimeConductorBounds,
selectInspectorTab,
waitForPlotsToRender,
renameObjectFromContextMenu
waitForPlotsToRender
};

View File

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

View File

@@ -1,9 +0,0 @@
/**
* Constants which may be used across all e2e tests.
*/
/**
* Time Constants
* - Used for overriding the browser clock in tests.
*/
export const MISSION_TIME = 1732413600000; // Saturday, November 23, 2024 6:00:00 PM GMT-08:00 (Thanksgiving Dinner Time)

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:8080",
"localStorage": [
{
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1689710399654,\"created\":1689710398656,\"persisted\":1689710399654},\"58f55f3a-46d9-4c37-a726-27b5d38b895a\":{\"identifier\":{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"},\"name\":\"Overlay Plot:b0ba67ab-e383-40c1-a181-82b174e8fdf0\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateVisualTestData.e2e.spec.js\\nGenerate Visual Test Data @localStorage\\nchrome\",\"modified\":1689710400878,\"location\":\"mine\",\"created\":1689710399651,\"persisted\":1689710400878},\"19f2e461-190e-4662-8d62-251e90bb7aac\":{\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"identifier\":{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\",\"infinityValues\":false,\"staleness\":false},\"modified\":1689710400433,\"location\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"created\":1689710400433,\"persisted\":1689710400433}}"
},
{
"name": "mct-recent-objects",
"value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"},\"name\":\"Overlay Plot:b0ba67ab-e383-40c1-a181-82b174e8fdf0\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateVisualTestData.e2e.spec.js\\nGenerate Visual Test Data @localStorage\\nchrome\",\"modified\":1689710400435,\"location\":\"mine\",\"created\":1689710399651,\"persisted\":1689710400436},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1689710399654,\"created\":1689710398656,\"persisted\":1689710399654},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"domainObject\":{\"identifier\":{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"},\"name\":\"Overlay Plot:b0ba67ab-e383-40c1-a181-82b174e8fdf0\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateVisualTestData.e2e.spec.js\\nGenerate Visual Test Data @localStorage\\nchrome\",\"modified\":1689710400435,\"location\":\"mine\",\"created\":1689710399651,\"persisted\":1689710400436}},{\"objectPath\":[{\"identifier\":{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"},\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\",\"infinityValues\":false,\"staleness\":false},\"modified\":1689710400433,\"location\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"created\":1689710400433,\"persisted\":1689710400433},{\"identifier\":{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"},\"name\":\"Overlay Plot:b0ba67ab-e383-40c1-a181-82b174e8fdf0\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateVisualTestData.e2e.spec.js\\nGenerate Visual Test Data @localStorage\\nchrome\",\"modified\":1689710400435,\"location\":\"mine\",\"created\":1689710399651,\"persisted\":1689710400436},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1689710399654,\"created\":1689710398656,\"persisted\":1689710399654},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/58f55f3a-46d9-4c37-a726-27b5d38b895a/19f2e461-190e-4662-8d62-251e90bb7aac\",\"domainObject\":{\"identifier\":{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"},\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\",\"infinityValues\":false,\"staleness\":false},\"modified\":1689710400433,\"location\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"created\":1689710400433,\"persisted\":1689710400433}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1689710399654,\"created\":1689710398656,\"persisted\":1689710399654},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine\",\"domainObject\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1689710399654,\"created\":1689710398656,\"persisted\":1689710399654}}]"
},
{
"name": "mct-tree-expanded",
"value": "[]"
}
]
}
]
}

File diff suppressed because one or more lines are too long

View File

@@ -1,26 +0,0 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:8080",
"localStorage": [
{
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"8c863964-4640-4db1-8a98-0e546c3c271d\",\"namespace\":\"\"},{\"key\":\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1678741852556,\"created\":1678741830859,\"persisted\":1678741852557},\"8c863964-4640-4db1-8a98-0e546c3c271d\":{\"identifier\":{\"key\":\"8c863964-4640-4db1-8a98-0e546c3c271d\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with Telemetry Object\\nchrome\",\"modified\":1678741862011,\"location\":\"mine\",\"created\":1678741839461,\"persisted\":1678741862011},\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\":{\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"identifier\":{\"key\":\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"namespace\":\"\"},\"telemetry\":{\"period\":\"1\",\"amplitude\":\"1\",\"offset\":\"0\",\"dataRateInHz\":\"1\",\"phase\":\"0\",\"randomness\":\"0\",\"loadDelay\":\"0\",\"infinityValues\":false,\"staleness\":false},\"modified\":1678741852553,\"location\":\"mine\",\"created\":1678741852553,\"persisted\":1678741852553}}"
},
{
"name": "mct-tree-expanded",
"value": "[]"
},
{
"name": "tcHistory",
"value": "{\"utc\":[{\"start\":1678740030748,\"end\":1678741830748}]}"
},
{
"name": "mct-recent-objects",
"value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"8c863964-4640-4db1-8a98-0e546c3c271d\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with Telemetry Object\\nchrome\",\"modified\":1678741860389,\"location\":\"mine\",\"created\":1678741839461,\"persisted\":1678741860389},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"8c863964-4640-4db1-8a98-0e546c3c271d\",\"namespace\":\"\"},{\"key\":\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1678741852556,\"created\":1678741830859,\"persisted\":1678741852557},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/8c863964-4640-4db1-8a98-0e546c3c271d\",\"domainObject\":{\"identifier\":{\"key\":\"8c863964-4640-4db1-8a98-0e546c3c271d\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with Telemetry Object\\nchrome\",\"modified\":1678741860389,\"location\":\"mine\",\"created\":1678741839461,\"persisted\":1678741860389}},{\"objectPath\":[{\"identifier\":{\"key\":\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"namespace\":\"\"},\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"telemetry\":{\"period\":\"1\",\"amplitude\":\"1\",\"offset\":\"0\",\"dataRateInHz\":\"1\",\"phase\":\"0\",\"randomness\":\"0\",\"loadDelay\":\"0\",\"infinityValues\":false,\"staleness\":false},\"modified\":1678741852553,\"location\":\"mine\",\"created\":1678741852553,\"persisted\":1678741852553},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"8c863964-4640-4db1-8a98-0e546c3c271d\",\"namespace\":\"\"},{\"key\":\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1678741852556,\"created\":1678741830859,\"persisted\":1678741852557},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"domainObject\":{\"identifier\":{\"key\":\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"namespace\":\"\"},\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"telemetry\":{\"period\":\"1\",\"amplitude\":\"1\",\"offset\":\"0\",\"dataRateInHz\":\"1\",\"phase\":\"0\",\"randomness\":\"0\",\"loadDelay\":\"0\",\"infinityValues\":false,\"staleness\":false},\"modified\":1678741852553,\"location\":\"mine\",\"created\":1678741852553,\"persisted\":1678741852553}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"8c863964-4640-4db1-8a98-0e546c3c271d\",\"namespace\":\"\"},{\"key\":\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1678741852556,\"created\":1678741830859,\"persisted\":1678741852557},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine\",\"domainObject\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"8c863964-4640-4db1-8a98-0e546c3c271d\",\"namespace\":\"\"},{\"key\":\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1678741852556,\"created\":1678741830859,\"persisted\":1678741852557}}]"
}
]
}
]
}

View File

@@ -1,26 +0,0 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:8080",
"localStorage": [
{
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1678741890986,\"created\":1678741885545,\"persisted\":1678741890987},\"db9fb115-7a72-4c45-81a4-1f6021156b4e\":{\"identifier\":{\"key\":\"db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"4c0b2c16-5f28-4906-abd3-befd16e5a77e\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"4c0b2c16-5f28-4906-abd3-befd16e5a77e\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with 5s Delay\\nchrome\",\"modified\":1678741904378,\"location\":\"mine\",\"created\":1678741890983,\"persisted\":1678741904385},\"4c0b2c16-5f28-4906-abd3-befd16e5a77e\":{\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"identifier\":{\"key\":\"4c0b2c16-5f28-4906-abd3-befd16e5a77e\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\",\"infinityValues\":false,\"staleness\":false},\"modified\":1678741896800,\"location\":\"db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"created\":1678741896800,\"persisted\":1678741896800}}"
},
{
"name": "mct-tree-expanded",
"value": "[]"
},
{
"name": "tcHistory",
"value": "{\"utc\":[{\"start\":1678740085436,\"end\":1678741885436}]}"
},
{
"name": "mct-recent-objects",
"value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"4c0b2c16-5f28-4906-abd3-befd16e5a77e\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with 5s Delay\\nchrome\",\"modified\":1678741896803,\"location\":\"mine\",\"created\":1678741890983,\"persisted\":1678741896803},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1678741890986,\"created\":1678741885545,\"persisted\":1678741890987},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"domainObject\":{\"identifier\":{\"key\":\"db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"4c0b2c16-5f28-4906-abd3-befd16e5a77e\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with 5s Delay\\nchrome\",\"modified\":1678741896803,\"location\":\"mine\",\"created\":1678741890983,\"persisted\":1678741896803}},{\"objectPath\":[{\"identifier\":{\"key\":\"4c0b2c16-5f28-4906-abd3-befd16e5a77e\",\"namespace\":\"\"},\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\",\"infinityValues\":false,\"staleness\":false},\"modified\":1678741896800,\"location\":\"db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"created\":1678741896800,\"persisted\":1678741896800},{\"identifier\":{\"key\":\"db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"4c0b2c16-5f28-4906-abd3-befd16e5a77e\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with 5s Delay\\nchrome\",\"modified\":1678741896803,\"location\":\"mine\",\"created\":1678741890983,\"persisted\":1678741896803},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1678741890986,\"created\":1678741885545,\"persisted\":1678741890987},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/db9fb115-7a72-4c45-81a4-1f6021156b4e/4c0b2c16-5f28-4906-abd3-befd16e5a77e\",\"domainObject\":{\"identifier\":{\"key\":\"4c0b2c16-5f28-4906-abd3-befd16e5a77e\",\"namespace\":\"\"},\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\",\"infinityValues\":false,\"staleness\":false},\"modified\":1678741896800,\"location\":\"db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"created\":1678741896800,\"persisted\":1678741896800}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1678741890986,\"created\":1678741885545,\"persisted\":1678741890987},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine\",\"domainObject\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1678741890986,\"created\":1678741885545,\"persisted\":1678741890987}}]"
}
]
}
]
}

View File

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

View File

@@ -1,260 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* global __dirname */
/**
* This test suite is dedicated to generating LocalStorage via Session Storage to be used
* in some visual test suites like controlledClock.visual.spec.js. This suite should run to completion
* and generate an artifact in ./e2e/test-data/<name>_storage.json . This will run
* on every commit to ensure that this object still loads into tests correctly and will retain the
* *.e2e.spec.js suffix.
*
* TODO: Provide additional validation of object properties as it grows.
* Verification of object properties happens in this file before the test-data is generated,
* and is additionally verified in the validation test suites below.
*/
const { test, expect } = require('../../pluginFixtures.js');
const {
createDomainObjectWithDefaults,
createExampleTelemetryObject,
selectInspectorTab
} = require('../../appActions.js');
const { MISSION_TIME } = require('../../constants.js');
const path = require('path');
const overlayPlotName = 'Overlay Plot with Telemetry Object';
test.describe('Generate Visual Test Data @localStorage @generatedata', () => {
test.use({
clockOptions: {
now: MISSION_TIME,
shouldAdvanceTime: true
}
});
test.beforeEach(async ({ page }) => {
// Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
// TODO: Visual test for the generated object here
// - Move to using appActions to create the overlay plot
// and embedded standard telemetry object
test('Generate Overlay Plot with Telemetry Object', async ({ page, context }) => {
// Create Overlay Plot
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
name: overlayPlotName
});
// Create Telemetry Object
const exampleTelemetry = await createExampleTelemetryObject(page);
// Make Link from Telemetry Object to Overlay Plot
await page.locator('button[title="More options"]').click();
// Select 'Create Link' from dropdown
await page.getByRole('menuitem', { name: ' Create Link' }).click();
// Search and Select for overlay Plot within Create Modal
await page.getByRole('dialog').getByRole('searchbox', { name: 'Search Input' }).click();
await page
.getByRole('dialog')
.getByRole('searchbox', { name: 'Search Input' })
.fill(overlayPlot.name);
await page
.getByRole('treeitem', { name: new RegExp(overlayPlot.name) })
.locator('a')
.click();
await page.getByRole('button', { name: 'Save' }).click();
await page.goto(overlayPlot.url);
// TODO: Flesh Out Assertions against created Objects
await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlotName);
await selectInspectorTab(page, 'Config');
await page
.getByRole('list', { name: 'Plot Series Properties' })
.locator('span')
.first()
.click();
// TODO: Modify the Overlay Plot to use fixed Scaling
// TODO: Verify Autoscaling.
// TODO: Fix accessibility of Plot Series Properties tables
// Assert that the Plot Series properties have the correct values
await expect(
page.locator('[role=cell]:has-text("Value")~[role=cell]:has-text("sin")')
).toBeVisible();
await expect(
page.locator(
'[role=cell]:has-text("Line Method")~[role=cell]:has-text("Linear interpolation")'
)
).toBeVisible();
await expect(
page.locator('[role=cell]:has-text("Markers")~[role=cell]:has-text("Point: 2px")')
).toBeVisible();
await expect(
page.locator('[role=cell]:has-text("Alarm Markers")~[role=cell]:has-text("Enabled")')
).toBeVisible();
await expect(
page.locator('[role=cell]:has-text("Limit Lines")~[role=cell]:has-text("Disabled")')
).toBeVisible();
await page.goto(exampleTelemetry.url);
await selectInspectorTab(page, 'Properties');
// TODO: assert Example Telemetry property values
// await page.goto(exampleTelemetry.url);
// Save localStorage for future test execution
await context.storageState({
path: path.join(__dirname, '../../../e2e/test-data/overlay_plot_storage.json')
});
});
// TODO: Merge this with previous test. Edit object created in previous test.
test('Generate Overlay Plot with 5s Delay', async ({ page, context }) => {
// add overlay plot with defaults
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
name: 'Overlay Plot with 5s Delay'
});
const swgWith5sDelay = await createExampleTelemetryObject(page, overlayPlot.uuid);
await page.goto(swgWith5sDelay.url);
await page.getByTitle('More options').click();
await page.getByRole('menuitem', { name: ' Edit Properties...' }).click();
//Edit Example Telemetry Object to include 5s loading Delay
await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000');
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
// focus the overlay plot
await page.goto(overlayPlot.url);
await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlot.name);
// Clear Recently Viewed
await page.getByRole('button', { name: 'Clear Recently Viewed' }).click();
await page.getByRole('button', { name: 'OK' }).click();
//Save localStorage for future test execution
await context.storageState({
path: path.join(__dirname, '../../../e2e/test-data/overlay_plot_with_delay_storage.json')
});
});
});
test.describe('Validate Overlay Plot with Telemetry Object @localStorage @generatedata', () => {
test.use({
storageState: path.join(__dirname, '../../../e2e/test-data/overlay_plot_storage.json')
});
test('Validate Overlay Plot with Telemetry Object', async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.locator('a').filter({ hasText: overlayPlotName }).click();
// TODO: Flesh Out Assertions against created Objects
await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlotName);
await selectInspectorTab(page, 'Config');
await page
.getByRole('list', { name: 'Plot Series Properties' })
.locator('span')
.first()
.click();
// TODO: Modify the Overlay Plot to use fixed Scaling
// TODO: Verify Autoscaling.
// TODO: Fix accessibility of Plot Series Properties tables
// Assert that the Plot Series properties have the correct values
await expect(
page.locator('[role=cell]:has-text("Value")~[role=cell]:has-text("sin")')
).toBeVisible();
await expect(
page.locator(
'[role=cell]:has-text("Line Method")~[role=cell]:has-text("Linear interpolation")'
)
).toBeVisible();
await expect(
page.locator('[role=cell]:has-text("Markers")~[role=cell]:has-text("Point: 2px")')
).toBeVisible();
await expect(
page.locator('[role=cell]:has-text("Alarm Markers")~[role=cell]:has-text("Enabled")')
).toBeVisible();
await expect(
page.locator('[role=cell]:has-text("Limit Lines")~[role=cell]:has-text("Disabled")')
).toBeVisible();
});
});
test.describe('Validate Overlay Plot with 5s Delay Telemetry Object @localStorage @generatedata', () => {
test.use({
storageState: path.join(
__dirname,
'../../../e2e/test-data/overlay_plot_with_delay_storage.json'
)
});
test('Validate Overlay Plot with Telemetry Object', async ({ page }) => {
const plotName = 'Overlay Plot with 5s Delay';
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.locator('a').filter({ hasText: plotName }).click();
// TODO: Flesh Out Assertions against created Objects
await expect(page.locator('.l-browse-bar__object-name')).toContainText(plotName);
await selectInspectorTab(page, 'Config');
await page
.getByRole('list', { name: 'Plot Series Properties' })
.locator('span')
.first()
.click();
// TODO: Modify the Overlay Plot to use fixed Scaling
// TODO: Verify Autoscaling.
// TODO: Fix accessibility of Plot Series Properties tables
// Assert that the Plot Series properties have the correct values
await expect(
page.locator('[role=cell]:has-text("Value")~[role=cell]:has-text("sin")')
).toBeVisible();
await expect(
page.locator(
'[role=cell]:has-text("Line Method")~[role=cell]:has-text("Linear interpolation")'
)
).toBeVisible();
await expect(
page.locator('[role=cell]:has-text("Markers")~[role=cell]:has-text("Point: 2px")')
).toBeVisible();
await expect(
page.locator('[role=cell]:has-text("Alarm Markers")~[role=cell]:has-text("Enabled")')
).toBeVisible();
await expect(
page.locator('[role=cell]:has-text("Limit Lines")~[role=cell]:has-text("Disabled")')
).toBeVisible();
});
});

View File

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

View File

@@ -0,0 +1,35 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
* This test suite template is to be used when verifying Test Data files found in /e2e/test-data/
*/
const { test } = require('../../baseFixtures');
test.describe('recycled_local_storage @localStorage', () => {
//We may want to do some additional level of verification of this file. For now, we just verify that it exists and can be used in a test suite.
test.use({ storageState: './e2e/test-data/recycled_local_storage.json' });
test('Can use recycled_local_storage file', async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
});

View File

@@ -28,10 +28,10 @@ const { createDomainObjectWithDefaults, createNotification } = require('../../ap
const { test, expect } = require('../../pluginFixtures');
test.describe('Notifications List', () => {
test.fixme('Notifications can be dismissed individually', async ({ page }) => {
test('Notifications can be dismissed individually', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6820'
description: 'https://github.com/nasa/openmct/issues/6122'
});
// Go to baseURL

View File

@@ -98,8 +98,6 @@ test.describe('Time List', () => {
const startBound = testPlan.TEST_GROUP[0].start;
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
await page.goto(timelist.url);
// Switch to fixed time mode with all plan events within the bounds
await page.goto(
`${timelist.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=timelist.view`
@@ -112,7 +110,7 @@ test.describe('Time List', () => {
await test.step('Does not show milliseconds in times', async () => {
// Get the first activity
const row = page.locator('.js-list-item').first();
const row = await page.locator('.js-list-item').first();
// Verify that none fo the times have milliseconds displayed.
// Example: 2024-11-17T16:00:00Z is correct and 2024-11-17T16:00:00.000Z is wrong

View File

@@ -21,11 +21,7 @@
*****************************************************************************/
const { test, expect } = require('../../../pluginFixtures');
const {
createDomainObjectWithDefaults,
createPlanFromJSON,
setIndependentTimeConductorBounds
} = require('../../../appActions');
const { createDomainObjectWithDefaults, createPlanFromJSON } = require('../../../appActions');
const testPlan = {
TEST_GROUP: [
@@ -82,6 +78,9 @@ test.describe('Time Strip', () => {
});
// Constant locators
const independentTimeConductorInputs = page.locator(
'.l-shell__main-independent-time-conductor .c-input--datetime'
);
const activityBounds = page.locator('.activity-bounds');
// Goto baseURL
@@ -123,7 +122,9 @@ test.describe('Time Strip', () => {
});
await test.step('TimeStrip can use the Independent Time Conductor', async () => {
expect(await activityBounds.count()).toEqual(5);
// Activate Independent Time Conductor in Fixed Time Mode
await page.click('.c-toggle-switch__slider');
expect(await activityBounds.count()).toEqual(0);
// Set the independent time bounds so that only one event is shown
const startBound = testPlan.TEST_GROUP[0].start;
@@ -131,7 +132,12 @@ test.describe('Time Strip', () => {
const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
await setIndependentTimeConductorBounds(page, startBoundString, endBoundString);
await independentTimeConductorInputs.nth(0).fill('');
await independentTimeConductorInputs.nth(0).fill(startBoundString);
await page.keyboard.press('Enter');
await independentTimeConductorInputs.nth(1).fill('');
await independentTimeConductorInputs.nth(1).fill(endBoundString);
await page.keyboard.press('Enter');
expect(await activityBounds.count()).toEqual(1);
});
@@ -150,6 +156,9 @@ test.describe('Time Strip', () => {
await page.click("button[title='Save']");
await page.click("li[title='Save and Finish Editing']");
// Activate Independent Time Conductor in Fixed Time Mode
await page.click('.c-toggle-switch__slider');
// All events should be displayed at this point because the
// initial independent context bounds will match the global bounds
expect(await activityBounds.count()).toEqual(5);
@@ -160,7 +169,12 @@ test.describe('Time Strip', () => {
const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
await setIndependentTimeConductorBounds(page, startBoundString, endBoundString);
await independentTimeConductorInputs.nth(0).fill('');
await independentTimeConductorInputs.nth(0).fill(startBoundString);
await page.keyboard.press('Enter');
await independentTimeConductorInputs.nth(1).fill('');
await independentTimeConductorInputs.nth(1).fill(endBoundString);
await page.keyboard.press('Enter');
// Verify that two events are displayed
expect(await activityBounds.count()).toEqual(2);

View File

@@ -41,7 +41,7 @@ test.describe('Clock Generator CRUD Operations', () => {
await page.click('button:has-text("Create")');
// Click Clock
await page.getByRole('menuitem').first().click();
await page.click('text=Clock');
// Click .icon-arrow-down
await page.locator('.icon-arrow-down').click();

View File

@@ -25,8 +25,7 @@ const {
createDomainObjectWithDefaults,
setStartOffset,
setFixedTimeMode,
setRealTimeMode,
setIndependentTimeConductorBounds
setRealTimeMode
} = require('../../../../appActions');
test.describe('Display Layout', () => {
@@ -232,27 +231,20 @@ test.describe('Display Layout', () => {
let layoutGridHolder = page.locator('.l-layout__grid-holder');
await exampleImageryTreeItem.dragTo(layoutGridHolder);
//adjust so that we can see the independent time conductor toggle
// Adjust object height
await page.locator('div[title="Resize object height"] > input').click();
await page.locator('div[title="Resize object height"] > input').fill('70');
// Adjust object width
await page.locator('div[title="Resize object width"] > input').click();
await page.locator('div[title="Resize object width"] > input').fill('70');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
const startDate = '2021-12-30 01:01:00.000Z';
const endDate = '2021-12-30 01:11:00.000Z';
await setIndependentTimeConductorBounds(page, startDate, endDate);
// flip on independent time conductor
await page.getByTitle('Enable independent Time Conductor').first().locator('label').click();
await page.getByRole('textbox').nth(1).fill('2021-12-30 01:11:00.000Z');
await page.getByRole('textbox').nth(0).fill('2021-12-30 01:01:00.000Z');
await page.getByRole('textbox').nth(1).click();
// check image date
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
// flip it off
await page.getByRole('switch').click();
await page.getByTitle('Disable independent Time Conductor').first().locator('label').click();
// timestamp shouldn't be in the past anymore
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
});
@@ -260,7 +252,6 @@ test.describe('Display Layout', () => {
test('When multiple plots are contained in a layout, we only ask for annotations once @couchdb', async ({
page
}) => {
await setFixedTimeMode(page);
// Create another Sine Wave Generator
const anotherSineWaveObject = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator'
@@ -317,20 +308,10 @@ test.describe('Display Layout', () => {
// wait for annotations requests to be batched and requested
await page.waitForLoadState('networkidle');
// Network requests for the composite telemetry with multiple items should be:
// 1. a single batched request for annotations
expect(networkRequests.length).toBe(1);
await setRealTimeMode(page);
networkRequests = [];
await page.reload();
// wait for annotations to not load (if we have any, we've got a problem)
await page.waitForLoadState('networkidle');
// In real time mode, we don't fetch annotations at all
expect(networkRequests.length).toBe(0);
});
});

View File

@@ -21,18 +21,11 @@
*****************************************************************************/
const { test, expect } = require('../../../../pluginFixtures');
const {
createDomainObjectWithDefaults,
setIndependentTimeConductorBounds
} = require('../../../../appActions');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
test.describe('Flexible Layout', () => {
let sineWaveObject;
let clockObject;
let treePane;
let sineWaveGeneratorTreeItem;
let clockTreeItem;
let flexibleLayout;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
@@ -45,27 +38,23 @@ test.describe('Flexible Layout', () => {
clockObject = await createDomainObjectWithDefaults(page, {
type: 'Clock'
});
// Create a Flexible Layout
flexibleLayout = await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout'
});
// Define the Sine Wave Generator and Clock tree items
treePane = page.getByRole('tree', {
name: 'Main Tree'
});
sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
clockTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(clockObject.name)
});
});
test('panes have the appropriate draggable attribute while in Edit and Browse modes', async ({
page
}) => {
await page.goto(flexibleLayout.url);
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
const clockTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(clockObject.name)
});
// Create a Flexible Layout
await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout'
});
// Edit Flexible Layout
await page.locator('[title="Edit"]').click();
@@ -86,79 +75,19 @@ test.describe('Flexible Layout', () => {
dragWrapper = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
await expect(dragWrapper).toHaveAttribute('draggable', 'false');
});
test('changing toolbar settings in edit mode is immediately reflected and persists upon save', async ({
page
}) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6942'
});
await page.goto(flexibleLayout.url);
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
// Add the Sine Wave Generator and Clock to the Flexible Layout
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await clockTreeItem.dragTo(page.locator('.c-fl__container.is-empty'));
// Click on the first frame to select it
await page.locator('.c-fl-container__frame').first().click();
await expect(page.locator('.c-fl-container__frame > .c-frame').first()).toHaveAttribute(
's-selected',
''
);
// Assert the toolbar is visible
await expect(page.locator('.c-toolbar')).toBeInViewport();
// Assert the layout is in columns orientation
expect(await page.locator('.c-fl--rows').count()).toEqual(0);
// Change the layout to rows orientation
await page.getByTitle('Columns layout').click();
// Assert the layout is in rows orientation
expect(await page.locator('.c-fl--rows').count()).toBeGreaterThan(0);
// Assert the frame of the first item is visible
await expect(page.locator('.c-so-view').first()).not.toHaveClass(/c-so-view--no-frame/);
// Hide the frame of the first item
await page.getByTitle('Frame visible').click();
// Assert the frame is hidden
await expect(page.locator('.c-so-view').first()).toHaveClass(/c-so-view--no-frame/);
// Assert there are 2 containers
expect(await page.locator('.c-fl-container').count()).toEqual(2);
// Add a container
await page.getByTitle('Add Container').click();
// Assert there are 3 containers
expect(await page.locator('.c-fl-container').count()).toEqual(3);
// Save Flexible Layout
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Nav away and back
await page.goto(sineWaveObject.url);
await page.goto(flexibleLayout.url);
// Wait for the first frame to be visible so we know the layout has loaded
await expect(page.locator('.c-fl-container').nth(0)).toBeInViewport();
// Assert the settings have persisted
expect(await page.locator('.c-fl-container').count()).toEqual(3);
expect(await page.locator('.c-fl--rows').count()).toBeGreaterThan(0);
await expect(page.locator('.c-so-view').first()).toHaveClass(/c-so-view--no-frame/);
});
test('items in a flexible layout can be removed with object tree context menu when viewing the flexible layout', async ({
page
}) => {
await page.goto(flexibleLayout.url);
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
// Create a Display Layout
await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout'
});
// Edit Flexible Layout
await page.locator('[title="Edit"]').click();
@@ -189,7 +118,17 @@ test.describe('Flexible Layout', () => {
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/3117'
});
await page.goto(flexibleLayout.url);
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
// Create a Flexible Layout
const flexibleLayout = await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout'
});
// Edit Flexible Layout
await page.locator('[title="Edit"]').click();
@@ -225,13 +164,19 @@ test.describe('Flexible Layout', () => {
const exampleImageryObject = await createDomainObjectWithDefaults(page, {
type: 'Example Imagery'
});
await page.goto(flexibleLayout.url);
// Edit Flexible Layout
// Create a Flexible Layout
await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout'
});
// Edit Display Layout
await page.locator('[title="Edit"]').click();
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// Add the Sine Wave Generator to the Flexible Layout and save changes
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const exampleImageryTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(exampleImageryObject.name)
});
@@ -242,17 +187,16 @@ test.describe('Flexible Layout', () => {
await page.locator('text=Save and Finish Editing').click();
// flip on independent time conductor
await setIndependentTimeConductorBounds(
page,
'2021-12-30 01:01:00.000Z',
'2021-12-30 01:11:00.000Z'
);
await page.getByTitle('Enable independent Time Conductor').first().locator('label').click();
await page.getByRole('textbox').nth(1).fill('2021-12-30 01:11:00.000Z');
await page.getByRole('textbox').nth(0).fill('2021-12-30 01:01:00.000Z');
await page.getByRole('textbox').nth(1).click();
// check image date
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
// flip it off
await page.getByRole('switch').click();
await page.getByTitle('Disable independent Time Conductor').first().locator('label').click();
// timestamp shouldn't be in the past anymore
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
});

View File

@@ -27,7 +27,7 @@ but only assume that example imagery is present.
/* globals process */
const { waitForAnimations } = require('../../../../baseFixtures');
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults, setRealTimeMode } = require('../../../../appActions');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const backgroundImageSelector = '.c-imagery__main-image__background-image';
const panHotkey = process.platform === 'linux' ? ['Shift', 'Alt'] : ['Alt'];
const tagHotkey = ['Shift', 'Alt'];
@@ -46,7 +46,6 @@ 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();
});
test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
@@ -72,65 +71,46 @@ test.describe('Example Imagery Object', () => {
});
test('Can use independent time conductor to change time', async ({ page }) => {
test.info().annotations.push({
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();
// Adding in delay to address flakiness of ITC test-- button event handlers not registering in time
await expect(page.locator('#independentTCToggle')).toBeChecked();
await expect(page.locator('.c-compact-tc').first()).toBeVisible();
await page.getByRole('button', { name: 'Independent Time Conductor Settings' }).click();
await page.getByRole('textbox', { name: 'Start date' }).fill('2021-12-30');
await page.keyboard.press('Tab');
await page.getByRole('textbox', { name: 'Start time' }).fill('01:01:00');
await page.keyboard.press('Tab');
await page.getByRole('textbox', { name: 'End date' }).fill('2021-12-30');
await page.keyboard.press('Tab');
await page.getByRole('textbox', { name: 'End time' }).fill('01:11:00');
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');
await page.getByTitle('Enable independent Time Conductor').locator('label').click();
await page.getByRole('textbox').nth(1).fill('2021-12-30 01:11:00.000Z');
await page.getByRole('textbox').nth(0).fill('2021-12-30 01:01:00.000Z');
await page.getByRole('textbox').nth(1).click();
// check image date
await expect(page.getByText('2021-12-30 01:01:00.000Z').first()).toBeVisible();
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
// flip it off
await page.getByRole('switch', { name: 'Disable Independent Time Conductor' }).click();
await page.getByTitle('Disable independent Time Conductor').locator('label').click();
// timestamp shouldn't be in the past anymore
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
// Test independent fixed time with global realtime
await setRealTimeMode(page);
await expect(
page.getByRole('switch', { name: 'Enable Independent Time Conductor' })
).toBeEnabled();
await page.getByRole('switch', { name: 'Enable Independent Time Conductor' }).click();
await page.getByRole('button', { name: /Fixed Timespan/ }).click();
await page.getByTestId('conductor-modeOption-realtime').click();
await page.getByTitle('Enable independent Time Conductor').locator('label').click();
// check image date to be in the past
await expect(page.getByText('2021-12-30 01:01:00.000Z').first()).toBeVisible();
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
// flip it off
await page.getByRole('switch', { name: 'Disable Independent Time Conductor' }).click();
await page.getByTitle('Disable independent Time Conductor').locator('label').click();
// timestamp shouldn't be in the past anymore
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
// Test independent realtime with global realtime
await page.getByRole('switch', { name: 'Enable Independent Time Conductor' }).click();
await page.getByTitle('Enable independent Time Conductor').locator('label').click();
// check image date
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
// change independent time to realtime
await page.getByRole('button', { name: 'Independent Time Conductor Settings' }).click();
await page.getByRole('button', { name: 'Independent Time Conductor Mode Menu' }).click();
await page.getByRole('menuitem', { name: /Real-Time/ }).click();
await page.getByRole('button', { name: /Fixed Timespan/ }).click();
await page.getByRole('menuitem', { name: /Local Clock/ }).click();
// timestamp shouldn't be in the past anymore
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
// back to the past
await page.getByRole('button', { name: 'Independent Time Conductor Mode Menu' }).click();
await page.getByRole('menuitem', { name: /Real-Time/ }).click();
await page.getByRole('button', { name: 'Independent Time Conductor Mode Menu' }).click();
await page
.getByRole('button', { name: /Local Clock/ })
.first()
.click();
await page.getByRole('menuitem', { name: /Fixed Timespan/ }).click();
// check image date to be in the past
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
@@ -267,7 +247,7 @@ test.describe('Example Imagery Object', () => {
test('Uses low fetch priority', async ({ page }) => {
const priority = await page.locator('.js-imageryView-image').getAttribute('fetchpriority');
expect(priority).toBe('low');
await expect(priority).toBe('low');
});
});
@@ -301,7 +281,7 @@ test.describe('Example Imagery in Display Layout', () => {
await setRealTimeMode(page);
// pause/play button
const pausePlayButton = page.locator('.c-button.pause-play');
const pausePlayButton = await page.locator('.c-button.pause-play');
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
@@ -324,7 +304,7 @@ test.describe('Example Imagery in Display Layout', () => {
await setRealTimeMode(page);
// pause/play button
const pausePlayButton = page.locator('.c-button.pause-play');
const pausePlayButton = await page.locator('.c-button.pause-play');
await pausePlayButton.click();
await expect.soft(pausePlayButton).toHaveClass(/is-paused/);
@@ -948,3 +928,15 @@ async function createImageryView(page) {
page.waitForSelector('.c-message-banner__message')
]);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function setRealTimeMode(page) {
await page.locator('.c-compact-tc').click();
await page.waitForSelector('.c-tc-input-popup', { state: 'visible' });
// Click mode dropdown
await page.getByRole('button', { name: ' Fixed Timespan ' }).click();
// Click realtime
await page.getByTestId('conductor-modeOption-realtime').click();
}

View File

@@ -51,9 +51,10 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
page.on('request', (request) => notebookElementsRequests.push(request));
//Clicking Add Page generates
let [notebookUrlRequest] = await Promise.all([
let [notebookUrlRequest, allDocsRequest] = await Promise.all([
// Waits for the next request with the specified url
page.waitForRequest(`**/openmct/${testNotebook.uuid}`),
page.waitForRequest('**/openmct/_all_docs?include_docs=true'),
// Triggers the request
page.click('[aria-label="Add Page"]')
]);
@@ -63,13 +64,15 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
// Assert that only two requests are made
// Network Requests are:
// 1) The actual POST to create the page
expect(notebookElementsRequests.length).toBe(1);
// 2) The shared worker event from 👆 request
expect(notebookElementsRequests.length).toBe(2);
// Assert on request object
expect(notebookUrlRequest.postDataJSON().metadata.name).toBe(testNotebook.name);
expect(notebookUrlRequest.postDataJSON().model.persisted).toBeGreaterThanOrEqual(
notebookUrlRequest.postDataJSON().model.modified
);
expect(allDocsRequest.postDataJSON().keys).toContain(testNotebook.uuid);
// Add an entry
// Network Requests are:

View File

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

View File

@@ -26,11 +26,56 @@ This test suite is dedicated to tests which verify notebook tag functionality.
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions');
const {
enterTextEntry,
createNotebookAndEntry,
createNotebookEntryAndTags
} = require('../../../../helper/notebookUtils');
const nbUtils = require('../../../../helper/notebookUtils');
/**
* Creates a notebook object and adds an entry.
* @param {import('@playwright/test').Page} - page to load
* @param {number} [iterations = 1] - the number of entries to create
*/
async function createNotebookAndEntry(page, iterations = 1) {
const notebook = createDomainObjectWithDefaults(page, { type: 'Notebook' });
for (let iteration = 0; iteration < iterations; iteration++) {
await nbUtils.enterTextEntry(page, `Entry ${iteration}`);
}
return notebook;
}
/**
* Creates a notebook object, adds an entry, and adds a tag.
* @param {import('@playwright/test').Page} page
* @param {number} [iterations = 1] - the number of entries (and tags) to create
*/
async function createNotebookEntryAndTags(page, iterations = 1) {
const notebook = await createNotebookAndEntry(page, iterations);
await selectInspectorTab(page, 'Annotations');
for (let iteration = 0; iteration < iterations; iteration++) {
// Hover and click "Add Tag" button
// Hover is needed here to "slow down" the actions while running in headless mode
await page.locator(`[aria-label="Notebook Entry"] >> nth = ${iteration}`).click();
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
// Click inside the tag search input
await page.locator('[placeholder="Type to select tag"]').click();
// Select the "Driving" tag
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
// Hover and click "Add Tag" button
// Hover is needed here to "slow down" the actions while running in headless mode
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
// Click inside the tag search input
await page.locator('[placeholder="Type to select tag"]').click();
// Select the "Science" tag
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
}
return notebook;
}
test.describe('Tagging in Notebooks @addInit', () => {
test.beforeEach(async ({ page }) => {
@@ -67,7 +112,7 @@ test.describe('Tagging in Notebooks @addInit', () => {
await createDomainObjectWithDefaults(page, { type: 'Notebook' });
await selectInspectorTab(page, 'Annotations');
await enterTextEntry(page, '');
await nbUtils.enterTextEntry(page, '');
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();

View File

@@ -24,7 +24,7 @@
Testsuite for plot autoscale.
*/
const { selectInspectorTab, setTimeConductorBounds } = require('../../../../appActions');
const { selectInspectorTab } = require('../../../../appActions');
const { test, expect } = require('../../../../pluginFixtures');
test.use({
viewport: {
@@ -107,7 +107,7 @@ test.describe('Autoscale', () => {
await page.keyboard.up('Alt');
// Ensure the drag worked.
await testYTicks(page, ['-0.50', '0.00', '0.50', '1.00', '1.50', '2.00', '2.50', '3.00']);
await testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00', '2.50', '3.00', '3.50']);
//Wait for canvas to stablize.
await canvas.hover({ trial: true });
@@ -131,7 +131,12 @@ async function setTimeRange(
// Set a specific time range for consistency, otherwise it will change
// on every test to a range based on the current time.
await setTimeConductorBounds(page, start, end);
const timeInputs = page.locator('input.c-input--datetime');
await timeInputs.first().click();
await timeInputs.first().fill(start);
await timeInputs.nth(1).click();
await timeInputs.nth(1).fill(end);
}
/**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -26,7 +26,7 @@ necessarily be used for reference when writing new tests in this area.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { selectInspectorTab, setTimeConductorBounds } = require('../../../../appActions');
const { selectInspectorTab } = require('../../../../appActions');
test.describe('Log plot tests', () => {
test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({
@@ -87,10 +87,12 @@ async function makeOverlayPlot(page, myItemsFolderName) {
// Set a specific time range for consistency, otherwise it will change
// on every test to a range based on the current time.
const start = '2022-03-29 22:00:00.000Z';
const end = '2022-03-29 22:00:30.000Z';
const timeInputs = page.locator('input.c-input--datetime');
await timeInputs.first().click();
await timeInputs.first().fill('2022-03-29 22:00:00.000Z');
await setTimeConductorBounds(page, start, end);
await timeInputs.nth(1).click();
await timeInputs.nth(1).fill('2022-03-29 22:00:30.000Z');
// create overlay plot

View File

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

View File

@@ -20,10 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const {
createDomainObjectWithDefaults,
setTimeConductorBounds
} = require('../../../../appActions');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const { test, expect } = require('../../../../pluginFixtures');
test.describe('Telemetry Table', () => {
@@ -54,14 +51,18 @@ test.describe('Telemetry Table', () => {
await expect(tableWrapper).toHaveClass(/is-paused/);
// Subtract 5 minutes from the current end bound datetime and set it
// Bring up the time conductor popup
let endDate = await page.locator('[aria-label="End bounds"]').textContent();
const endTimeInput = page.locator('input[type="text"].c-input--datetime').nth(1);
await endTimeInput.click();
let endDate = await endTimeInput.inputValue();
endDate = new Date(endDate);
endDate.setUTCMinutes(endDate.getUTCMinutes() - 5);
endDate = endDate.toISOString().replace(/T/, ' ');
await setTimeConductorBounds(page, undefined, endDate);
await endTimeInput.fill('');
await endTimeInput.fill(endDate);
await page.keyboard.press('Enter');
await expect(tableWrapper).not.toHaveClass(/is-paused/);

View File

@@ -25,8 +25,7 @@ const {
setFixedTimeMode,
setRealTimeMode,
setStartOffset,
setEndOffset,
setTimeConductorBounds
setEndOffset
} = require('../../../../appActions');
test.describe('Time conductor operations', () => {
@@ -41,36 +40,38 @@ test.describe('Time conductor operations', () => {
let endDate = 'xxxx-01-01 02:00:00.000Z';
endDate = year + endDate.substring(4);
await setTimeConductorBounds(page, startDate, endDate);
const startTimeLocator = page.locator('input[type="text"]').first();
const endTimeLocator = page.locator('input[type="text"]').nth(1);
// Click start time
await startTimeLocator.click();
// Click end time
await endTimeLocator.click();
await endTimeLocator.fill(endDate.toString());
await startTimeLocator.fill(startDate.toString());
// invalid start date
startDate = year + 1 + startDate.substring(4);
await setTimeConductorBounds(page, startDate);
await startTimeLocator.fill(startDate.toString());
await endTimeLocator.click();
// Bring up the time conductor popup
const timeConductorMode = await page.locator('.c-compact-tc');
await timeConductorMode.click();
const startDateLocator = page.locator('input[type="text"]').first();
const endDateLocator = page.locator('input[type="text"]').nth(2);
await endDateLocator.click();
const startDateValidityStatus = await startDateLocator.evaluate((element) =>
const startDateValidityStatus = await startTimeLocator.evaluate((element) =>
element.checkValidity()
);
expect(startDateValidityStatus).not.toBeTruthy();
// fix to valid start date
startDate = year - 1 + startDate.substring(4);
await setTimeConductorBounds(page, startDate);
await startTimeLocator.fill(startDate.toString());
// invalid end date
endDate = year - 2 + endDate.substring(4);
await setTimeConductorBounds(page, undefined, endDate);
await endTimeLocator.fill(endDate.toString());
await startTimeLocator.click();
await startDateLocator.click();
const endDateValidityStatus = await endDateLocator.evaluate((element) =>
const endDateValidityStatus = await endTimeLocator.evaluate((element) =>
element.checkValidity()
);
expect(endDateValidityStatus).not.toBeTruthy();
@@ -82,11 +83,11 @@ test.describe('Time conductor operations', () => {
test.describe('Time conductor input fields real-time mode', () => {
test('validate input fields in real-time mode', async ({ page }) => {
const startOffset = {
startSecs: '23'
secs: '23'
};
const endOffset = {
endSecs: '31'
secs: '31'
};
// Go to baseURL
@@ -99,13 +100,15 @@ test.describe('Time conductor input fields real-time mode', () => {
await setStartOffset(page, startOffset);
// Verify time was updated on time offset button
await expect(page.locator('.c-compact-tc__setting-value.icon-minus')).toContainText('00:30:23');
await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText(
'00:30:23'
);
// Set end time offset
await setEndOffset(page, endOffset);
// Verify time was updated on preceding time offset button
await expect(page.locator('.c-compact-tc__setting-value.icon-plus')).toContainText('00:00:31');
await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:31');
});
/**
@@ -116,12 +119,12 @@ test.describe('Time conductor input fields real-time mode', () => {
page
}) => {
const startOffset = {
startMins: '30',
startSecs: '23'
mins: '30',
secs: '23'
};
const endOffset = {
endSecs: '01'
secs: '01'
};
// Convert offsets to milliseconds
@@ -147,10 +150,12 @@ test.describe('Time conductor input fields real-time mode', () => {
await setRealTimeMode(page);
// Verify updated start time offset persists after mode switch
await expect(page.locator('.c-compact-tc__setting-value.icon-minus')).toContainText('00:30:23');
await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText(
'00:30:23'
);
// Verify updated end time offset persists after mode switch
await expect(page.locator('.c-compact-tc__setting-value.icon-plus')).toContainText('00:00:01');
await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01');
// Verify url parameters persist after mode switch
await page.waitForNavigation({ waitUntil: 'networkidle' });
@@ -198,11 +203,11 @@ test.describe('Time Conductor History', () => {
// with startBound at 2022-01-01 00:00:00.000Z
// and endBound at 2022-01-01 00:00:00.200Z
await page.goto(
'./#/browse/mine?view=grid&tc.mode=fixed&tc.startBound=1640995200000&tc.endBound=1640995200200&tc.timeSystem=utc&hideInspector=true'
'./#/browse/mine?view=grid&tc.mode=fixed&tc.startBound=1640995200000&tc.endBound=1640995200200&tc.timeSystem=utc&hideInspector=true',
{ waitUntil: 'networkidle' }
);
await page.getByRole('button', { name: 'Time Conductor Settings' }).click();
await page.getByRole('button', { name: 'Time Conductor History' }).hover({ trial: true });
await page.getByRole('button', { name: 'Time Conductor History' }).click();
await page.locator("[aria-label='Time Conductor History']").hover({ trial: true });
await page.locator("[aria-label='Time Conductor History']").click();
// Validate history item format
const historyItem = page.locator('text="2022-01-01 00:00:00 + 200ms"');

View File

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

View File

@@ -62,11 +62,6 @@ test.describe('Recent Objects', () => {
test('Navigated objects show up in recents, object renames and deletions are reflected', async ({
page
}) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6818'
});
// Verify that both created objects appear in the list and are in the correct order
await assertInitialRecentObjectsListState();
@@ -95,6 +90,7 @@ test.describe('Recent Objects', () => {
).toBeGreaterThan(0);
expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy();
// Delete
await page.click('button[title="Show selected item in tree"]');
// Delete the folder via the left tree pane treeitem context menu
await page
@@ -110,7 +106,6 @@ test.describe('Recent Objects', () => {
await expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeHidden();
await expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeHidden();
});
test('Clicking on an object in the path of a recent object navigates to the object', async ({
page,
openmctConfig

View File

@@ -1,78 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to tests for renaming objects, and their global application effects.
*/
const { test, expect } = require('../../baseFixtures.js');
const {
createDomainObjectWithDefaults,
renameObjectFromContextMenu
} = require('../../appActions.js');
test.describe('Renaming objects', () => {
test.beforeEach(async ({ page }) => {
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
});
test('When renaming objects, the browse bar and various components all update', async ({
page
}) => {
const folder = await createDomainObjectWithDefaults(page, {
type: 'Folder'
});
// Create a new 'Clock' object with default settings
const clock = await createDomainObjectWithDefaults(page, {
type: 'Clock',
parent: folder.uuid
});
// Rename
clock.name = `${clock.name}-NEW!`;
await renameObjectFromContextMenu(page, clock.url, clock.name);
// check inspector for new name
const titleValue = await page
.getByLabel('Title inspector properties')
.getByLabel('inspector property value')
.textContent();
expect(titleValue).toBe(clock.name);
// check browse bar for new name
await expect(page.locator(`.l-browse-bar >> text=${clock.name}`)).toBeVisible();
// check tree item for new name
await expect(
page.getByRole('listitem', {
name: clock.name
})
).toBeVisible();
// check recent objects for new name
await expect(
page.getByRole('navigation', {
name: clock.name
})
).toBeVisible();
// check title for new name
const title = await page.title();
expect(title).toBe(clock.name);
});
});

View File

@@ -77,11 +77,11 @@ test.describe('Grand Search', () => {
// Click [aria-label="OpenMCT Search"] a >> nth=0
await page.locator('[aria-label="Search Result"] >> nth=0').click();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeInViewport();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('foo');
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toBeInViewport();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
await page

View File

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

View File

@@ -0,0 +1,121 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is an initial example for memory leak testing using performance. This configuration and execution must
be kept separate from the traditional performance measurements to avoid any "observer" effects associated with tracing
or profiling playwright and/or the browser.
Based on a pattern identified in https://github.com/trentmwillis/devtools-protocol-demos/blob/master/testing-demos/memory-leak-by-heap.js
and https://github.com/paulirish/automated-chrome-profiling/issues/3
Best path forward: https://github.com/cowchimp/headless-devtools/blob/master/src/Memory/example.js
*/
const { test, expect } = require('@playwright/test');
const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
// eslint-disable-next-line playwright/no-skipped-test
test.describe.skip('Memory Performance tests', () => {
test.beforeEach(async ({ page, browser }, testInfo) => {
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
// Click a:has-text("My Items")
await page.locator('a:has-text("My Items")').click({
button: 'right'
});
// Click text=Import from JSON
await page.locator('text=Import from JSON').click();
// Upload Performance Display Layout.json
await page.setInputFiles('#fileElem', filePath);
// Click text=OK
await page.locator('text=OK').click();
await expect(
page.locator('a:has-text("Performance Display Layout Display Layout")')
).toBeVisible();
});
test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => {
await page.goto('./', { waitUntil: 'networkidle' });
// To to Search Available after Launch
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill Search input
await page
.locator('[aria-label="OpenMCT Search"] input[type="search"]')
.fill('Performance Display Layout');
//Search Result Appears and is clicked
await Promise.all([
page.waitForNavigation(),
page.locator('a:has-text("Performance Display Layout")').first().click()
]);
//Time to Example Imagery Frame loads within Display Layout
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' });
//Time to Example Imagery object loads
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' });
const client = await page.context().newCDPSession(page);
await client.send('HeapProfiler.enable');
await client.send('HeapProfiler.startSampling');
// await client.send('HeapProfiler.collectGarbage');
await client.send('Performance.enable');
let performanceMetricsBefore = await client.send('Performance.getMetrics');
console.log(performanceMetricsBefore.metrics);
//await client.send('Performance.disable');
//Open Large view
await page.locator('button:has-text("Large View")').click();
await client.send('HeapProfiler.takeHeapSnapshot');
//Time to Imagery Rendered in Large Frame
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' });
//Time to Example Imagery object loads
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' });
// Click Close Icon
await page.locator('.c-click-icon').click();
//Time to Example Imagery Frame loads within Display Layout
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' });
//Time to Example Imagery object loads
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' });
await client.send('HeapProfiler.collectGarbage');
//await client.send('Performance.enable');
let performanceMetricsAfter = await client.send('Performance.getMetrics');
console.log(performanceMetricsAfter.metrics);
//await client.send('Performance.disable');
});
});

View File

@@ -1,299 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { test, expect } = require('@playwright/test');
const memoryLeakFilePath = 'e2e/test-data/memory-leak-detection.json';
/**
* Executes tests to verify that views are not leaking memory on navigation away. This sort of
* memory leak is generally caused by a failure to clean up registered listeners.
*
* These tests are executed on a set of pre-built displays loaded from ../test-data/memory-leak-detection.json.
*
* In order to modify the test data set:
* 1. Run Open MCT locally (npm start)
* 2. Right click on a folder in the tree, and select "Import From JSON"
* 3. In the subsequent dialog, select the file ../test-data/memory-leak-detection.json
* 4. Click "OK"
* 5. Modify test objects as desired
* 6. Right click on the "Memory Leak Detection" folder, and select "Export to JSON"
* 7. Copy the exported file to ../test-data/memory-leak-detection.json
*
*/
const NAV_LEAK_TIMEOUT = 10 * 1000; // 10s
test.describe('Navigation memory leak is not detected in', () => {
test.beforeEach(async ({ page }) => {
// Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.locator('a:has-text("My Items")').click({
button: 'right'
});
await page.locator('text=Import from JSON').click();
// Upload memory-leak-detection.json
await page.setInputFiles('#fileElem', memoryLeakFilePath);
await page.locator('text=OK').click();
await expect(page.locator('a:has-text("Memory Leak Detection")')).toBeVisible();
});
test('plot view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'overlay-plot-single-1hz-swg', {
timeout: NAV_LEAK_TIMEOUT
});
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('stacked plot view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'stacked-plot-single-1hz-swg', {
timeout: NAV_LEAK_TIMEOUT
});
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('LAD table view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-single-1hz-swg', {
timeout: NAV_LEAK_TIMEOUT
});
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('LAD table set', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-set-single-1hz-swg', {
timeout: NAV_LEAK_TIMEOUT
});
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
//TODO: Figure out why using the `table-row` component inside the `table` component leaks TelemetryTableRow objects
test('telemetry table view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'telemetry-table-single-1hz-swg',
{
timeout: NAV_LEAK_TIMEOUT
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
//TODO: Figure out why using the `SideBar` component inside the leaks Notebook objects
test('notebook view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'notebook-memory-leak-detection-test',
{
timeout: NAV_LEAK_TIMEOUT
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('display layout of a single SWG alphanumeric', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'display-layout-single-1hz-swg',
{
timeout: NAV_LEAK_TIMEOUT
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('display layout of a single SWG plot', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'display-layout-single-overlay-plot',
{
timeout: NAV_LEAK_TIMEOUT
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
//TODO: Figure out why `svg` in the CompassRose component leaks imagery
test('example imagery view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'example-imagery-memory-leak-test',
{
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('display layout of example imagery views', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'display-layout-images-memory-leak-test',
{
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('display layout with plots of swgs, alphanumerics, and condition sets, ', async ({
page
}) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'display-layout-simple-telemetry',
{
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('flexible layout with plots of swgs', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'flexible-layout-plots-memory-leak-test',
{
timeout: NAV_LEAK_TIMEOUT
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('flexible layout of example imagery views', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'flexible-layout-images-memory-leak-test',
{
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('tabbed view of display layouts and time strips', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'tab-view-simple-memory-leak-test',
{
timeout: NAV_LEAK_TIMEOUT * 6 * 2 // 2 min
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('time strip view of telemetry', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'time-strip-telemetry-memory-leak-test',
{
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
/**
*
* @param {import('@playwright/test').Page} page
* @param {*} objectName
* @returns
*/
async function navigateToObjectAndDetectMemoryLeak(page, objectName) {
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill Search input
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill(objectName);
//Search Result Appears and is clicked
await Promise.all([
page.locator(`div.c-gsearch-result__title:has-text("${objectName}")`).first().click(),
page.waitForNavigation()
]);
// Register a finalization listener on the root node for the view. This tends to be the last thing to be
// garbage collected since it has either direct or indirect references to all resources used by the view. Therefore it's a pretty good proxy
// for detecting memory leaks.
await page.evaluate(() => {
window.gcPromise = new Promise((resolve) => {
// eslint-disable-next-line no-undef
window.fr = new FinalizationRegistry(resolve);
window.fr.register(
window.openmct.layout.$refs.browseObject.$refs.objectViewWrapper.firstChild,
'navigatedObject',
window.openmct.layout.$refs.browseObject.$refs.objectViewWrapper.firstChild
);
});
});
// Nav back to folder
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
await page.waitForNavigation();
// This next code block blocks until the finalization listener is called and the gcPromise resolved. This means that the root node for the view has been garbage collected.
// In the event that the root node is not garbage collected, the gcPromise will never resolve and the test will time out.
await page.evaluate(() => {
const gcPromise = window.gcPromise;
window.gcPromise = null;
// Manually invoke the garbage collector once all references are removed.
window.gc();
return gcPromise;
});
// Clean up the finalization registry since we don't need it any more.
await page.evaluate(() => {
window.fr = null;
});
// If we get here without timing out, it means the garbage collection promise resolved and the test passed.
return true;
}
});

View File

@@ -1,273 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
Tests to verify plot tagging performance.
*/
const { test, expect } = require('../../pluginFixtures');
const {
createDomainObjectWithDefaults,
setRealTimeMode,
setFixedTimeMode,
waitForPlotsToRender
} = require('../../appActions');
test.describe.fixme('Plot Tagging Performance', () => {
/**
* Given a canvas and a set of points, tags the points on the canvas.
* @param {import('@playwright/test').Page} page
* @param {HTMLCanvasElement} canvas a telemetry item with a plot
* @param {Number} xEnd a telemetry item with a plot
* @param {Number} yEnd a telemetry item with a plot
* @returns {Promise}
*/
async function createTags({ page, canvas, xEnd = 700, yEnd = 520 }) {
await canvas.hover({ trial: true });
//Alt+Shift Drag Start to select some points to tag
await page.keyboard.down('Alt');
await page.keyboard.down('Shift');
await canvas.dragTo(canvas, {
sourcePosition: {
x: 1,
y: 1
},
targetPosition: {
x: xEnd,
y: yEnd
}
});
//Alt Drag End
await page.keyboard.up('Alt');
await page.keyboard.up('Shift');
//Wait for canvas to stablize.
await canvas.hover({ trial: true });
// add some tags
await page.getByText('Annotations').click();
await page.getByRole('button', { name: /Add Tag/ }).click();
await page.getByPlaceholder('Type to select tag').click();
await page.getByText('Driving').click();
await page.getByRole('button', { name: /Add Tag/ }).click();
await page.getByPlaceholder('Type to select tag').click();
await page.getByText('Science').click();
}
/**
* Given a telemetry item (e.g., a Sine Wave Generator) with a plot, tests that the plot can be tagged.
* @param {import('@playwright/test').Page} page
* @param {import('../../../../appActions').CreatedObjectInfo} telemetryItem a telemetry item with a plot
* @returns {Promise}
*/
async function testTelemetryItem(page, telemetryItem) {
// Check that telemetry item also received the tag
await page.goto(telemetryItem.url);
await expect(page.getByText('No tags to display for this item')).toBeVisible();
const canvas = page.locator('canvas').nth(1);
//Wait for canvas to stablize.
await canvas.hover({ trial: true });
// click on the tagged plot point
await canvas.click({
position: {
x: 100,
y: 100
}
});
await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Driving')).toBeHidden();
}
/**
* Given a page, tests that tags are searchable, deletable, and persist across reloads.
* @param {import('@playwright/test').Page} page
* @returns {Promise}
*/
async function basicTagsTests(page) {
// Search for Driving
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Clicking elsewhere should cause annotation selection to be cleared
await expect(page.getByText('No tags to display for this item')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('driv');
// click on the search result
await page
.getByRole('searchbox', { name: 'OpenMCT Search' })
.getByText(/Sine Wave/)
.first()
.click();
// Delete Driving
await page.hover('[aria-label="Tag"]:has-text("Driving")');
await page.locator('[aria-label="Remove tag Driving"]').click();
// Search for Science
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
await expect(page.locator('[aria-label="Search Result"]').nth(0)).toContainText('Science');
await expect(page.locator('[aria-label="Search Result"]').nth(0)).not.toContainText('Drilling');
// Search for Driving
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('driv');
await expect(page.getByText('No results found')).toBeVisible();
//Reload Page
await page.reload({ waitUntil: 'domcontentloaded' });
// wait for plots to load
await waitForPlotsToRender(page);
await page.getByText('Annotations').click();
await expect(page.getByText('No tags to display for this item')).toBeVisible();
const canvas = page.locator('canvas').nth(1);
// click on the tagged plot point
await canvas.click({
position: {
x: 100,
y: 100
}
});
await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Driving')).toBeHidden();
}
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Tags work with Overlay Plots', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6822'
});
//Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
test.slow();
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
const alphaSineWave = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Alpha Sine Wave',
parent: overlayPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Beta Sine Wave',
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url);
let canvas = page.locator('canvas').nth(1);
// Switch to real-time mode
// Adding tags should pause the plot
await setRealTimeMode(page);
await createTags({
page,
canvas
});
await setFixedTimeMode(page);
await basicTagsTests(page);
await testTelemetryItem(page, alphaSineWave);
// set to real time mode
await setRealTimeMode(page);
// Search for Science
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
// click on the search result
await page
.getByRole('searchbox', { name: 'OpenMCT Search' })
.getByText('Alpha Sine Wave')
.first()
.click();
// wait for plots to load
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
// expect plot to be paused
await expect(page.locator('[title="Resume displaying real-time data"]')).toBeVisible();
await setFixedTimeMode(page);
});
test('Tags work with Plot View of telemetry items', async ({ page }) => {
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator'
});
const canvas = page.locator('canvas').nth(1);
await createTags({
page,
canvas
});
await basicTagsTests(page);
});
test('Tags work with Stacked Plots', async ({ page }) => {
const stackedPlot = await createDomainObjectWithDefaults(page, {
type: 'Stacked Plot'
});
const alphaSineWave = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Alpha Sine Wave',
parent: stackedPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Beta Sine Wave',
parent: stackedPlot.uuid
});
await page.goto(stackedPlot.url);
const canvas = page.locator('canvas').nth(1);
await createTags({
page,
canvas,
xEnd: 700,
yEnd: 240
});
await basicTagsTests(page);
await testTelemetryItem(page, alphaSineWave);
});
});

View File

@@ -0,0 +1,62 @@
/* eslint-disable no-undef */
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* global __dirname */
/*
Collection of Visual Tests set to run with modified init scripts to inject plugins not otherwise available in the default contexts.
These should only use functional expect statements to verify assumptions about the state
in a test and not for functional verification of correctness. Visual tests are not supposed
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
*/
// eslint-disable-next-line no-unused-vars
const { test, expect } = require('../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../appActions');
const percySnapshot = require('@percy/playwright');
const path = require('path');
const CUSTOM_NAME = 'CUSTOM_NAME';
test.describe('Visual - addInit', () => {
test.use({
clockOptions: {
now: 0, //Set browser clock to UNIX Epoch
shouldAdvanceTime: false //Don't advance the clock
}
});
test('Restricted Notebook is visually correct @addInit @unstable', async ({ page, theme }) => {
await page.addInitScript({
path: path.join(__dirname, '../../helper', './addInitRestrictedNotebook.js')
});
//Go to baseURL
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
await createDomainObjectWithDefaults(page, { type: CUSTOM_NAME });
// Take a snapshot of the newly created CUSTOM_NAME notebook
await percySnapshot(page, `Restricted Notebook with CUSTOM_NAME (theme: '${theme}')`);
});
});

View File

@@ -21,10 +21,7 @@
*****************************************************************************/
const { test } = require('../../../pluginFixtures.js');
const {
expandTreePaneItemByName,
createDomainObjectWithDefaults
} = require('../../../appActions.js');
const { createDomainObjectWithDefaults } = require('../../../appActions.js');
const percySnapshot = require('@percy/playwright');
@@ -91,3 +88,14 @@ test.describe('Visual - Tree Pane', () => {
});
});
});
/**
* @param {import('@playwright/test').Page} page
* @param {string} name
*/
async function expandTreePaneItemByName(page, name) {
const treePane = page.getByTestId('tree-pane');
const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
const expandTriangle = treeItem.locator('.c-disclosure-triangle');
await expandTriangle.click();
}

View File

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

View File

@@ -0,0 +1,170 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
Collection of Visual Tests set to run in a default context. The tests within this suite
are only meant to run against openmct started by `npm start` within the
`./e2e/playwright-visual.config.js` file.
These should only use functional expect statements to verify assumptions about the state
in a test and not for functional verification of correctness. Visual tests are not supposed
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
*/
const { test, expect } = require('../../pluginFixtures');
const percySnapshot = require('@percy/playwright');
const { createDomainObjectWithDefaults } = require('../../appActions');
test.describe('Visual - Default', () => {
test.beforeEach(async ({ page }) => {
//Go to baseURL and Hide Tree
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
});
test.use({
clockOptions: {
now: 0, //Set browser clock to UNIX Epoch
shouldAdvanceTime: false //Don't advance the clock
}
});
test('Visual - Root and About', async ({ page, theme }) => {
// Verify that Create button is actionable
await expect(page.locator('button:has-text("Create")')).toBeEnabled();
// Take a snapshot of the Dashboard
await percySnapshot(page, `Root (theme: '${theme}')`);
// Click About button
await page.click('.l-shell__app-logo');
// Modify the Build information in 'about' to be consistent run-over-run
const versionInformationLocator = page.locator('ul.t-info.l-info.s-info').first();
await expect(versionInformationLocator).toBeEnabled();
await versionInformationLocator.evaluate(
(node) =>
(node.innerHTML =
'<li>Version: visual-snapshot</li> <li>Build Date: Mon Nov 15 2021 08:07:51 GMT-0800 (Pacific Standard Time)</li> <li>Revision: 93049cdbc6c047697ca204893db9603b864b8c9f</li> <li>Branch: master</li>')
);
// Take a snapshot of the About modal
await percySnapshot(page, `About (theme: '${theme}')`);
});
test('Visual - Default Condition Set @unstable', async ({ page, theme }) => {
await createDomainObjectWithDefaults(page, { type: 'Condition Set' });
// Take a snapshot of the newly created Condition Set object
await percySnapshot(page, `Default Condition Set (theme: '${theme}')`);
});
test('Visual - Default Condition Widget @unstable', async ({ page, theme }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5349'
});
await createDomainObjectWithDefaults(page, { type: 'Condition Widget' });
// Take a snapshot of the newly created Condition Widget object
await percySnapshot(page, `Default Condition Widget (theme: '${theme}')`);
});
test('Visual - Time Conductor start time is less than end time', async ({ page, theme }) => {
const year = new Date().getFullYear();
let startDate = 'xxxx-01-01 01:00:00.000Z';
startDate = year + startDate.substring(4);
let endDate = 'xxxx-01-01 02:00:00.000Z';
endDate = year + endDate.substring(4);
await page.locator('input[type="text"]').nth(1).fill(endDate.toString());
await page.locator('input[type="text"]').first().fill(startDate.toString());
// verify no error msg
await percySnapshot(page, `Default Time conductor (theme: '${theme}')`);
startDate = year + 1 + startDate.substring(4);
await page.locator('input[type="text"]').first().fill(startDate.toString());
await page.locator('input[type="text"]').nth(1).click();
// verify error msg for start time (unable to capture snapshot of popup)
await percySnapshot(page, `Start time error (theme: '${theme}')`);
startDate = year - 1 + startDate.substring(4);
await page.locator('input[type="text"]').first().fill(startDate.toString());
endDate = year - 2 + endDate.substring(4);
await page.locator('input[type="text"]').nth(1).fill(endDate.toString());
await page.locator('input[type="text"]').first().click();
// verify error msg for end time (unable to capture snapshot of popup)
await percySnapshot(page, `End time error (theme: '${theme}')`);
});
test('Visual - Sine Wave Generator Form', async ({ page, theme }) => {
//Click the Create button
await page.click('button:has-text("Create")');
// Click text=Sine Wave Generator
await page.click('text=Sine Wave Generator');
await percySnapshot(page, `Default Sine Wave Generator Form (theme: '${theme}')`);
await page.locator('.field.control.l-input-sm input').first().click();
await page.locator('.field.control.l-input-sm input').first().fill('');
// Validate red x mark
await percySnapshot(page, `removed amplitude property value (theme: '${theme}')`);
});
test('Visual - Save Successful Banner @unstable', async ({ page, theme }) => {
await createDomainObjectWithDefaults(page, { type: 'Timer' });
await page.locator('.c-message-banner__message').hover({ trial: true });
await percySnapshot(page, `Banner message shown (theme: '${theme}')`);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
await percySnapshot(page, `Banner message gone (theme: '${theme}')`);
});
test('Visual - Display Layout Icon is correct', async ({ page, theme }) => {
//Click the Create button
await page.click('button:has-text("Create")');
//Hover on Display Layout option.
await page.locator('text=Display Layout').hover();
await percySnapshot(page, `Display Layout Create Menu (theme: '${theme}')`);
});
test('Visual - Default Gauge is correct @unstable', async ({ page, theme }) => {
await createDomainObjectWithDefaults(page, { type: 'Gauge' });
// Take a snapshot of the newly created Gauge object
await percySnapshot(page, `Default Gauge (theme: '${theme}')`);
});
});

View File

@@ -1,101 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
Collection of Visual Tests set to run in a default context with default Plugins. The tests within this suite
are only meant to run against openmct's app.js started by `npm run start` within the
`./e2e/playwright-visual.config.js` file.
*/
const { test, expect } = require('../../pluginFixtures');
const percySnapshot = require('@percy/playwright');
const { createDomainObjectWithDefaults } = require('../../appActions');
test.describe('Visual - Default', () => {
test.beforeEach(async ({ page }) => {
//Go to baseURL and Hide Tree
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
});
test('Visual - Default Dashboard', async ({ page, theme }) => {
// Verify that Create button is actionable
await expect(page.locator('button:has-text("Create")')).toBeEnabled();
// Take a snapshot of the Dashboard
await percySnapshot(page, `Default Dashboard (theme: '${theme}')`);
});
test('Visual - Default Condition Set', async ({ page, theme }) => {
await createDomainObjectWithDefaults(page, {
type: 'Condition Set',
name: 'Default Condition Set'
});
// Take a snapshot of the newly created Condition Set object
await percySnapshot(page, `Default Condition Set (theme: '${theme}')`);
});
test('Visual - Default Condition Widget', async ({ page, theme }) => {
await createDomainObjectWithDefaults(page, {
type: 'Condition Widget',
name: 'Default Condition Widget'
});
// Take a snapshot of the newly created Condition Widget object
await percySnapshot(page, `Default Condition Widget (theme: '${theme}')`);
});
test('Visual - Sine Wave Generator Form', async ({ page, theme }) => {
//Click the Create button
await page.click('button:has-text("Create")');
// Click text=Sine Wave Generator
await page.click('text=Sine Wave Generator');
await percySnapshot(page, `Default Sine Wave Generator Form (theme: '${theme}')`);
await page.locator('.field.control.l-input-sm input').first().click();
await page.locator('.field.control.l-input-sm input').first().fill('');
// Validate red x mark
await percySnapshot(page, `removed amplitude property value (theme: '${theme}')`);
});
test('Visual - Display Layout Icon is correct in Create Menu', async ({ page, theme }) => {
// Click the Create button
await page.click('button:has-text("Create")');
// Hover on Display Layout option.
await page.locator('text=Display Layout').hover();
await percySnapshot(page, `Display Layout Create Menu (theme: '${theme}')`);
});
test('Visual - Default Gauge', async ({ page, theme }) => {
await createDomainObjectWithDefaults(page, {
type: 'Gauge',
name: 'Default Gauge'
});
// Take a snapshot of the newly created Gauge object
await percySnapshot(page, `Default Gauge (theme: '${theme}')`);
});
});

View File

@@ -26,12 +26,12 @@ const percySnapshot = require('@percy/playwright');
const utils = require('../../helper/faultUtils');
test.describe('Fault Management Visual Tests', () => {
test.describe('The Fault Management Plugin Visual Test', () => {
test('icon test', async ({ page, theme }) => {
await page.addInitScript({
path: path.join(__dirname, '../../helper/', 'addInitFaultManagementPlugin.js')
});
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
await percySnapshot(page, `Fault Management icon appears in tree (theme: '${theme}')`);
});

View File

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

View File

@@ -39,10 +39,7 @@ test.describe("Visual - Check Notification Info Banner of 'Save successful'", ()
theme
}) => {
// Create a clock domain object
await createDomainObjectWithDefaults(page, {
type: 'Clock',
name: 'Default Clock'
});
await createDomainObjectWithDefaults(page, { type: 'Clock' });
// Verify there is a button with aria-label="Review 1 Notification"
expect(await page.locator('button[aria-label="Review 1 Notification"]').isVisible()).toBe(true);
// Verify there is a button with aria-label="Clear all notifications"
@@ -55,14 +52,12 @@ test.describe("Visual - Check Notification Info Banner of 'Save successful'", ()
expect(await page.locator('div[role="dialog"]').isVisible()).toBe(true);
// Verify the div with role="dialog" contains text "Save successful"
expect(await page.locator('div[role="dialog"]').innerText()).toContain('Save successful');
await percySnapshot(page, `Notification banner shows Save successful (theme: '${theme}')`);
await percySnapshot(page, `Notification banner - ${theme}`);
// Verify there is a button with text "Dismiss"
expect(await page.locator('button:has-text("Dismiss")').isVisible()).toBe(true);
await percySnapshot(page, `Notification banner shows Dismiss (theme: '${theme}')`);
// Click on button with text "Dismiss"
await page.locator('button:has-text("Dismiss")').click();
// Verify there is no div with role="dialog"
expect(await page.locator('div[role="dialog"]').isVisible()).toBe(false);
await percySnapshot(page, `Notification banner dismissed (theme: '${theme}')`);
});
});

View File

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

View File

@@ -62,7 +62,7 @@ export default class SinewaveLimitProvider extends EventEmitter {
const id = this.#getObjectKeyString(domainObject);
if (this.#isRealTime === undefined) {
this.#updateRealTime(this.#openmct.time.getMode());
this.#updateRealTime(this.#openmct.time.clock());
}
this.#handleClockUpdate();
@@ -92,15 +92,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('clock', this.#updateRealTime, this);
} else if (!observers && this.#watchingTheClock) {
this.#watchingTheClock = false;
this.#openmct.time.off('modeChanged', this.#updateRealTime, this);
this.#openmct.time.off('clock', this.#updateRealTime, this);
}
}
#updateRealTime(mode) {
this.#isRealTime = mode !== 'fixed';
#updateRealTime(clock) {
this.#isRealTime = clock !== undefined;
if (!this.#isRealTime) {
Object.keys(this.#observingStaleness).forEach((id) => {

View File

@@ -1,6 +1,6 @@
{
"name": "openmct",
"version": "3.0.2",
"version": "3.0.0-SNAPSHOT",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.22.5",
@@ -8,7 +8,7 @@
"@deploysentinel/playwright": "0.3.4",
"@percy/cli": "1.26.0",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.36.2",
"@playwright/test": "1.32.3",
"@types/eventemitter3": "1.2.0",
"@types/jasmine": "4.3.4",
"@types/lodash": "4.14.192",
@@ -55,6 +55,7 @@
"moment-timezone": "0.5.41",
"nyc": "15.1.0",
"painterro": "1.2.78",
"playwright-core": "1.32.3",
"plotly.js-basic-dist": "2.20.0",
"plotly.js-gl2d-dist": "2.20.0",
"prettier": "2.8.7",
@@ -79,10 +80,8 @@
"clean": "rm -rf ./dist ./node_modules ./package-lock.json ./coverage ./html-test-results ./test-results ./.nyc_output ",
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
"start": "npx webpack serve --config ./.webpack/webpack.dev.js",
"start:prod": "npx webpack serve --config ./.webpack/webpack.prod.js",
"start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.js",
"lint": "eslint example src e2e --ext .js openmct.js --max-warnings=0 && eslint example src --ext .vue",
"lint:spelling": "cspell \"**/*.{js,md,vue}\" --show-context --gitignore",
"lint": "eslint example src e2e --ext .js,.vue openmct.js --max-warnings=0",
"lint:fix": "eslint example src e2e --ext .js,.vue openmct.js --fix",
"build:prod": "webpack --config ./.webpack/webpack.prod.js",
"build:dev": "webpack --config ./.webpack/webpack.dev.js",
@@ -93,18 +92,13 @@
"test:debug": "KARMA_DEBUG=true karma start",
"test:e2e": "npx playwright test",
"test:e2e:couchdb": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @couchdb --workers=1",
"test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert \"@unstable|@couchdb|@generatedata\"",
"test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert \"@unstable|@couchdb\"",
"test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable",
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
"test:e2e:generatedata": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @generatedata",
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots",
"test:e2e:visual:ci": "percy exec --config ./e2e/.percy.ci.yml --partial -- npx playwright test --config=e2e/playwright-visual.config.js --project=chrome --grep-invert @unstable",
"test:e2e:visual:full": "percy exec --config ./e2e/.percy.nightly.yml -- npx playwright test --config=e2e/playwright-visual.config.js --grep-invert @unstable",
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js --grep-invert @unstable",
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js --grep-invert @couchdb",
"test:e2e:watch": "npx playwright test --ui --config=e2e/playwright-ci.config.js",
"test:perf:contract": "npx playwright test --config=e2e/playwright-performance-dev.config.js",
"test:perf:localhost": "npx playwright test --config=e2e/playwright-performance-prod.config.js --project=chrome",
"test:perf:memory": "npx playwright test --config=e2e/playwright-performance-prod.config.js --project=chrome-memory",
"test:perf": "npx playwright test --config=e2e/playwright-performance.config.js",
"update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2023/gm' ./src/ui/layout/AboutDialog.vue",
"update-copyright-date": "npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\s\\(c\\)\\s20\\d\\d\\-20\\d\\d/Copyright \\(c\\)\\ 2014\\-2023/gm'",
"cov:e2e:report": "nyc report --reporter=lcovonly --report-dir=./coverage/e2e",

View File

@@ -43,6 +43,7 @@ define([
'./plugins/duplicate/plugin',
'./plugins/importFromJSONAction/plugin',
'./plugins/exportAsJSONAction/plugin',
'./ui/components/components',
'vue'
], function (
EventEmitter,
@@ -67,6 +68,7 @@ define([
DuplicateActionPlugin,
ImportFromJSONAction,
ExportAsJSONAction,
components,
Vue
) {
/**
@@ -428,6 +430,7 @@ define([
};
MCT.prototype.plugins = plugins;
MCT.prototype.components = components.default;
return MCT;
});

View File

@@ -92,7 +92,7 @@ class ActionsAPI extends EventEmitter {
if (this._actionCollections.has(key)) {
let actionCollection = this._actionCollections.get(key);
actionCollection.off('destroy', this._updateCachedActionCollections);
delete actionCollection.applicableActions;
this._actionCollections.delete(key);
}
}

View File

@@ -100,7 +100,7 @@ export default class AnnotationAPI extends EventEmitter {
creatable: false,
cssClass: 'icon-notebook',
initialize: function (domainObject) {
domainObject.targets = domainObject.targets || [];
domainObject.targets = domainObject.targets || {};
domainObject._deleted = domainObject._deleted || false;
domainObject.originalContextPath = domainObject.originalContextPath || '';
domainObject.tags = domainObject.tags || [];
@@ -117,10 +117,10 @@ export default class AnnotationAPI extends EventEmitter {
* @property {ANNOTATION_TYPES} annotationType the type of annotation to create (e.g., PLOT_SPATIAL)
* @property {Tag[]} tags tags to add to the annotation, e.g., SCIENCE for science related annotations
* @property {String} contentText Some text to add to the annotation, e.g. ("This annotation is about science")
* @property {Array<Object>} targets The targets ID keystrings and their specific properties.
* For plots, this will be a bounding box, e.g.: {keyString: "d8385009-789d-457b-acc7-d50ba2fd55ea", maxY: 100, minY: 0, maxX: 100, minX: 0}
* @property {Object<string, Object>} targets The targets ID keystrings and their specific properties.
* For plots, this will be a bounding box, e.g.: {maxY: 100, minY: 0, maxX: 100, minX: 0}
* For notebooks, this will be an entry ID, e.g.: {entryId: "entry-ecb158f5-d23c-45e1-a704-649b382622ba"}
* @property {DomainObject>[]} targetDomainObjects the domain objects this annotation points to (e.g., telemetry objects for a plot)
* @property {DomainObject>} targetDomainObjects the targets ID keystrings and the domain objects this annotation points to (e.g., telemetry objects for a plot)
*/
/**
* @method create
@@ -141,15 +141,11 @@ export default class AnnotationAPI extends EventEmitter {
throw new Error(`Unknown annotation type: ${annotationType}`);
}
if (!targets.length) {
if (!Object.keys(targets).length) {
throw new Error(`At least one target is required to create an annotation`);
}
if (targets.some((target) => !target.keyString)) {
throw new Error(`All targets require a keyString to create an annotation`);
}
if (!targetDomainObjects.length) {
if (!Object.keys(targetDomainObjects).length) {
throw new Error(`At least one targetDomainObject is required to create an annotation`);
}
@@ -185,7 +181,7 @@ export default class AnnotationAPI extends EventEmitter {
const success = await this.openmct.objects.save(createdObject);
if (success) {
this.emit('annotationCreated', createdObject);
targetDomainObjects.forEach((targetDomainObject) => {
Object.values(targetDomainObjects).forEach((targetDomainObject) => {
this.#updateAnnotationModified(targetDomainObject);
});
@@ -325,10 +321,7 @@ export default class AnnotationAPI extends EventEmitter {
}
#addTagMetaInformationToTags(tags) {
// Convert to Set and back to Array to remove duplicates
const uniqueTags = [...new Set(tags)];
return uniqueTags.map((tagKey) => {
return tags.map((tagKey) => {
const tagModel = this.availableTags[tagKey];
tagModel.tagID = tagKey;
@@ -370,8 +363,7 @@ export default class AnnotationAPI extends EventEmitter {
const modelAddedToResults = await Promise.all(
results.map(async (result) => {
const targetModels = await Promise.all(
result.targets.map(async (target) => {
const targetID = target.keyString;
Object.keys(result.targets).map(async (targetID) => {
const targetModel = await this.openmct.objects.get(targetID);
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
const originalPathObjects = await this.openmct.objects.getOriginalPath(targetKeyString);
@@ -418,12 +410,13 @@ export default class AnnotationAPI extends EventEmitter {
#breakApartSeparateTargets(results) {
const separateResults = [];
results.forEach((result) => {
result.targets.forEach((target) => {
const targetID = target.keyString;
Object.keys(result.targets).forEach((targetID) => {
const separatedResult = {
...result
};
separatedResult.targets = [target];
separatedResult.targets = {
[targetID]: result.targets[targetID]
};
separatedResult.targetModels = result.targetModels.filter((targetModel) => {
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);

View File

@@ -62,12 +62,11 @@ describe('The Annotation API', () => {
key: 'anAnnotationKey',
namespace: 'fooNameSpace'
},
targets: [
{
keyString: 'fooNameSpace:some-object',
targets: {
'fooNameSpace:some-object': {
entryId: 'fooBarEntry'
}
]
}
};
mockObjectProvider = jasmine.createSpyObj('mock provider', ['create', 'update', 'get']);
@@ -122,7 +121,7 @@ describe('The Annotation API', () => {
tags: ['sometag'],
contentText: 'fooContext',
targetDomainObjects: [mockDomainObject],
targets: [{ keyString: 'fooTarget' }]
targets: { fooTarget: {} }
};
const annotationObject = await openmct.annotation.create(annotationCreationArguments);
expect(annotationObject).toBeDefined();
@@ -137,7 +136,7 @@ describe('The Annotation API', () => {
tags: ['sometag'],
contentText: 'fooContext',
targetDomainObjects: [mockDomainObject],
targets: [{ keyString: 'fooTarget' }]
targets: { fooTarget: {} }
};
openmct.annotation.setNamespaceToSaveAnnotations('fooNameSpace');
const annotationObject = await openmct.annotation.create(annotationCreationArguments);
@@ -167,7 +166,7 @@ describe('The Annotation API', () => {
tags: ['sometag'],
contentText: 'fooContext',
targetDomainObjects: [mockDomainObject],
targets: [{ keyString: 'fooTarget' }]
targets: { fooTarget: {} }
};
openmct.annotation.setNamespaceToSaveAnnotations('nameespaceThatDoesNotExist');
await openmct.annotation.create(annotationCreationArguments);
@@ -184,7 +183,7 @@ describe('The Annotation API', () => {
tags: ['sometag'],
contentText: 'fooContext',
targetDomainObjects: [mockDomainObject],
targets: [{ keyString: 'fooTarget' }]
targets: { fooTarget: {} }
};
openmct.annotation.setNamespaceToSaveAnnotations('immutableProvider');
await openmct.annotation.create(annotationCreationArguments);
@@ -203,7 +202,7 @@ describe('The Annotation API', () => {
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
tags: ['aWonderfulTag'],
contentText: 'fooContext',
targets: [{ keyString: 'fooNameSpace:some-object', entryId: 'fooBarEntry' }],
targets: { 'fooNameSpace:some-object': { entryId: 'fooBarEntry' } },
targetDomainObjects: [mockDomainObject]
};
});
@@ -273,19 +272,17 @@ describe('The Annotation API', () => {
let comparator;
beforeEach(() => {
targets = [
{
keyString: 'fooTarget',
targets = {
fooTarget: {
foo: 42
}
];
otherTargets = [
{
keyString: 'fooTarget',
};
otherTargets = {
fooTarget: {
bar: 42
}
];
comparator = (t1, t2) => t1[0].foo === t2[0].bar;
};
comparator = (t1, t2) => t1.fooTarget.foo === t2.fooTarget.bar;
});
it('can add a comparator function', () => {

View File

@@ -185,10 +185,9 @@ export default class CompositionProvider {
return;
}
const onMutation = this.#onMutation.bind(this);
this.#publicAPI.objects.eventEmitter.on('mutation', onMutation);
this.#publicAPI.objects.eventEmitter.on('mutation', this.#onMutation.bind(this));
this.topicListener = () => {
this.#publicAPI.objects.eventEmitter.off('mutation', onMutation);
this.#publicAPI.objects.eventEmitter.off('mutation', this.#onMutation.bind(this));
};
}

View File

@@ -22,9 +22,9 @@
<template>
<div class="form-row c-form__row" :class="[{ first: first }, cssClass]" @onChange="onChange">
<label class="c-form-row__label" :title="row.description" :for="`form-${row.key}`">
<div class="c-form-row__label" :title="row.description">
{{ row.name }}
</label>
</div>
<div class="c-form-row__state-indicator" :class="reqClass"></div>
<div v-if="row.control" ref="rowElement" class="c-form-row__controls"></div>
</div>

View File

@@ -123,6 +123,7 @@ export default {
formatDatetime(timestamp = this.model.value) {
if (!timestamp) {
this.resetValues();
return;
}
@@ -136,7 +137,7 @@ export default {
const data = {
model,
value: new Date(timestamp).toISOString()
value: timestamp
};
this.$emit('onChange', data);

View File

@@ -23,14 +23,7 @@
<template>
<span class="form-control shell">
<span class="field control" :class="model.cssClass">
<input
:id="`form-${model.key}`"
v-model="field"
:name="model.key"
type="text"
:size="model.size"
@input="updateText()"
/>
<input v-model="field" type="text" :size="model.size" @input="updateText()" />
</span>
</span>
</template>

View File

@@ -23,7 +23,6 @@
import MenuAPI from './MenuAPI';
import Menu from './menu';
import { createOpenMct, createMouseEvent, resetApplicationState } from '../../utils/testing';
import Vue from 'vue';
describe('The Menu API', () => {
let openmct;
@@ -138,13 +137,14 @@ describe('The Menu API', () => {
it('invokes the destroy method when menu is dismissed', (done) => {
menuOptions.onDestroy = done;
spyOn(menuAPI, '_clearMenuComponent').and.callThrough();
menuAPI.showMenu(x, y, actionsArray, menuOptions);
const vueComponent = menuAPI.menuComponent.component;
spyOn(vueComponent, '$destroy');
document.body.click();
expect(menuAPI._clearMenuComponent).toHaveBeenCalled();
expect(vueComponent.$destroy).toHaveBeenCalled();
});
it('invokes the onDestroy callback if passed in', (done) => {
@@ -185,7 +185,7 @@ describe('The Menu API', () => {
superMenuItem.dispatchEvent(mouseOverEvent);
const itemDescription = document.querySelector('.l-item-description__description');
Vue.nextTick(() => {
menuAPI.menuComponent.component.$nextTick(() => {
expect(menuElement).not.toBeNull();
expect(itemDescription.innerText).toEqual(actionsArray[0].description);

View File

@@ -30,6 +30,7 @@
role="menuitem"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || null"
@click="action.onItemClicked"
>
{{ action.name }}
@@ -52,6 +53,7 @@
role="menuitem"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || null"
@click="action.onItemClicked"
>
{{ action.name }}

View File

@@ -34,6 +34,7 @@
role="menuitem"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || null"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
@@ -58,6 +59,7 @@
role="menuitem"
:class="action.cssClass"
:title="action.description"
:data-testid="action.testId || null"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"

View File

@@ -52,12 +52,12 @@ class Menu extends EventEmitter {
}
dismiss() {
this.emit('destroy');
if (this.destroy) {
this.destroy();
this.destroy = null;
}
document.removeEventListener('click', this.dismiss);
this.emit('destroy');
}
showMenu() {

View File

@@ -374,7 +374,7 @@ class InMemorySearchProvider {
delete provider.pendingIndex[keyString];
try {
if (domainObject && domainObject.identifier) {
if (domainObject) {
await provider.index(domainObject);
}
} catch (error) {
@@ -435,8 +435,7 @@ class InMemorySearchProvider {
}
localIndexAnnotation(objectToIndex, model) {
model.targets.forEach((target) => {
const targetID = target.keyString;
Object.keys(model.targets).forEach((targetID) => {
if (!this.localIndexedAnnotationsByDomainObject[targetID]) {
this.localIndexedAnnotationsByDomainObject[targetID] = [];
}

View File

@@ -57,8 +57,7 @@
};
function indexAnnotation(objectToIndex, model) {
model.targets.forEach((target) => {
const targetID = target.keyString;
Object.keys(model.targets).forEach((targetID) => {
if (!indexedAnnotationsByDomainObject[targetID]) {
indexedAnnotationsByDomainObject[targetID] = [];
}

View File

@@ -554,34 +554,28 @@ export default class ObjectAPI {
*/
async getTelemetryPath(identifier, telemetryIdentifier) {
const objectDetails = await this.get(identifier);
let telemetryPath = [];
if (objectDetails?.type === 'folder') {
return telemetryPath;
}
let sourceTelemetry = null;
if (telemetryIdentifier && utils.identifierEquals(identifier, telemetryIdentifier)) {
sourceTelemetry = identifier;
} else if (objectDetails.composition) {
sourceTelemetry = objectDetails.composition[0];
const telemetryPath = [];
if (objectDetails.composition && !['folder'].includes(objectDetails.type)) {
let sourceTelemetry = objectDetails.composition[0];
if (telemetryIdentifier) {
sourceTelemetry = objectDetails.composition.find((telemetrySource) =>
utils.identifierEquals(telemetrySource, telemetryIdentifier)
sourceTelemetry = objectDetails.composition.find(
(telemetrySource) =>
this.makeKeyString(telemetrySource) === this.makeKeyString(telemetryIdentifier)
);
}
const compositionElement = await this.get(sourceTelemetry);
if (!['yamcs.telemetry', 'generator'].includes(compositionElement.type)) {
return telemetryPath;
}
const telemetryKey = compositionElement.identifier.key;
const telemetryPathObjects = await this.getOriginalPath(telemetryKey);
telemetryPathObjects.forEach((pathObject) => {
if (pathObject.type === 'root') {
return;
}
telemetryPath.unshift(pathObject.name);
});
}
const compositionElement = await this.get(sourceTelemetry);
if (!['yamcs.telemetry', 'generator', 'yamcs.aggregate'].includes(compositionElement.type)) {
return telemetryPath;
}
const telemetryPathObjects = await this.getOriginalPath(compositionElement.identifier);
telemetryPath = telemetryPathObjects
.reverse()
.filter((pathObject) => pathObject.type !== 'root')
.map((pathObject) => pathObject.name);
return telemetryPath;
}

View File

@@ -23,9 +23,6 @@ describe('The Object API', () => {
return USERNAME;
}
});
},
getPossibleRoles() {
return Promise.resolve([]);
}
};
openmct = createOpenMct();

View File

@@ -27,7 +27,7 @@
v-if="dismissable"
aria-label="Close"
class="c-click-icon c-overlay__close-button icon-x"
@click.stop="destroy"
@click="destroy"
></button>
<div
ref="element"
@@ -71,16 +71,16 @@ export default {
});
},
methods: {
destroy() {
destroy: function () {
if (this.dismissable) {
this.dismiss();
}
},
buttonClickHandler(method) {
buttonClickHandler: function (method) {
method();
this.$emit('destroy');
},
getElementForFocus() {
getElementForFocus: function () {
const defaultElement = this.$refs.element;
if (!this.$refs.buttons) {
return defaultElement;

View File

@@ -204,8 +204,7 @@ export default class TelemetryAPI {
*/
standardizeRequestOptions(options = {}) {
if (!Object.hasOwn(options, 'start')) {
const bounds = options.timeContext?.getBounds();
if (bounds?.start) {
if (options.timeContext?.getBounds()) {
options.start = options.timeContext.getBounds().start;
} else {
options.start = this.openmct.time.getBounds().start;
@@ -213,8 +212,7 @@ export default class TelemetryAPI {
}
if (!Object.hasOwn(options, 'end')) {
const bounds = options.timeContext?.getBounds();
if (bounds?.end) {
if (options.timeContext?.getBounds()) {
options.end = options.timeContext.getBounds().end;
} else {
options.end = this.openmct.time.getBounds().end;

View File

@@ -71,7 +71,6 @@ export default class TelemetryCollection extends EventEmitter {
this.requestAbort = undefined;
this.isStrategyLatest = this.options.strategy === 'latest';
this.dataOutsideTimeBounds = false;
this.modeChanged = false;
}
/**
@@ -307,12 +306,6 @@ export default class TelemetryCollection extends EventEmitter {
* @private
*/
_bounds(bounds, isTick) {
if (this.modeChanged) {
this.modeChanged = false;
this._reset();
return;
}
let startChanged = this.lastBounds.start !== bounds.start;
let endChanged = this.lastBounds.end !== bounds.end;
@@ -446,8 +439,7 @@ export default class TelemetryCollection extends EventEmitter {
}
_timeModeChanged() {
//We're need this so that when the bounds change comes in after this mode change, we can reset and request historic telemetry
this.modeChanged = true;
this._reset();
}
/**

View File

@@ -299,22 +299,6 @@ class IndependentTimeContext extends TimeContext {
return this.mode;
}
isRealTime() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.isRealTime(...arguments);
} else {
return super.isRealTime(...arguments);
}
}
now() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.now(...arguments);
} else {
return super.now(...arguments);
}
}
/**
* Causes this time context to follow another time context (either the global context, or another upstream time context)
* This allows views to have their own time context which points to the appropriate upstream context as necessary, achieving nesting.
@@ -408,9 +392,6 @@ class IndependentTimeContext extends TimeContext {
if (viewKey && key === viewKey) {
//this is necessary as the upstream context gets reassigned after this
this.stopFollowingTimeContext();
if (this.activeClock !== undefined) {
this.activeClock.off('tick', this.tick);
}
let timeContext = this.globalTimeContext;

View File

@@ -42,7 +42,6 @@ class TimeContext extends EventEmitter {
this.activeClock = undefined;
this.offsets = undefined;
this.mode = undefined;
this.warnCounts = {};
this.tick = this.tick.bind(this);
}
@@ -649,17 +648,6 @@ class TimeContext extends EventEmitter {
}
#warnMethodDeprecated(method, newMethod) {
const MAX_CALLS = 1; // Only warn once per unique method and newMethod combination
const key = `${method}.${newMethod}`;
const currentWarnCount = this.warnCounts[key] || 0;
if (currentWarnCount >= MAX_CALLS) {
return; // Don't warn if already warned once
}
this.warnCounts[key] = currentWarnCount + 1;
let message = `[DEPRECATION WARNING]: The ${method} API method is deprecated and will be removed in a future version of Open MCT.`;
if (newMethod) {

View File

@@ -57,22 +57,13 @@ class TooltipAPI {
* @private for platform-internal use
*/
showTooltip(tooltip) {
this.removeAllTooltips();
this.activeToolTips.push(tooltip);
tooltip.show();
}
/**
* API method to allow for removing all tooltips
*/
removeAllTooltips() {
if (!this.activeToolTips?.length) {
return;
}
for (let i = this.activeToolTips.length - 1; i > -1; i--) {
this.activeToolTips[i].destroy();
this.activeToolTips.splice(i, 1);
}
this.activeToolTips.push(tooltip);
tooltip.show();
}
/**

View File

@@ -3,7 +3,6 @@
height: auto;
width: auto;
padding: $interiorMargin;
overflow-wrap: break-word;
}
.c-tooltip {

View File

@@ -32,7 +32,6 @@ describe('The User Status API', () => {
'setPollQuestion',
'getPollQuestion',
'getCurrentUser',
'getPossibleRoles',
'getPossibleStatuses',
'getAllStatusRoles',
'canSetPollQuestion',
@@ -43,7 +42,6 @@ describe('The User Status API', () => {
mockUser = new openmct.user.User('test-user', 'A test user');
userProvider.getCurrentUser.and.returnValue(Promise.resolve(mockUser));
userProvider.getPossibleStatuses.and.returnValue(Promise.resolve([]));
userProvider.getPossibleRoles.and.returnValue(Promise.resolve([]));
userProvider.getAllStatusRoles.and.returnValue(Promise.resolve([]));
userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(false));
userProvider.isLoggedIn.and.returnValue(true);

View File

@@ -151,7 +151,7 @@ export default {
);
const ladTable = this.ladTableObjects[index];
delete this.ladTelemetryObjects[ladTable.key];
this.$delete(this.ladTelemetryObjects, ladTable.key);
this.ladTableObjects.splice(index, 1);
this.shouldShowUnitsCheckbox();
@@ -224,7 +224,7 @@ export default {
}
if (!showUnitsCheckbox && this.headers?.units) {
delete this.headers.units;
this.$delete(this.headers, 'units');
}
},
metadataHasUnits(domainObject) {

View File

@@ -74,6 +74,7 @@ export default {
return {
ladTableObjects: [],
ladTelemetryObjects: {},
compositions: [],
viewContext: {},
staleObjects: [],
configuration: this.ladTableConfiguration.getConfiguration()
@@ -114,9 +115,6 @@ export default {
return '';
}
},
created() {
this.compositions = [];
},
mounted() {
this.ladTableConfiguration.on('change', this.handleConfigurationChange);
this.composition = this.openmct.composition.get(this.domainObject);
@@ -180,7 +178,7 @@ export default {
this.unwatchStaleness(combinedKey);
});
delete this.ladTelemetryObjects[ladTable.key];
this.$delete(this.ladTelemetryObjects, ladTable.key);
this.ladTableObjects.splice(index, 1);
},
reorderLadTables(reorderPlan) {

View File

@@ -75,7 +75,6 @@ describe('The LAD Table', () => {
child = document.createElement('div');
parent.appendChild(child);
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(false);
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
ladPlugin = new LadPlugin();

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
import { createOpenMct, resetApplicationState } from 'utils/testing';
xdescribe('The URLTimeSettingsSynchronizer', () => {
describe('The URLTimeSettingsSynchronizer', () => {
let appHolder;
let openmct;
let resolveFunction;

View File

@@ -171,7 +171,7 @@ xdescribe('AutoflowTabularPlugin', () => {
return [{ hint: hints[0] }];
});
view = provider.view(testObject, [testObject]);
view = provider.view(testObject);
view.show(testContainer);
return Vue.nextTick();

View File

@@ -68,12 +68,7 @@ define([], function () {
this.updateRowData.bind(this)
);
const options = {
size: 1,
strategy: 'latest',
timeContext: this.openmct.time.getContextForView([])
};
this.openmct.telemetry.request(this.domainObject, options).then(
this.openmct.telemetry.request(this.domainObject, { size: 1 }).then(
function (history) {
if (!this.initialized && history.length > 0) {
this.updateRowData(history[history.length - 1]);

View File

@@ -86,19 +86,16 @@ export default {
handler: 'updateData'
}
},
created() {
this.registerListeners();
},
mounted() {
this.plotResizeObserver.observe(this.$refs.plotWrapper);
Plotly.newPlot(this.$refs.plot, Array.from(this.data), this.getLayout(), {
responsive: true,
displayModeBar: false
});
this.registerListeners();
},
beforeUnmount() {
if (this.plotResizeObserver) {
this.plotResizeObserver.disconnect();
this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
clearTimeout(this.resizeTimer);
}
@@ -229,6 +226,7 @@ export default {
window.dispatchEvent(new Event('resize'));
}, 250);
});
this.plotResizeObserver.observe(this.$refs.plotWrapper);
}
},
reset() {

View File

@@ -43,16 +43,17 @@ export default function BarGraphViewProvider(openmct) {
return domainObject && domainObject.type === BAR_GRAPH_KEY;
},
view(domainObject, objectPath) {
view: function (domainObject, objectPath) {
let _destroy = null;
let component = null;
return {
show(element) {
show: function (element) {
let isCompact = isCompactView(objectPath);
const { vNode, destroy } = mount(
{
el: element,
components: {
BarGraphView
},
@@ -78,7 +79,7 @@ export default function BarGraphViewProvider(openmct) {
_destroy = destroy;
component = vNode.componentInstance;
},
destroy() {
destroy: function () {
_destroy();
},
onClearData() {

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