Compare commits
94 Commits
release/2.
...
v3.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
133e7c3071 | ||
|
|
f13ed680c2 | ||
|
|
23b7e92836 | ||
|
|
d4e2716298 | ||
|
|
af4ef8c0ba | ||
|
|
28e4453218 | ||
|
|
97deec2c92 | ||
|
|
4f559fdccf | ||
|
|
b923af8705 | ||
|
|
cbe49674e3 | ||
|
|
1f7dd12315 | ||
|
|
0b949d16f0 | ||
|
|
1423c23297 | ||
|
|
b043f26e49 | ||
|
|
856d88597e | ||
|
|
c3ac07ebaf | ||
|
|
2002396d0e | ||
|
|
7980abcb38 | ||
|
|
dedfd3b6f7 | ||
|
|
5b7b722ae8 | ||
|
|
f5433c0d3b | ||
|
|
5619994e83 | ||
|
|
339640e0d6 | ||
|
|
32b68cf0df | ||
|
|
60e5bbc590 | ||
|
|
492e8055e5 | ||
|
|
796616fe3f | ||
|
|
0f5d3afc4a | ||
|
|
44415b3769 | ||
|
|
f705bf9a61 | ||
|
|
50559ac502 | ||
|
|
f0ef93dd3f | ||
|
|
3ae14cf786 | ||
|
|
194eb43607 | ||
|
|
3c2b032526 | ||
|
|
d4e51cbaf1 | ||
|
|
7c58b19c3e | ||
|
|
16e1ac2529 | ||
|
|
4885c816dc | ||
|
|
42b545917c | ||
|
|
85974fc5f1 | ||
|
|
761d4ce7e4 | ||
|
|
5b1298f221 | ||
|
|
662d14354c | ||
|
|
e386036dbf | ||
|
|
6e79e5e2b0 | ||
|
|
32529ff6b2 | ||
|
|
92329b3d8e | ||
|
|
cde8fbbb0d | ||
|
|
795d7a7ec7 | ||
|
|
5031010a00 | ||
|
|
ac22bebe76 | ||
|
|
d08ea62932 | ||
|
|
293f25df19 | ||
|
|
9c22bcfb3e | ||
|
|
3b0e05ed14 | ||
|
|
ff7f55574d | ||
|
|
58f869b21b | ||
|
|
834a19f996 | ||
|
|
1d7cd64652 | ||
|
|
68ed7bf0e5 | ||
|
|
4b39ef3235 | ||
|
|
b685b9582e | ||
|
|
d8ac209a96 | ||
|
|
f254d4f078 | ||
|
|
c75a82dca5 | ||
|
|
9423591e4d | ||
|
|
5a7174bf2a | ||
|
|
d305443445 | ||
|
|
bd5cb8139c | ||
|
|
022dffd419 | ||
|
|
4c5de37cff | ||
|
|
fb5bbde154 | ||
|
|
9a01cee5fa | ||
|
|
8b2d3b0622 | ||
|
|
60df9e79c1 | ||
|
|
5a1e544a4c | ||
|
|
040ef0b998 | ||
|
|
f77287530b | ||
|
|
3cc93c0656 | ||
|
|
d71287b318 | ||
|
|
943a40680f | ||
|
|
351e6a0fbf | ||
|
|
1f514dde3d | ||
|
|
47121cfbe8 | ||
|
|
44c4d4ff47 | ||
|
|
dc1d046822 | ||
|
|
cdb20b9950 | ||
|
|
a9158a90d5 | ||
|
|
07373817b0 | ||
|
|
9247951456 | ||
|
|
47c5863edf | ||
|
|
295bfe9294 | ||
|
|
1c6214fe79 |
@@ -2,7 +2,7 @@ version: 2.1
|
||||
executors:
|
||||
pw-focal-development:
|
||||
docker:
|
||||
- image: mcr.microsoft.com/playwright:v1.32.3-focal
|
||||
- image: mcr.microsoft.com/playwright:v1.36.2-focal
|
||||
environment:
|
||||
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
|
||||
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
|
||||
@@ -162,7 +162,7 @@ jobs:
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: <<parameters.node-version>>
|
||||
- run: npx playwright@1.32.3 install #Necessary for bare ubuntu machine
|
||||
- run: npx playwright@1.36.2 install #Necessary for bare ubuntu machine
|
||||
- run: |
|
||||
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
|
||||
docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
|
||||
@@ -197,7 +197,9 @@ jobs:
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: <<parameters.node-version>>
|
||||
- run: npm run test:perf
|
||||
- run: npm run test:perf:memory
|
||||
- run: npm run test:perf:localhost
|
||||
- run: npm run test:perf:contract
|
||||
- store_test_results:
|
||||
path: test-results/results.xml
|
||||
- store_artifacts:
|
||||
@@ -213,11 +215,13 @@ jobs:
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
suite:
|
||||
type: string # ci or full
|
||||
executor: pw-focal-development
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: <<parameters.node-version>>
|
||||
- run: npm run test:e2e:visual
|
||||
- run: npm run test:e2e:visual:<<parameters.suite>>
|
||||
- store_test_results:
|
||||
path: test-results/results.xml
|
||||
- store_artifacts:
|
||||
@@ -245,6 +249,8 @@ workflows:
|
||||
- perf-test:
|
||||
node-version: lts/hydrogen
|
||||
- visual-test:
|
||||
name: visual-test-ci
|
||||
suite: ci
|
||||
node-version: lts/hydrogen
|
||||
|
||||
the-nightly: #These jobs do not run on PRs, but against master at night
|
||||
@@ -264,6 +270,8 @@ workflows:
|
||||
- perf-test:
|
||||
node-version: lts/hydrogen
|
||||
- visual-test:
|
||||
name: visual-test-nightly
|
||||
suite: full
|
||||
node-version: lts/hydrogen
|
||||
- e2e-couchdb:
|
||||
node-version: lts/hydrogen
|
||||
|
||||
499
.cspell.json
Normal file
@@ -0,0 +1,499 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -13,7 +13,7 @@ module.exports = {
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:compat/recommended',
|
||||
'plugin:vue/recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:you-dont-need-lodash-underscore/compatible',
|
||||
'plugin:prettier/recommended'
|
||||
],
|
||||
@@ -28,6 +28,10 @@ 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',
|
||||
'you-dont-need-lodash-underscore/omit': 'off',
|
||||
'you-dont-need-lodash-underscore/throttle': 'off',
|
||||
|
||||
23
.github/release.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
changelog:
|
||||
categories:
|
||||
- title: 🏕 Features
|
||||
labels:
|
||||
- type:feature
|
||||
- title: 🎉 Enhancements
|
||||
labels:
|
||||
- type:enhancement
|
||||
exclude:
|
||||
labels:
|
||||
- type:feature
|
||||
- title: 🔧 Maintenance
|
||||
labels:
|
||||
- type:maintenance
|
||||
- title: ⚡ Performance
|
||||
labels:
|
||||
- performance
|
||||
- title: 👒 Dependencies
|
||||
labels:
|
||||
- dependencies
|
||||
- title: 🐛 Bug Fixes
|
||||
labels:
|
||||
- '*'
|
||||
40
.github/workflows/e2e-couchdb.yml
vendored
@@ -1,21 +1,43 @@
|
||||
name: 'e2e-couchdb'
|
||||
on:
|
||||
push:
|
||||
branches: master
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- opened
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
jobs:
|
||||
e2e-couchdb:
|
||||
if: ${{ github.event.label.name == 'pr:e2e:couchdb' }} || ${{ github.event.action == 'opened' }}
|
||||
if: contains(github.event.pull_request.labels.*.name, 'pr:e2e:couchdb') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event.action == 'opened'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 'lts/gallium'
|
||||
- run: npx playwright@1.32.3 install
|
||||
- run: npm install
|
||||
node-version: 'lts/hydrogen'
|
||||
|
||||
- name: Cache NPM dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- run: npm install --cache ~/.npm --no-audit --progress=false
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- run: npx playwright@1.36.2 install
|
||||
|
||||
- name: Start CouchDB Docker Container and Init with Setup Scripts
|
||||
run: |
|
||||
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
|
||||
@@ -23,26 +45,32 @@ jobs:
|
||||
sleep 3
|
||||
bash src/plugins/persistence/couch/setup-couchdb.sh
|
||||
bash src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
|
||||
|
||||
- name: Run CouchDB Tests and publish to deploysentinel
|
||||
env:
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha }}
|
||||
run: npm run test:e2e:couchdb
|
||||
|
||||
- name: Publish Results to Codecov.io
|
||||
env:
|
||||
SUPER_SECRET: ${{ secrets.CODECOV_TOKEN }}
|
||||
run: npm run cov:e2e:full:publish
|
||||
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: test-results
|
||||
|
||||
- name: Archive html test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: html-test-results
|
||||
|
||||
- name: Remove pr:e2e:couchdb label (if present)
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'pr:e2e:couchdb') }}
|
||||
if: always()
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
@@ -56,5 +84,5 @@ jobs:
|
||||
name: labelToRemove
|
||||
});
|
||||
} catch (error) {
|
||||
core.warning(`Failed to remove 'pr:e2e:couchdb' label: ${error.message}`);
|
||||
core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);
|
||||
}
|
||||
|
||||
61
.github/workflows/e2e-pr.yml
vendored
@@ -1,37 +1,41 @@
|
||||
name: 'e2e-pr'
|
||||
on:
|
||||
push:
|
||||
branches: master
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- opened
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
jobs:
|
||||
e2e-full:
|
||||
if: ${{ github.event.label.name == 'pr:e2e' }}
|
||||
if: contains(github.event.pull_request.labels.*.name, 'pr:e2e') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
steps:
|
||||
- name: Trigger Success
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: "nasa",
|
||||
repo: "openmct",
|
||||
body: 'Started e2e Run. Follow along: https://github.com/nasa/openmct/actions/runs/' + context.runId
|
||||
})
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: npx playwright@1.32.3 install
|
||||
node-version: 'lts/hydrogen'
|
||||
|
||||
- name: Cache NPM dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- run: npx playwright@1.36.2 install
|
||||
- run: npx playwright install chrome-beta
|
||||
- run: npm install
|
||||
- run: npm install --cache ~/.npm --no-audit --progress=false
|
||||
- run: npm run test:e2e:full -- --max-failures=40
|
||||
- run: npm run cov:e2e:report || true
|
||||
- shell: bash
|
||||
@@ -44,30 +48,9 @@ jobs:
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: test-results
|
||||
- name: Test success
|
||||
if: ${{ success() }}
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: "nasa",
|
||||
repo: "openmct",
|
||||
body: 'Success ✅ ! Build artifacts are here: https://github.com/nasa/openmct/actions/runs/' + context.runId
|
||||
})
|
||||
- name: Test failure
|
||||
if: ${{ failure() }}
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: "nasa",
|
||||
repo: "openmct",
|
||||
body: 'Failure ❌ ! Build artifacts are here: https://github.com/nasa/openmct/actions/runs/' + context.runId
|
||||
})
|
||||
|
||||
- name: Remove pr:e2e label (if present)
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'pr:e2e') }}
|
||||
if: always()
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
@@ -81,5 +64,5 @@ jobs:
|
||||
name: labelToRemove
|
||||
});
|
||||
} catch (error) {
|
||||
core.warning(`Failed to remove 'pr:e2e' label: ${error.message}`);
|
||||
}
|
||||
core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);
|
||||
}
|
||||
4
.github/workflows/npm-prerelease.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: lts/hydrogen
|
||||
- run: npm install
|
||||
- run: |
|
||||
echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" >> ~/.npmrc
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: lts/hydrogen
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- run: npm install
|
||||
- run: npm publish --access=public --tag unstable
|
||||
|
||||
51
.github/workflows/pr-platform.yml
vendored
@@ -1,13 +1,19 @@
|
||||
name: 'pr-platform'
|
||||
on:
|
||||
push:
|
||||
branches: master
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
|
||||
types:
|
||||
- labeled
|
||||
- opened
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
jobs:
|
||||
e2e-full:
|
||||
if: ${{ github.event.label.name == 'pr:platform' }}
|
||||
pr-platform:
|
||||
if: contains(github.event.pull_request.labels.*.name, 'pr:platform') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -16,18 +22,49 @@ jobs:
|
||||
- macos-latest
|
||||
- windows-latest
|
||||
node_version:
|
||||
- 16
|
||||
- 18
|
||||
- lts/gallium
|
||||
- lts/hydrogen
|
||||
architecture:
|
||||
- x64
|
||||
|
||||
name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node_version }}
|
||||
architecture: ${{ matrix.architecture }}
|
||||
- run: npm install
|
||||
|
||||
- name: Cache NPM dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-${{ matrix.node_version }}-${{ hashFiles('**/package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ matrix.node_version }}-
|
||||
|
||||
- run: npm install --cache ~/.npm --no-audit --progress=false
|
||||
|
||||
- run: npm test
|
||||
|
||||
- run: npm run lint -- --quiet
|
||||
|
||||
- name: Remove pr:platform label (if present)
|
||||
if: always()
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo, number } = context.issue;
|
||||
const labelToRemove = 'pr:platform';
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: number,
|
||||
name: labelToRemove
|
||||
});
|
||||
} catch (error) {
|
||||
core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"trailingComma": "none",
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
"printWidth": 100,
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
|
||||
@@ -33,6 +33,16 @@ const projectRootDir = path.resolve(__dirname, '..');
|
||||
/** @type {import('webpack').Configuration} */
|
||||
const config = {
|
||||
context: projectRootDir,
|
||||
devServer: {
|
||||
client: {
|
||||
progress: true,
|
||||
overlay: {
|
||||
// Disable overlay for runtime errors.
|
||||
// See: https://github.com/webpack/webpack-dev-server/issues/4771
|
||||
runtimeErrors: false
|
||||
}
|
||||
}
|
||||
},
|
||||
entry: {
|
||||
openmct: './openmct.js',
|
||||
generatorWorker: './example/generator/generatorWorker.js',
|
||||
@@ -67,8 +77,8 @@ const config = {
|
||||
MCT: path.join(projectRootDir, 'src/MCT'),
|
||||
testUtils: path.join(projectRootDir, 'src/utils/testUtils.js'),
|
||||
objectUtils: path.join(projectRootDir, 'src/api/objects/object-utils.js'),
|
||||
kdbush: path.join(projectRootDir, 'node_modules/kdbush/kdbush.min.js'),
|
||||
utils: path.join(projectRootDir, 'src/utils')
|
||||
utils: path.join(projectRootDir, 'src/utils'),
|
||||
vue: path.join(projectRootDir, 'node_modules/@vue/compat/dist/vue.esm-bundler.js'),
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
@@ -100,6 +110,12 @@ const config = {
|
||||
new MiniCssExtractPlugin({
|
||||
filename: '[name].css',
|
||||
chunkFilename: '[name].css'
|
||||
}),
|
||||
// Add a UTF-8 BOM to CSS output to avoid random mojibake
|
||||
new webpack.BannerPlugin({
|
||||
test: /.*Theme\.css$/,
|
||||
raw: true,
|
||||
banner: '@charset "UTF-8";',
|
||||
})
|
||||
],
|
||||
module: {
|
||||
@@ -122,7 +138,16 @@ const config = {
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
use: 'vue-loader'
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
compilerOptions: {
|
||||
hoistStatic: false,
|
||||
whitespace: 'preserve',
|
||||
compatConfig: {
|
||||
MODE: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.html$/,
|
||||
|
||||
@@ -25,11 +25,6 @@ module.exports = merge(common, {
|
||||
'**/.*' // dotfiles and dotfolders
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
vue: path.join(projectRootDir, 'node_modules/vue/dist/vue.js')
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
__OPENMCT_ROOT_RELATIVE__: '"dist/"'
|
||||
@@ -50,14 +45,6 @@ module.exports = merge(common, {
|
||||
directory: path.join(__dirname, '..', '/dist'),
|
||||
publicPath: '/dist',
|
||||
watch: false
|
||||
},
|
||||
client: {
|
||||
progress: true,
|
||||
overlay: {
|
||||
// Disable overlay for runtime errors.
|
||||
// See: https://github.com/webpack/webpack-dev-server/issues/4771
|
||||
runtimeErrors: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -13,11 +13,6 @@ const projectRootDir = path.resolve(__dirname, '..');
|
||||
|
||||
module.exports = merge(common, {
|
||||
mode: 'production',
|
||||
resolve: {
|
||||
alias: {
|
||||
vue: path.join(projectRootDir, 'node_modules/vue/dist/vue.min.js')
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
__OPENMCT_ROOT_RELATIVE__: '""'
|
||||
|
||||
6
API.md
@@ -2,7 +2,7 @@
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
**Table of Contents**
|
||||
|
||||
- [Building Applications With Open MCT](#developing-applications-with-open-mct)
|
||||
- [Developing Applications With Open MCT](#developing-applications-with-open-mct)
|
||||
- [Scope and purpose of this document](#scope-and-purpose-of-this-document)
|
||||
- [Building From Source](#building-from-source)
|
||||
- [Starting an Open MCT application](#starting-an-open-mct-application)
|
||||
@@ -26,7 +26,7 @@
|
||||
- [Value Hints](#value-hints)
|
||||
- [The Time Conductor and Telemetry](#the-time-conductor-and-telemetry)
|
||||
- [Telemetry Providers](#telemetry-providers)
|
||||
- [Telemetry Requests and Responses.](#telemetry-requests-and-responses)
|
||||
- [Telemetry Requests and Responses](#telemetry-requests-and-responses)
|
||||
- [Request Strategies **draft**](#request-strategies-draft)
|
||||
- [`latest` request strategy](#latest-request-strategy)
|
||||
- [`minmax` request strategy](#minmax-request-strategy)
|
||||
@@ -873,6 +873,8 @@ function without any arguments.
|
||||
|
||||
#### Stopping an active clock
|
||||
|
||||
_As of July 2023, this method will be deprecated. Open MCT will always have a ticking clock._
|
||||
|
||||
The `stopClock` method can be used to stop an active clock, and to clear it. It
|
||||
will stop the clock from ticking, and set the active clock to `undefined`.
|
||||
|
||||
|
||||
17
e2e/.percy.ci.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
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;
|
||||
}
|
||||
17
e2e/.percy.nightly.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
version: 2
|
||||
snapshot:
|
||||
widths: [1024, 2000]
|
||||
min-height: 1440 # px
|
||||
discovery:
|
||||
concurrency: 2 # https://github.com/percy/cli/discussions/1067
|
||||
140
e2e/README.md
@@ -72,19 +72,30 @@ Visual Testing is an essential part of our e2e strategy as it ensures that the a
|
||||
For a better understanding of the visual issues which affect Open MCT, please see our bug tracker with the `label:visual` filter applied [here](https://github.com/nasa/openmct/issues?q=label%3Abug%3Avisual+)
|
||||
To read about how to write a good visual test, please see [How to write a great Visual Test](#how-to-write-a-great-visual-test).
|
||||
|
||||
`npm run test:e2e:visual` will run all of the visual tests against a local instance of Open MCT. If no `PERCY_TOKEN` API key is found in the terminal or command line environment variables, no visual comparisons will be made.
|
||||
`npm run test:e2e:visual` commands will run all of the visual tests against a local instance of Open MCT. If no `PERCY_TOKEN` API key is found in the terminal or command line environment variables, no visual comparisons will be made.
|
||||
|
||||
- `npm run test:e2e:visual:ci` will run against every commit and PR.
|
||||
- `npm run test:e2e:visual:full` will run every night with additional comparisons made for Larger Displays and with the `snow` theme.
|
||||
#### Percy.io
|
||||
|
||||
To make this possible, we're leveraging a 3rd party service, [Percy](https://percy.io/). This service maintains a copy of all changes, users, scm-metadata, and baselines to verify that the application looks and feels the same _unless approved by a Open MCT developer_. To request a Percy API token, please reach out to the Open MCT Dev team on GitHub. For more information, please see the official [Percy documentation](https://docs.percy.io/docs/visual-testing-basics)
|
||||
To make this possible, we're leveraging a 3rd party service, [Percy](https://percy.io/). This service maintains a copy of all changes, users, scm-metadata, and baselines to verify that the application looks and feels the same _unless approved by a Open MCT developer_. To request a Percy API token, please reach out to the Open MCT Dev team on GitHub. For more information, please see the official [Percy documentation](https://docs.percy.io/docs/visual-testing-basics).
|
||||
|
||||
### (Advanced) Snapshot Testing
|
||||
At present, we are using percy with two configuration files: `./e2e/.percy.nightly.yml` and `./e2e/.percy.ci.yml`. This is mainly to reduce the number of snapshots.
|
||||
|
||||
Snapshot testing is very similar to visual testing but allows us to be more precise in detecting change without relying on a 3rd party service. Unfortuantely, this precision requires advanced test setup and teardown and so we're using this pattern as a last resort.
|
||||
### Advanced: Snapshot Testing (Not Recommended)
|
||||
|
||||
To give an example, if a _single_ visual test assertion for an Overlay plot is run through multiple DOM rendering engines at various viewports to see how the Plot looks. If that same test were run as a snapshot test, it could only be executed against a single browser, on a single platform (ubuntu docker container).
|
||||
While snapshot testing offers a precise way to detect changes in your application without relying on third-party services like Percy.io, we've found that it doesn't offer any advantages over visual testing in our use-cases. Therefore, snapshot testing is **not recommended** for further implementation.
|
||||
|
||||
#### CI vs Manual Checks
|
||||
Snapshot tests can be reliably executed in Continuous Integration (CI) environments but lack the manual oversight provided by visual testing platforms like Percy.io. This means they may miss issues that a human reviewer could catch during manual checks.
|
||||
|
||||
#### Example
|
||||
A single visual test assertion in Percy.io can be executed across 10 different browser and resolution combinations without additional setup, providing comprehensive testing with minimal configuration. In contrast, a snapshot test is restricted to a single OS and browser resolution, requiring more effort to achieve the same level of coverage.
|
||||
|
||||
|
||||
#### Further Reading
|
||||
For those interested in the mechanics of snapshot testing with Playwright, you can refer to the [Playwright Snapshots Documentation](https://playwright.dev/docs/test-snapshots). However, keep in mind that we do not recommend using this approach.
|
||||
|
||||
Read more about [Playwright Snapshots](https://playwright.dev/docs/test-snapshots)
|
||||
|
||||
#### Open MCT's implementation
|
||||
|
||||
@@ -123,17 +134,17 @@ npm run test:e2e:updatesnapshots
|
||||
|
||||
## Performance Testing
|
||||
|
||||
The open source performance tests function mostly as a contract for the locator logic, functionality, and assumptions will work in our downstream, closed source test suites.
|
||||
The open source performance tests function in three ways which match their naming and folder structure:
|
||||
|
||||
They're found under `./e2e/tests/performance` and are to be executed with the following npm script:
|
||||
|
||||
`npm run test:perf`
|
||||
`./e2e/tests/performance` - The tests at the root of this folder path detect functional changes which are mostly apparent with large performance regressions like [this](https://github.com/nasa/openmct/issues/6879). These tests run against openmct webpack in `production-mode` with the `npm run test:perf:localhost` script.
|
||||
`./e2e/tests/performance/contract/` - These tests serve as [contracts](https://martinfowler.com/bliki/ContractTest.html) for the locator logic, functionality, and assumptions will work in our downstream, closed source test suites. These tests run against openmct webpack in `dev-mode` with the `npm run test:perf:contract` script.
|
||||
`./e2e/tests/performance/memory/` - These tests execute memory leak detection checks in various ways. This is expected to evolve as we move to the `memlab` project. These tests run against openmct webpack in `production-mode` with the `npm run test:perf:memory` script.
|
||||
|
||||
These tests are expected to become blocking and gating with assertions as we extend the capabilities of Playwright.
|
||||
|
||||
## Test Architecture and CI
|
||||
|
||||
### Architecture (TODO)
|
||||
### Architecture
|
||||
|
||||
### File Structure
|
||||
|
||||
@@ -147,8 +158,11 @@ Our file structure follows the type of type of testing being excercised at the e
|
||||
|`./tests/functional/example/` | Tests which specifically verify the example plugins (e.g.: Sine Wave Generator).|
|
||||
|`./tests/functional/plugins/` | Tests which loosely test each plugin. This folder is the most likely to change. Note: some `@snapshot` tests are still contained within this structure.|
|
||||
|`./tests/framework/` | Tests which verify that our testing framework's functionality and assumptions will continue to work based on further refactoring or Playwright version changes (e.g.: verifying custom fixtures and appActions).|
|
||||
|`./tests/performance/` | Performance tests.|
|
||||
|`./tests/performance/` | Performance tests which should be run on every commit.|
|
||||
|`./tests/performance/contract/` | A subset of performance tests which are designed to provide a contract between the open source tests which are run on every commit and the downstream tests which are run post merge and with other frameworks.|
|
||||
|`./tests/performance/memory` | A subset of performance tests which are designed to test for memory leaks.|
|
||||
|`./tests/visual/` | Visual tests.|
|
||||
|`./tests/visual/component/` | Visual tests which are only run against a single component.|
|
||||
|`./appActions.js` | Contains common methods which can be leveraged by test case authors to quickly move through the application when writing new tests.|
|
||||
|`./baseFixture.js` | Contains base fixtures which only extend default `@playwright/test` functionality. The expectation is that these fixtures will be removed as the native Playwright API improves|
|
||||
|
||||
@@ -165,6 +179,7 @@ Open MCT is leveraging the [config file](https://playwright.dev/docs/test-config
|
||||
|`./playwright-ci.config.js` | Used when running in CI or to debug CI issues locally|
|
||||
|`./playwright-local.config.js` | Used when running locally|
|
||||
|`./playwright-performance.config.js` | Used when running performance tests in CI or locally|
|
||||
|`./playwright-performance-devmode.config.js` | Used when running performance tests in CI or locally|
|
||||
|`./playwright-visual.config.js` | Used to run the visual tests in CI or locally|
|
||||
|
||||
#### Test Tags
|
||||
@@ -182,6 +197,7 @@ Current list of test tags:
|
||||
|`@snapshot` | Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of the playwright container.|
|
||||
|`@unstable` | A new test or test which is known to be flaky.|
|
||||
|`@2p` | Indicates that multiple users are involved, or multiple tabs/pages are used. Useful for testing multi-user interactivity.|
|
||||
|`@generatedata` | Indicates that a test is used to generate testdata or test the generated test data. Usually to be associated with localstorage, but this may grow over time.|
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
@@ -200,6 +216,7 @@ CircleCI
|
||||
- Stable e2e tests against ubuntu and chrome
|
||||
- Performance tests against ubuntu and chrome
|
||||
- e2e tests are linted
|
||||
- Visual tests are run in a single resolution on the default `espresso` theme
|
||||
|
||||
#### 2. Per-Merge Testing
|
||||
|
||||
@@ -207,18 +224,19 @@ Github Actions / Workflow
|
||||
|
||||
- Full suite against all browsers/projects. Triggered with Github Label Event 'pr:e2e'
|
||||
- CouchDB Tests. Triggered on PR Create and again with Github Label Event 'pr:e2e:couchdb'
|
||||
- Visual Tests. Triggered with Github Label Event 'pr:visual'
|
||||
|
||||
#### 3. Scheduled / Batch Testing
|
||||
|
||||
Nightly Testing in Circle CI
|
||||
|
||||
- Full e2e suite against ubuntu and chrome
|
||||
- Full e2e suite against ubuntu and chrome, firefox, and an MMOC resolution profile
|
||||
- Performance tests against ubuntu and chrome
|
||||
- CouchDB suite
|
||||
- Visual Tests are run in the full profile
|
||||
|
||||
Github Actions / Workflow
|
||||
|
||||
- Visual Test baseline generation.
|
||||
- None at the moment
|
||||
|
||||
#### Parallelism and Fast Feedback
|
||||
|
||||
@@ -250,7 +268,7 @@ A testcase and testsuite are to be unmarked as @unstable when:
|
||||
|
||||
#### **What's supported:**
|
||||
|
||||
We are leveraging the `browserslist` project to declare our supported list of browsers.
|
||||
We are leveraging the `browserslist` project to declare our supported list of browsers. We support macOS, Windows, and ubuntu 20+.
|
||||
|
||||
#### **Where it's tested:**
|
||||
|
||||
@@ -264,11 +282,17 @@ We also have the need to execute our e2e tests across this published list of bro
|
||||
- A stable version of Chromium from the official chromium channels. This is always at least 1 version ahead of desktop chrome.
|
||||
- `playwright-chrome`
|
||||
- The stable channel of Chrome from the official chrome channels. This is always 2 versions behind chromium.
|
||||
- `playwright-firefox`
|
||||
- Firefox Latest Stable. Modified slightly by the playwright team to support a CDP Shim.
|
||||
|
||||
In terms of operating system testing, we're only limited by what the CI providers are able to support. The bulk of our testing is performed on the official playwright container which is based on ubuntu. Github Actions allows us to use `windows-latest` and `mac-latest` and is run as needed.
|
||||
|
||||
#### **Mobile**
|
||||
|
||||
We have the Mission-need to support iPad. To run our iPad suite, please see our `playwright-*.config.js` with the 'iPad' project.
|
||||
|
||||
In general, our test suite is not designed to run against mobile devices as the mobile experience is a focused version of the application. Core functionality is missing (chiefly the 'Create' button) and so this will likely turn into a separate suite.
|
||||
|
||||
#### **Skipping or executing tests based on browser, os, and/os browser version:**
|
||||
|
||||
Conditionally skipping tests based on browser (**RECOMMENDED**):
|
||||
@@ -295,14 +319,27 @@ Skipping based on browser version (Rarely used): <https://github.com/microsoft/p
|
||||
|
||||
## Test Design, Best Practices, and Tips & Tricks
|
||||
|
||||
### Test Design (TODO)
|
||||
### Test Design
|
||||
|
||||
- How to make tests robust to function in other contexts (VISTA, VIPER, etc.)
|
||||
- Leverage the use of `appActions.js` methods such as `createDomainObjectWithDefaults()`
|
||||
- How to make tests faster and more resilient
|
||||
- When possible, navigate directly by URL:
|
||||
#### Test as the User
|
||||
|
||||
```javascript
|
||||
In general, strive to test only through the UI as a user would. As stated in the [Playwright Best Practices](https://playwright.dev/docs/best-practices#test-user-visible-behavior):
|
||||
|
||||
> "Automated tests should verify that the application code works for the end users, and avoid relying on implementation details such as things which users will not typically use, see, or even know about such as the name of a function, whether something is an array, or the CSS class of some element. The end user will see or interact with what is rendered on the page, so your test should typically only see/interact with the same rendered output."
|
||||
|
||||
By adhering to this principle, we can create tests that are both robust and reflective of actual user experiences.
|
||||
|
||||
#### How to make tests robust to function in other contexts (VISTA, COUCHDB, YAMCS, VIPER, etc.)
|
||||
1. Leverage the use of `appActions.js` methods such as `createDomainObjectWithDefaults()`. This ensures that your tests will create unique instances of objects for your test to interact with.
|
||||
1. Do not assert on the order or structure of objects available unless you created them yourself. These tests may be used against a persistent datastore like couchdb with many objects in the tree.
|
||||
1. Do not search for your created objects. Open MCT does not performance uniqueness checks so it's possible that your tests will break when run twice.
|
||||
1. Avoid creating locator aliases. This likely means that you're compensating for a bad locator. Improve the application instead.
|
||||
1. Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });` instead of `{ waitUntil: 'networkidle' }`. Tests run against deployments with websockets often have issues with the networkidle detection.
|
||||
|
||||
#### How to make tests faster and more resilient
|
||||
1. Avoid app interaction when possible. The best way of doing this is to navigate directly by URL:
|
||||
|
||||
```js
|
||||
// You can capture the CreatedObjectInfo returned from this appAction:
|
||||
const clock = await createDomainObjectWithDefaults(page, { type: 'Clock' });
|
||||
|
||||
@@ -310,12 +347,14 @@ Skipping based on browser version (Rarely used): <https://github.com/microsoft/p
|
||||
await page.goto(clock.url);
|
||||
```
|
||||
|
||||
- Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });`
|
||||
1. Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });`
|
||||
- Initial navigation should _almost_ always use the `{ waitUntil: 'domcontentloaded' }` option.
|
||||
- Avoid repeated setup to test to test a single assertion. Write longer tests with multiple soft assertions.
|
||||
1. Avoid repeated setup to test a single assertion. Write longer tests with multiple soft assertions.
|
||||
This ensures that your changes will be picked up with large refactors.
|
||||
|
||||
### How to write a great test (WIP)
|
||||
### How to write a great test
|
||||
|
||||
- Avoid using css locators to find elements to the page. Use modern web accessible locators like `getByRole`
|
||||
- Use our [App Actions](./appActions.js) for performing common actions whenever applicable.
|
||||
- Use `waitForPlotsToRender()` before asserting against anything that is dependent upon plot series data being loaded and drawn.
|
||||
- If you create an object outside of using the `createDomainObjectWithDefaults` App Action, make sure to fill in the 'Notes' section of your object with `page.testNotes`:
|
||||
@@ -328,7 +367,29 @@ Skipping based on browser version (Rarely used): <https://github.com/microsoft/p
|
||||
await notesInput.fill(testNotes);
|
||||
```
|
||||
|
||||
#### How to write a great visual test (TODO)
|
||||
#### How to Write a Great Visual Test
|
||||
|
||||
1. **Look for the Unknown Unknowns**: Avoid asserting on specific differences in the visual diff. Visual tests are most effective for identifying unknown unknowns.
|
||||
|
||||
2. **Get the App into Interesting States**: Prioritize getting Open MCT into unusual layouts or behaviors before capturing a visual snapshot. For instance, you could open a dropdown menu.
|
||||
|
||||
3. **Expect the Unexpected**: Use functional expect statements only to verify assumptions about the state between steps. A great visual test doesn't fail during the test itself, but rather when changes are reviewed in Percy.io.
|
||||
|
||||
4. **Control Variability**: Account for variations inherent in working with time-based telemetry and clocks.
|
||||
- Utilize `percyCSS` to ignore time-based elements. For more details, consult our [percyCSS file](./.percy.ci.yml).
|
||||
- Use Open MCT's fixed-time mode.
|
||||
- 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 network test
|
||||
|
||||
@@ -345,12 +406,35 @@ For now, our best practices exist as self-tested, living documentation in our [e
|
||||
|
||||
For best practices with regards to mocking network responses, see our [couchdb.e2e.spec.js](./tests/functional/couchdb.e2e.spec.js) file.
|
||||
|
||||
### Tips & Tricks (TODO)
|
||||
### Tips & Tricks
|
||||
|
||||
The following contains a list of tips and tricks which don't exactly fit into a FAQ or Best Practices doc.
|
||||
|
||||
- (Advanced) Overriding the Browser's Clock
|
||||
It is possible to override the browser's clock in order to control time-based elements. Since this can cause unwanted behavior (i.e. Tree not rendering), only use this sparingly. To do this, use the `overrideClock` fixture as such:
|
||||
|
||||
```js
|
||||
const { test, expect } = require('../../pluginFixtures.js');
|
||||
|
||||
test.describe('foo test suite', () => {
|
||||
|
||||
// All subsequent tests in this suite will override the clock
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: 1732413600000, // A timestamp given as milliseconds since the epoch
|
||||
shouldAdvanceTime: true // Should the clock tick?
|
||||
}
|
||||
});
|
||||
|
||||
test('bar test', async ({ page }) => {
|
||||
// ...
|
||||
});
|
||||
});
|
||||
```
|
||||
More info and options for `overrideClock` can be found in [baseFixtures.js](baseFixtures.js)
|
||||
|
||||
- Working with multiple pages
|
||||
There are instances where multiple browser pages will need to be opened to verify multi-page or multi-tab application behavior.
|
||||
There are instances where multiple browser pages will needed to verify multi-page or multi-tab application behavior. Make sure to use the `@2p` annotation as well as name each page appropriately: i.e. `page1` and `page2` or `tab1` and `tab2` depending on the intended use case. Generally pages should be used unless testing `sharedWorker` code, specifically.
|
||||
|
||||
### Reporting
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
* @property {string} type the type of domain object to create (e.g.: "Sine Wave Generator").
|
||||
* @property {string} [name] the desired name of the created domain object.
|
||||
* @property {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the Identifier or uuid of the parent object.
|
||||
* @property {Object<string, string>} [customParameters] any additional parameters to be passed to the domain object's form. E.g. '[aria-label="Data Rate (hz)"]': {'0.1'}
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -65,7 +66,10 @@ const { expect } = require('@playwright/test');
|
||||
* @param {CreateObjectOptions} options
|
||||
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
|
||||
*/
|
||||
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) {
|
||||
async function createDomainObjectWithDefaults(
|
||||
page,
|
||||
{ type, name, parent = 'mine', customParameters = {} }
|
||||
) {
|
||||
if (!name) {
|
||||
name = `${type}:${genUuid()}`;
|
||||
}
|
||||
@@ -74,7 +78,7 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
|
||||
|
||||
// Navigate to the parent object. This is necessary to create the object
|
||||
// in the correct location, such as a folder, layout, or plot.
|
||||
await page.goto(`${parentUrl}?hideTree=true`);
|
||||
await page.goto(`${parentUrl}`);
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
@@ -94,6 +98,13 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
|
||||
await notesInput.fill(page.testNotes);
|
||||
}
|
||||
|
||||
// If there are any further parameters, fill them in
|
||||
for (const [key, value] of Object.entries(customParameters)) {
|
||||
const input = page.locator(`form[name="mctForm"] ${key}`);
|
||||
await input.fill('');
|
||||
await input.fill(value);
|
||||
}
|
||||
|
||||
// Click OK button and wait for Navigate event
|
||||
await Promise.all([
|
||||
page.waitForLoadState(),
|
||||
@@ -168,7 +179,7 @@ async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
|
||||
|
||||
// Navigate to the parent object. This is necessary to create the object
|
||||
// in the correct location, such as a folder, layout, or plot.
|
||||
await page.goto(`${parentUrl}?hideTree=true`);
|
||||
await page.goto(`${parentUrl}`);
|
||||
|
||||
// Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
@@ -177,7 +188,7 @@ async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
|
||||
await page.click(`li:text("Plan")`);
|
||||
|
||||
// Modify the name input field of the domain object to accept 'name'
|
||||
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
|
||||
const nameInput = page.getByLabel('Title', { exact: true });
|
||||
await nameInput.fill('');
|
||||
await nameInput.fill(name);
|
||||
|
||||
@@ -208,6 +219,64 @@ 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.
|
||||
@@ -271,13 +340,13 @@ async function getFocusedObjectUuid(page) {
|
||||
* URLs returned will be of the form `'./browse/#/mine/<uuid0>/<uuid1>/...'`
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} uuid the uuid of the object to get the url for
|
||||
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} identifier the uuid or identifier of the object to get the url for
|
||||
* @returns {Promise<string>} the url of the object
|
||||
*/
|
||||
async function getHashUrlToDomainObject(page, uuid) {
|
||||
await page.waitForLoadState('load'); //Add some determinism
|
||||
const hashUrl = await page.evaluate(async (objectUuid) => {
|
||||
const path = await window.openmct.objects.getOriginalPath(objectUuid);
|
||||
async function getHashUrlToDomainObject(page, identifier) {
|
||||
await page.waitForLoadState('load');
|
||||
const hashUrl = await page.evaluate(async (objectIdentifier) => {
|
||||
const path = await window.openmct.objects.getOriginalPath(objectIdentifier);
|
||||
let url =
|
||||
'./#/browse/' +
|
||||
[...path]
|
||||
@@ -291,7 +360,7 @@ async function getHashUrlToDomainObject(page, uuid) {
|
||||
}
|
||||
|
||||
return url;
|
||||
}, uuid);
|
||||
}, identifier);
|
||||
|
||||
return hashUrl;
|
||||
}
|
||||
@@ -300,6 +369,7 @@ async function getHashUrlToDomainObject(page, uuid) {
|
||||
* Utilizes the OpenMCT API to detect if the UI is in Edit mode.
|
||||
* @private
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} identifier
|
||||
* @return {Promise<boolean>} true if the Open MCT is in Edit Mode
|
||||
*/
|
||||
async function _isInEditMode(page, identifier) {
|
||||
@@ -314,13 +384,13 @@ async function _isInEditMode(page, identifier) {
|
||||
*/
|
||||
async function setTimeConductorMode(page, isFixedTimespan = true) {
|
||||
// Click 'mode' button
|
||||
await page.locator('.c-mode-button').click();
|
||||
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode Menu' }).click();
|
||||
// Switch time conductor mode
|
||||
if (isFixedTimespan) {
|
||||
await page.locator('data-testid=conductor-modeOption-fixed').click();
|
||||
await page.getByRole('menuitem', { name: /Fixed Timespan/ }).click();
|
||||
} else {
|
||||
await page.locator('data-testid=conductor-modeOption-realtime').click();
|
||||
await page.getByRole('menuitem', { name: /Real-Time/ }).click();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,9 +412,12 @@ async function setRealTimeMode(page) {
|
||||
|
||||
/**
|
||||
* @typedef {Object} OffsetValues
|
||||
* @property {string | undefined} hours
|
||||
* @property {string | undefined} mins
|
||||
* @property {string | undefined} secs
|
||||
* @property {string | undefined} startHours
|
||||
* @property {string | undefined} startMins
|
||||
* @property {string | undefined} startSecs
|
||||
* @property {string | undefined} endHours
|
||||
* @property {string | undefined} endMins
|
||||
* @property {string | undefined} endSecs
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -353,23 +426,36 @@ async function setRealTimeMode(page) {
|
||||
* @param {OffsetValues} offset
|
||||
* @param {import('@playwright/test').Locator} offsetButton
|
||||
*/
|
||||
async function setTimeConductorOffset(page, { hours, mins, secs }, offsetButton) {
|
||||
await offsetButton.click();
|
||||
|
||||
if (hours) {
|
||||
await page.fill('.pr-time-controls__hrs', hours);
|
||||
async function setTimeConductorOffset(
|
||||
page,
|
||||
{ startHours, startMins, startSecs, endHours, endMins, endSecs }
|
||||
) {
|
||||
if (startHours) {
|
||||
await page.getByRole('spinbutton', { name: 'Start offset hours' }).fill(startHours);
|
||||
}
|
||||
|
||||
if (mins) {
|
||||
await page.fill('.pr-time-controls__mins', mins);
|
||||
if (startMins) {
|
||||
await page.getByRole('spinbutton', { name: 'Start offset minutes' }).fill(startMins);
|
||||
}
|
||||
|
||||
if (secs) {
|
||||
await page.fill('.pr-time-controls__secs', secs);
|
||||
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);
|
||||
}
|
||||
|
||||
// Click the check button
|
||||
await page.locator('.pr-time__buttons .icon-check').click();
|
||||
await page.locator('.pr-time-input--buttons .icon-check').click();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -378,8 +464,9 @@ async function setTimeConductorOffset(page, { hours, mins, secs }, offsetButton)
|
||||
* @param {OffsetValues} offset
|
||||
*/
|
||||
async function setStartOffset(page, offset) {
|
||||
const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
|
||||
await setTimeConductorOffset(page, offset, startOffsetButton);
|
||||
// Click 'mode' button
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||
await setTimeConductorOffset(page, offset);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -388,8 +475,73 @@ async function setStartOffset(page, offset) {
|
||||
* @param {OffsetValues} offset
|
||||
*/
|
||||
async function setEndOffset(page, offset) {
|
||||
const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
|
||||
await setTimeConductorOffset(page, offset, endOffsetButton);
|
||||
// Click 'mode' button
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).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));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -401,14 +553,7 @@ async function setEndOffset(page, offset) {
|
||||
async function selectInspectorTab(page, name) {
|
||||
const inspectorTabs = page.getByRole('tablist');
|
||||
const inspectorTab = inspectorTabs.getByTitle(name);
|
||||
const inspectorTabClass = await inspectorTab.getAttribute('class');
|
||||
const isSelectedInspectorTab = inspectorTabClass.includes('is-current');
|
||||
|
||||
// do not click a tab that is already selected or it will timeout your test
|
||||
// do to a { pointer-events: none; } on selected tabs
|
||||
if (!isSelectedInspectorTab) {
|
||||
await inspectorTab.click();
|
||||
}
|
||||
await inspectorTab.click();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -495,9 +640,25 @@ async function getCanvasPixels(page, canvasSelector) {
|
||||
return getTelemValuePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} myItemsFolderName
|
||||
* @param {string} url
|
||||
* @param {string} newName
|
||||
*/
|
||||
async function renameObjectFromContextMenu(page, url, newName) {
|
||||
await openObjectTreeContextMenu(page, url);
|
||||
await page.click('li:text("Edit Properties")');
|
||||
const nameInput = page.getByLabel('Title', { exact: true });
|
||||
await nameInput.fill('');
|
||||
await nameInput.fill(newName);
|
||||
await page.click('[aria-label="Save"]');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
module.exports = {
|
||||
createDomainObjectWithDefaults,
|
||||
createExampleTelemetryObject,
|
||||
createNotification,
|
||||
createPlanFromJSON,
|
||||
expandEntireTree,
|
||||
@@ -505,11 +666,15 @@ module.exports = {
|
||||
getCanvasPixels,
|
||||
getHashUrlToDomainObject,
|
||||
getFocusedObjectUuid,
|
||||
navigateToObjectWithFixedTimeBounds,
|
||||
openObjectTreeContextMenu,
|
||||
setFixedTimeMode,
|
||||
setRealTimeMode,
|
||||
setStartOffset,
|
||||
setEndOffset,
|
||||
setTimeConductorBounds,
|
||||
setIndependentTimeConductorBounds,
|
||||
selectInspectorTab,
|
||||
waitForPlotsToRender
|
||||
waitForPlotsToRender,
|
||||
renameObjectFromContextMenu
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
*/
|
||||
|
||||
const base = require('@playwright/test');
|
||||
const { expect } = base;
|
||||
const { expect, request } = base;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { v4: uuid } = require('uuid');
|
||||
@@ -72,8 +72,13 @@ 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,
|
||||
@@ -85,6 +90,7 @@ exports.test = base.test.extend({
|
||||
*
|
||||
* @see {@link https://github.com/microsoft/playwright/issues/6347 Github RFE}
|
||||
* @see {@link https://github.com/sinonjs/fake-timers/#var-clock--faketimersinstallconfig SinonJS FakeTimers Config}
|
||||
* @type {import('@types/sinonjs__fake-timers').FakeTimerInstallOpts}
|
||||
*/
|
||||
clockOptions: [undefined, { option: true }],
|
||||
overrideClock: [
|
||||
@@ -143,7 +149,24 @@ exports.test = base.test.extend({
|
||||
* Extends the base page class to enable console log error detection.
|
||||
* @see {@link https://github.com/microsoft/playwright/discussions/11690 Github Discussion}
|
||||
*/
|
||||
page: async ({ page, failOnConsoleError }, use) => {
|
||||
page: async ({ page, failOnConsoleError, clockOptions }, use) => {
|
||||
// If overriding the clock, we must also override the Date.now()
|
||||
// function in the generatorWorker context. This is necessary
|
||||
// to ensure that example telemetry data is generated for the new clock time.
|
||||
if (clockOptions?.now !== undefined) {
|
||||
page.on(
|
||||
'worker',
|
||||
(worker) => {
|
||||
if (worker.url().includes('generatorWorker')) {
|
||||
worker.evaluate((time) => {
|
||||
self.Date.now = () => time;
|
||||
});
|
||||
}
|
||||
},
|
||||
clockOptions.now
|
||||
);
|
||||
}
|
||||
|
||||
// Capture any console errors during test execution
|
||||
const messages = [];
|
||||
page.on('console', (msg) => messages.push(msg));
|
||||
@@ -179,4 +202,5 @@ exports.test = base.test.extend({
|
||||
});
|
||||
|
||||
exports.expect = expect;
|
||||
exports.request = request;
|
||||
exports.waitForAnimations = waitForAnimations;
|
||||
|
||||
9
e2e/constants.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* 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)
|
||||
@@ -20,9 +20,11 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { createDomainObjectWithDefaults } = require('../appActions');
|
||||
const { selectInspectorTab, 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
|
||||
@@ -62,8 +64,86 @@ async function commitEntry(page) {
|
||||
await page.locator('.c-ne__save-button > button').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function startAndAddRestrictedNotebookObject(page) {
|
||||
// eslint-disable-next-line no-undef
|
||||
await page.addInitScript({ path: path.join(__dirname, 'addInitRestrictedNotebook.js') });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
return createDomainObjectWithDefaults(page, {
|
||||
type: CUSTOM_NAME,
|
||||
name: 'Restricted Test Notebook'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function lockPage(page) {
|
||||
const commitButton = page.locator('button:has-text("Commit Entries")');
|
||||
await commitButton.click();
|
||||
|
||||
//Wait until Lock Banner is visible
|
||||
await page.locator('text=Lock Page').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a notebook object and adds an entry.
|
||||
* @param {import('@playwright/test').Page} - page to load
|
||||
* @param {number} [iterations = 1] - the number of entries to create
|
||||
*/
|
||||
async function createNotebookAndEntry(page, iterations = 1) {
|
||||
const notebook = createDomainObjectWithDefaults(page, { type: 'Notebook' });
|
||||
|
||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||
await enterTextEntry(page, `Entry ${iteration}`);
|
||||
}
|
||||
|
||||
return notebook;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a notebook object, adds an entry, and adds a tag.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} [iterations = 1] - the number of entries (and tags) to create
|
||||
*/
|
||||
async function createNotebookEntryAndTags(page, iterations = 1) {
|
||||
const notebook = await createNotebookAndEntry(page, iterations);
|
||||
await 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
|
||||
dragAndDropEmbed,
|
||||
startAndAddRestrictedNotebookObject,
|
||||
lockPage,
|
||||
createNotebookEntryAndTags,
|
||||
createNotebookAndEntry
|
||||
};
|
||||
|
||||
@@ -77,7 +77,6 @@ const config = {
|
||||
}
|
||||
],
|
||||
['junit', { outputFile: '../test-results/results.xml' }],
|
||||
['github'],
|
||||
['@deploysentinel/playwright']
|
||||
]
|
||||
};
|
||||
|
||||
@@ -2,25 +2,24 @@
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
const CI = process.env.CI === 'true';
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 1, //Only for debugging purposes for trace: 'on-first-retry'
|
||||
testDir: 'tests/performance/',
|
||||
testMatch: '*.contract.perf.spec.js', //Run everything except contract tests which require marks in dev mode
|
||||
timeout: 60 * 1000,
|
||||
workers: 1, //Only run in serial with 1 worker
|
||||
webServer: {
|
||||
command: 'npm run start', //coverage not generated
|
||||
command: 'npm run start', //need development mode for performance.marks and others
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: !CI
|
||||
reuseExistingServer: false
|
||||
},
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: CI, //Only if running locally
|
||||
ignoreHTTPSErrors: true,
|
||||
headless: true,
|
||||
ignoreHTTPSErrors: false, //HTTP performance varies!
|
||||
screenshot: 'off',
|
||||
trace: 'on-first-retry',
|
||||
video: 'off'
|
||||
@@ -28,6 +27,7 @@ const config = {
|
||||
projects: [
|
||||
{
|
||||
name: 'chrome',
|
||||
testIgnore: '*.memory.perf.spec.js', //Do not run memory tests without proper flags. Shouldn't get here
|
||||
use: {
|
||||
browserName: 'chromium'
|
||||
}
|
||||
60
e2e/playwright-performance-prod.config.js
Normal file
@@ -0,0 +1,60 @@
|
||||
/* eslint-disable no-undef */
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 0, //Only for debugging purposes for trace: 'on-first-retry'
|
||||
testDir: 'tests/performance/',
|
||||
testIgnore: '*.contract.perf.spec.js', //Run everything except contract tests which require marks in dev mode
|
||||
timeout: 60 * 1000,
|
||||
workers: 1, //Only run in serial with 1 worker
|
||||
webServer: {
|
||||
command: 'npm run start:prod', //Production mode
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: false //Must be run with this option to prevent dev mode
|
||||
},
|
||||
use: {
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: true,
|
||||
ignoreHTTPSErrors: false, //HTTP performance varies!
|
||||
screenshot: 'off',
|
||||
trace: 'on-first-retry',
|
||||
video: 'off'
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chrome-memory',
|
||||
testMatch: '*.memory.perf.spec.js', //Only run memory tests
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
launchOptions: {
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-notifications',
|
||||
'--use-fake-ui-for-media-stream',
|
||||
'--use-fake-device-for-media-stream',
|
||||
'--js-flags=--no-move-object-start --expose-gc',
|
||||
'--enable-precise-memory-info',
|
||||
'--display=:100'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'chrome',
|
||||
testIgnore: '*.memory.perf.spec.js', //Do not run memory tests without proper flags
|
||||
use: {
|
||||
browserName: 'chromium'
|
||||
}
|
||||
}
|
||||
],
|
||||
reporter: [
|
||||
['list'],
|
||||
['junit', { outputFile: '../test-results/results.xml' }],
|
||||
['json', { outputFile: '../test-results/results.json' }]
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
@@ -26,7 +26,7 @@
|
||||
* and appActions. These fixtures should be generalized across all plugins.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('./baseFixtures');
|
||||
const { test, expect, request } = require('./baseFixtures');
|
||||
// const { createDomainObjectWithDefaults } = require('./appActions');
|
||||
const path = require('path');
|
||||
|
||||
@@ -45,8 +45,6 @@ const path = require('path');
|
||||
// const createdObjects = new Map();
|
||||
|
||||
/**
|
||||
* **NOTE: This feature is a work-in-progress and should not currently be used.**
|
||||
*
|
||||
* This action will create a domain object for the test to reference and return the uuid. If an object
|
||||
* of a given name already exists, it will return the uuid of that object to the test instead of creating
|
||||
* a new file. The intent is to move object creation out of test suites which are not explicitly worried
|
||||
@@ -65,10 +63,7 @@ const path = require('path');
|
||||
|
||||
// await createDomainObjectWithDefaults(page, type, name);
|
||||
|
||||
// // Once object is created, get the uuid from the url
|
||||
// const uuid = await page.evaluate(() => {
|
||||
// return window.location.href.match(/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/)[0];
|
||||
// });
|
||||
// const uuid = getHashUrlToDomainObject(page);
|
||||
|
||||
// createdObjects.set(objectName, uuid);
|
||||
|
||||
@@ -146,7 +141,9 @@ exports.test = test.extend({
|
||||
await use({ myItemsFolderName });
|
||||
}
|
||||
});
|
||||
|
||||
exports.expect = expect;
|
||||
exports.request = request;
|
||||
|
||||
/**
|
||||
* Takes a readable stream and returns a string.
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"cookies": [],
|
||||
"origins": [
|
||||
{
|
||||
"origin": "http://localhost:8080",
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "tcHistory",
|
||||
"value": "{\"utc\":[{\"start\":1658617611983,\"end\":1658619411983}]}"
|
||||
},
|
||||
{
|
||||
"name": "mct",
|
||||
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"7fa5749b-8969-494c-9d85-c272516d333c\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1658619412848,\"modified\":1658619412848},\"7fa5749b-8969-494c-9d85-c272516d333c\":{\"identifier\":{\"key\":\"7fa5749b-8969-494c-9d85-c272516d333c\",\"namespace\":\"\"},\"name\":\"Unnamed Overlay Plot\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\",\"namespace\":\"\"}}]},\"modified\":1658619413566,\"location\":\"mine\",\"persisted\":1658619413567},\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\":{\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"identifier\":{\"key\":\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\"},\"modified\":1658619413552,\"location\":\"7fa5749b-8969-494c-9d85-c272516d333c\",\"persisted\":1658619413552}}"
|
||||
},
|
||||
{
|
||||
"name": "mct-tree-expanded",
|
||||
"value": "[\"/browse/mine\"]"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
1
e2e/test-data/memory-leak-detection.json
Normal file
26
e2e/test-data/overlay_plot_storage.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"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}}]"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
26
e2e/test-data/overlay_plot_with_delay_storage.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"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}}]"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -4,19 +4,23 @@
|
||||
{
|
||||
"origin": "http://localhost:8080",
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "tcHistory",
|
||||
"value": "{\"utc\":[{\"start\":1658617494563,\"end\":1658619294563},{\"start\":1658617090044,\"end\":1658618890044},{\"start\":1658616460484,\"end\":1658618260484},{\"start\":1658608882159,\"end\":1658610682159},{\"start\":1654537164464,\"end\":1654538964464},{\"start\":1652301954635,\"end\":1652303754635}]}"
|
||||
},
|
||||
{
|
||||
"name": "mct",
|
||||
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1658619295366,\"modified\":1658619295366},\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"73f2d9ae-d1f3-4561-b7fc-ecd5df557249\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1652303755999,\"location\":\"mine\",\"persisted\":1652303756002},\"2d02a680-eb7e-4645-bba2-dd298f76efb8\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4291d80c-303c-4d8d-85e1-10f012b864fb\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1654538965702,\"location\":\"mine\",\"persisted\":1654538965702},\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"2b6bf89f-877b-42b8-acc1-a9a575efdbe1\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658610682787,\"location\":\"mine\",\"persisted\":1658610682787},\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"b9a9c413-4b94-401d-b0c7-5e404f182616\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658618261112,\"location\":\"mine\",\"persisted\":1658618261112},\"3e294eae-6124-409b-a870-554d1bdcdd6f\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"108043b1-9c88-4e1d-8deb-fbf2cdb528f9\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658618890910,\"location\":\"mine\",\"persisted\":1658618890910},\"ec24d05d-5df5-4c96-9241-b73636cd19a9\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4062bd9b-b788-43dd-ab0a-8fa10a78d4b3\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658619295363,\"location\":\"mine\",\"persisted\":1658619295363}}"
|
||||
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1689710689554,\"modified\":1689710689554},\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"73f2d9ae-d1f3-4561-b7fc-ecd5df557249\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1652303755999,\"location\":\"mine\",\"persisted\":1652303756002},\"2d02a680-eb7e-4645-bba2-dd298f76efb8\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4291d80c-303c-4d8d-85e1-10f012b864fb\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1654538965702,\"location\":\"mine\",\"persisted\":1654538965702},\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"2b6bf89f-877b-42b8-acc1-a9a575efdbe1\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658610682787,\"location\":\"mine\",\"persisted\":1658610682787},\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"b9a9c413-4b94-401d-b0c7-5e404f182616\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658618261112,\"location\":\"mine\",\"persisted\":1658618261112},\"3e294eae-6124-409b-a870-554d1bdcdd6f\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"108043b1-9c88-4e1d-8deb-fbf2cdb528f9\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658618890910,\"location\":\"mine\",\"persisted\":1658618890910},\"ec24d05d-5df5-4c96-9241-b73636cd19a9\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4062bd9b-b788-43dd-ab0a-8fa10a78d4b3\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658619295363,\"location\":\"mine\",\"persisted\":1658619295363},\"0ec517e8-6c11-4d98-89b5-c300fe61b304\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"2f1585da-6f7e-4ccd-8a20-590fdf177b5d\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1689710689550,\"location\":\"mine\",\"created\":1689710689550,\"persisted\":1689710689550}}"
|
||||
},
|
||||
{
|
||||
"name": "mct-tree-expanded",
|
||||
"value": "[]"
|
||||
},
|
||||
{
|
||||
"name": "tcHistory",
|
||||
"value": "{\"utc\":[{\"start\":1658617494563,\"end\":1658619294563},{\"start\":1658617090044,\"end\":1658618890044},{\"start\":1658616460484,\"end\":1658618260484},{\"start\":1658608882159,\"end\":1658610682159},{\"start\":1654537164464,\"end\":1654538964464},{\"start\":1652301954635,\"end\":1652303754635}]}"
|
||||
},
|
||||
{
|
||||
"name": "mct-recent-objects",
|
||||
"value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"2f1585da-6f7e-4ccd-8a20-590fdf177b5d\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1689710689550,\"location\":\"mine\",\"created\":1689710689550,\"persisted\":1689710689550},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1689710689554,\"modified\":1689710689554},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"domainObject\":{\"identifier\":{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"2f1585da-6f7e-4ccd-8a20-590fdf177b5d\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1689710689550,\"location\":\"mine\",\"created\":1689710689550,\"persisted\":1689710689550}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1689710689554,\"modified\":1689710689554},{\"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\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1689710689554,\"modified\":1689710689554}}]"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,8 @@ relates to how we've extended it (i.e. ./e2e/baseFixtures.js) and assumptions ma
|
||||
const { test } = require('../../baseFixtures.js');
|
||||
|
||||
test.describe('baseFixtures tests', () => {
|
||||
test('Verify that tests fail if console.error is thrown', async ({ page }) => {
|
||||
//Skip this test for now https://github.com/nasa/openmct/issues/6785
|
||||
test.fixme('Verify that tests fail if console.error is thrown', async ({ page }) => {
|
||||
test.fail();
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
@@ -117,6 +117,35 @@ test.describe('Renaming Timer Object', () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The next most important concept in our testing is working with telemetry objects. Telemetry is at the core of Open MCT
|
||||
* and we have developed a great pattern for working with it.
|
||||
*/
|
||||
test.describe('Advanced: Working with telemetry objects', () => {
|
||||
let displayLayout;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Create a Display Layout with a meaningful name
|
||||
displayLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: 'Display Layout with Embedded SWG'
|
||||
});
|
||||
// Create Telemetry object within the parent object created above
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'Telemetry',
|
||||
parent: displayLayout.uuid //reference the display layout in the creation process
|
||||
});
|
||||
});
|
||||
test('Can directly navigate to a Display Layout with embedded telemetry', async ({ page }) => {
|
||||
//Now you can directly navigate to the displayLayout created in the beforeEach with the embedded telemetry
|
||||
await page.goto(displayLayout.url);
|
||||
//Expect the created Telemetry Object to be visible when directly navigating to the displayLayout
|
||||
await expect(page.getByTitle('Sine')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Structure:
|
||||
* Custom functions should be declared last.
|
||||
|
||||
260
e2e/tests/framework/generateLocalStorageData.e2e.spec.js
Normal file
@@ -0,0 +1,260 @@
|
||||
/*****************************************************************************
|
||||
* 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();
|
||||
});
|
||||
});
|
||||
@@ -1,64 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to generating LocalStorage via Session Storage to be used
|
||||
in some visual test suites like controlledClock.visual.spec.js. This suite should run to completion
|
||||
and generate an artifact named ./e2e/test-data/VisualTestData_storage.json . This will run
|
||||
on every Commit to ensure that this object still loads into tests correctly and will retain the
|
||||
.e2e.spec.js suffix.
|
||||
|
||||
TODO: Provide additional validation of object properties as it grows.
|
||||
|
||||
*/
|
||||
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions.js');
|
||||
const { test, expect } = require('../../pluginFixtures.js');
|
||||
|
||||
test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, { type: 'Overlay Plot' });
|
||||
|
||||
// click create button
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// add sine wave generator with defaults
|
||||
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
|
||||
|
||||
//Add a 5000 ms Delay
|
||||
await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000');
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('button:has-text("OK")').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
// focus the overlay plot
|
||||
await page.goto(overlayPlot.url);
|
||||
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlot.name);
|
||||
//Save localStorage for future test execution
|
||||
await context.storageState({ path: './e2e/test-data/VisualTestData_storage.json' });
|
||||
});
|
||||
@@ -41,7 +41,7 @@ test.describe('Form Validation Behavior', () => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await page.click('button:has-text("Create")');
|
||||
await page.click(':nth-match(:text("Folder"), 2)');
|
||||
await page.getByRole('menuitem', { name: ' Folder' }).click();
|
||||
|
||||
// Fill in empty string into title and trigger validation with 'Tab'
|
||||
await page.click('text=Properties Title Notes >> input[type="text"]');
|
||||
@@ -192,8 +192,12 @@ test.describe('Persistence operations @couchdb', () => {
|
||||
]);
|
||||
|
||||
//Slow down the test a bit
|
||||
await expect(page.getByRole('treeitem', { name: ` ${myItemsFolderName}` })).toBeVisible();
|
||||
await expect(page2.getByRole('treeitem', { name: ` ${myItemsFolderName}` })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: `Expand ${myItemsFolderName} folder` })
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page2.getByRole('button', { name: `Expand ${myItemsFolderName} folder` })
|
||||
).toBeVisible();
|
||||
|
||||
// Both pages: Click the Create button
|
||||
await Promise.all([
|
||||
|
||||
@@ -28,10 +28,10 @@ const { createDomainObjectWithDefaults, createNotification } = require('../../ap
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
|
||||
test.describe('Notifications List', () => {
|
||||
test('Notifications can be dismissed individually', async ({ page }) => {
|
||||
test.fixme('Notifications can be dismissed individually', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/6122'
|
||||
description: 'https://github.com/nasa/openmct/issues/6820'
|
||||
});
|
||||
|
||||
// Go to baseURL
|
||||
|
||||
124
e2e/tests/functional/planning/timelist.e2e.spec.js
Normal file
@@ -0,0 +1,124 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults, createPlanFromJSON } = require('../../../appActions');
|
||||
|
||||
const testPlan = {
|
||||
TEST_GROUP: [
|
||||
{
|
||||
name: 'Past event 1',
|
||||
start: 1660320408000,
|
||||
end: 1660343797000,
|
||||
type: 'TEST-GROUP',
|
||||
color: 'orange',
|
||||
textColor: 'white'
|
||||
},
|
||||
{
|
||||
name: 'Past event 2',
|
||||
start: 1660406808000,
|
||||
end: 1660429160000,
|
||||
type: 'TEST-GROUP',
|
||||
color: 'orange',
|
||||
textColor: 'white'
|
||||
},
|
||||
{
|
||||
name: 'Past event 3',
|
||||
start: 1660493208000,
|
||||
end: 1660503981000,
|
||||
type: 'TEST-GROUP',
|
||||
color: 'orange',
|
||||
textColor: 'white'
|
||||
},
|
||||
{
|
||||
name: 'Past event 4',
|
||||
start: 1660579608000,
|
||||
end: 1660624108000,
|
||||
type: 'TEST-GROUP',
|
||||
color: 'orange',
|
||||
textColor: 'white'
|
||||
},
|
||||
{
|
||||
name: 'Past event 5',
|
||||
start: 1660666008000,
|
||||
end: 1660681529000,
|
||||
type: 'TEST-GROUP',
|
||||
color: 'orange',
|
||||
textColor: 'white'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
test.describe('Time List', () => {
|
||||
test('Create a Time List, add a single Plan to it and verify all the activities are displayed with no milliseconds', async ({
|
||||
page
|
||||
}) => {
|
||||
// Goto baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const timelist = await test.step('Create a Time List', async () => {
|
||||
const createdTimeList = await createDomainObjectWithDefaults(page, { type: 'Time List' });
|
||||
const objectName = await page.locator('.l-browse-bar__object-name').innerText();
|
||||
expect(objectName).toBe(createdTimeList.name);
|
||||
|
||||
return createdTimeList;
|
||||
});
|
||||
|
||||
await test.step('Create a Plan and add it to the timelist', async () => {
|
||||
const createdPlan = await createPlanFromJSON(page, {
|
||||
name: 'Test Plan',
|
||||
json: testPlan
|
||||
});
|
||||
|
||||
await page.goto(timelist.url);
|
||||
// Expand the tree to show the plan
|
||||
await page.click("button[title='Show selected item in tree']");
|
||||
await page.dragAndDrop(`role=treeitem[name=/${createdPlan.name}/]`, '.c-object-view');
|
||||
await page.click("button[title='Save']");
|
||||
await page.click("li[title='Save and Finish Editing']");
|
||||
const startBound = testPlan.TEST_GROUP[0].start;
|
||||
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
|
||||
|
||||
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`
|
||||
);
|
||||
|
||||
// Verify all events are displayed
|
||||
const eventCount = await page.locator('.js-list-item').count();
|
||||
expect(eventCount).toEqual(testPlan.TEST_GROUP.length);
|
||||
});
|
||||
|
||||
await test.step('Does not show milliseconds in times', async () => {
|
||||
// Get the first activity
|
||||
const row = 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
|
||||
|
||||
await expect(row.locator('.--start')).not.toContainText('.');
|
||||
await expect(row.locator('.--end')).not.toContainText('.');
|
||||
await expect(row.locator('.--duration')).not.toContainText('.');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -21,7 +21,11 @@
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults, createPlanFromJSON } = require('../../../appActions');
|
||||
const {
|
||||
createDomainObjectWithDefaults,
|
||||
createPlanFromJSON,
|
||||
setIndependentTimeConductorBounds
|
||||
} = require('../../../appActions');
|
||||
|
||||
const testPlan = {
|
||||
TEST_GROUP: [
|
||||
@@ -78,9 +82,6 @@ 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
|
||||
@@ -122,9 +123,7 @@ test.describe('Time Strip', () => {
|
||||
});
|
||||
|
||||
await test.step('TimeStrip can use the Independent Time Conductor', async () => {
|
||||
// Activate Independent Time Conductor in Fixed Time Mode
|
||||
await page.click('.c-toggle-switch__slider');
|
||||
expect(await activityBounds.count()).toEqual(0);
|
||||
expect(await activityBounds.count()).toEqual(5);
|
||||
|
||||
// Set the independent time bounds so that only one event is shown
|
||||
const startBound = testPlan.TEST_GROUP[0].start;
|
||||
@@ -132,12 +131,7 @@ test.describe('Time Strip', () => {
|
||||
const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
|
||||
const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
|
||||
|
||||
await independentTimeConductorInputs.nth(0).fill('');
|
||||
await independentTimeConductorInputs.nth(0).fill(startBoundString);
|
||||
await page.keyboard.press('Enter');
|
||||
await independentTimeConductorInputs.nth(1).fill('');
|
||||
await independentTimeConductorInputs.nth(1).fill(endBoundString);
|
||||
await page.keyboard.press('Enter');
|
||||
await setIndependentTimeConductorBounds(page, startBoundString, endBoundString);
|
||||
expect(await activityBounds.count()).toEqual(1);
|
||||
});
|
||||
|
||||
@@ -156,9 +150,6 @@ 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);
|
||||
@@ -169,12 +160,7 @@ test.describe('Time Strip', () => {
|
||||
const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
|
||||
const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
|
||||
|
||||
await independentTimeConductorInputs.nth(0).fill('');
|
||||
await independentTimeConductorInputs.nth(0).fill(startBoundString);
|
||||
await page.keyboard.press('Enter');
|
||||
await independentTimeConductorInputs.nth(1).fill('');
|
||||
await independentTimeConductorInputs.nth(1).fill(endBoundString);
|
||||
await page.keyboard.press('Enter');
|
||||
await setIndependentTimeConductorBounds(page, startBoundString, endBoundString);
|
||||
|
||||
// Verify that two events are displayed
|
||||
expect(await activityBounds.count()).toEqual(2);
|
||||
|
||||
@@ -41,7 +41,7 @@ test.describe('Clock Generator CRUD Operations', () => {
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click Clock
|
||||
await page.click('text=Clock');
|
||||
await page.getByRole('menuitem').first().click();
|
||||
|
||||
// Click .icon-arrow-down
|
||||
await page.locator('.icon-arrow-down').click();
|
||||
|
||||
@@ -25,7 +25,8 @@ const {
|
||||
createDomainObjectWithDefaults,
|
||||
setStartOffset,
|
||||
setFixedTimeMode,
|
||||
setRealTimeMode
|
||||
setRealTimeMode,
|
||||
setIndependentTimeConductorBounds
|
||||
} = require('../../../../appActions');
|
||||
|
||||
test.describe('Display Layout', () => {
|
||||
@@ -205,6 +206,132 @@ test.describe('Display Layout', () => {
|
||||
|
||||
expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
|
||||
});
|
||||
|
||||
test('independent time works with display layouts and its children', async ({ page }) => {
|
||||
await setFixedTimeMode(page);
|
||||
// Create Example Imagery
|
||||
const exampleImageryObject = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Example Imagery'
|
||||
});
|
||||
// Create a Display Layout
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout'
|
||||
});
|
||||
// Edit Display Layout
|
||||
await page.locator('[title="Edit"]').click();
|
||||
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||
// Add the Sine Wave Generator to the Display Layout and save changes
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
const exampleImageryTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(exampleImageryObject.name)
|
||||
});
|
||||
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);
|
||||
|
||||
// check image date
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
|
||||
|
||||
// flip it off
|
||||
await page.getByRole('switch').click();
|
||||
// timestamp shouldn't be in the past anymore
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
|
||||
});
|
||||
|
||||
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'
|
||||
});
|
||||
// Create a Display Layout
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: 'Test Display Layout'
|
||||
});
|
||||
// Edit Display Layout
|
||||
await page.locator('[title="Edit"]').click();
|
||||
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||
// Add the Sine Wave Generator to the Display Layout and save changes
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
|
||||
let layoutGridHolder = page.locator('.l-layout__grid-holder');
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder, { force: true });
|
||||
|
||||
await page.getByText('View type').click();
|
||||
await page.getByText('Overlay Plot').click();
|
||||
|
||||
const anotherSineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(anotherSineWaveObject.name)
|
||||
});
|
||||
layoutGridHolder = page.locator('.l-layout__grid-holder');
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
await anotherSineWaveGeneratorTreeItem.dragTo(layoutGridHolder, { force: true });
|
||||
|
||||
await page.getByText('View type').click();
|
||||
await page.getByText('Overlay Plot').click();
|
||||
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Time to inspect some network traffic
|
||||
let networkRequests = [];
|
||||
page.on('request', (request) => {
|
||||
const searchRequest = request.url().endsWith('_find');
|
||||
const fetchRequest = request.resourceType() === 'fetch';
|
||||
if (searchRequest && fetchRequest) {
|
||||
networkRequests.push(request);
|
||||
}
|
||||
});
|
||||
|
||||
await page.reload();
|
||||
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,11 +21,18 @@
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const {
|
||||
createDomainObjectWithDefaults,
|
||||
setIndependentTimeConductorBounds
|
||||
} = 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' });
|
||||
|
||||
@@ -38,23 +45,27 @@ test.describe('Flexible Layout', () => {
|
||||
clockObject = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Clock'
|
||||
});
|
||||
|
||||
// Create a Flexible Layout
|
||||
flexibleLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Flexible Layout'
|
||||
});
|
||||
|
||||
// Define the Sine Wave Generator and Clock tree items
|
||||
treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
clockTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(clockObject.name)
|
||||
});
|
||||
});
|
||||
test('panes have the appropriate draggable attribute while in Edit and Browse modes', async ({
|
||||
page
|
||||
}) => {
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
const clockTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(clockObject.name)
|
||||
});
|
||||
// Create a Flexible Layout
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Flexible Layout'
|
||||
});
|
||||
await page.goto(flexibleLayout.url);
|
||||
// Edit Flexible Layout
|
||||
await page.locator('[title="Edit"]').click();
|
||||
|
||||
@@ -75,19 +86,79 @@ test.describe('Flexible Layout', () => {
|
||||
dragWrapper = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
|
||||
await expect(dragWrapper).toHaveAttribute('draggable', 'false');
|
||||
});
|
||||
test('changing toolbar settings in edit mode is immediately reflected and persists upon save', async ({
|
||||
page
|
||||
}) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/6942'
|
||||
});
|
||||
|
||||
await page.goto(flexibleLayout.url);
|
||||
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
|
||||
// Add the Sine Wave Generator and Clock to the Flexible Layout
|
||||
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
|
||||
await clockTreeItem.dragTo(page.locator('.c-fl__container.is-empty'));
|
||||
|
||||
// Click on the first frame to select it
|
||||
await page.locator('.c-fl-container__frame').first().click();
|
||||
await expect(page.locator('.c-fl-container__frame > .c-frame').first()).toHaveAttribute(
|
||||
's-selected',
|
||||
''
|
||||
);
|
||||
|
||||
// Assert the toolbar is visible
|
||||
await expect(page.locator('.c-toolbar')).toBeInViewport();
|
||||
|
||||
// Assert the layout is in columns orientation
|
||||
expect(await page.locator('.c-fl--rows').count()).toEqual(0);
|
||||
|
||||
// Change the layout to rows orientation
|
||||
await page.getByTitle('Columns layout').click();
|
||||
|
||||
// Assert the layout is in rows orientation
|
||||
expect(await page.locator('.c-fl--rows').count()).toBeGreaterThan(0);
|
||||
|
||||
// Assert the frame of the first item is visible
|
||||
await expect(page.locator('.c-so-view').first()).not.toHaveClass(/c-so-view--no-frame/);
|
||||
|
||||
// Hide the frame of the first item
|
||||
await page.getByTitle('Frame visible').click();
|
||||
|
||||
// Assert the frame is hidden
|
||||
await expect(page.locator('.c-so-view').first()).toHaveClass(/c-so-view--no-frame/);
|
||||
|
||||
// Assert there are 2 containers
|
||||
expect(await page.locator('.c-fl-container').count()).toEqual(2);
|
||||
|
||||
// Add a container
|
||||
await page.getByTitle('Add Container').click();
|
||||
|
||||
// Assert there are 3 containers
|
||||
expect(await page.locator('.c-fl-container').count()).toEqual(3);
|
||||
|
||||
// Save Flexible Layout
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Nav away and back
|
||||
await page.goto(sineWaveObject.url);
|
||||
await page.goto(flexibleLayout.url);
|
||||
|
||||
// Wait for the first frame to be visible so we know the layout has loaded
|
||||
await expect(page.locator('.c-fl-container').nth(0)).toBeInViewport();
|
||||
|
||||
// Assert the settings have persisted
|
||||
expect(await page.locator('.c-fl-container').count()).toEqual(3);
|
||||
expect(await page.locator('.c-fl--rows').count()).toBeGreaterThan(0);
|
||||
await expect(page.locator('.c-so-view').first()).toHaveClass(/c-so-view--no-frame/);
|
||||
});
|
||||
test('items in a flexible layout can be removed with object tree context menu when viewing the flexible layout', async ({
|
||||
page
|
||||
}) => {
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
// Create a Display Layout
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Flexible Layout'
|
||||
});
|
||||
await page.goto(flexibleLayout.url);
|
||||
// Edit Flexible Layout
|
||||
await page.locator('[title="Edit"]').click();
|
||||
|
||||
@@ -118,17 +189,7 @@ test.describe('Flexible Layout', () => {
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/3117'
|
||||
});
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
|
||||
// Create a Flexible Layout
|
||||
const flexibleLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Flexible Layout'
|
||||
});
|
||||
await page.goto(flexibleLayout.url);
|
||||
// Edit Flexible Layout
|
||||
await page.locator('[title="Edit"]').click();
|
||||
|
||||
@@ -158,4 +219,41 @@ test.describe('Flexible Layout', () => {
|
||||
// Verify that the item has been removed from the layout
|
||||
expect(await page.locator('.c-fl-container__frame').count()).toEqual(0);
|
||||
});
|
||||
|
||||
test('independent time works with flexible layouts and its children', async ({ page }) => {
|
||||
// Create Example Imagery
|
||||
const exampleImageryObject = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Example Imagery'
|
||||
});
|
||||
|
||||
await page.goto(flexibleLayout.url);
|
||||
// Edit Flexible Layout
|
||||
await page.locator('[title="Edit"]').click();
|
||||
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||
const exampleImageryTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(exampleImageryObject.name)
|
||||
});
|
||||
// Add the Sine Wave Generator to the Flexible Layout and save changes
|
||||
await exampleImageryTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
|
||||
|
||||
await page.locator('button[title="Save"]').click();
|
||||
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'
|
||||
);
|
||||
|
||||
// check image date
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
|
||||
|
||||
// flip it off
|
||||
await page.getByRole('switch').click();
|
||||
// timestamp shouldn't be in the past anymore
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,9 +27,10 @@ but only assume that example imagery is present.
|
||||
/* globals process */
|
||||
const { waitForAnimations } = require('../../../../baseFixtures');
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const { createDomainObjectWithDefaults, setRealTimeMode } = require('../../../../appActions');
|
||||
const backgroundImageSelector = '.c-imagery__main-image__background-image';
|
||||
const panHotkey = process.platform === 'linux' ? ['Shift', 'Alt'] : ['Alt'];
|
||||
const tagHotkey = ['Shift', 'Alt'];
|
||||
const expectedAltText = process.platform === 'linux' ? 'Shift+Alt drag to pan' : 'Alt drag to pan';
|
||||
const thumbnailUrlParamsRegexp = /\?w=100&h=100/;
|
||||
|
||||
@@ -44,7 +45,8 @@ 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(backgroundImageSelector).hover({ trial: true });
|
||||
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 }) => {
|
||||
@@ -69,14 +71,79 @@ test.describe('Example Imagery Object', () => {
|
||||
await dragContrastSliderAndAssertFilterValues(page);
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
// check image date
|
||||
await expect(page.getByText('2021-12-30 01:01:00.000Z').first()).toBeVisible();
|
||||
|
||||
// flip it off
|
||||
await page.getByRole('switch', { name: 'Disable Independent Time Conductor' }).click();
|
||||
// timestamp shouldn't be in the past anymore
|
||||
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();
|
||||
// check image date to be in the past
|
||||
await expect(page.getByText('2021-12-30 01:01:00.000Z').first()).toBeVisible();
|
||||
// flip it off
|
||||
await page.getByRole('switch', { name: 'Disable Independent Time Conductor' }).click();
|
||||
// timestamp shouldn't be in the past anymore
|
||||
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();
|
||||
// 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();
|
||||
// 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('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();
|
||||
});
|
||||
|
||||
test('Can use alt+drag to move around image once zoomed in', async ({ page }) => {
|
||||
const deltaYStep = 100; //equivalent to 1x zoom
|
||||
|
||||
await page.locator(backgroundImageSelector).hover({ trial: true });
|
||||
await page.locator('.c-imagery__main-image__bg').hover({ trial: true });
|
||||
|
||||
// zoom in
|
||||
await page.mouse.wheel(0, deltaYStep * 2);
|
||||
await page.locator(backgroundImageSelector).hover({ trial: true });
|
||||
await page.locator('.c-imagery__main-image__bg').hover({ trial: true });
|
||||
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
|
||||
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
|
||||
@@ -131,6 +198,36 @@ test.describe('Example Imagery Object', () => {
|
||||
expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);
|
||||
});
|
||||
|
||||
test('Can use alt+shift+drag to create a tag', async ({ page }) => {
|
||||
const canvas = page.locator('canvas');
|
||||
await canvas.hover({ trial: true });
|
||||
|
||||
const canvasBoundingBox = await canvas.boundingBox();
|
||||
const canvasCenterX = canvasBoundingBox.x + canvasBoundingBox.width / 2;
|
||||
const canvasCenterY = canvasBoundingBox.y + canvasBoundingBox.height / 2;
|
||||
|
||||
await Promise.all(tagHotkey.map((x) => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
// steps not working for me here
|
||||
await page.mouse.move(canvasCenterX - 20, canvasCenterY - 20);
|
||||
await page.mouse.move(canvasCenterX - 100, canvasCenterY - 100);
|
||||
await page.mouse.up();
|
||||
await Promise.all(tagHotkey.map((x) => page.keyboard.up(x)));
|
||||
|
||||
//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();
|
||||
});
|
||||
|
||||
test('Can use + - buttons to zoom on the image @unstable', async ({ page }) => {
|
||||
await buttonZoomOnImageAndAssert(page);
|
||||
});
|
||||
@@ -158,11 +255,9 @@ test.describe('Example Imagery Object', () => {
|
||||
test('Using the zoom features does not pause telemetry', async ({ page }) => {
|
||||
const pausePlayButton = page.locator('.c-button.pause-play');
|
||||
|
||||
// open the time conductor drop down
|
||||
await page.locator('.c-mode-button').click();
|
||||
// switch to realtime
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// Click local clock
|
||||
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
|
||||
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
|
||||
|
||||
// Zoom in via button
|
||||
@@ -172,7 +267,7 @@ test.describe('Example Imagery Object', () => {
|
||||
|
||||
test('Uses low fetch priority', async ({ page }) => {
|
||||
const priority = await page.locator('.js-imageryView-image').getAttribute('fetchpriority');
|
||||
await expect(priority).toBe('low');
|
||||
expect(priority).toBe('low');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -185,24 +280,7 @@ test.describe('Example Imagery in Display Layout', () => {
|
||||
displayLayout = await createDomainObjectWithDefaults(page, { type: 'Display Layout' });
|
||||
await page.goto(displayLayout.url);
|
||||
|
||||
/* Create Sine Wave Generator with minimum Image Load Delay */
|
||||
// Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=Example Imagery
|
||||
await page.click('li[role="menuitem"]:has-text("Example Imagery")');
|
||||
|
||||
// Clear and set Image load delay to minimum value
|
||||
await page.locator('input[type="number"]').fill('');
|
||||
await page.locator('input[type="number"]').fill('5000');
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle' }),
|
||||
page.click('button:has-text("OK")'),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
await createImageryView(page);
|
||||
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(
|
||||
'Unnamed Example Imagery'
|
||||
@@ -219,14 +297,11 @@ test.describe('Example Imagery in Display Layout', () => {
|
||||
description: 'https://github.com/nasa/openmct/issues/3647'
|
||||
});
|
||||
|
||||
// Click time conductor mode button
|
||||
await page.locator('.c-mode-button').click();
|
||||
|
||||
// set realtime mode
|
||||
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// pause/play button
|
||||
const pausePlayButton = await page.locator('.c-button.pause-play');
|
||||
const pausePlayButton = page.locator('.c-button.pause-play');
|
||||
|
||||
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
|
||||
|
||||
@@ -245,14 +320,11 @@ test.describe('Example Imagery in Display Layout', () => {
|
||||
description: 'https://github.com/nasa/openmct/issues/3647'
|
||||
});
|
||||
|
||||
// Click time conductor mode button
|
||||
await page.locator('.c-mode-button').click();
|
||||
|
||||
// set realtime mode
|
||||
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// pause/play button
|
||||
const pausePlayButton = await page.locator('.c-button.pause-play');
|
||||
const pausePlayButton = page.locator('.c-button.pause-play');
|
||||
await pausePlayButton.click();
|
||||
await expect.soft(pausePlayButton).toHaveClass(/is-paused/);
|
||||
|
||||
@@ -315,9 +387,47 @@ test.describe('Example Imagery in Display Layout', () => {
|
||||
await page.locator('div[title="Resize object height"] > input').click();
|
||||
await page.locator('div[title="Resize object height"] > input').fill('100');
|
||||
|
||||
expect(thumbsWrapperLocator.isVisible()).toBeTruthy();
|
||||
await expect(thumbsWrapperLocator).toBeVisible();
|
||||
await expect(thumbsWrapperLocator).not.toHaveClass(/is-small-thumbs/);
|
||||
});
|
||||
|
||||
/**
|
||||
* Toggle layer visibility checkbox by clicking on checkbox label
|
||||
* - should toggle checkbox and layer visibility for that image view
|
||||
* - should NOT toggle checkbox and layer visibity for the first image view in display
|
||||
*/
|
||||
test('Toggle layer visibility by clicking on label', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/6709'
|
||||
});
|
||||
await createImageryView(page);
|
||||
await page.goto(displayLayout.url);
|
||||
|
||||
const imageElements = page.locator('.c-imagery__main-image-wrapper');
|
||||
|
||||
await expect(imageElements).toHaveCount(2);
|
||||
|
||||
const imageOne = page.locator('.c-imagery__main-image-wrapper').nth(0);
|
||||
const imageTwo = page.locator('.c-imagery__main-image-wrapper').nth(1);
|
||||
const imageOneWrapper = imageOne.locator('.image-wrapper');
|
||||
const imageTwoWrapper = imageTwo.locator('.image-wrapper');
|
||||
|
||||
await imageTwo.hover();
|
||||
|
||||
await imageTwo.locator('button[title="Layers"]').click();
|
||||
|
||||
const imageTwoLayersMenuContent = imageTwo.locator('button[title="Layers"] + div');
|
||||
const imageTwoLayersToggleLabel = imageTwoLayersMenuContent.locator('label').last();
|
||||
|
||||
await imageTwoLayersToggleLabel.click();
|
||||
|
||||
const imageOneLayers = imageOneWrapper.locator('.layer-image');
|
||||
const imageTwoLayers = imageTwoWrapper.locator('.layer-image');
|
||||
|
||||
await expect(imageOneLayers).toHaveCount(0);
|
||||
await expect(imageTwoLayers).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Example Imagery in Flexible layout', () => {
|
||||
@@ -492,11 +602,8 @@ async function performImageryViewOperationsAndAssert(page) {
|
||||
const nextImageButton = page.locator('.c-nav--next');
|
||||
await nextImageButton.click();
|
||||
|
||||
// Click time conductor mode button
|
||||
await page.locator('.c-mode-button').click();
|
||||
|
||||
// Select local clock mode
|
||||
await page.locator('[data-testid=conductor-modeOption-realtime]').click();
|
||||
// set realtime mode
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// Zoom in on next image
|
||||
await mouseZoomOnImageAndAssert(page, 2);
|
||||
@@ -692,7 +799,6 @@ async function panZoomAndAssertImageProperties(page) {
|
||||
async function mouseZoomOnImageAndAssert(page, factor = 2) {
|
||||
// Zoom in
|
||||
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
|
||||
await page.locator(backgroundImageSelector).hover({ trial: true });
|
||||
const deltaYStep = 100; // equivalent to 1x zoom
|
||||
await page.mouse.wheel(0, deltaYStep * factor);
|
||||
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
@@ -703,7 +809,7 @@ async function mouseZoomOnImageAndAssert(page, factor = 2) {
|
||||
await page.mouse.move(imageCenterX, imageCenterY);
|
||||
|
||||
// Wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({ trial: true });
|
||||
await page.locator('.c-imagery__main-image__bg').hover({ trial: true });
|
||||
const imageMouseZoomed = await page.locator(backgroundImageSelector).boundingBox();
|
||||
|
||||
if (factor > 0) {
|
||||
@@ -819,3 +925,26 @@ async function resetImageryPanAndZoom(page) {
|
||||
await panZoomResetBtn.click();
|
||||
await waitForAnimations(backgroundImage);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function createImageryView(page) {
|
||||
// Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=Example Imagery
|
||||
await page.click('li[role="menuitem"]:has-text("Example Imagery")');
|
||||
|
||||
// Clear and set Image load delay to minimum value
|
||||
await page.locator('input[type="number"]').fill('');
|
||||
await page.locator('input[type="number"]').fill('5000');
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle' }),
|
||||
page.click('button:has-text("OK")'),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -51,10 +51,9 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
page.on('request', (request) => notebookElementsRequests.push(request));
|
||||
|
||||
//Clicking Add Page generates
|
||||
let [notebookUrlRequest, allDocsRequest] = await Promise.all([
|
||||
let [notebookUrlRequest] = 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"]')
|
||||
]);
|
||||
@@ -64,15 +63,13 @@ 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
|
||||
// 2) The shared worker event from 👆 request
|
||||
expect(notebookElementsRequests.length).toBe(2);
|
||||
expect(notebookElementsRequests.length).toBe(1);
|
||||
|
||||
// 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:
|
||||
|
||||
@@ -19,18 +19,18 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/* global __dirname */
|
||||
|
||||
const { test, expect, streamToString } = require('../../../../pluginFixtures');
|
||||
const { openObjectTreeContextMenu } = require('../../../../appActions');
|
||||
const {
|
||||
openObjectTreeContextMenu,
|
||||
createDomainObjectWithDefaults
|
||||
} = require('../../../../appActions');
|
||||
const path = require('path');
|
||||
const nbUtils = require('../../../../helper/notebookUtils');
|
||||
lockPage,
|
||||
dragAndDropEmbed,
|
||||
enterTextEntry,
|
||||
startAndAddRestrictedNotebookObject
|
||||
} = require('../../../../helper/notebookUtils');
|
||||
|
||||
const TEST_TEXT = 'Testing text for entries.';
|
||||
const TEST_TEXT_NAME = 'Test Page';
|
||||
const CUSTOM_NAME = 'CUSTOM_NAME';
|
||||
|
||||
test.describe('Restricted Notebook', () => {
|
||||
let notebook;
|
||||
@@ -68,7 +68,7 @@ test.describe('Restricted Notebook', () => {
|
||||
});
|
||||
|
||||
test('Can be locked if at least one page has one entry @addInit', async ({ page }) => {
|
||||
await nbUtils.enterTextEntry(page, TEST_TEXT);
|
||||
await enterTextEntry(page, TEST_TEXT);
|
||||
|
||||
const commitButton = page.locator('button:has-text("Commit Entries")');
|
||||
expect(await commitButton.count()).toEqual(1);
|
||||
@@ -79,7 +79,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
|
||||
let notebook;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
notebook = await startAndAddRestrictedNotebookObject(page);
|
||||
await nbUtils.enterTextEntry(page, TEST_TEXT);
|
||||
await enterTextEntry(page, TEST_TEXT);
|
||||
await lockPage(page);
|
||||
|
||||
// open sidebar
|
||||
@@ -125,7 +125,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
|
||||
expect.soft(newPageCount).toEqual(1);
|
||||
|
||||
// enter test text
|
||||
await nbUtils.enterTextEntry(page, TEST_TEXT);
|
||||
await enterTextEntry(page, TEST_TEXT);
|
||||
|
||||
// expect new page to be lockable
|
||||
const commitButton = page.getByRole('button', { name: ' Commit Entries' });
|
||||
@@ -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('listitem', { name: 'Delete Page' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Delete Page' }).click();
|
||||
// Click OK button
|
||||
await page.getByRole('button', { name: 'Ok' }).click();
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
|
||||
// deleted page, should no longer exist
|
||||
const deletedPageElement = page.getByText(TEST_TEXT_NAME);
|
||||
@@ -147,12 +147,12 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
|
||||
test.describe('Restricted Notebook with a page locked and with an embed @addInit', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const notebook = await startAndAddRestrictedNotebookObject(page);
|
||||
await nbUtils.dragAndDropEmbed(page, notebook);
|
||||
await dragAndDropEmbed(page, notebook);
|
||||
});
|
||||
|
||||
test('Allows embeds to be deleted if page unlocked @addInit', async ({ page }) => {
|
||||
// Click .c-ne__embed__name .c-popup-menu-button
|
||||
await page.locator('.c-ne__embed__name .c-icon-button').click(); // embed popup menu
|
||||
// Click embed popup menu
|
||||
await page.locator('.c-ne__embed__name .c-icon-button').click();
|
||||
|
||||
const embedMenu = page.locator('body >> .c-menu');
|
||||
await expect(embedMenu).toContainText('Remove This Embed');
|
||||
@@ -160,8 +160,8 @@ test.describe('Restricted Notebook with a page locked and with an embed @addInit
|
||||
|
||||
test('Disallows embeds to be deleted if page locked @addInit', async ({ page }) => {
|
||||
await lockPage(page);
|
||||
// Click .c-ne__embed__name .c-popup-menu-button
|
||||
await page.locator('.c-ne__embed__name .c-icon-button').click(); // embed popup menu
|
||||
// Click embed popup menu
|
||||
await page.locator('.c-ne__embed__name .c-icon-button').click();
|
||||
|
||||
const embedMenu = page.locator('body >> .c-menu');
|
||||
await expect(embedMenu).not.toContainText('Remove This Embed');
|
||||
@@ -174,7 +174,7 @@ test.describe('can export restricted notebook as text', () => {
|
||||
});
|
||||
|
||||
test('basic functionality ', async ({ page }) => {
|
||||
await nbUtils.enterTextEntry(page, `Foo bar entry`);
|
||||
await enterTextEntry(page, `Foo bar entry`);
|
||||
// Click on 3 Dot Menu
|
||||
await page.locator('button[title="More options"]').click();
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
@@ -182,6 +182,8 @@ test.describe('can export restricted notebook as text', () => {
|
||||
await page.getByRole('menuitem', { name: /Export Notebook as Text/ }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
//Verify exported text as a stream of text instead of a file read from the filesystem
|
||||
const download = await downloadPromise;
|
||||
const readStream = await download.createReadStream();
|
||||
const exportedText = await streamToString(readStream);
|
||||
@@ -193,26 +195,3 @@ test.describe('can export restricted notebook as text', () => {
|
||||
test.fixme('can export all notebook tags', async ({ page }) => {});
|
||||
test.fixme('can export all notebook snapshots', async ({ page }) => {});
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function startAndAddRestrictedNotebookObject(page) {
|
||||
await page.addInitScript({
|
||||
path: path.join(__dirname, '../../../../helper/', 'addInitRestrictedNotebook.js')
|
||||
});
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
return createDomainObjectWithDefaults(page, { type: CUSTOM_NAME });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function lockPage(page) {
|
||||
const commitButton = page.locator('button:has-text("Commit Entries")');
|
||||
await commitButton.click();
|
||||
|
||||
//Wait until Lock Banner is visible
|
||||
await page.locator('text=Lock Page').click();
|
||||
}
|
||||
|
||||
@@ -26,56 +26,11 @@ This test suite is dedicated to tests which verify notebook tag functionality.
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions');
|
||||
const nbUtils = require('../../../../helper/notebookUtils');
|
||||
|
||||
/**
|
||||
* Creates a notebook object and adds an entry.
|
||||
* @param {import('@playwright/test').Page} - page to load
|
||||
* @param {number} [iterations = 1] - the number of entries to create
|
||||
*/
|
||||
async function createNotebookAndEntry(page, iterations = 1) {
|
||||
const notebook = createDomainObjectWithDefaults(page, { type: 'Notebook' });
|
||||
|
||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||
await nbUtils.enterTextEntry(page, `Entry ${iteration}`);
|
||||
}
|
||||
|
||||
return notebook;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a notebook object, adds an entry, and adds a tag.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} [iterations = 1] - the number of entries (and tags) to create
|
||||
*/
|
||||
async function createNotebookEntryAndTags(page, iterations = 1) {
|
||||
const notebook = await createNotebookAndEntry(page, iterations);
|
||||
await selectInspectorTab(page, 'Annotations');
|
||||
|
||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||
// Hover and click "Add Tag" button
|
||||
// Hover is needed here to "slow down" the actions while running in headless mode
|
||||
await page.locator(`[aria-label="Notebook Entry"] >> nth = ${iteration}`).click();
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
|
||||
// Click inside the tag search input
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
// Select the "Driving" tag
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
|
||||
|
||||
// Hover and click "Add Tag" button
|
||||
// Hover is needed here to "slow down" the actions while running in headless mode
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
// Click inside the tag search input
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
// Select the "Science" tag
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
|
||||
}
|
||||
|
||||
return notebook;
|
||||
}
|
||||
const {
|
||||
enterTextEntry,
|
||||
createNotebookAndEntry,
|
||||
createNotebookEntryAndTags
|
||||
} = require('../../../../helper/notebookUtils');
|
||||
|
||||
test.describe('Tagging in Notebooks @addInit', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -112,7 +67,7 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
await createDomainObjectWithDefaults(page, { type: 'Notebook' });
|
||||
await selectInspectorTab(page, 'Annotations');
|
||||
|
||||
await nbUtils.enterTextEntry(page, '');
|
||||
await enterTextEntry(page, '');
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
|
||||
|
||||
@@ -47,6 +47,11 @@ test.describe('Operator Status', () => {
|
||||
path: path.join(__dirname, '../../../../helper/', 'addInitOperatorStatus.js')
|
||||
});
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.getByText('Select Role')).toBeVisible();
|
||||
// set role
|
||||
await page.getByRole('button', { name: 'Select' }).click();
|
||||
// dismiss role confirmation popup
|
||||
await page.getByRole('button', { name: 'Dismiss' }).click();
|
||||
});
|
||||
|
||||
// verify that operator status is visible
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
Testsuite for plot autoscale.
|
||||
*/
|
||||
|
||||
const { selectInspectorTab } = require('../../../../appActions');
|
||||
const { selectInspectorTab, setTimeConductorBounds } = 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.00', '0.50', '1.00', '1.50', '2.00', '2.50', '3.00', '3.50']);
|
||||
await testYTicks(page, ['-0.50', '0.00', '0.50', '1.00', '1.50', '2.00', '2.50', '3.00']);
|
||||
|
||||
//Wait for canvas to stablize.
|
||||
await canvas.hover({ trial: true });
|
||||
@@ -131,12 +131,7 @@ 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.
|
||||
|
||||
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);
|
||||
await setTimeConductorBounds(page, start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 20 KiB |
@@ -26,7 +26,7 @@ necessarily be used for reference when writing new tests in this area.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { selectInspectorTab } = require('../../../../appActions');
|
||||
const { selectInspectorTab, setTimeConductorBounds } = require('../../../../appActions');
|
||||
|
||||
test.describe('Log plot tests', () => {
|
||||
test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({
|
||||
@@ -87,12 +87,10 @@ 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 timeInputs = page.locator('input.c-input--datetime');
|
||||
await timeInputs.first().click();
|
||||
await timeInputs.first().fill('2022-03-29 22:00:00.000Z');
|
||||
const start = '2022-03-29 22:00:00.000Z';
|
||||
const end = '2022-03-29 22:00:30.000Z';
|
||||
|
||||
await timeInputs.nth(1).click();
|
||||
await timeInputs.nth(1).fill('2022-03-29 22:00:30.000Z');
|
||||
await setTimeConductorBounds(page, start, end);
|
||||
|
||||
// create overlay plot
|
||||
|
||||
|
||||
@@ -26,7 +26,11 @@ necessarily be used for reference when writing new tests in this area.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions');
|
||||
const {
|
||||
createDomainObjectWithDefaults,
|
||||
selectInspectorTab,
|
||||
waitForPlotsToRender
|
||||
} = require('../../../../appActions');
|
||||
|
||||
test.describe('Stacked Plot', () => {
|
||||
let stackedPlot;
|
||||
@@ -227,4 +231,45 @@ test.describe('Stacked Plot', () => {
|
||||
page.locator('[aria-label="Plot Series Properties"] .c-object-label')
|
||||
).toContainText(swgC.name);
|
||||
});
|
||||
|
||||
test('the legend toggles between aggregate and per child', async ({ page }) => {
|
||||
await page.goto(stackedPlot.url);
|
||||
|
||||
// Go into edit mode
|
||||
await page.click('button[title="Edit"]');
|
||||
|
||||
await selectInspectorTab(page, 'Config');
|
||||
|
||||
let legendProperties = await page.locator('[aria-label="Legend Properties"]');
|
||||
await legendProperties.locator('[title="Display legends per sub plot."]~div input').uncheck();
|
||||
|
||||
await assertAggregateLegendIsVisible(page);
|
||||
|
||||
// Save (exit edit mode)
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('li[title="Save and Finish Editing"]').click();
|
||||
|
||||
await assertAggregateLegendIsVisible(page);
|
||||
|
||||
await page.reload();
|
||||
|
||||
await assertAggregateLegendIsVisible(page);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Asserts that aggregate stacked plot legend is visible
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function assertAggregateLegendIsVisible(page) {
|
||||
// Wait for plot series data to load
|
||||
await waitForPlotsToRender(page);
|
||||
// Wait for plot legend to be shown
|
||||
await page.waitForSelector('.js-stacked-plot-legend', { state: 'attached' });
|
||||
// There should be 3 legend items
|
||||
expect(
|
||||
await page
|
||||
.locator('.js-stacked-plot-legend .c-plot-legend__wrapper div.plot-legend-item')
|
||||
.count()
|
||||
).toBe(3);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ const {
|
||||
createDomainObjectWithDefaults,
|
||||
setRealTimeMode,
|
||||
setFixedTimeMode,
|
||||
waitForPlotsToRender
|
||||
waitForPlotsToRender,
|
||||
selectInspectorTab
|
||||
} = require('../../../../appActions');
|
||||
|
||||
test.describe('Plot Tagging', () => {
|
||||
@@ -41,7 +42,7 @@ test.describe('Plot Tagging', () => {
|
||||
* @param {Number} yEnd a telemetry item with a plot
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function createTags({ page, canvas, xEnd, yEnd }) {
|
||||
async function createTags({ page, canvas, xEnd = 700, yEnd = 520 }) {
|
||||
await canvas.hover({ trial: true });
|
||||
|
||||
//Alt+Shift Drag Start to select some points to tag
|
||||
@@ -90,15 +91,17 @@ 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);
|
||||
|
||||
//Wait for canvas to stablize.
|
||||
await expect(canvas).toBeInViewport();
|
||||
await canvas.hover({ trial: true });
|
||||
|
||||
// click on the tagged plot point
|
||||
await canvas.click({
|
||||
position: {
|
||||
x: 325,
|
||||
y: 377
|
||||
x: 100,
|
||||
y: 100
|
||||
}
|
||||
});
|
||||
|
||||
@@ -146,7 +149,10 @@ test.describe('Plot Tagging', () => {
|
||||
// wait for plots to load
|
||||
await waitForPlotsToRender(page);
|
||||
|
||||
await page.getByText('Annotations').click();
|
||||
await expect(page.getByRole('tab', { name: 'Annotations' })).not.toHaveClass(/is-current/);
|
||||
await selectInspectorTab(page, 'Annotations');
|
||||
await expect(page.getByRole('tab', { name: 'Annotations' })).toHaveClass(/is-current/);
|
||||
|
||||
await expect(page.getByText('No tags to display for this item')).toBeVisible();
|
||||
|
||||
const canvas = page.locator('canvas').nth(1);
|
||||
@@ -167,8 +173,10 @@ test.describe('Plot Tagging', () => {
|
||||
});
|
||||
|
||||
test('Tags work with Overlay Plots', async ({ page }) => {
|
||||
//Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
|
||||
test.slow();
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/6822'
|
||||
});
|
||||
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot'
|
||||
@@ -177,13 +185,19 @@ test.describe('Plot Tagging', () => {
|
||||
const alphaSineWave = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'Alpha Sine Wave',
|
||||
parent: overlayPlot.uuid
|
||||
parent: overlayPlot.uuid,
|
||||
customParameters: {
|
||||
'[aria-label="Data Rate (hz)"]': '0.01'
|
||||
}
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'Beta Sine Wave',
|
||||
parent: overlayPlot.uuid
|
||||
parent: overlayPlot.uuid,
|
||||
customParameters: {
|
||||
'[aria-label="Data Rate (hz)"]': '0.02'
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(overlayPlot.url);
|
||||
@@ -196,9 +210,7 @@ test.describe('Plot Tagging', () => {
|
||||
|
||||
await createTags({
|
||||
page,
|
||||
canvas,
|
||||
xEnd: 700,
|
||||
yEnd: 480
|
||||
canvas
|
||||
});
|
||||
|
||||
await setFixedTimeMode(page);
|
||||
@@ -228,15 +240,15 @@ test.describe('Plot Tagging', () => {
|
||||
|
||||
test('Tags work with Plot View of telemetry items', async ({ page }) => {
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator'
|
||||
type: 'Sine Wave Generator',
|
||||
customParameters: {
|
||||
'[aria-label="Data Rate (hz)"]': '0.01'
|
||||
}
|
||||
});
|
||||
|
||||
const canvas = page.locator('canvas').nth(1);
|
||||
await createTags({
|
||||
page,
|
||||
canvas,
|
||||
xEnd: 700,
|
||||
yEnd: 480
|
||||
canvas
|
||||
});
|
||||
await basicTagsTests(page);
|
||||
});
|
||||
@@ -249,13 +261,19 @@ test.describe('Plot Tagging', () => {
|
||||
const alphaSineWave = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'Alpha Sine Wave',
|
||||
parent: stackedPlot.uuid
|
||||
parent: stackedPlot.uuid,
|
||||
customParameters: {
|
||||
'[aria-label="Data Rate (hz)"]': '0.01'
|
||||
}
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'Beta Sine Wave',
|
||||
parent: stackedPlot.uuid
|
||||
parent: stackedPlot.uuid,
|
||||
customParameters: {
|
||||
'[aria-label="Data Rate (hz)"]': '0.02'
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(stackedPlot.url);
|
||||
@@ -266,7 +284,7 @@ test.describe('Plot Tagging', () => {
|
||||
page,
|
||||
canvas,
|
||||
xEnd: 700,
|
||||
yEnd: 215
|
||||
yEnd: 240
|
||||
});
|
||||
await basicTagsTests(page);
|
||||
await testTelemetryItem(page, alphaSineWave);
|
||||
|
||||
@@ -20,7 +20,10 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const {
|
||||
createDomainObjectWithDefaults,
|
||||
setTimeConductorBounds
|
||||
} = require('../../../../appActions');
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
|
||||
test.describe('Telemetry Table', () => {
|
||||
@@ -51,18 +54,14 @@ test.describe('Telemetry Table', () => {
|
||||
await expect(tableWrapper).toHaveClass(/is-paused/);
|
||||
|
||||
// Subtract 5 minutes from the current end bound datetime and set it
|
||||
const endTimeInput = page.locator('input[type="text"].c-input--datetime').nth(1);
|
||||
await endTimeInput.click();
|
||||
|
||||
let endDate = await endTimeInput.inputValue();
|
||||
// Bring up the time conductor popup
|
||||
let endDate = await page.locator('[aria-label="End bounds"]').textContent();
|
||||
endDate = new Date(endDate);
|
||||
|
||||
endDate.setUTCMinutes(endDate.getUTCMinutes() - 5);
|
||||
endDate = endDate.toISOString().replace(/T/, ' ');
|
||||
|
||||
await endTimeInput.fill('');
|
||||
await endTimeInput.fill(endDate);
|
||||
await page.keyboard.press('Enter');
|
||||
await setTimeConductorBounds(page, undefined, endDate);
|
||||
|
||||
await expect(tableWrapper).not.toHaveClass(/is-paused/);
|
||||
|
||||
|
||||
@@ -25,7 +25,8 @@ const {
|
||||
setFixedTimeMode,
|
||||
setRealTimeMode,
|
||||
setStartOffset,
|
||||
setEndOffset
|
||||
setEndOffset,
|
||||
setTimeConductorBounds
|
||||
} = require('../../../../appActions');
|
||||
|
||||
test.describe('Time conductor operations', () => {
|
||||
@@ -40,38 +41,36 @@ test.describe('Time conductor operations', () => {
|
||||
let endDate = 'xxxx-01-01 02:00:00.000Z';
|
||||
endDate = year + endDate.substring(4);
|
||||
|
||||
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());
|
||||
await setTimeConductorBounds(page, startDate, endDate);
|
||||
|
||||
// invalid start date
|
||||
startDate = year + 1 + startDate.substring(4);
|
||||
await startTimeLocator.fill(startDate.toString());
|
||||
await endTimeLocator.click();
|
||||
await setTimeConductorBounds(page, startDate);
|
||||
|
||||
const startDateValidityStatus = await startTimeLocator.evaluate((element) =>
|
||||
// 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) =>
|
||||
element.checkValidity()
|
||||
);
|
||||
expect(startDateValidityStatus).not.toBeTruthy();
|
||||
|
||||
// fix to valid start date
|
||||
startDate = year - 1 + startDate.substring(4);
|
||||
await startTimeLocator.fill(startDate.toString());
|
||||
await setTimeConductorBounds(page, startDate);
|
||||
|
||||
// invalid end date
|
||||
endDate = year - 2 + endDate.substring(4);
|
||||
await endTimeLocator.fill(endDate.toString());
|
||||
await startTimeLocator.click();
|
||||
await setTimeConductorBounds(page, undefined, endDate);
|
||||
|
||||
const endDateValidityStatus = await endTimeLocator.evaluate((element) =>
|
||||
await startDateLocator.click();
|
||||
|
||||
const endDateValidityStatus = await endDateLocator.evaluate((element) =>
|
||||
element.checkValidity()
|
||||
);
|
||||
expect(endDateValidityStatus).not.toBeTruthy();
|
||||
@@ -83,11 +82,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 = {
|
||||
secs: '23'
|
||||
startSecs: '23'
|
||||
};
|
||||
|
||||
const endOffset = {
|
||||
secs: '31'
|
||||
endSecs: '31'
|
||||
};
|
||||
|
||||
// Go to baseURL
|
||||
@@ -100,15 +99,13 @@ 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('data-testid=conductor-start-offset-button')).toContainText(
|
||||
'00:30:23'
|
||||
);
|
||||
await expect(page.locator('.c-compact-tc__setting-value.icon-minus')).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('data-testid=conductor-end-offset-button')).toContainText('00:00:31');
|
||||
await expect(page.locator('.c-compact-tc__setting-value.icon-plus')).toContainText('00:00:31');
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -119,12 +116,12 @@ test.describe('Time conductor input fields real-time mode', () => {
|
||||
page
|
||||
}) => {
|
||||
const startOffset = {
|
||||
mins: '30',
|
||||
secs: '23'
|
||||
startMins: '30',
|
||||
startSecs: '23'
|
||||
};
|
||||
|
||||
const endOffset = {
|
||||
secs: '01'
|
||||
endSecs: '01'
|
||||
};
|
||||
|
||||
// Convert offsets to milliseconds
|
||||
@@ -150,12 +147,10 @@ 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('data-testid=conductor-start-offset-button')).toContainText(
|
||||
'00:30:23'
|
||||
);
|
||||
await expect(page.locator('.c-compact-tc__setting-value.icon-minus')).toContainText('00:30:23');
|
||||
|
||||
// Verify updated end time offset persists after mode switch
|
||||
await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01');
|
||||
await expect(page.locator('.c-compact-tc__setting-value.icon-plus')).toContainText('00:00:01');
|
||||
|
||||
// Verify url parameters persist after mode switch
|
||||
await page.waitForNavigation({ waitUntil: 'networkidle' });
|
||||
@@ -203,11 +198,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',
|
||||
{ waitUntil: 'networkidle' }
|
||||
'./#/browse/mine?view=grid&tc.mode=fixed&tc.startBound=1640995200000&tc.endBound=1640995200200&tc.timeSystem=utc&hideInspector=true'
|
||||
);
|
||||
await page.locator("[aria-label='Time Conductor History']").hover({ trial: true });
|
||||
await page.locator("[aria-label='Time Conductor History']").click();
|
||||
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();
|
||||
|
||||
// Validate history item format
|
||||
const historyItem = page.locator('text="2022-01-01 00:00:00 + 200ms"');
|
||||
|
||||
@@ -25,12 +25,15 @@ const {
|
||||
openObjectTreeContextMenu,
|
||||
createDomainObjectWithDefaults
|
||||
} = require('../../../../appActions');
|
||||
import { MISSION_TIME } from '../../../../constants';
|
||||
|
||||
test.describe('Timer', () => {
|
||||
let timer;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
|
||||
await assertTimerElements(page, timer);
|
||||
});
|
||||
|
||||
test('Can perform actions on the Timer', async ({ page }) => {
|
||||
@@ -63,6 +66,70 @@ test.describe('Timer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Timer with target date', () => {
|
||||
let timer;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
|
||||
await assertTimerElements(page, timer);
|
||||
});
|
||||
|
||||
// Override clock
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: MISSION_TIME,
|
||||
shouldAdvanceTime: true
|
||||
}
|
||||
});
|
||||
|
||||
test('Can count down to a target date', async ({ page }) => {
|
||||
// Set the target date to 2024-11-24 03:30:00
|
||||
await page.getByTitle('More options').click();
|
||||
await page.getByRole('menuitem', { name: /Edit Properties.../ }).click();
|
||||
await page.getByPlaceholder('YYYY-MM-DD').fill('2024-11-24');
|
||||
await page.locator('input[name="hour"]').fill('3');
|
||||
await page.locator('input[name="min"]').fill('30');
|
||||
await page.locator('input[name="sec"]').fill('00');
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
// Get the current timer seconds value
|
||||
const timerSecValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1);
|
||||
await expect(page.locator('.c-timer__direction')).toHaveClass(/icon-minus/);
|
||||
|
||||
// Wait for the timer to count down and assert
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const newTimerValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1);
|
||||
return Number(newTimerValue);
|
||||
})
|
||||
.toBeLessThan(Number(timerSecValue));
|
||||
});
|
||||
|
||||
test('Can count up from a target date', async ({ page }) => {
|
||||
// Set the target date to 2020-11-23 03:30:00
|
||||
await page.getByTitle('More options').click();
|
||||
await page.getByRole('menuitem', { name: /Edit Properties.../ }).click();
|
||||
await page.getByPlaceholder('YYYY-MM-DD').fill('2020-11-23');
|
||||
await page.locator('input[name="hour"]').fill('3');
|
||||
await page.locator('input[name="min"]').fill('30');
|
||||
await page.locator('input[name="sec"]').fill('00');
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
// Get the current timer seconds value
|
||||
const timerSecValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1);
|
||||
await expect(page.locator('.c-timer__direction')).toHaveClass(/icon-plus/);
|
||||
|
||||
// Wait for the timer to count up and assert
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const newTimerValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1);
|
||||
return Number(newTimerValue);
|
||||
})
|
||||
.toBeGreaterThan(Number(timerSecValue));
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Actions that can be performed on a timer from context menus.
|
||||
* @typedef {'Start' | 'Stop' | 'Pause' | 'Restart at 0'} TimerAction
|
||||
@@ -141,14 +208,17 @@ function buttonTitleFromAction(action) {
|
||||
* @param {TimerAction} action
|
||||
*/
|
||||
async function assertTimerStateAfterAction(page, action) {
|
||||
const timerValue = page.locator('.c-timer__value');
|
||||
let timerStateClass;
|
||||
switch (action) {
|
||||
case 'Start':
|
||||
case 'Restart at 0':
|
||||
timerStateClass = 'is-started';
|
||||
expect(await timerValue.innerText()).toBe('0D 00:00:00');
|
||||
break;
|
||||
case 'Stop':
|
||||
timerStateClass = 'is-stopped';
|
||||
expect(await timerValue.innerText()).toBe('--:--:--');
|
||||
break;
|
||||
case 'Pause':
|
||||
timerStateClass = 'is-paused';
|
||||
@@ -157,3 +227,25 @@ async function assertTimerStateAfterAction(page, action) {
|
||||
|
||||
await expect.soft(page.locator('.c-timer')).toHaveClass(new RegExp(timerStateClass));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that all the major components of a timer are present in the DOM.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {import('../../../../appActions').CreatedObjectInfo} timer
|
||||
*/
|
||||
async function assertTimerElements(page, timer) {
|
||||
const timerElement = page.locator('.c-timer');
|
||||
const resetButton = page.getByRole('button', { name: 'Reset' });
|
||||
const pausePlayButton = page
|
||||
.getByRole('button', { name: 'Pause' })
|
||||
.or(page.getByRole('button', { name: 'Start' }));
|
||||
const timerDirectionIcon = page.locator('.c-timer__direction');
|
||||
const timerValue = page.locator('.c-timer__value');
|
||||
|
||||
expect(await page.locator('.l-browse-bar__object-name').innerText()).toBe(timer.name);
|
||||
expect(timerElement).toBeAttached();
|
||||
expect(resetButton).toBeAttached();
|
||||
expect(pausePlayButton).toBeAttached();
|
||||
expect(timerDirectionIcon).toBeAttached();
|
||||
expect(timerValue).toBeAttached();
|
||||
}
|
||||
|
||||
@@ -62,6 +62,11 @@ 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();
|
||||
|
||||
@@ -90,7 +95,6 @@ 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
|
||||
@@ -106,6 +110,7 @@ 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
|
||||
|
||||
78
e2e/tests/functional/renaming.e2e.spec.js
Normal file
@@ -0,0 +1,78 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests for renaming objects, and their global application effects.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../baseFixtures.js');
|
||||
const {
|
||||
createDomainObjectWithDefaults,
|
||||
renameObjectFromContextMenu
|
||||
} = require('../../appActions.js');
|
||||
|
||||
test.describe('Renaming objects', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
});
|
||||
|
||||
test('When renaming objects, the browse bar and various components all update', async ({
|
||||
page
|
||||
}) => {
|
||||
const folder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder'
|
||||
});
|
||||
// Create a new 'Clock' object with default settings
|
||||
const clock = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Clock',
|
||||
parent: folder.uuid
|
||||
});
|
||||
|
||||
// Rename
|
||||
clock.name = `${clock.name}-NEW!`;
|
||||
await renameObjectFromContextMenu(page, clock.url, clock.name);
|
||||
// check inspector for new name
|
||||
const titleValue = await page
|
||||
.getByLabel('Title inspector properties')
|
||||
.getByLabel('inspector property value')
|
||||
.textContent();
|
||||
expect(titleValue).toBe(clock.name);
|
||||
// check browse bar for new name
|
||||
await expect(page.locator(`.l-browse-bar >> text=${clock.name}`)).toBeVisible();
|
||||
// check tree item for new name
|
||||
await expect(
|
||||
page.getByRole('listitem', {
|
||||
name: clock.name
|
||||
})
|
||||
).toBeVisible();
|
||||
// check recent objects for new name
|
||||
await expect(
|
||||
page.getByRole('navigation', {
|
||||
name: clock.name
|
||||
})
|
||||
).toBeVisible();
|
||||
// check title for new name
|
||||
const title = await page.title();
|
||||
expect(title).toBe(clock.name);
|
||||
});
|
||||
});
|
||||
@@ -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')).toBeHidden();
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeInViewport();
|
||||
|
||||
// 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')).toBeHidden();
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toBeInViewport();
|
||||
|
||||
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
|
||||
await page
|
||||
|
||||
398
e2e/tests/functional/tooltips.e2e.spec.js
Normal file
@@ -0,0 +1,398 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which can quickly verify that any openmct installation is
|
||||
operable and that any type of testing can proceed.
|
||||
|
||||
Ideally, smoke tests should make zero assumptions about how and where they are run. This makes them
|
||||
more resilient to change and therefor a better indicator of failure. Smoke tests will also run quickly
|
||||
as they cover a very "thin surface" of functionality.
|
||||
|
||||
When deciding between authoring new smoke tests or functional tests, ask yourself "would I feel
|
||||
comfortable running this test during a live mission?" Avoid creating or deleting Domain Objects.
|
||||
Make no assumptions about the order that elements appear in the DOM.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults, expandEntireTree } = require('../../appActions');
|
||||
|
||||
test.describe('Verify tooltips', () => {
|
||||
let folder1;
|
||||
let folder2;
|
||||
let folder3;
|
||||
let sineWaveObject1;
|
||||
let sineWaveObject2;
|
||||
let sineWaveObject3;
|
||||
|
||||
const swg1Path = 'My Items / Folder Foo / SWG 1';
|
||||
const swg2Path = 'My Items / Folder Foo / Folder Bar / SWG 2';
|
||||
const swg3Path = 'My Items / Folder Foo / Folder Bar / Folder Baz / SWG 3';
|
||||
|
||||
test.beforeEach(async ({ page, openmctConfig }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
folder1 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Folder Foo'
|
||||
});
|
||||
folder2 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Folder Bar',
|
||||
parent: folder1.uuid
|
||||
});
|
||||
folder3 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Folder Baz',
|
||||
parent: folder2.uuid
|
||||
});
|
||||
// Create Sine Wave Generator
|
||||
sineWaveObject1 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'SWG 1',
|
||||
parent: folder1.uuid
|
||||
});
|
||||
sineWaveObject1.path = swg1Path;
|
||||
sineWaveObject2 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'SWG 2',
|
||||
parent: folder2.uuid
|
||||
});
|
||||
sineWaveObject2.path = swg2Path;
|
||||
sineWaveObject3 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'SWG 3',
|
||||
parent: folder3.uuid
|
||||
});
|
||||
sineWaveObject3.path = swg3Path;
|
||||
|
||||
// Expand all folders
|
||||
await expandEntireTree(page);
|
||||
});
|
||||
|
||||
// LAD Tables - DONE
|
||||
// Expanded collapsed plot legend - DONE
|
||||
// Object Labels - DONE
|
||||
// Display Layout headers - DONE
|
||||
// Flexible Layout headers - DONE
|
||||
// Tab View layout headers - DONE
|
||||
// Search - DONE
|
||||
// Gauge -
|
||||
// Notebook Embed - DONE
|
||||
// Telemetry Table -
|
||||
// Timeline Objects
|
||||
// Tree - DONE
|
||||
// Recent Objects
|
||||
|
||||
test('display correct paths for LAD tables', async ({ page, openmctConfig }) => {
|
||||
// Create LAD table
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'LAD Table',
|
||||
name: 'Test LAD Table'
|
||||
});
|
||||
// Edit LAD table
|
||||
await page.locator('[title="Edit"]').click();
|
||||
|
||||
// Add the Sine Wave Generator to the LAD table and save changes
|
||||
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-lad-table-wrapper');
|
||||
await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.c-lad-table-wrapper');
|
||||
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-lad-table-wrapper');
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
await page.keyboard.down('Control');
|
||||
|
||||
async function getToolTip(object) {
|
||||
await page.locator('.c-create-button').hover();
|
||||
await page.getByRole('cell', { name: object.name }).hover();
|
||||
let tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
return tooltipText.replace('\n', '').trim();
|
||||
}
|
||||
|
||||
expect(await getToolTip(sineWaveObject1)).toBe(sineWaveObject1.path);
|
||||
expect(await getToolTip(sineWaveObject2)).toBe(sineWaveObject2.path);
|
||||
expect(await getToolTip(sineWaveObject3)).toBe(sineWaveObject3.path);
|
||||
});
|
||||
|
||||
test('display correct paths for expanded and collapsed plot legend items', async ({ page }) => {
|
||||
// Create Overlay Plot
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot',
|
||||
name: 'Test Overlay Plots'
|
||||
});
|
||||
// Edit Overlay Plot
|
||||
await page.locator('[title="Edit"]').click();
|
||||
|
||||
// Add the Sine Wave Generator to the LAD table and save changes
|
||||
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.gl-plot');
|
||||
await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.gl-plot');
|
||||
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.gl-plot');
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
await page.keyboard.down('Control');
|
||||
|
||||
async function getCollapsedLegendToolTip(object) {
|
||||
await page.locator('.c-create-button').hover();
|
||||
await page
|
||||
.locator('.plot-series-name', { has: page.locator(`text="${object.name} Hz"`) })
|
||||
.hover();
|
||||
let tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
return tooltipText.replace('\n', '').trim();
|
||||
}
|
||||
|
||||
async function getExpandedLegendToolTip(object) {
|
||||
await page.locator('.c-create-button').hover();
|
||||
await page
|
||||
.locator('.plot-series-name', { has: page.locator(`text="${object.name}"`) })
|
||||
.hover();
|
||||
let tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
return tooltipText.replace('\n', '').trim();
|
||||
}
|
||||
|
||||
expect(await getCollapsedLegendToolTip(sineWaveObject1)).toBe(sineWaveObject1.path);
|
||||
expect(await getCollapsedLegendToolTip(sineWaveObject2)).toBe(sineWaveObject2.path);
|
||||
expect(await getCollapsedLegendToolTip(sineWaveObject3)).toBe(sineWaveObject3.path);
|
||||
|
||||
await page.keyboard.up('Control');
|
||||
await page.locator('.gl-plot-legend__view-control.c-disclosure-triangle').click();
|
||||
await page.keyboard.down('Control');
|
||||
|
||||
expect(await getExpandedLegendToolTip(sineWaveObject1)).toBe(sineWaveObject1.path);
|
||||
expect(await getExpandedLegendToolTip(sineWaveObject2)).toBe(sineWaveObject2.path);
|
||||
expect(await getExpandedLegendToolTip(sineWaveObject3)).toBe(sineWaveObject3.path);
|
||||
});
|
||||
|
||||
test('display correct paths when hovering over object labels', async ({ page }) => {
|
||||
async function getObjectLabelTooltip(object) {
|
||||
await page
|
||||
.locator('.c-tree__item__name.c-object-label__name', {
|
||||
has: page.locator(`text="${object.name}"`)
|
||||
})
|
||||
.click();
|
||||
await page.keyboard.down('Control');
|
||||
await page
|
||||
.locator('.l-browse-bar__object-name.c-object-label__name', {
|
||||
has: page.locator(`text="${object.name}"`)
|
||||
})
|
||||
.hover();
|
||||
const tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
await page.keyboard.up('Control');
|
||||
return tooltipText.replace('\n', '').trim();
|
||||
}
|
||||
|
||||
expect(await getObjectLabelTooltip(sineWaveObject1)).toBe(sineWaveObject1.path);
|
||||
expect(await getObjectLabelTooltip(sineWaveObject3)).toBe(sineWaveObject3.path);
|
||||
});
|
||||
|
||||
test('display correct paths when hovering over display layout pane headers', async ({ page }) => {
|
||||
// Create Overlay Plot
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot',
|
||||
name: 'Test Overlay Plot'
|
||||
});
|
||||
// Edit Overlay Plot
|
||||
await page.locator('[title="Edit"]').click();
|
||||
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.gl-plot');
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Create Stacked Plot
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Stacked Plot',
|
||||
name: 'Test Stacked Plot'
|
||||
});
|
||||
// Edit Stacked Plot
|
||||
await page.locator('[title="Edit"]').click();
|
||||
await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.c-plot--stacked.holder');
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Create Display Layout
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: 'Test Display Layout'
|
||||
});
|
||||
// Edit Display Layout
|
||||
await page.locator('[title="Edit"]').click();
|
||||
|
||||
await page.dragAndDrop("text='Test Overlay Plot'", '.l-layout__grid-holder', {
|
||||
targetPosition: { x: 0, y: 0 }
|
||||
});
|
||||
await page.dragAndDrop("text='Test Stacked Plot'", '.l-layout__grid-holder', {
|
||||
targetPosition: { x: 0, y: 250 }
|
||||
});
|
||||
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.l-layout__grid-holder', {
|
||||
targetPosition: { x: 500, y: 200 }
|
||||
});
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
await page.keyboard.down('Control');
|
||||
|
||||
await page.getByText('Test Overlay Plot').nth(2).hover();
|
||||
let tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
tooltipText = tooltipText.replace('\n', '').trim();
|
||||
expect(tooltipText).toBe('My Items / Test Overlay Plot');
|
||||
|
||||
// await page.keyboard.up('Control');
|
||||
// await page.locator('.c-plot-legend__view-control >> nth=0').click();
|
||||
// await page.keyboard.down('Control');
|
||||
// await page.locator('.plot-wrapper-expanded-legend .plot-series-name').first().hover();
|
||||
// tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
// tooltipText = tooltipText.replace('\n', '').trim();
|
||||
// expect(tooltipText).toBe(sineWaveObject1.path);
|
||||
|
||||
await page.getByText('Test Stacked Plot').nth(2).hover();
|
||||
tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
tooltipText = tooltipText.replace('\n', '').trim();
|
||||
expect(tooltipText).toBe('My Items / Test Stacked Plot');
|
||||
|
||||
await page.getByText('SWG 3').nth(2).hover();
|
||||
tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
tooltipText = tooltipText.replace('\n', '').trim();
|
||||
expect(sineWaveObject3.path).toBe(tooltipText);
|
||||
});
|
||||
|
||||
test('display correct paths when hovering over flexible object labels', async ({ page }) => {
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Flexible Layout',
|
||||
name: 'Test Flexible Layout'
|
||||
});
|
||||
|
||||
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-fl__container >> nth=0');
|
||||
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-fl__container >> nth=1');
|
||||
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
await page.keyboard.down('Control');
|
||||
await page.getByText('SWG 1').nth(2).hover();
|
||||
let tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
tooltipText = tooltipText.replace('\n', '').trim();
|
||||
expect(tooltipText).toBe(sineWaveObject1.path);
|
||||
|
||||
await page.getByText('SWG 3').nth(2).hover();
|
||||
tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
tooltipText = tooltipText.replace('\n', '').trim();
|
||||
expect(tooltipText).toBe(sineWaveObject3.path);
|
||||
});
|
||||
|
||||
test('display correct paths when hovering over tab view labels', async ({ page }) => {
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Tabs View',
|
||||
name: 'Test Tabs View'
|
||||
});
|
||||
|
||||
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-tabs-view__tabs-holder');
|
||||
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-tabs-view__tabs-holder');
|
||||
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
await page.keyboard.down('Control');
|
||||
await page.getByText('SWG 1').nth(2).hover();
|
||||
let tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
tooltipText = tooltipText.replace('\n', '').trim();
|
||||
expect(tooltipText).toBe(sineWaveObject1.path);
|
||||
|
||||
await page.getByText('SWG 3').nth(2).hover();
|
||||
tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
tooltipText = tooltipText.replace('\n', '').trim();
|
||||
expect(tooltipText).toBe(sineWaveObject3.path);
|
||||
});
|
||||
|
||||
test('display correct paths when hovering tree items', async ({ page }) => {
|
||||
await page.keyboard.down('Control');
|
||||
await page.getByText('SWG 1').nth(0).hover();
|
||||
let tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
tooltipText = tooltipText.replace('\n', '').trim();
|
||||
expect(tooltipText).toBe(sineWaveObject1.path);
|
||||
|
||||
await page.getByText('SWG 3').nth(0).hover();
|
||||
tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
tooltipText = tooltipText.replace('\n', '').trim();
|
||||
expect(tooltipText).toBe(sineWaveObject3.path);
|
||||
});
|
||||
|
||||
test('display correct paths when hovering search items', async ({ page }) => {
|
||||
await page.getByRole('searchbox', { name: 'Search Input' }).click();
|
||||
await page.fill('.c-search__input', 'SWG 3');
|
||||
|
||||
await page.keyboard.down('Control');
|
||||
await page.locator('.c-gsearch-result__title').hover();
|
||||
let tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
tooltipText = tooltipText.replace('\n', '').trim();
|
||||
expect(tooltipText).toBe(sineWaveObject3.path);
|
||||
});
|
||||
|
||||
test('display path for source telemetry when hovering over gauge', ({ page }) => {
|
||||
expect(true).toBe(true);
|
||||
// await createDomainObjectWithDefaults(page, {
|
||||
// type: 'Gauge',
|
||||
// name: 'Test Gauge'
|
||||
// });
|
||||
// await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-gauge__wrapper');
|
||||
// await page.keyboard.down('Control');
|
||||
// await page.locator('.c-gauge__current-value-text-wrapper').hover();
|
||||
// let tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
// tooltipText = tooltipText.replace('\n', '').trim();
|
||||
// expect(tooltipText).toBe(sineWaveObject3.path);
|
||||
});
|
||||
|
||||
test('display tooltip path for notebook embeds', async ({ page }) => {
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Notebook',
|
||||
name: 'Test Notebook'
|
||||
});
|
||||
|
||||
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-notebook__drag-area');
|
||||
await page.keyboard.down('Control');
|
||||
await page.locator('.c-ne__embed').hover();
|
||||
let tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
tooltipText = tooltipText.replace('\n', '').trim();
|
||||
expect(tooltipText).toBe(sineWaveObject3.path);
|
||||
});
|
||||
|
||||
// test('display tooltip path for telemetry table names', async ({ page }) => {
|
||||
// await setEndOffset(page, { secs: '10' });
|
||||
// await createDomainObjectWithDefaults(page, {
|
||||
// type: 'Telemetry Table',
|
||||
// name: 'Test Telemetry Table'
|
||||
// });
|
||||
|
||||
// await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-telemetry-table');
|
||||
// await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-telemetry-table');
|
||||
|
||||
// await page.locator('button[title="Save"]').click();
|
||||
// await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// // .c-telemetry-table__body
|
||||
|
||||
// await page.keyboard.down('Control');
|
||||
|
||||
// await page.locator('.noselect > [title="SWG 3"]').first().hover();
|
||||
// let tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
// tooltipText = tooltipText.replace('\n', '').trim();
|
||||
// expect(tooltipText).toBe(sineWaveObject3.path);
|
||||
// });
|
||||
});
|
||||
@@ -23,7 +23,7 @@
|
||||
const { test, expect } = require('../../pluginFixtures.js');
|
||||
const {
|
||||
createDomainObjectWithDefaults,
|
||||
openObjectTreeContextMenu
|
||||
renameObjectFromContextMenu
|
||||
} = require('../../appActions.js');
|
||||
|
||||
test.describe('Main Tree', () => {
|
||||
@@ -174,6 +174,42 @@ test.describe('Main Tree', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
test('Opening and closing an item before the request has been fulfilled will abort the request @couchdb', async ({
|
||||
page,
|
||||
openmctConfig
|
||||
}) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
let requestWasAborted = false;
|
||||
|
||||
page.on('requestfailed', (request) => {
|
||||
// check if the request was aborted
|
||||
if (request.failure().errorText === 'net::ERR_ABORTED') {
|
||||
requestWasAborted = true;
|
||||
}
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Foo'
|
||||
});
|
||||
|
||||
// Intercept and delay request
|
||||
const delayInMs = 500;
|
||||
|
||||
await page.route('**', async (route, request) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, delayInMs));
|
||||
route.continue();
|
||||
});
|
||||
|
||||
// Quickly Expand/close the root folder
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: `Expand ${myItemsFolderName} folder`
|
||||
})
|
||||
.dblclick({ delay: 400 });
|
||||
|
||||
expect(requestWasAborted).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -213,18 +249,3 @@ async function expandTreePaneItemByName(page, name) {
|
||||
});
|
||||
await treeItem.locator('.c-disclosure-triangle').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} myItemsFolderName
|
||||
* @param {string} url
|
||||
* @param {string} newName
|
||||
*/
|
||||
async function renameObjectFromContextMenu(page, url, newName) {
|
||||
await openObjectTreeContextMenu(page, url);
|
||||
await page.click('li:text("Edit Properties")');
|
||||
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
|
||||
await nameInput.fill('');
|
||||
await nameInput.fill(newName);
|
||||
await page.click('[aria-label="Save"]');
|
||||
}
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is an initial example for memory leak testing using performance. This configuration and execution must
|
||||
be kept separate from the traditional performance measurements to avoid any "observer" effects associated with tracing
|
||||
or profiling playwright and/or the browser.
|
||||
|
||||
Based on a pattern identified in https://github.com/trentmwillis/devtools-protocol-demos/blob/master/testing-demos/memory-leak-by-heap.js
|
||||
and https://github.com/paulirish/automated-chrome-profiling/issues/3
|
||||
|
||||
Best path forward: https://github.com/cowchimp/headless-devtools/blob/master/src/Memory/example.js
|
||||
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
|
||||
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.describe.skip('Memory Performance tests', () => {
|
||||
test.beforeEach(async ({ page, browser }, testInfo) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click a:has-text("My Items")
|
||||
await page.locator('a:has-text("My Items")').click({
|
||||
button: 'right'
|
||||
});
|
||||
|
||||
// Click text=Import from JSON
|
||||
await page.locator('text=Import from JSON').click();
|
||||
|
||||
// Upload Performance Display Layout.json
|
||||
await page.setInputFiles('#fileElem', filePath);
|
||||
|
||||
// Click text=OK
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
await expect(
|
||||
page.locator('a:has-text("Performance Display Layout Display Layout")')
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// To to Search Available after Launch
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
// Fill Search input
|
||||
await page
|
||||
.locator('[aria-label="OpenMCT Search"] input[type="search"]')
|
||||
.fill('Performance Display Layout');
|
||||
//Search Result Appears and is clicked
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('a:has-text("Performance Display Layout")').first().click()
|
||||
]);
|
||||
|
||||
//Time to Example Imagery Frame loads within Display Layout
|
||||
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' });
|
||||
//Time to Example Imagery object loads
|
||||
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' });
|
||||
|
||||
const client = await page.context().newCDPSession(page);
|
||||
await client.send('HeapProfiler.enable');
|
||||
await client.send('HeapProfiler.startSampling');
|
||||
// await client.send('HeapProfiler.collectGarbage');
|
||||
await client.send('Performance.enable');
|
||||
|
||||
let performanceMetricsBefore = await client.send('Performance.getMetrics');
|
||||
console.log(performanceMetricsBefore.metrics);
|
||||
|
||||
//await client.send('Performance.disable');
|
||||
|
||||
//Open Large view
|
||||
await page.locator('button:has-text("Large View")').click();
|
||||
await client.send('HeapProfiler.takeHeapSnapshot');
|
||||
|
||||
//Time to Imagery Rendered in Large Frame
|
||||
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' });
|
||||
|
||||
//Time to Example Imagery object loads
|
||||
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' });
|
||||
|
||||
// Click Close Icon
|
||||
await page.locator('.c-click-icon').click();
|
||||
|
||||
//Time to Example Imagery Frame loads within Display Layout
|
||||
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' });
|
||||
//Time to Example Imagery object loads
|
||||
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' });
|
||||
|
||||
await client.send('HeapProfiler.collectGarbage');
|
||||
//await client.send('Performance.enable');
|
||||
|
||||
let performanceMetricsAfter = await client.send('Performance.getMetrics');
|
||||
console.log(performanceMetricsAfter.metrics);
|
||||
|
||||
//await client.send('Performance.disable');
|
||||
});
|
||||
});
|
||||
299
e2e/tests/performance/memory/navigation.memory.perf.spec.js
Normal file
@@ -0,0 +1,299 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
const memoryLeakFilePath = 'e2e/test-data/memory-leak-detection.json';
|
||||
/**
|
||||
* Executes tests to verify that views are not leaking memory on navigation away. This sort of
|
||||
* memory leak is generally caused by a failure to clean up registered listeners.
|
||||
*
|
||||
* These tests are executed on a set of pre-built displays loaded from ../test-data/memory-leak-detection.json.
|
||||
*
|
||||
* In order to modify the test data set:
|
||||
* 1. Run Open MCT locally (npm start)
|
||||
* 2. Right click on a folder in the tree, and select "Import From JSON"
|
||||
* 3. In the subsequent dialog, select the file ../test-data/memory-leak-detection.json
|
||||
* 4. Click "OK"
|
||||
* 5. Modify test objects as desired
|
||||
* 6. Right click on the "Memory Leak Detection" folder, and select "Export to JSON"
|
||||
* 7. Copy the exported file to ../test-data/memory-leak-detection.json
|
||||
*
|
||||
*/
|
||||
|
||||
const NAV_LEAK_TIMEOUT = 10 * 1000; // 10s
|
||||
test.describe('Navigation memory leak is not detected in', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await page.locator('a:has-text("My Items")').click({
|
||||
button: 'right'
|
||||
});
|
||||
|
||||
await page.locator('text=Import from JSON').click();
|
||||
|
||||
// Upload memory-leak-detection.json
|
||||
await page.setInputFiles('#fileElem', memoryLeakFilePath);
|
||||
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
await expect(page.locator('a:has-text("Memory Leak Detection")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('plot view', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(page, 'overlay-plot-single-1hz-swg', {
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
});
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('stacked plot view', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(page, 'stacked-plot-single-1hz-swg', {
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
});
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('LAD table view', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-single-1hz-swg', {
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
});
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('LAD table set', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-set-single-1hz-swg', {
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
});
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
//TODO: Figure out why using the `table-row` component inside the `table` component leaks TelemetryTableRow objects
|
||||
test('telemetry table view', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'telemetry-table-single-1hz-swg',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
}
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
//TODO: Figure out why using the `SideBar` component inside the leaks Notebook objects
|
||||
test('notebook view', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'notebook-memory-leak-detection-test',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
}
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('display layout of a single SWG alphanumeric', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'display-layout-single-1hz-swg',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
}
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('display layout of a single SWG plot', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'display-layout-single-overlay-plot',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
}
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
//TODO: Figure out why `svg` in the CompassRose component leaks imagery
|
||||
test('example imagery view', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'example-imagery-memory-leak-test',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
|
||||
}
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('display layout of example imagery views', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'display-layout-images-memory-leak-test',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
|
||||
}
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('display layout with plots of swgs, alphanumerics, and condition sets, ', async ({
|
||||
page
|
||||
}) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'display-layout-simple-telemetry',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
|
||||
}
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('flexible layout with plots of swgs', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'flexible-layout-plots-memory-leak-test',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
}
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('flexible layout of example imagery views', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'flexible-layout-images-memory-leak-test',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
|
||||
}
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('tabbed view of display layouts and time strips', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'tab-view-simple-memory-leak-test',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT * 6 * 2 // 2 min
|
||||
}
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('time strip view of telemetry', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'time-strip-telemetry-memory-leak-test',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
|
||||
}
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {*} objectName
|
||||
* @returns
|
||||
*/
|
||||
async function navigateToObjectAndDetectMemoryLeak(page, objectName) {
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
// Fill Search input
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill(objectName);
|
||||
|
||||
//Search Result Appears and is clicked
|
||||
await Promise.all([
|
||||
page.locator(`div.c-gsearch-result__title:has-text("${objectName}")`).first().click(),
|
||||
page.waitForNavigation()
|
||||
]);
|
||||
|
||||
// Register a finalization listener on the root node for the view. This tends to be the last thing to be
|
||||
// garbage collected since it has either direct or indirect references to all resources used by the view. Therefore it's a pretty good proxy
|
||||
// for detecting memory leaks.
|
||||
await page.evaluate(() => {
|
||||
window.gcPromise = new Promise((resolve) => {
|
||||
// eslint-disable-next-line no-undef
|
||||
window.fr = new FinalizationRegistry(resolve);
|
||||
window.fr.register(
|
||||
window.openmct.layout.$refs.browseObject.$refs.objectViewWrapper.firstChild,
|
||||
'navigatedObject',
|
||||
window.openmct.layout.$refs.browseObject.$refs.objectViewWrapper.firstChild
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Nav back to folder
|
||||
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
|
||||
await page.waitForNavigation();
|
||||
|
||||
// This next code block blocks until the finalization listener is called and the gcPromise resolved. This means that the root node for the view has been garbage collected.
|
||||
// In the event that the root node is not garbage collected, the gcPromise will never resolve and the test will time out.
|
||||
await page.evaluate(() => {
|
||||
const gcPromise = window.gcPromise;
|
||||
window.gcPromise = null;
|
||||
|
||||
// Manually invoke the garbage collector once all references are removed.
|
||||
window.gc();
|
||||
|
||||
return gcPromise;
|
||||
});
|
||||
|
||||
// Clean up the finalization registry since we don't need it any more.
|
||||
await page.evaluate(() => {
|
||||
window.fr = null;
|
||||
});
|
||||
|
||||
// If we get here without timing out, it means the garbage collection promise resolved and the test passed.
|
||||
return true;
|
||||
}
|
||||
});
|
||||
273
e2e/tests/performance/tagging.perf.spec.js
Normal file
@@ -0,0 +1,273 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Tests to verify plot tagging performance.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const {
|
||||
createDomainObjectWithDefaults,
|
||||
setRealTimeMode,
|
||||
setFixedTimeMode,
|
||||
waitForPlotsToRender
|
||||
} = require('../../appActions');
|
||||
|
||||
test.describe.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);
|
||||
});
|
||||
});
|
||||
@@ -1,62 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/* global __dirname */
|
||||
/*
|
||||
Collection of Visual Tests set to run with modified init scripts to inject plugins not otherwise available in the default contexts.
|
||||
|
||||
These should only use functional expect statements to verify assumptions about the state
|
||||
in a test and not for functional verification of correctness. Visual tests are not supposed
|
||||
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
|
||||
|
||||
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
const path = require('path');
|
||||
|
||||
const CUSTOM_NAME = 'CUSTOM_NAME';
|
||||
|
||||
test.describe('Visual - addInit', () => {
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: 0, //Set browser clock to UNIX Epoch
|
||||
shouldAdvanceTime: false //Don't advance the clock
|
||||
}
|
||||
});
|
||||
|
||||
test('Restricted Notebook is visually correct @addInit @unstable', async ({ page, theme }) => {
|
||||
await page.addInitScript({
|
||||
path: path.join(__dirname, '../../helper', './addInitRestrictedNotebook.js')
|
||||
});
|
||||
//Go to baseURL
|
||||
await page.goto('./#/browse/mine?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}')`);
|
||||
});
|
||||
});
|
||||
52
e2e/tests/visual/components/about.visual.spec.js
Normal file
@@ -0,0 +1,52 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Tests the branding associated with the default deployment. At least the about modal for now
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../pluginFixtures');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
|
||||
test.describe('Visual - Branding', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Go to baseURL and Hide Tree
|
||||
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
|
||||
});
|
||||
|
||||
test('Visual - About Modal', async ({ page, theme }) => {
|
||||
// Click About button
|
||||
await page.click('.l-shell__app-logo');
|
||||
|
||||
// Modify the Build information in 'about' to be consistent run-over-run
|
||||
const versionInformationLocator = page.locator('ul.t-info.l-info.s-info').first();
|
||||
await expect(versionInformationLocator).toBeEnabled();
|
||||
await versionInformationLocator.evaluate(
|
||||
(node) =>
|
||||
(node.innerHTML =
|
||||
'<li>Version: visual-snapshot</li> <li>Build Date: Mon Nov 15 2021 08:07:51 GMT-0800 (Pacific Standard Time)</li> <li>Revision: 93049cdbc6c047697ca204893db9603b864b8c9f</li> <li>Branch: master</li>')
|
||||
);
|
||||
|
||||
// Take a snapshot of the About modal
|
||||
await percySnapshot(page, `About (theme: '${theme}')`);
|
||||
});
|
||||
});
|
||||
@@ -21,7 +21,10 @@
|
||||
*****************************************************************************/
|
||||
|
||||
const { test } = require('../../../pluginFixtures.js');
|
||||
const { createDomainObjectWithDefaults } = require('../../../appActions.js');
|
||||
const {
|
||||
expandTreePaneItemByName,
|
||||
createDomainObjectWithDefaults
|
||||
} = require('../../../appActions.js');
|
||||
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
|
||||
@@ -88,14 +91,3 @@ test.describe('Visual - Tree Pane', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} name
|
||||
*/
|
||||
async function expandTreePaneItemByName(page, name) {
|
||||
const treePane = page.getByTestId('tree-pane');
|
||||
const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
|
||||
const expandTriangle = treeItem.locator('.c-disclosure-triangle');
|
||||
await expandTriangle.click();
|
||||
}
|
||||
|
||||
@@ -21,31 +21,37 @@
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Collection of Visual Tests set to run in a default context. The tests within this suite
|
||||
are only meant to run against openmct started by `npm start` within the
|
||||
`./e2e/playwright-visual.config.js` file.
|
||||
|
||||
Collection of Visual Tests set to run with browser clock manipulate made possible with the
|
||||
clockOptions plugin fixture.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
|
||||
test.describe('Visual - Controlled Clock @localStorage', () => {
|
||||
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.use({
|
||||
storageState: './e2e/test-data/VisualTestData_storage.json',
|
||||
storageState: './e2e/test-data/overlay_plot_with_delay_storage.json',
|
||||
clockOptions: {
|
||||
now: 0, //Set browser clock to UNIX Epoch
|
||||
shouldAdvanceTime: false //Don't advance the clock
|
||||
}
|
||||
});
|
||||
|
||||
test('Overlay Plot Loading Indicator @localStorage', async ({ page, theme }) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
|
||||
|
||||
await page.locator('a:has-text("Unnamed Overlay Plot Overlay Plot")').click();
|
||||
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();
|
||||
//Ensure that we're on the Unnamed Overlay Plot object
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(
|
||||
'Overlay Plot with Telemetry Object'
|
||||
);
|
||||
|
||||
//Wait for canvas to be rendered and stop animating
|
||||
await page.locator('canvas >> nth=1').hover({ trial: true });
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Collection of Visual Tests set to run in a default context. The tests within this suite
|
||||
are only meant to run against openmct started by `npm start` within the
|
||||
`./e2e/playwright-visual.config.js` file.
|
||||
|
||||
These should only use functional expect statements to verify assumptions about the state
|
||||
in a test and not for functional verification of correctness. Visual tests are not supposed
|
||||
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
|
||||
|
||||
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
|
||||
test.describe('Visual - Default', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Go to baseURL and Hide Tree
|
||||
await page.goto('./#/browse/mine?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}')`);
|
||||
});
|
||||
});
|
||||
101
e2e/tests/visual/defaultPlugins.visual.spec.js
Normal file
@@ -0,0 +1,101 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Collection of Visual Tests set to run in a default context with default Plugins. The tests within this suite
|
||||
are only meant to run against openmct's app.js started by `npm run start` within the
|
||||
`./e2e/playwright-visual.config.js` file.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
|
||||
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}')`);
|
||||
});
|
||||
});
|
||||
@@ -26,12 +26,12 @@ const percySnapshot = require('@percy/playwright');
|
||||
|
||||
const utils = require('../../helper/faultUtils');
|
||||
|
||||
test.describe('The Fault Management Plugin Visual Test', () => {
|
||||
test.describe('Fault Management Visual Tests', () => {
|
||||
test('icon test', async ({ page, theme }) => {
|
||||
await page.addInitScript({
|
||||
path: path.join(__dirname, '../../helper/', 'addInitFaultManagementPlugin.js')
|
||||
});
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await percySnapshot(page, `Fault Management icon appears in tree (theme: '${theme}')`);
|
||||
});
|
||||
|
||||
@@ -22,14 +22,35 @@
|
||||
|
||||
const { test } = require('../../pluginFixtures');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
const { expandTreePaneItemByName, createDomainObjectWithDefaults } = require('../../appActions');
|
||||
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}')`);
|
||||
});
|
||||
});
|
||||
|
||||
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'
|
||||
@@ -43,8 +64,36 @@ 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}')`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,7 +39,10 @@ test.describe("Visual - Check Notification Info Banner of 'Save successful'", ()
|
||||
theme
|
||||
}) => {
|
||||
// Create a clock domain object
|
||||
await createDomainObjectWithDefaults(page, { type: 'Clock' });
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Clock',
|
||||
name: 'Default 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"
|
||||
@@ -52,12 +55,14 @@ 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 - ${theme}`);
|
||||
await percySnapshot(page, `Notification banner shows Save successful (theme: '${theme}')`);
|
||||
// Verify there is a button with text "Dismiss"
|
||||
expect(await page.locator('button:has-text("Dismiss")').isVisible()).toBe(true);
|
||||
await percySnapshot(page, `Notification banner shows Dismiss (theme: '${theme}')`);
|
||||
// Click on button with text "Dismiss"
|
||||
await page.locator('button:has-text("Dismiss")').click();
|
||||
// Verify there is no div with role="dialog"
|
||||
expect(await page.locator('div[role="dialog"]').isVisible()).toBe(false);
|
||||
await percySnapshot(page, `Notification banner dismissed (theme: '${theme}')`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,59 +30,72 @@ const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
|
||||
test.describe('Grand Search', () => {
|
||||
let clock;
|
||||
let displayLayout;
|
||||
test.beforeEach(async ({ page, theme }) => {
|
||||
//Go to baseURL and Hide Tree
|
||||
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
|
||||
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
|
||||
});
|
||||
});
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: 0, //Set browser clock to UNIX Epoch
|
||||
shouldAdvanceTime: false //Don't advance the clock
|
||||
}
|
||||
});
|
||||
//This needs to be rewritten to use a non clock or non display layout object
|
||||
test('Can search for objects, and subsequent search dropdown behaves properly @unstable', async ({
|
||||
|
||||
test('Can search for folder object, and subsequent search dropdown behaves properly', async ({
|
||||
page,
|
||||
theme
|
||||
}) => {
|
||||
// await createDomainObjectWithDefaults(page, 'Display Layout');
|
||||
// await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
// await page.locator('text=Save and Finish Editing').click();
|
||||
const folder1 = 'Folder1';
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: folder1
|
||||
});
|
||||
const searchInput = page.getByRole('searchbox', { name: 'Search Input' });
|
||||
const searchResults = page.getByRole('searchbox', { name: 'OpenMCT Search' });
|
||||
// Navigate to display layout
|
||||
await page.goto(displayLayout.url);
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill(folder1);
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toContainText(folder1);
|
||||
await percySnapshot(page, 'Searching for Folder Object');
|
||||
// Search for the clock object
|
||||
await searchInput.click();
|
||||
await searchInput.fill(clock.name);
|
||||
await expect(searchResults.getByText('Visual Test Clock')).toBeVisible();
|
||||
|
||||
//Searching for an object returns that object in the grandsearch
|
||||
await percySnapshot(page, `Searching for Clock Object (theme: '${theme}')`);
|
||||
|
||||
// Enter Edit mode on the Display Layout
|
||||
await page.getByRole('button', { name: 'Edit' }).click();
|
||||
|
||||
// Navigate to the clock object while in edit mode on the display layout
|
||||
await searchInput.click();
|
||||
await searchResults.getByText('Visual Test Clock').click();
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
|
||||
await page.locator('[aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock').click();
|
||||
await percySnapshot(
|
||||
page,
|
||||
'Preview for clock should display when editing enabled and search item clicked'
|
||||
`Preview for clock should display when editing enabled and search item clicked (theme: '${theme}')`
|
||||
);
|
||||
|
||||
await page.locator('[aria-label="Close"]').click();
|
||||
await percySnapshot(page, 'Search should still be showing after preview closed');
|
||||
// Close the preview
|
||||
await page.getByRole('button', { name: 'Close' }).click();
|
||||
await percySnapshot(
|
||||
page,
|
||||
`Search should still be showing after preview closed (theme: '${theme}')`
|
||||
);
|
||||
|
||||
await page
|
||||
.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button')
|
||||
.nth(1)
|
||||
.click();
|
||||
// Save and finish editing the Display Layout
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
// Search for the clock object
|
||||
await searchInput.click();
|
||||
await searchInput.fill(clock.name);
|
||||
await expect(searchResults.getByText('Visual Test Clock')).toBeVisible();
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
|
||||
// Navigate to the clock object while not in edit mode on the display layout
|
||||
await searchResults.getByText('Visual Test Clock').click();
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl');
|
||||
|
||||
await Promise.all([page.waitForNavigation(), page.locator('text=Unnamed Clock').click()]);
|
||||
await percySnapshot(
|
||||
page,
|
||||
`Clicking on search results should navigate to them if not editing (theme: '${theme}')`
|
||||
|
||||
@@ -63,16 +63,24 @@ const STATUSES = [
|
||||
* @implements {StatusUserProvider}
|
||||
*/
|
||||
export default class ExampleUserProvider extends EventEmitter {
|
||||
constructor(openmct, { defaultStatusRole } = { defaultStatusRole: undefined }) {
|
||||
constructor(
|
||||
openmct,
|
||||
{ statusRoles } = {
|
||||
statusRoles: []
|
||||
}
|
||||
) {
|
||||
super();
|
||||
|
||||
this.openmct = openmct;
|
||||
this.user = undefined;
|
||||
this.loggedIn = false;
|
||||
this.autoLoginUser = undefined;
|
||||
this.status = STATUSES[0];
|
||||
this.statusRoleValues = statusRoles.map((role) => ({
|
||||
role: role,
|
||||
status: STATUSES[0]
|
||||
}));
|
||||
this.pollQuestion = undefined;
|
||||
this.defaultStatusRole = defaultStatusRole;
|
||||
this.statusRoles = statusRoles;
|
||||
|
||||
this.ExampleUser = createExampleUser(this.openmct.user.User);
|
||||
this.loginPromise = undefined;
|
||||
@@ -94,14 +102,13 @@ export default class ExampleUserProvider extends EventEmitter {
|
||||
return this.loginPromise;
|
||||
}
|
||||
|
||||
canProvideStatusForRole() {
|
||||
return Promise.resolve(true);
|
||||
canProvideStatusForRole(role) {
|
||||
return this.statusRoles.includes(role);
|
||||
}
|
||||
|
||||
canSetPollQuestion() {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
hasRole(roleId) {
|
||||
if (!this.loggedIn) {
|
||||
Promise.resolve(undefined);
|
||||
@@ -110,16 +117,18 @@ export default class ExampleUserProvider extends EventEmitter {
|
||||
return Promise.resolve(this.user.getRoles().includes(roleId));
|
||||
}
|
||||
|
||||
getStatusRoleForCurrentUser() {
|
||||
return Promise.resolve(this.defaultStatusRole);
|
||||
getPossibleRoles() {
|
||||
return this.user.getRoles();
|
||||
}
|
||||
|
||||
getAllStatusRoles() {
|
||||
return Promise.resolve([this.defaultStatusRole]);
|
||||
return Promise.resolve(this.statusRoles);
|
||||
}
|
||||
|
||||
getStatusForRole(role) {
|
||||
return Promise.resolve(this.status);
|
||||
const statusForRole = this.statusRoleValues.find((statusRole) => statusRole.role === role);
|
||||
|
||||
return Promise.resolve(statusForRole?.status);
|
||||
}
|
||||
|
||||
async getDefaultStatusForRole(role) {
|
||||
@@ -130,7 +139,8 @@ export default class ExampleUserProvider extends EventEmitter {
|
||||
|
||||
setStatusForRole(role, status) {
|
||||
status.timestamp = Date.now();
|
||||
this.status = status;
|
||||
const matchingIndex = this.statusRoleValues.findIndex((statusRole) => statusRole.role === role);
|
||||
this.statusRoleValues[matchingIndex].status = status;
|
||||
this.emit('statusChange', {
|
||||
role,
|
||||
status
|
||||
@@ -175,7 +185,7 @@ export default class ExampleUserProvider extends EventEmitter {
|
||||
// for testing purposes, this will skip the form, this wouldn't be used in
|
||||
// a normal authentication process
|
||||
if (this.autoLoginUser) {
|
||||
this.user = new this.ExampleUser(id, this.autoLoginUser, ['example-role']);
|
||||
this.user = new this.ExampleUser(id, this.autoLoginUser, ['flight', 'driver', 'observer']);
|
||||
this.loggedIn = true;
|
||||
|
||||
return Promise.resolve();
|
||||
|
||||
@@ -21,16 +21,18 @@
|
||||
*****************************************************************************/
|
||||
|
||||
import ExampleUserProvider from './ExampleUserProvider';
|
||||
const AUTO_LOGIN_USER = 'mct-user';
|
||||
const STATUS_ROLES = ['flight', 'driver'];
|
||||
|
||||
export default function ExampleUserPlugin(
|
||||
{ autoLoginUser, defaultStatusRole } = {
|
||||
autoLoginUser: 'guest',
|
||||
defaultStatusRole: 'test-role'
|
||||
{ autoLoginUser, statusRoles } = {
|
||||
autoLoginUser: AUTO_LOGIN_USER,
|
||||
statusRoles: STATUS_ROLES
|
||||
}
|
||||
) {
|
||||
return function install(openmct) {
|
||||
const userProvider = new ExampleUserProvider(openmct, {
|
||||
defaultStatusRole
|
||||
statusRoles
|
||||
});
|
||||
|
||||
if (autoLoginUser !== undefined) {
|
||||
|
||||
@@ -62,7 +62,7 @@ export default class SinewaveLimitProvider extends EventEmitter {
|
||||
const id = this.#getObjectKeyString(domainObject);
|
||||
|
||||
if (this.#isRealTime === undefined) {
|
||||
this.#updateRealTime(this.#openmct.time.clock());
|
||||
this.#updateRealTime(this.#openmct.time.getMode());
|
||||
}
|
||||
|
||||
this.#handleClockUpdate();
|
||||
@@ -92,15 +92,15 @@ export default class SinewaveLimitProvider extends EventEmitter {
|
||||
|
||||
if (observers && !this.#watchingTheClock) {
|
||||
this.#watchingTheClock = true;
|
||||
this.#openmct.time.on('clock', this.#updateRealTime, this);
|
||||
this.#openmct.time.on('modeChanged', this.#updateRealTime, this);
|
||||
} else if (!observers && this.#watchingTheClock) {
|
||||
this.#watchingTheClock = false;
|
||||
this.#openmct.time.off('clock', this.#updateRealTime, this);
|
||||
this.#openmct.time.off('modeChanged', this.#updateRealTime, this);
|
||||
}
|
||||
}
|
||||
|
||||
#updateRealTime(clock) {
|
||||
this.#isRealTime = clock !== undefined;
|
||||
#updateRealTime(mode) {
|
||||
this.#isRealTime = mode !== 'fixed';
|
||||
|
||||
if (!this.#isRealTime) {
|
||||
Object.keys(this.#observingStaleness).forEach((id) => {
|
||||
|
||||
@@ -156,9 +156,9 @@ export default function () {
|
||||
key: 'thumbnail',
|
||||
...formatThumbnail
|
||||
});
|
||||
openmct.telemetry.addProvider(getRealtimeProvider());
|
||||
openmct.telemetry.addProvider(getHistoricalProvider());
|
||||
openmct.telemetry.addProvider(getLadProvider());
|
||||
openmct.telemetry.addProvider(getRealtimeProvider(openmct));
|
||||
openmct.telemetry.addProvider(getHistoricalProvider(openmct));
|
||||
openmct.telemetry.addProvider(getLadProvider(openmct));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -207,14 +207,14 @@ function getImageLoadDelay(domainObject) {
|
||||
return imageLoadDelay;
|
||||
}
|
||||
|
||||
function getRealtimeProvider() {
|
||||
function getRealtimeProvider(openmct) {
|
||||
return {
|
||||
supportsSubscribe: (domainObject) => domainObject.type === 'example.imagery',
|
||||
subscribe: (domainObject, callback) => {
|
||||
const delay = getImageLoadDelay(domainObject);
|
||||
const interval = setInterval(() => {
|
||||
const imageSamples = getImageSamples(domainObject.configuration);
|
||||
const datum = pointForTimestamp(Date.now(), domainObject.name, imageSamples, delay);
|
||||
const datum = pointForTimestamp(openmct.time.now(), domainObject.name, imageSamples, delay);
|
||||
callback(datum);
|
||||
}, delay);
|
||||
|
||||
@@ -225,7 +225,7 @@ function getRealtimeProvider() {
|
||||
};
|
||||
}
|
||||
|
||||
function getHistoricalProvider() {
|
||||
function getHistoricalProvider(openmct) {
|
||||
return {
|
||||
supportsRequest: (domainObject, options) => {
|
||||
return domainObject.type === 'example.imagery' && options.strategy !== 'latest';
|
||||
@@ -233,17 +233,12 @@ function getHistoricalProvider() {
|
||||
request: (domainObject, options) => {
|
||||
const delay = getImageLoadDelay(domainObject);
|
||||
let start = options.start;
|
||||
const end = Math.min(options.end, Date.now());
|
||||
const end = Math.min(options.end, openmct.time.now());
|
||||
const data = [];
|
||||
while (start <= end && data.length < delay) {
|
||||
data.push(
|
||||
pointForTimestamp(
|
||||
start,
|
||||
domainObject.name,
|
||||
getImageSamples(domainObject.configuration),
|
||||
delay
|
||||
)
|
||||
);
|
||||
const imageSamples = getImageSamples(domainObject.configuration);
|
||||
const generatedDataPoint = pointForTimestamp(start, domainObject.name, imageSamples, delay);
|
||||
data.push(generatedDataPoint);
|
||||
start += delay;
|
||||
}
|
||||
|
||||
@@ -252,7 +247,7 @@ function getHistoricalProvider() {
|
||||
};
|
||||
}
|
||||
|
||||
function getLadProvider() {
|
||||
function getLadProvider(openmct) {
|
||||
return {
|
||||
supportsRequest: (domainObject, options) => {
|
||||
return domainObject.type === 'example.imagery' && options.strategy === 'latest';
|
||||
@@ -260,7 +255,7 @@ function getLadProvider() {
|
||||
request: (domainObject, options) => {
|
||||
const delay = getImageLoadDelay(domainObject);
|
||||
const datum = pointForTimestamp(
|
||||
Date.now(),
|
||||
openmct.time.now(),
|
||||
domainObject.name,
|
||||
getImageSamples(domainObject.configuration),
|
||||
delay
|
||||
|
||||
@@ -24,7 +24,7 @@ function SimpleVuePlugin() {
|
||||
container.appendChild(vm.$mount().$el);
|
||||
},
|
||||
destroy: function (container) {
|
||||
vm.$destroy();
|
||||
//vm.$destroy();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -92,7 +92,9 @@
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body></body>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
<script>
|
||||
const THIRTY_SECONDS = 30 * 1000;
|
||||
const ONE_MINUTE = THIRTY_SECONDS * 2;
|
||||
|
||||
@@ -56,6 +56,7 @@ if (document.currentScript) {
|
||||
* @property {import('./src/api/notifications/NotificationAPI').default} notifications
|
||||
* @property {import('./src/api/Editor').default} editor
|
||||
* @property {import('./src/api/overlays/OverlayAPI')} overlays
|
||||
* @property {import('./src/api/tooltips/ToolTipAPI')} tooltips
|
||||
* @property {import('./src/api/menu/MenuAPI').default} menus
|
||||
* @property {import('./src/api/actions/ActionsAPI').default} actions
|
||||
* @property {import('./src/api/status/StatusAPI').default} status
|
||||
|
||||
65
package.json
@@ -1,35 +1,38 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "2.2.4-SNAPSHOT",
|
||||
"version": "3.0.2",
|
||||
"description": "The Open MCT core platform",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.21.8",
|
||||
"@babel/eslint-parser": "7.22.5",
|
||||
"@braintree/sanitize-url": "6.0.2",
|
||||
"@deploysentinel/playwright": "0.3.4",
|
||||
"@percy/cli": "1.24.0",
|
||||
"@percy/cli": "1.26.0",
|
||||
"@percy/playwright": "1.0.4",
|
||||
"@playwright/test": "1.32.3",
|
||||
"@playwright/test": "1.36.2",
|
||||
"@types/eventemitter3": "1.2.0",
|
||||
"@types/jasmine": "4.3.1",
|
||||
"@types/jasmine": "4.3.4",
|
||||
"@types/lodash": "4.14.192",
|
||||
"@vue/compat": "^3.1.0",
|
||||
"@vue/compiler-sfc": "^3.1.0",
|
||||
"babel-loader": "9.1.0",
|
||||
"babel-plugin-istanbul": "6.1.1",
|
||||
"codecov": "3.8.3",
|
||||
"comma-separated-values": "3.6.4",
|
||||
"copy-webpack-plugin": "11.0.0",
|
||||
"css-loader": "6.7.3",
|
||||
"css-loader": "6.8.1",
|
||||
"d3-axis": "3.0.0",
|
||||
"d3-scale": "3.3.0",
|
||||
"d3-selection": "3.0.0",
|
||||
"eslint": "8.40.0",
|
||||
"eslint-plugin-compat": "4.1.4",
|
||||
"eslint": "8.43.0",
|
||||
"eslint-config-prettier": "8.8.0",
|
||||
"eslint-plugin-compat": "4.1.4",
|
||||
"eslint-plugin-playwright": "0.12.0",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-vue": "9.13.0",
|
||||
"eslint-plugin-vue": "9.15.0",
|
||||
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
|
||||
"eventemitter3": "1.2.0",
|
||||
"file-saver": "2.0.5",
|
||||
"flatbush": "4.2.0",
|
||||
"git-rev-sync": "3.0.2",
|
||||
"html2canvas": "1.4.1",
|
||||
"imports-loader": "4.0.1",
|
||||
@@ -44,43 +47,42 @@
|
||||
"karma-sourcemap-loader": "0.4.0",
|
||||
"karma-spec-reporter": "0.0.36",
|
||||
"karma-webpack": "5.0.0",
|
||||
"kdbush": "3.0.0",
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"mini-css-extract-plugin": "2.7.5",
|
||||
"mini-css-extract-plugin": "2.7.6",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.3.2",
|
||||
"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",
|
||||
"printj": "1.3.1",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sanitize-html": "2.10.0",
|
||||
"sass": "1.62.1",
|
||||
"sass-loader": "13.2.2",
|
||||
"sanitize-html": "2.11.0",
|
||||
"sass": "1.63.4",
|
||||
"sass-loader": "13.3.2",
|
||||
"sinon": "15.1.0",
|
||||
"style-loader": "3.3.2",
|
||||
"typescript": "5.0.4",
|
||||
"style-loader": "3.3.3",
|
||||
"typescript": "5.1.3",
|
||||
"uuid": "9.0.0",
|
||||
"vue": "2.6.14",
|
||||
"vue-eslint-parser": "9.3.0",
|
||||
"vue-loader": "15.9.8",
|
||||
"vue-template-compiler": "2.6.14",
|
||||
"webpack": "5.84.0",
|
||||
"vue": "^3.1.0",
|
||||
"vue-eslint-parser": "9.3.1",
|
||||
"webpack": "5.88.0",
|
||||
"vue-loader": "^16.0.0",
|
||||
"webpack-cli": "5.1.1",
|
||||
"webpack-dev-server": "4.13.3",
|
||||
"webpack-merge": "5.8.0"
|
||||
"webpack-dev-server": "4.15.1",
|
||||
"webpack-merge": "5.9.0"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf ./dist ./node_modules ./package-lock.json ./coverage ./html-test-results ./test-results ./.nyc_output ",
|
||||
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
|
||||
"start": "npx webpack serve --config ./.webpack/webpack.dev.js",
|
||||
"start:prod": "npx webpack serve --config ./.webpack/webpack.prod.js",
|
||||
"start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.js",
|
||||
"lint": "eslint example src e2e --ext .js,.vue openmct.js --max-warnings=0",
|
||||
"lint": "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: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",
|
||||
@@ -91,13 +93,18 @@
|
||||
"test:debug": "KARMA_DEBUG=true karma start",
|
||||
"test:e2e": "npx playwright test",
|
||||
"test:e2e:couchdb": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @couchdb --workers=1",
|
||||
"test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert \"@unstable|@couchdb\"",
|
||||
"test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert \"@unstable|@couchdb|@generatedata\"",
|
||||
"test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable",
|
||||
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
|
||||
"test:e2e:generatedata": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @generatedata",
|
||||
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots",
|
||||
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js --grep-invert @unstable",
|
||||
"test:e2e:visual:ci": "percy exec --config ./e2e/.percy.ci.yml --partial -- npx playwright test --config=e2e/playwright-visual.config.js --project=chrome --grep-invert @unstable",
|
||||
"test:e2e:visual:full": "percy exec --config ./e2e/.percy.nightly.yml -- npx playwright test --config=e2e/playwright-visual.config.js --grep-invert @unstable",
|
||||
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js --grep-invert @couchdb",
|
||||
"test:perf": "npx playwright test --config=e2e/playwright-performance.config.js",
|
||||
"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",
|
||||
"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",
|
||||
@@ -111,7 +118,7 @@
|
||||
"url": "https://github.com/nasa/openmct.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.19.1"
|
||||
"node": ">=16.19.1 <20"
|
||||
},
|
||||
"browserslist": [
|
||||
"Firefox ESR",
|
||||
|
||||
49
src/MCT.js
@@ -24,6 +24,7 @@ define([
|
||||
'EventEmitter',
|
||||
'./api/api',
|
||||
'./api/overlays/OverlayAPI',
|
||||
'./api/tooltips/ToolTipAPI',
|
||||
'./selection/Selection',
|
||||
'./plugins/plugins',
|
||||
'./ui/registries/ViewRegistry',
|
||||
@@ -42,12 +43,12 @@ define([
|
||||
'./plugins/duplicate/plugin',
|
||||
'./plugins/importFromJSONAction/plugin',
|
||||
'./plugins/exportAsJSONAction/plugin',
|
||||
'./ui/components/components',
|
||||
'vue'
|
||||
], function (
|
||||
EventEmitter,
|
||||
api,
|
||||
OverlayAPI,
|
||||
ToolTipAPI,
|
||||
Selection,
|
||||
plugins,
|
||||
ViewRegistry,
|
||||
@@ -66,7 +67,6 @@ define([
|
||||
DuplicateActionPlugin,
|
||||
ImportFromJSONAction,
|
||||
ExportAsJSONAction,
|
||||
components,
|
||||
Vue
|
||||
) {
|
||||
/**
|
||||
@@ -94,6 +94,7 @@ define([
|
||||
};
|
||||
|
||||
this.destroy = this.destroy.bind(this);
|
||||
this.defaultClock = 'local';
|
||||
[
|
||||
/**
|
||||
* Tracks current selection state of the application.
|
||||
@@ -220,6 +221,8 @@ define([
|
||||
|
||||
['overlays', () => new OverlayAPI.default()],
|
||||
|
||||
['tooltips', () => new ToolTipAPI.default()],
|
||||
|
||||
['menus', () => new api.MenuAPI(this)],
|
||||
|
||||
['actions', () => new api.ActionsAPI(this)],
|
||||
@@ -338,7 +341,17 @@ define([
|
||||
* @param {HTMLElement} [domElement] the DOM element in which to run
|
||||
* MCT; if undefined, MCT will be run in the body of the document
|
||||
*/
|
||||
MCT.prototype.start = function (domElement = document.body, isHeadlessMode = false) {
|
||||
MCT.prototype.start = function (
|
||||
domElement = document.body.firstElementChild,
|
||||
isHeadlessMode = false
|
||||
) {
|
||||
// Create element to mount Layout if it doesn't exist
|
||||
if (domElement === null) {
|
||||
domElement = document.createElement('div');
|
||||
document.body.appendChild(domElement);
|
||||
}
|
||||
domElement.id = 'openmct-app';
|
||||
|
||||
if (this.types.get('layout') === undefined) {
|
||||
this.install(
|
||||
this.plugins.DisplayLayout({
|
||||
@@ -349,6 +362,10 @@ define([
|
||||
|
||||
this.element = domElement;
|
||||
|
||||
if (!this.time.getClock()) {
|
||||
this.time.setClock(this.defaultClock);
|
||||
}
|
||||
|
||||
this.router.route(/^\/$/, () => {
|
||||
this.router.setPath('/browse/');
|
||||
});
|
||||
@@ -361,25 +378,30 @@ define([
|
||||
*/
|
||||
|
||||
if (!isHeadlessMode) {
|
||||
const appLayout = new Vue({
|
||||
const appLayout = Vue.createApp({
|
||||
components: {
|
||||
Layout: Layout.default
|
||||
},
|
||||
provide: {
|
||||
openmct: this
|
||||
openmct: Vue.markRaw(this)
|
||||
},
|
||||
template: '<Layout ref="layout"></Layout>'
|
||||
});
|
||||
domElement.appendChild(appLayout.$mount().$el);
|
||||
const component = appLayout.mount(domElement);
|
||||
component.$nextTick(() => {
|
||||
this.layout = component.$refs.layout;
|
||||
this.app = appLayout;
|
||||
Browse(this);
|
||||
window.addEventListener('beforeunload', this.destroy);
|
||||
this.router.start();
|
||||
this.emit('start');
|
||||
});
|
||||
} else {
|
||||
window.addEventListener('beforeunload', this.destroy);
|
||||
|
||||
this.layout = appLayout.$refs.layout;
|
||||
Browse(this);
|
||||
this.router.start();
|
||||
this.emit('start');
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', this.destroy);
|
||||
|
||||
this.router.start();
|
||||
this.emit('start');
|
||||
};
|
||||
|
||||
MCT.prototype.startHeadless = function () {
|
||||
@@ -406,7 +428,6 @@ define([
|
||||
};
|
||||
|
||||
MCT.prototype.plugins = plugins;
|
||||
MCT.prototype.components = components.default;
|
||||
|
||||
return MCT;
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,9 @@ const ANNOTATION_LAST_CREATED = 'annotationLastCreated';
|
||||
* @constructor
|
||||
*/
|
||||
export default class AnnotationAPI extends EventEmitter {
|
||||
/** @type {Map<ANNOTATION_TYPES, Array<(a, b) => boolean >>} */
|
||||
#targetComparatorMap;
|
||||
|
||||
/**
|
||||
* @param {OpenMCT} openmct
|
||||
*/
|
||||
@@ -84,6 +87,7 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
this.openmct = openmct;
|
||||
this.availableTags = {};
|
||||
this.namespaceToSaveAnnotations = '';
|
||||
this.#targetComparatorMap = new Map();
|
||||
|
||||
this.ANNOTATION_TYPES = ANNOTATION_TYPES;
|
||||
this.ANNOTATION_TYPE = ANNOTATION_TYPE;
|
||||
@@ -96,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 || [];
|
||||
@@ -113,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 {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}
|
||||
* @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}
|
||||
* For notebooks, this will be an entry ID, e.g.: {entryId: "entry-ecb158f5-d23c-45e1-a704-649b382622ba"}
|
||||
* @property {DomainObject>} targetDomainObjects the targets ID keystrings and the domain objects this annotation points to (e.g., telemetry objects for a plot)
|
||||
* @property {DomainObject>[]} targetDomainObjects the domain objects this annotation points to (e.g., telemetry objects for a plot)
|
||||
*/
|
||||
/**
|
||||
* @method create
|
||||
@@ -137,11 +141,15 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
throw new Error(`Unknown annotation type: ${annotationType}`);
|
||||
}
|
||||
|
||||
if (!Object.keys(targets).length) {
|
||||
if (!targets.length) {
|
||||
throw new Error(`At least one target is required to create an annotation`);
|
||||
}
|
||||
|
||||
if (!Object.keys(targetDomainObjects).length) {
|
||||
if (targets.some((target) => !target.keyString)) {
|
||||
throw new Error(`All targets require a keyString to create an annotation`);
|
||||
}
|
||||
|
||||
if (!targetDomainObjects.length) {
|
||||
throw new Error(`At least one targetDomainObject is required to create an annotation`);
|
||||
}
|
||||
|
||||
@@ -177,7 +185,7 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
const success = await this.openmct.objects.save(createdObject);
|
||||
if (success) {
|
||||
this.emit('annotationCreated', createdObject);
|
||||
Object.values(targetDomainObjects).forEach((targetDomainObject) => {
|
||||
targetDomainObjects.forEach((targetDomainObject) => {
|
||||
this.#updateAnnotationModified(targetDomainObject);
|
||||
});
|
||||
|
||||
@@ -246,15 +254,16 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
/**
|
||||
* @method getAnnotations
|
||||
* @param {Identifier} domainObjectIdentifier - The domain object identifier to use to search for annotations. For example, a telemetry object identifier.
|
||||
* @param {AbortSignal} abortSignal - An abort signal to cancel the search
|
||||
* @returns {DomainObject[]} Returns an array of annotations that match the search query
|
||||
*/
|
||||
async getAnnotations(domainObjectIdentifier) {
|
||||
async getAnnotations(domainObjectIdentifier, abortSignal = null) {
|
||||
const keyStringQuery = this.openmct.objects.makeKeyString(domainObjectIdentifier);
|
||||
const searchResults = (
|
||||
await Promise.all(
|
||||
this.openmct.objects.search(
|
||||
keyStringQuery,
|
||||
null,
|
||||
abortSignal,
|
||||
this.openmct.objects.SEARCH_TYPES.ANNOTATIONS
|
||||
)
|
||||
)
|
||||
@@ -316,7 +325,10 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
}
|
||||
|
||||
#addTagMetaInformationToTags(tags) {
|
||||
return tags.map((tagKey) => {
|
||||
// Convert to Set and back to Array to remove duplicates
|
||||
const uniqueTags = [...new Set(tags)];
|
||||
|
||||
return uniqueTags.map((tagKey) => {
|
||||
const tagModel = this.availableTags[tagKey];
|
||||
tagModel.tagID = tagKey;
|
||||
|
||||
@@ -358,7 +370,8 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
const modelAddedToResults = await Promise.all(
|
||||
results.map(async (result) => {
|
||||
const targetModels = await Promise.all(
|
||||
Object.keys(result.targets).map(async (targetID) => {
|
||||
result.targets.map(async (target) => {
|
||||
const targetID = target.keyString;
|
||||
const targetModel = await this.openmct.objects.get(targetID);
|
||||
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
|
||||
const originalPathObjects = await this.openmct.objects.getOriginalPath(targetKeyString);
|
||||
@@ -384,7 +397,8 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
const combinedResults = [];
|
||||
results.forEach((currentAnnotation) => {
|
||||
const existingAnnotation = combinedResults.find((annotationToFind) => {
|
||||
return _.isEqual(currentAnnotation.targets, annotationToFind.targets);
|
||||
const { annotationType, targets } = currentAnnotation;
|
||||
return this.areAnnotationTargetsEqual(annotationType, targets, annotationToFind.targets);
|
||||
});
|
||||
if (!existingAnnotation) {
|
||||
combinedResults.push(currentAnnotation);
|
||||
@@ -404,13 +418,12 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
#breakApartSeparateTargets(results) {
|
||||
const separateResults = [];
|
||||
results.forEach((result) => {
|
||||
Object.keys(result.targets).forEach((targetID) => {
|
||||
result.targets.forEach((target) => {
|
||||
const targetID = target.keyString;
|
||||
const separatedResult = {
|
||||
...result
|
||||
};
|
||||
separatedResult.targets = {
|
||||
[targetID]: result.targets[targetID]
|
||||
};
|
||||
separatedResult.targets = [target];
|
||||
separatedResult.targetModels = result.targetModels.filter((targetModel) => {
|
||||
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
|
||||
|
||||
@@ -460,4 +473,35 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
|
||||
return breakApartSeparateTargets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a comparator function for a given annotation type.
|
||||
* The comparator functions will be used to determine if two annotations
|
||||
* have the same target.
|
||||
* @param {ANNOTATION_TYPES} annotationType
|
||||
* @param {(t1, t2) => boolean} comparator
|
||||
*/
|
||||
addTargetComparator(annotationType, comparator) {
|
||||
const comparatorList = this.#targetComparatorMap.get(annotationType) ?? [];
|
||||
comparatorList.push(comparator);
|
||||
this.#targetComparatorMap.set(annotationType, comparatorList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two sets of targets to see if they are equal. First checks if
|
||||
* any targets comparators evaluate to true, then falls back to a deep
|
||||
* equality check.
|
||||
* @param {ANNOTATION_TYPES} annotationType
|
||||
* @param {*} targets
|
||||
* @param {*} otherTargets
|
||||
* @returns true if the targets are equal, false otherwise
|
||||
*/
|
||||
areAnnotationTargetsEqual(annotationType, targets, otherTargets) {
|
||||
const targetComparatorList = this.#targetComparatorMap.get(annotationType);
|
||||
return (
|
||||
(targetComparatorList?.length &&
|
||||
targetComparatorList.some((targetComparator) => targetComparator(targets, otherTargets))) ||
|
||||
_.isEqual(targets, otherTargets)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,11 +62,12 @@ describe('The Annotation API', () => {
|
||||
key: 'anAnnotationKey',
|
||||
namespace: 'fooNameSpace'
|
||||
},
|
||||
targets: {
|
||||
'fooNameSpace:some-object': {
|
||||
targets: [
|
||||
{
|
||||
keyString: 'fooNameSpace:some-object',
|
||||
entryId: 'fooBarEntry'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
mockObjectProvider = jasmine.createSpyObj('mock provider', ['create', 'update', 'get']);
|
||||
@@ -121,7 +122,7 @@ describe('The Annotation API', () => {
|
||||
tags: ['sometag'],
|
||||
contentText: 'fooContext',
|
||||
targetDomainObjects: [mockDomainObject],
|
||||
targets: { fooTarget: {} }
|
||||
targets: [{ keyString: 'fooTarget' }]
|
||||
};
|
||||
const annotationObject = await openmct.annotation.create(annotationCreationArguments);
|
||||
expect(annotationObject).toBeDefined();
|
||||
@@ -136,7 +137,7 @@ describe('The Annotation API', () => {
|
||||
tags: ['sometag'],
|
||||
contentText: 'fooContext',
|
||||
targetDomainObjects: [mockDomainObject],
|
||||
targets: { fooTarget: {} }
|
||||
targets: [{ keyString: 'fooTarget' }]
|
||||
};
|
||||
openmct.annotation.setNamespaceToSaveAnnotations('fooNameSpace');
|
||||
const annotationObject = await openmct.annotation.create(annotationCreationArguments);
|
||||
@@ -166,7 +167,7 @@ describe('The Annotation API', () => {
|
||||
tags: ['sometag'],
|
||||
contentText: 'fooContext',
|
||||
targetDomainObjects: [mockDomainObject],
|
||||
targets: { fooTarget: {} }
|
||||
targets: [{ keyString: 'fooTarget' }]
|
||||
};
|
||||
openmct.annotation.setNamespaceToSaveAnnotations('nameespaceThatDoesNotExist');
|
||||
await openmct.annotation.create(annotationCreationArguments);
|
||||
@@ -183,7 +184,7 @@ describe('The Annotation API', () => {
|
||||
tags: ['sometag'],
|
||||
contentText: 'fooContext',
|
||||
targetDomainObjects: [mockDomainObject],
|
||||
targets: { fooTarget: {} }
|
||||
targets: [{ keyString: 'fooTarget' }]
|
||||
};
|
||||
openmct.annotation.setNamespaceToSaveAnnotations('immutableProvider');
|
||||
await openmct.annotation.create(annotationCreationArguments);
|
||||
@@ -202,7 +203,7 @@ describe('The Annotation API', () => {
|
||||
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
|
||||
tags: ['aWonderfulTag'],
|
||||
contentText: 'fooContext',
|
||||
targets: { 'fooNameSpace:some-object': { entryId: 'fooBarEntry' } },
|
||||
targets: [{ keyString: 'fooNameSpace:some-object', entryId: 'fooBarEntry' }],
|
||||
targetDomainObjects: [mockDomainObject]
|
||||
};
|
||||
});
|
||||
@@ -265,4 +266,54 @@ describe('The Annotation API', () => {
|
||||
expect(results.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Target Comparators', () => {
|
||||
let targets;
|
||||
let otherTargets;
|
||||
let comparator;
|
||||
|
||||
beforeEach(() => {
|
||||
targets = [
|
||||
{
|
||||
keyString: 'fooTarget',
|
||||
foo: 42
|
||||
}
|
||||
];
|
||||
otherTargets = [
|
||||
{
|
||||
keyString: 'fooTarget',
|
||||
bar: 42
|
||||
}
|
||||
];
|
||||
comparator = (t1, t2) => t1[0].foo === t2[0].bar;
|
||||
});
|
||||
|
||||
it('can add a comparator function', () => {
|
||||
const notebookAnnotationType = openmct.annotation.ANNOTATION_TYPES.NOTEBOOK;
|
||||
expect(
|
||||
openmct.annotation.areAnnotationTargetsEqual(notebookAnnotationType, targets, otherTargets)
|
||||
).toBeFalse(); // without a comparator, these should NOT be equal
|
||||
// Register a comparator function for the notebook annotation type
|
||||
openmct.annotation.addTargetComparator(notebookAnnotationType, comparator);
|
||||
expect(
|
||||
openmct.annotation.areAnnotationTargetsEqual(notebookAnnotationType, targets, otherTargets)
|
||||
).toBeTrue(); // the comparator should make these equal
|
||||
});
|
||||
|
||||
it('falls back to deep equality check if no comparator functions', () => {
|
||||
const annotationTypeWithoutComparator = openmct.annotation.ANNOTATION_TYPES.GEOSPATIAL;
|
||||
const areEqual = openmct.annotation.areAnnotationTargetsEqual(
|
||||
annotationTypeWithoutComparator,
|
||||
targets,
|
||||
targets
|
||||
);
|
||||
const areNotEqual = openmct.annotation.areAnnotationTargetsEqual(
|
||||
annotationTypeWithoutComparator,
|
||||
targets,
|
||||
otherTargets
|
||||
);
|
||||
expect(areEqual).toBeTrue();
|
||||
expect(areNotEqual).toBeFalse();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -185,9 +185,10 @@ export default class CompositionProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#publicAPI.objects.eventEmitter.on('mutation', this.#onMutation.bind(this));
|
||||
const onMutation = this.#onMutation.bind(this);
|
||||
this.#publicAPI.objects.eventEmitter.on('mutation', onMutation);
|
||||
this.topicListener = () => {
|
||||
this.#publicAPI.objects.eventEmitter.off('mutation', this.#onMutation.bind(this));
|
||||
this.#publicAPI.objects.eventEmitter.off('mutation', onMutation);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
*****************************************************************************/
|
||||
import objectUtils from '../objects/object-utils';
|
||||
import CompositionProvider from './CompositionProvider';
|
||||
import { toRaw } from 'vue';
|
||||
|
||||
/**
|
||||
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
|
||||
@@ -167,7 +168,7 @@ export default class DefaultCompositionProvider extends CompositionProvider {
|
||||
*/
|
||||
add(parent, childId) {
|
||||
if (!this.includes(parent, childId)) {
|
||||
const composition = structuredClone(parent.composition);
|
||||
const composition = structuredClone(toRaw(parent.composition));
|
||||
composition.push(childId);
|
||||
this.publicAPI.objects.mutate(parent, 'composition', composition);
|
||||
}
|
||||
|
||||
@@ -10,8 +10,7 @@ import TextAreaField from './components/controls/TextAreaField.vue';
|
||||
import TextField from './components/controls/TextField.vue';
|
||||
import ToggleSwitchField from './components/controls/ToggleSwitchField.vue';
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
import mount from 'utils/mount';
|
||||
export const DEFAULT_CONTROLS_MAP = {
|
||||
autocomplete: AutoCompleteField,
|
||||
checkbox: CheckBoxField,
|
||||
@@ -69,31 +68,40 @@ export default class FormControl {
|
||||
*/
|
||||
_getControlViewProvider(control) {
|
||||
const self = this;
|
||||
let rowComponent;
|
||||
let _destroy = null;
|
||||
|
||||
return {
|
||||
show(element, model, onChange) {
|
||||
rowComponent = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
FormControlComponent: DEFAULT_CONTROLS_MAP[control]
|
||||
const { vNode, destroy } = mount(
|
||||
{
|
||||
el: element,
|
||||
components: {
|
||||
FormControlComponent: DEFAULT_CONTROLS_MAP[control]
|
||||
},
|
||||
provide: {
|
||||
openmct: self.openmct
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
model,
|
||||
onChange
|
||||
};
|
||||
},
|
||||
template: `<FormControlComponent :model="model" @onChange="onChange"></FormControlComponent>`
|
||||
},
|
||||
provide: {
|
||||
openmct: self.openmct
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
model,
|
||||
onChange
|
||||
};
|
||||
},
|
||||
template: `<FormControlComponent :model="model" @onChange="onChange"></FormControlComponent>`
|
||||
});
|
||||
{
|
||||
element,
|
||||
app: self.openmct.app
|
||||
}
|
||||
);
|
||||
_destroy = destroy;
|
||||
|
||||
return rowComponent;
|
||||
return vNode;
|
||||
},
|
||||
destroy() {
|
||||
rowComponent.$destroy();
|
||||
if (_destroy) {
|
||||
_destroy();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
import FormController from './FormController';
|
||||
import FormProperties from './components/FormProperties.vue';
|
||||
|
||||
import Vue from 'vue';
|
||||
import _ from 'lodash';
|
||||
import mount from 'utils/mount';
|
||||
|
||||
export default class FormsAPI {
|
||||
constructor(openmct) {
|
||||
@@ -156,25 +156,28 @@ export default class FormsAPI {
|
||||
formCancel = onFormAction(reject);
|
||||
});
|
||||
|
||||
const vm = new Vue({
|
||||
components: { FormProperties },
|
||||
provide: {
|
||||
openmct: self.openmct
|
||||
const { destroy } = mount(
|
||||
{
|
||||
components: { FormProperties },
|
||||
provide: {
|
||||
openmct: self.openmct
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formStructure,
|
||||
onChange: onFormPropertyChange,
|
||||
onCancel: formCancel,
|
||||
onSave: formSave
|
||||
};
|
||||
},
|
||||
template:
|
||||
'<FormProperties :model="formStructure" @onChange="onChange" @onCancel="onCancel" @onSave="onSave"></FormProperties>'
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formStructure,
|
||||
onChange: onFormPropertyChange,
|
||||
onCancel: formCancel,
|
||||
onSave: formSave
|
||||
};
|
||||
},
|
||||
template:
|
||||
'<FormProperties :model="formStructure" @onChange="onChange" @onCancel="onCancel" @onSave="onSave"></FormProperties>'
|
||||
}).$mount();
|
||||
|
||||
const formElement = vm.$el;
|
||||
element.append(formElement);
|
||||
{
|
||||
element,
|
||||
app: self.openmct.app
|
||||
}
|
||||
);
|
||||
|
||||
function onFormPropertyChange(data) {
|
||||
if (onChange) {
|
||||
@@ -195,8 +198,7 @@ export default class FormsAPI {
|
||||
|
||||
function onFormAction(callback) {
|
||||
return () => {
|
||||
formElement.remove();
|
||||
vm.$destroy();
|
||||
destroy();
|
||||
|
||||
if (callback) {
|
||||
callback(changes);
|
||||
|
||||
@@ -141,7 +141,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
onChange(data) {
|
||||
this.$set(this.invalidProperties, data.model.key, data.invalid);
|
||||
this.invalidProperties[data.model.key] = data.invalid;
|
||||
|
||||
this.$emit('onChange', data);
|
||||
},
|
||||
|
||||
@@ -22,13 +22,11 @@
|
||||
|
||||
<template>
|
||||
<div class="form-row c-form__row" :class="[{ first: first }, cssClass]" @onChange="onChange">
|
||||
<div class="c-form-row__label" :title="row.description">
|
||||
<label class="c-form-row__label" :title="row.description" :for="`form-${row.key}`">
|
||||
{{ row.name }}
|
||||
</div>
|
||||
</label>
|
||||
<div class="c-form-row__state-indicator" :class="reqClass"></div>
|
||||
<div v-if="row.control" class="c-form-row__controls">
|
||||
<div ref="rowElement"></div>
|
||||
</div>
|
||||
<div v-if="row.control" ref="rowElement" class="c-form-row__controls"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -91,7 +89,7 @@ export default {
|
||||
|
||||
this.formControl.show(this.$refs.rowElement, this.row, this.onChange);
|
||||
},
|
||||
destroyed() {
|
||||
unmounted() {
|
||||
const destroy = this.formControl.destroy;
|
||||
if (destroy) {
|
||||
destroy();
|
||||
|
||||
@@ -166,7 +166,7 @@ export default {
|
||||
this.options = this.model.options;
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
unmounted() {
|
||||
document.body.removeEventListener('click', this.handleOutsideClick);
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -123,7 +123,6 @@ export default {
|
||||
formatDatetime(timestamp = this.model.value) {
|
||||
if (!timestamp) {
|
||||
this.resetValues();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -137,7 +136,7 @@ export default {
|
||||
|
||||
const data = {
|
||||
model,
|
||||
value: timestamp
|
||||
value: new Date(timestamp).toISOString()
|
||||
};
|
||||
|
||||
this.$emit('onChange', data);
|
||||
|
||||
@@ -23,7 +23,14 @@
|
||||
<template>
|
||||
<span class="form-control shell">
|
||||
<span class="field control" :class="model.cssClass">
|
||||
<input v-model="field" type="text" :size="model.size" @input="updateText()" />
|
||||
<input
|
||||
:id="`form-${model.key}`"
|
||||
v-model="field"
|
||||
:name="model.key"
|
||||
type="text"
|
||||
:size="model.size"
|
||||
@input="updateText()"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
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;
|
||||
@@ -137,14 +138,13 @@ describe('The Menu API', () => {
|
||||
it('invokes the destroy method when menu is dismissed', (done) => {
|
||||
menuOptions.onDestroy = done;
|
||||
|
||||
menuAPI.showMenu(x, y, actionsArray, menuOptions);
|
||||
spyOn(menuAPI, '_clearMenuComponent').and.callThrough();
|
||||
|
||||
const vueComponent = menuAPI.menuComponent.component;
|
||||
spyOn(vueComponent, '$destroy');
|
||||
menuAPI.showMenu(x, y, actionsArray, menuOptions);
|
||||
|
||||
document.body.click();
|
||||
|
||||
expect(vueComponent.$destroy).toHaveBeenCalled();
|
||||
expect(menuAPI._clearMenuComponent).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');
|
||||
|
||||
menuAPI.menuComponent.component.$nextTick(() => {
|
||||
Vue.nextTick(() => {
|
||||
expect(menuElement).not.toBeNull();
|
||||
expect(itemDescription.innerText).toEqual(actionsArray[0].description);
|
||||
|
||||
|
||||
@@ -20,17 +20,16 @@
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<template>
|
||||
<div class="c-menu" :class="options.menuClass">
|
||||
<div class="c-menu" :class="options.menuClass" :style="styleObject">
|
||||
<ul v-if="options.actions.length && options.actions[0].length" role="menu">
|
||||
<template v-for="(actionGroups, index) in options.actions">
|
||||
<div :key="index" role="group">
|
||||
<template v-for="(actionGroups, index) in options.actions" :key="index">
|
||||
<div role="group">
|
||||
<li
|
||||
v-for="action in actionGroups"
|
||||
:key="action.name"
|
||||
role="menuitem"
|
||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||
:title="action.description"
|
||||
:data-testid="action.testId || false"
|
||||
@click="action.onItemClicked"
|
||||
>
|
||||
{{ action.name }}
|
||||
@@ -42,8 +41,8 @@
|
||||
class="c-menu__section-separator"
|
||||
></div>
|
||||
<li v-if="actionGroups.length === 0" :key="index">No actions defined.</li>
|
||||
</div></template
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</ul>
|
||||
|
||||
<ul v-else role="menu">
|
||||
@@ -53,7 +52,6 @@
|
||||
role="menuitem"
|
||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||
:title="action.description"
|
||||
:data-testid="action.testId || false"
|
||||
@click="action.onItemClicked"
|
||||
>
|
||||
{{ action.name }}
|
||||
@@ -64,7 +62,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import popupMenuMixin from '../mixins/popupMenuMixin';
|
||||
export default {
|
||||
mixins: [popupMenuMixin],
|
||||
inject: ['options']
|
||||
};
|
||||
</script>
|
||||
|
||||