mirror of
https://github.com/nasa/openmct.git
synced 2025-06-25 02:29:24 +00:00
Compare commits
82 Commits
omm-r5.3.1
...
openmct-st
Author | SHA1 | Date | |
---|---|---|---|
f04c274d33 | |||
3624236c26 | |||
0126542411 | |||
2c49e62863 | |||
c2df3cdd14 | |||
02be8a9875 | |||
3bd57c8fff | |||
94091b25ec | |||
c191ffb37d | |||
5f03dc45ee | |||
14ac758760 | |||
eb709a60cb | |||
eba1a48a44 | |||
4a0654dbcb | |||
9b6d339d69 | |||
f90afb9277 | |||
018dfb1e28 | |||
c72a02aaa3 | |||
bf3fd66942 | |||
8414ded1ec | |||
646c871c76 | |||
ba401b3341 | |||
5ef02ec4a2 | |||
d788031019 | |||
d870874649 | |||
711a7a2eb5 | |||
c105a08cfe | |||
b87375a809 | |||
9fed056d22 | |||
251bf21933 | |||
a180bf7c02 | |||
ed8a54f0f9 | |||
ff3c2da0f9 | |||
28d5821120 | |||
f5ee457274 | |||
9d2770e4d2 | |||
8b25009816 | |||
074fe4481a | |||
fbd928b842 | |||
110947db09 | |||
ef91e92fbc | |||
d201cac4ac | |||
dcb3ccfec7 | |||
78522cd4f1 | |||
ca232d45cc | |||
df495c841a | |||
92a37ef36b | |||
fd731ca430 | |||
263b1cd3d5 | |||
978fc8b5a3 | |||
698ccc5a35 | |||
e5aa5b5a5f | |||
b942988ef8 | |||
1eec20f2ea | |||
767a2048eb | |||
e65cf1661c | |||
0eae48646c | |||
0ba8a275d2 | |||
d8d32cc3ac | |||
a800848fe1 | |||
6881d98ba6 | |||
48d077cd2e | |||
030dd93c91 | |||
03bf6fc0a3 | |||
ef0a2ed5d2 | |||
a40aa84752 | |||
d3b69dda82 | |||
d3126ebf5c | |||
4479cbc7a2 | |||
f8ff44dac0 | |||
8f4280d15b | |||
6daa27ff31 | |||
43f6c3f85d | |||
1a7c76cf3e | |||
cee9cd7bd1 | |||
c42df20281 | |||
b4149bd2b3 | |||
f436ac9ba0 | |||
8493b481dd | |||
28723b59b7 | |||
9fa7de0b77 | |||
54bfc84ada |
@ -1,291 +1,36 @@
|
||||
version: 2.1
|
||||
orbs:
|
||||
node: circleci/node@5.2.0
|
||||
browser-tools: circleci/browser-tools@1.3.0
|
||||
executors:
|
||||
pw-focal-development:
|
||||
docker:
|
||||
- image: mcr.microsoft.com/playwright:v1.45.2-focal
|
||||
environment:
|
||||
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
|
||||
PERCY_POSTINSTALL_BROWSER: "true" # Needed to store the percy browser in cache deps
|
||||
PERCY_LOGLEVEL: "debug" # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742)
|
||||
PERCY_PARALLEL_TOTAL: 2
|
||||
ubuntu:
|
||||
machine:
|
||||
image: ubuntu-2204:current
|
||||
docker_layer_caching: true
|
||||
commands:
|
||||
build_and_install:
|
||||
description: "All steps used to build and install."
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
steps:
|
||||
- checkout
|
||||
- node/install:
|
||||
node-version: << parameters.node-version >>
|
||||
- node/install-packages
|
||||
generate_and_store_version_and_filesystem_artifacts:
|
||||
description: "Track important packages and files"
|
||||
steps:
|
||||
- run: |
|
||||
[[ $EUID -ne 0 ]] && (sudo mkdir -p /tmp/artifacts && sudo chmod 777 /tmp/artifacts) || (mkdir -p /tmp/artifacts && chmod 777 /tmp/artifacts)
|
||||
printenv NODE_ENV >> /tmp/artifacts/NODE_ENV.txt || true
|
||||
npm -v >> /tmp/artifacts/npm-version.txt
|
||||
node -v >> /tmp/artifacts/node-version.txt
|
||||
ls -latR >> /tmp/artifacts/dir.txt
|
||||
- store_artifacts:
|
||||
path: /tmp/artifacts/
|
||||
generate_e2e_code_cov_report:
|
||||
description: "Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test"
|
||||
parameters:
|
||||
suite:
|
||||
type: string
|
||||
steps:
|
||||
- run: npm run cov:e2e:report || true
|
||||
- run: npm run cov:e2e:<<parameters.suite>>:publish
|
||||
version: 2
|
||||
jobs:
|
||||
npm-audit:
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
executor: pw-focal-development
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/node:13-browsers
|
||||
environment:
|
||||
CHROME_BIN: "/usr/bin/google-chrome"
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: <<parameters.node-version>>
|
||||
- run: npm audit --audit-level=low
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
lint:
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
executor: pw-focal-development
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: <<parameters.node-version>>
|
||||
- run: npm run lint
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
unit-test:
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
executor: pw-focal-development
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: <<parameters.node-version>>
|
||||
- browser-tools/install-chrome:
|
||||
replace-existing: false
|
||||
- run:
|
||||
command: |
|
||||
mkdir -p dist/reports/tests/
|
||||
TESTFILES=$(circleci tests glob "src/**/*Spec.js")
|
||||
echo "$TESTFILES" | circleci tests run --command="xargs npm run test" --verbose
|
||||
- run: npm run cov:unit:publish
|
||||
- store_test_results:
|
||||
path: dist/reports/tests/
|
||||
- store_artifacts:
|
||||
path: coverage
|
||||
- when:
|
||||
condition:
|
||||
equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
|
||||
steps:
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
e2e-test:
|
||||
parameters:
|
||||
suite: #ci or full
|
||||
type: string
|
||||
executor: pw-focal-development
|
||||
parallelism: 7
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: lts/hydrogen
|
||||
- when: #Only install chrome-beta when running the 'full' suite to save $$$
|
||||
condition:
|
||||
equal: ["full", <<parameters.suite>>]
|
||||
steps:
|
||||
- run: npx playwright install chrome-beta
|
||||
- run:
|
||||
command: |
|
||||
mkdir test-results
|
||||
TESTFILES=$(circleci tests glob "e2e/**/*.spec.js")
|
||||
echo "$TESTFILES" | circleci tests run --command="xargs npm run test:e2e:<<parameters.suite>>" --verbose --split-by=timings
|
||||
- when:
|
||||
condition:
|
||||
equal: [42, 42] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
|
||||
steps:
|
||||
- generate_e2e_code_cov_report:
|
||||
suite: <<parameters.suite>>
|
||||
- store_test_results:
|
||||
path: test-results/results.xml
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
path: coverage
|
||||
- store_artifacts:
|
||||
path: html-test-results
|
||||
- when:
|
||||
condition:
|
||||
equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
|
||||
steps:
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
e2e-mobile:
|
||||
executor: pw-focal-development
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: lts/hydrogen
|
||||
- run: npm run test:e2e:mobile
|
||||
- when:
|
||||
condition:
|
||||
equal: [42, 42] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
|
||||
steps:
|
||||
- generate_e2e_code_cov_report:
|
||||
suite: full
|
||||
- store_test_results:
|
||||
path: test-results/results.xml
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
path: coverage
|
||||
- store_artifacts:
|
||||
path: html-test-results
|
||||
- when:
|
||||
condition:
|
||||
equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
|
||||
steps:
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
e2e-couchdb:
|
||||
executor: ubuntu
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: lts/hydrogen
|
||||
- run: npx playwright@1.45.2 install #Necessary for bare ubuntu machine
|
||||
- run: |
|
||||
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
|
||||
docker compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
|
||||
sleep 3
|
||||
bash src/plugins/persistence/couch/setup-couchdb.sh
|
||||
- run: sh src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh #Replace LocalStorage Plugin with CouchDB
|
||||
- run: npm run test:e2e:couchdb
|
||||
- when:
|
||||
condition:
|
||||
equal: [42, 42] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
|
||||
steps:
|
||||
- generate_e2e_code_cov_report:
|
||||
suite: full #add to full suite
|
||||
- store_test_results:
|
||||
path: test-results/results.xml
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
path: coverage
|
||||
- store_artifacts:
|
||||
path: html-test-results
|
||||
- when:
|
||||
condition:
|
||||
equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
|
||||
steps:
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
mem-test:
|
||||
executor: pw-focal-development
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: lts/hydrogen
|
||||
- run: npm run test:perf:memory
|
||||
- store_test_results:
|
||||
path: test-results/results.xml
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
path: html-test-results
|
||||
- when:
|
||||
condition:
|
||||
equal: [42, 42] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
|
||||
steps:
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
perf-test:
|
||||
executor: pw-focal-development
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: lts/hydrogen
|
||||
- run: npm run test:perf:localhost
|
||||
- run: npm run test:perf:contract
|
||||
- store_test_results:
|
||||
path: test-results/results.xml
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
path: html-test-results
|
||||
- when:
|
||||
condition:
|
||||
equal: [42, 42] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
|
||||
steps:
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
visual-a11y:
|
||||
parameters:
|
||||
suite:
|
||||
type: string # ci or full
|
||||
executor: pw-focal-development
|
||||
parallelism: 2
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: lts/iron
|
||||
- run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:visual:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
|
||||
- store_test_results:
|
||||
path: test-results/results.xml
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
path: html-test-results
|
||||
- when:
|
||||
condition:
|
||||
equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
|
||||
steps:
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
- checkout
|
||||
- run:
|
||||
name: Update npm
|
||||
command: 'sudo npm install -g npm@latest'
|
||||
- restore_cache:
|
||||
key: dependency-cache-{{ checksum "package.json" }}
|
||||
- run:
|
||||
name: Installing dependencies (npm install)
|
||||
command: npm install
|
||||
- save_cache:
|
||||
key: dependency-cache-{{ checksum "package.json" }}
|
||||
paths:
|
||||
- node_modules
|
||||
- run:
|
||||
name: npm run test:coverage
|
||||
command: npm run test:coverage
|
||||
- run:
|
||||
name: npm run lint
|
||||
command: npm run lint
|
||||
- store_artifacts:
|
||||
path: dist
|
||||
prefix: dist
|
||||
|
||||
workflows:
|
||||
overall-circleci-commit-status: #These jobs run on every commit
|
||||
version: 2
|
||||
test:
|
||||
jobs:
|
||||
- lint:
|
||||
name: node20-lint
|
||||
node-version: lts/iron
|
||||
- unit-test:
|
||||
name: node18-chrome
|
||||
node-version: lts/hydrogen
|
||||
- e2e-test:
|
||||
name: e2e-ci
|
||||
suite: ci
|
||||
- e2e-mobile
|
||||
- visual-a11y:
|
||||
name: visual-a11y-ci
|
||||
suite: ci
|
||||
- perf-test
|
||||
- mem-test
|
||||
|
||||
the-nightly: #These jobs do not run on PRs, but against master at night
|
||||
jobs:
|
||||
- unit-test:
|
||||
name: node20-chrome-nightly
|
||||
node-version: lts/iron
|
||||
- unit-test:
|
||||
name: node18-chrome
|
||||
node-version: lts/hydrogen
|
||||
- npm-audit:
|
||||
node-version: lts/hydrogen
|
||||
- e2e-test:
|
||||
name: e2e-full-nightly
|
||||
suite: full
|
||||
- e2e-mobile
|
||||
- perf-test
|
||||
- mem-test
|
||||
- visual-a11y:
|
||||
name: visual-a11y-nightly
|
||||
suite: full
|
||||
- e2e-couchdb
|
||||
triggers:
|
||||
- schedule:
|
||||
cron: "0 0 * * *"
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- build
|
||||
|
499
.cspell.json
499
.cspell.json
@ -1,499 +0,0 @@
|
||||
{
|
||||
"version": "0.2",
|
||||
"language": "en,en-us",
|
||||
"words": [
|
||||
"gress",
|
||||
"doctoc",
|
||||
"minmax",
|
||||
"openmct",
|
||||
"datasources",
|
||||
"Sinewave",
|
||||
"deregistration",
|
||||
"unregisters",
|
||||
"codecov",
|
||||
"carryforward",
|
||||
"Chacon",
|
||||
"Straub",
|
||||
"OWASP",
|
||||
"Testathon",
|
||||
"Testathons",
|
||||
"testathon",
|
||||
"npmjs",
|
||||
"treeitem",
|
||||
"timespan",
|
||||
"Timespan",
|
||||
"spinbutton",
|
||||
"popout",
|
||||
"textbox",
|
||||
"tablist",
|
||||
"Telem",
|
||||
"codecoverage",
|
||||
"browserless",
|
||||
"networkidle",
|
||||
"nums",
|
||||
"mgmt",
|
||||
"faultname",
|
||||
"gantt",
|
||||
"sharded",
|
||||
"MMOC",
|
||||
"codegen",
|
||||
"viewports",
|
||||
"updatesnapshots",
|
||||
"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",
|
||||
"pathing",
|
||||
"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",
|
||||
"TIMESYSTEM",
|
||||
"Metadatas",
|
||||
"unsub",
|
||||
"callbacktwo",
|
||||
"unsubscribetwo",
|
||||
"telem",
|
||||
"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",
|
||||
"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",
|
||||
"blockquote",
|
||||
"blockquotes",
|
||||
"Blockquote",
|
||||
"Blockquotes",
|
||||
"oger",
|
||||
"lcovonly",
|
||||
"gcov",
|
||||
"WCAG",
|
||||
"stackedplot",
|
||||
"Andale",
|
||||
"unnormalized",
|
||||
"checksnapshots",
|
||||
"specced",
|
||||
"composables",
|
||||
"countup",
|
||||
"darkmatter",
|
||||
"Undeletes",
|
||||
"SSSZ"
|
||||
],
|
||||
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US", "en-gb", "misc"],
|
||||
"ignorePaths": [
|
||||
"package.json",
|
||||
"dist/**",
|
||||
"package-lock.json",
|
||||
"node_modules",
|
||||
"coverage",
|
||||
"*.log",
|
||||
"html-test-results",
|
||||
"test-results"
|
||||
]
|
||||
}
|
192
.eslintrc.cjs
192
.eslintrc.cjs
@ -1,192 +0,0 @@
|
||||
const LEGACY_FILES = ['example/**'];
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
const config = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2024: true,
|
||||
jasmine: true,
|
||||
amd: true,
|
||||
node: true
|
||||
},
|
||||
globals: {
|
||||
_: 'readonly',
|
||||
__webpack_public_path__: 'writeable',
|
||||
__OPENMCT_VERSION__: 'readonly',
|
||||
__OPENMCT_BUILD_DATE__: 'readonly',
|
||||
__OPENMCT_REVISION__: 'readonly',
|
||||
__OPENMCT_BUILD_BRANCH__: 'readonly'
|
||||
},
|
||||
plugins: ['prettier', 'unicorn', 'simple-import-sort'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:compat/recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:you-dont-need-lodash-underscore/compatible',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:no-unsanitized/DOM'
|
||||
],
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@babel/eslint-parser',
|
||||
requireConfigFile: false,
|
||||
allowImportExportEverywhere: true,
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: {
|
||||
impliedStrict: true
|
||||
},
|
||||
sourceType: 'module'
|
||||
},
|
||||
rules: {
|
||||
'simple-import-sort/imports': 'warn',
|
||||
'simple-import-sort/exports': 'warn',
|
||||
'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',
|
||||
'vue/component-name-in-template-casing': ['error', 'PascalCase'],
|
||||
'prettier/prettier': 'error',
|
||||
'you-dont-need-lodash-underscore/omit': 'off',
|
||||
'you-dont-need-lodash-underscore/throttle': 'off',
|
||||
'you-dont-need-lodash-underscore/flatten': 'off',
|
||||
'you-dont-need-lodash-underscore/get': 'off',
|
||||
'no-bitwise': 'error',
|
||||
curly: 'error',
|
||||
eqeqeq: 'error',
|
||||
'guard-for-in': 'error',
|
||||
'no-extend-native': 'error',
|
||||
'no-inner-declarations': 'off',
|
||||
'no-use-before-define': ['error', 'nofunc'],
|
||||
'no-caller': 'error',
|
||||
'no-irregular-whitespace': 'error',
|
||||
'no-new': 'error',
|
||||
'no-shadow': 'error',
|
||||
'no-undef': 'error',
|
||||
'no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
vars: 'all',
|
||||
args: 'none'
|
||||
}
|
||||
],
|
||||
'no-console': 'off',
|
||||
'new-cap': [
|
||||
'error',
|
||||
{
|
||||
capIsNew: false,
|
||||
properties: false
|
||||
}
|
||||
],
|
||||
'dot-notation': 'error',
|
||||
|
||||
// https://eslint.org/docs/rules/no-case-declarations
|
||||
'no-case-declarations': 'error',
|
||||
// https://eslint.org/docs/rules/max-classes-per-file
|
||||
'max-classes-per-file': ['error', 1],
|
||||
// https://eslint.org/docs/rules/no-eq-null
|
||||
'no-eq-null': 'error',
|
||||
// https://eslint.org/docs/rules/no-eval
|
||||
'no-eval': 'error',
|
||||
// https://eslint.org/docs/rules/no-implicit-globals
|
||||
'no-implicit-globals': 'error',
|
||||
// https://eslint.org/docs/rules/no-implied-eval
|
||||
'no-implied-eval': 'error',
|
||||
// https://eslint.org/docs/rules/no-lone-blocks
|
||||
'no-lone-blocks': 'error',
|
||||
// https://eslint.org/docs/rules/no-loop-func
|
||||
'no-loop-func': 'error',
|
||||
// https://eslint.org/docs/rules/no-new-func
|
||||
'no-new-func': 'error',
|
||||
// https://eslint.org/docs/rules/no-new-wrappers
|
||||
'no-new-wrappers': 'error',
|
||||
// https://eslint.org/docs/rules/no-octal-escape
|
||||
'no-octal-escape': 'error',
|
||||
// https://eslint.org/docs/rules/no-proto
|
||||
'no-proto': 'error',
|
||||
// https://eslint.org/docs/rules/no-return-await
|
||||
'no-return-await': 'error',
|
||||
// https://eslint.org/docs/rules/no-script-url
|
||||
'no-script-url': 'error',
|
||||
// https://eslint.org/docs/rules/no-self-compare
|
||||
'no-self-compare': 'error',
|
||||
// https://eslint.org/docs/rules/no-sequences
|
||||
'no-sequences': 'error',
|
||||
// https://eslint.org/docs/rules/no-unmodified-loop-condition
|
||||
'no-unmodified-loop-condition': 'error',
|
||||
// https://eslint.org/docs/rules/no-useless-call
|
||||
'no-useless-call': 'error',
|
||||
// https://eslint.org/docs/rules/no-nested-ternary
|
||||
'no-nested-ternary': 'error',
|
||||
// https://eslint.org/docs/rules/no-useless-computed-key
|
||||
'no-useless-computed-key': 'error',
|
||||
// https://eslint.org/docs/rules/no-var
|
||||
'no-var': 'error',
|
||||
// https://eslint.org/docs/rules/one-var
|
||||
'one-var': ['error', 'never'],
|
||||
// https://eslint.org/docs/rules/default-case-last
|
||||
'default-case-last': 'error',
|
||||
// https://eslint.org/docs/rules/default-param-last
|
||||
'default-param-last': 'error',
|
||||
// https://eslint.org/docs/rules/grouped-accessor-pairs
|
||||
'grouped-accessor-pairs': 'error',
|
||||
// https://eslint.org/docs/rules/no-constructor-return
|
||||
'no-constructor-return': 'error',
|
||||
// https://eslint.org/docs/rules/array-callback-return
|
||||
'array-callback-return': 'error',
|
||||
// https://eslint.org/docs/rules/no-invalid-this
|
||||
'no-invalid-this': 'error', // Believe this one actually surfaces some bugs
|
||||
// https://eslint.org/docs/rules/func-style
|
||||
'func-style': ['error', 'declaration'],
|
||||
// https://eslint.org/docs/rules/no-unused-expressions
|
||||
'no-unused-expressions': 'error',
|
||||
// https://eslint.org/docs/rules/no-useless-concat
|
||||
'no-useless-concat': 'error',
|
||||
// https://eslint.org/docs/rules/radix
|
||||
radix: 'error',
|
||||
// https://eslint.org/docs/rules/require-await
|
||||
'require-await': 'error',
|
||||
// https://eslint.org/docs/rules/no-alert
|
||||
'no-alert': 'error',
|
||||
// https://eslint.org/docs/rules/no-useless-constructor
|
||||
'no-useless-constructor': 'error',
|
||||
// https://eslint.org/docs/rules/no-duplicate-imports
|
||||
'no-duplicate-imports': 'error',
|
||||
|
||||
// https://eslint.org/docs/rules/no-implicit-coercion
|
||||
'no-implicit-coercion': 'error',
|
||||
//https://eslint.org/docs/rules/no-unneeded-ternary
|
||||
'no-unneeded-ternary': 'error',
|
||||
'unicorn/filename-case': [
|
||||
'error',
|
||||
{
|
||||
cases: {
|
||||
pascalCase: true
|
||||
},
|
||||
ignore: ['^.*\\.(js|cjs|mjs)$']
|
||||
}
|
||||
],
|
||||
'vue/first-attribute-linebreak': 'error',
|
||||
'vue/multiline-html-element-content-newline': 'off',
|
||||
'vue/singleline-html-element-content-newline': 'off',
|
||||
'vue/no-mutating-props': 'off' // TODO: Remove this rule and fix resulting errors
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: LEGACY_FILES,
|
||||
rules: {
|
||||
'no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
vars: 'all',
|
||||
args: 'none',
|
||||
varsIgnorePattern: 'controller'
|
||||
}
|
||||
],
|
||||
'no-nested-ternary': 'off',
|
||||
'no-var': 'off',
|
||||
'one-var': 'off'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = config;
|
260
.eslintrc.js
Normal file
260
.eslintrc.js
Normal file
@ -0,0 +1,260 @@
|
||||
const LEGACY_FILES = ["platform/**", "example/**"];
|
||||
module.exports = {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"jasmine": true,
|
||||
"amd": true
|
||||
},
|
||||
"globals": {
|
||||
"_": "readonly"
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:vue/recommended",
|
||||
"plugin:you-dont-need-lodash-underscore/compatible"
|
||||
],
|
||||
"parser": "vue-eslint-parser",
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint",
|
||||
"allowImportExportEverywhere": true,
|
||||
"ecmaVersion": 2015,
|
||||
"ecmaFeatures": {
|
||||
"impliedStrict": true
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"you-dont-need-lodash-underscore/omit": "off",
|
||||
"you-dont-need-lodash-underscore/throttle": "off",
|
||||
"you-dont-need-lodash-underscore/flatten": "off",
|
||||
"no-bitwise": "error",
|
||||
"curly": "error",
|
||||
"eqeqeq": "error",
|
||||
"guard-for-in": "error",
|
||||
"no-extend-native": "error",
|
||||
"no-inner-declarations": "off",
|
||||
"no-use-before-define": ["error", "nofunc"],
|
||||
"no-caller": "error",
|
||||
"no-sequences": "error",
|
||||
"no-irregular-whitespace": "error",
|
||||
"no-new": "error",
|
||||
"no-shadow": "error",
|
||||
"no-undef": "error",
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"vars": "all",
|
||||
"args": "none"
|
||||
}
|
||||
],
|
||||
"no-console": "off",
|
||||
"no-trailing-spaces": "error",
|
||||
"space-before-function-paren": [
|
||||
"error",
|
||||
{
|
||||
"anonymous": "always",
|
||||
"asyncArrow": "always",
|
||||
"named": "never",
|
||||
}
|
||||
],
|
||||
"array-bracket-spacing": "error",
|
||||
"space-in-parens": "error",
|
||||
"space-before-blocks": "error",
|
||||
"comma-dangle": "error",
|
||||
"eol-last": "error",
|
||||
"new-cap": [
|
||||
"error",
|
||||
{
|
||||
"capIsNew": false,
|
||||
"properties": false
|
||||
}
|
||||
],
|
||||
"dot-notation": "error",
|
||||
"indent": ["error", 4],
|
||||
|
||||
// https://eslint.org/docs/rules/no-case-declarations
|
||||
"no-case-declarations": "error",
|
||||
// https://eslint.org/docs/rules/max-classes-per-file
|
||||
"max-classes-per-file": ["error", 1],
|
||||
// https://eslint.org/docs/rules/no-eq-null
|
||||
"no-eq-null": "error",
|
||||
// https://eslint.org/docs/rules/no-eval
|
||||
"no-eval": "error",
|
||||
// https://eslint.org/docs/rules/no-floating-decimal
|
||||
"no-floating-decimal": "error",
|
||||
// https://eslint.org/docs/rules/no-implicit-globals
|
||||
"no-implicit-globals": "error",
|
||||
// https://eslint.org/docs/rules/no-implied-eval
|
||||
"no-implied-eval": "error",
|
||||
// https://eslint.org/docs/rules/no-lone-blocks
|
||||
"no-lone-blocks": "error",
|
||||
// https://eslint.org/docs/rules/no-loop-func
|
||||
"no-loop-func": "error",
|
||||
// https://eslint.org/docs/rules/no-new-func
|
||||
"no-new-func": "error",
|
||||
// https://eslint.org/docs/rules/no-new-wrappers
|
||||
"no-new-wrappers": "error",
|
||||
// https://eslint.org/docs/rules/no-octal-escape
|
||||
"no-octal-escape": "error",
|
||||
// https://eslint.org/docs/rules/no-proto
|
||||
"no-proto": "error",
|
||||
// https://eslint.org/docs/rules/no-return-await
|
||||
"no-return-await": "error",
|
||||
// https://eslint.org/docs/rules/no-script-url
|
||||
"no-script-url": "error",
|
||||
// https://eslint.org/docs/rules/no-self-compare
|
||||
"no-self-compare": "error",
|
||||
// https://eslint.org/docs/rules/no-sequences
|
||||
"no-sequences": "error",
|
||||
// https://eslint.org/docs/rules/no-unmodified-loop-condition
|
||||
"no-unmodified-loop-condition": "error",
|
||||
// https://eslint.org/docs/rules/no-useless-call
|
||||
"no-useless-call": "error",
|
||||
// https://eslint.org/docs/rules/wrap-iife
|
||||
"wrap-iife": "error",
|
||||
// https://eslint.org/docs/rules/no-nested-ternary
|
||||
"no-nested-ternary": "error",
|
||||
// https://eslint.org/docs/rules/switch-colon-spacing
|
||||
"switch-colon-spacing": "error",
|
||||
// https://eslint.org/docs/rules/no-useless-computed-key
|
||||
"no-useless-computed-key": "error",
|
||||
// https://eslint.org/docs/rules/rest-spread-spacing
|
||||
"rest-spread-spacing": ["error"],
|
||||
// https://eslint.org/docs/rules/no-var
|
||||
"no-var": "error",
|
||||
// https://eslint.org/docs/rules/one-var
|
||||
"one-var": ["error", "never"],
|
||||
// https://eslint.org/docs/rules/default-case-last
|
||||
"default-case-last": "error",
|
||||
// https://eslint.org/docs/rules/default-param-last
|
||||
"default-param-last": "error",
|
||||
// https://eslint.org/docs/rules/grouped-accessor-pairs
|
||||
"grouped-accessor-pairs": "error",
|
||||
// https://eslint.org/docs/rules/no-constructor-return
|
||||
"no-constructor-return": "error",
|
||||
// https://eslint.org/docs/rules/array-callback-return
|
||||
"array-callback-return": "error",
|
||||
// https://eslint.org/docs/rules/no-invalid-this
|
||||
"no-invalid-this": "error", // Believe this one actually surfaces some bugs
|
||||
// https://eslint.org/docs/rules/func-style
|
||||
"func-style": ["error", "declaration"],
|
||||
// https://eslint.org/docs/rules/no-unused-expressions
|
||||
"no-unused-expressions": "error",
|
||||
// https://eslint.org/docs/rules/no-useless-concat
|
||||
"no-useless-concat": "error",
|
||||
// https://eslint.org/docs/rules/radix
|
||||
"radix": "error",
|
||||
// https://eslint.org/docs/rules/require-await
|
||||
"require-await": "error",
|
||||
// https://eslint.org/docs/rules/no-alert
|
||||
"no-alert": "error",
|
||||
// https://eslint.org/docs/rules/no-useless-constructor
|
||||
"no-useless-constructor": "error",
|
||||
// https://eslint.org/docs/rules/no-duplicate-imports
|
||||
"no-duplicate-imports": "error",
|
||||
|
||||
// https://eslint.org/docs/rules/no-implicit-coercion
|
||||
"no-implicit-coercion": "error",
|
||||
//https://eslint.org/docs/rules/no-unneeded-ternary
|
||||
"no-unneeded-ternary": "error",
|
||||
// https://eslint.org/docs/rules/semi
|
||||
"semi": ["error", "always"],
|
||||
// https://eslint.org/docs/rules/no-multi-spaces
|
||||
"no-multi-spaces": "error",
|
||||
// https://eslint.org/docs/rules/key-spacing
|
||||
"key-spacing": ["error", {
|
||||
"afterColon": true
|
||||
}],
|
||||
// https://eslint.org/docs/rules/keyword-spacing
|
||||
"keyword-spacing": ["error", {
|
||||
"before": true,
|
||||
"after": true
|
||||
}],
|
||||
// https://eslint.org/docs/rules/comma-spacing
|
||||
// Also requires one line code fix
|
||||
"comma-spacing": ["error", {
|
||||
"after": true
|
||||
}],
|
||||
//https://eslint.org/docs/rules/no-whitespace-before-property
|
||||
"no-whitespace-before-property": "error",
|
||||
// https://eslint.org/docs/rules/object-curly-newline
|
||||
"object-curly-newline": ["error", {"consistent": true, "multiline": true}],
|
||||
// https://eslint.org/docs/rules/object-property-newline
|
||||
"object-property-newline": "error",
|
||||
// https://eslint.org/docs/rules/brace-style
|
||||
"brace-style": "error",
|
||||
// https://eslint.org/docs/rules/no-multiple-empty-lines
|
||||
"no-multiple-empty-lines": ["error", {"max": 1}],
|
||||
// https://eslint.org/docs/rules/operator-linebreak
|
||||
"operator-linebreak": ["error", "before", {"overrides": {"=": "after"}}],
|
||||
// https://eslint.org/docs/rules/padding-line-between-statements
|
||||
"padding-line-between-statements":["error", {
|
||||
"blankLine": "always",
|
||||
"prev": "multiline-block-like",
|
||||
"next": "*"
|
||||
}, {
|
||||
"blankLine": "always",
|
||||
"prev": "*",
|
||||
"next": "return"
|
||||
}],
|
||||
// https://eslint.org/docs/rules/space-infix-ops
|
||||
"space-infix-ops": "error",
|
||||
// https://eslint.org/docs/rules/space-unary-ops
|
||||
"space-unary-ops": ["error", {"words": true, "nonwords": false}],
|
||||
// https://eslint.org/docs/rules/arrow-spacing
|
||||
"arrow-spacing": "error",
|
||||
// https://eslint.org/docs/rules/semi-spacing
|
||||
"semi-spacing": ["error", {"before": false, "after": true}],
|
||||
|
||||
"vue/html-indent": [
|
||||
"error",
|
||||
4,
|
||||
{
|
||||
"attribute": 1,
|
||||
"baseIndent": 0,
|
||||
"closeBracket": 0,
|
||||
"alignAttributesVertically": true,
|
||||
"ignores": []
|
||||
}
|
||||
],
|
||||
"vue/html-self-closing": ["error",
|
||||
{
|
||||
"html": {
|
||||
"void": "never",
|
||||
"normal": "never",
|
||||
"component": "always"
|
||||
},
|
||||
"svg": "always",
|
||||
"math": "always"
|
||||
}
|
||||
],
|
||||
"vue/max-attributes-per-line": ["error", {
|
||||
"singleline": 1,
|
||||
"multiline": {
|
||||
"max": 1,
|
||||
"allowFirstLine": true
|
||||
}
|
||||
}],
|
||||
"vue/multiline-html-element-content-newline": "off",
|
||||
"vue/singleline-html-element-content-newline": "off",
|
||||
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": LEGACY_FILES,
|
||||
"rules": {
|
||||
"no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"vars": "all",
|
||||
"args": "none",
|
||||
"varsIgnorePattern": "controller"
|
||||
}
|
||||
],
|
||||
"no-nested-ternary": "off",
|
||||
"no-var": "off",
|
||||
"one-var": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
@ -1,16 +0,0 @@
|
||||
# git-blame ignored revisions
|
||||
# To configure, run:
|
||||
# git config blame.ignoreRevsFile .git-blame-ignore-revs
|
||||
# Requires Git > 2.23
|
||||
# See https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revs-fileltfilegt
|
||||
|
||||
# vue-eslint update 2019
|
||||
14a0f84c1bcd56886d7c9e4e6afa8f7d292734e5
|
||||
# eslint changes 2022
|
||||
d80b6923541704ab925abf0047cbbc58735c27e2
|
||||
# Copyright year update 2022
|
||||
4a9744e916d24122a81092f6b7950054048ba860
|
||||
# Copyright year update 2023
|
||||
8040b275fcf2ba71b42cd72d4daa64bb25c19c2d
|
||||
# Apply `prettier` formatting
|
||||
caa7bc6faebc204f67aedae3e35fb0d0d3ce27a7
|
47
.github/ISSUE_TEMPLATE/bug_report.md
vendored
47
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,47 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: File a Bug !
|
||||
title: ''
|
||||
labels: type:bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--- Focus on user impact in the title. Use the Summary Field to -->
|
||||
<!--- describe the problem technically. -->
|
||||
|
||||
#### Summary
|
||||
<!--- A description of the issue encountered. When possible, a description -->
|
||||
<!--- of the impact of the issue. What use case does it impede?-->
|
||||
|
||||
#### Expected vs Current Behavior
|
||||
<!--- Tell us what should have happened -->
|
||||
|
||||
#### Steps to Reproduce
|
||||
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
|
||||
<!--- reproduce this bug. Include code to reproduce, if relevant -->
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
|
||||
#### Environment
|
||||
<!--- If encountered on local machine, execute the following:
|
||||
<!--- npx envinfo --system --browsers --npmPackages --binaries --markdown -->
|
||||
* Open MCT Version: <!--- date of build, version, or SHA -->
|
||||
* Deployment Type: <!--- npm dev? VIPER Dev? openmct-yamcs? -->
|
||||
* OS:
|
||||
* Browser:
|
||||
|
||||
#### Impact Check List
|
||||
<!--- Please select from the following options -->
|
||||
- [ ] Data loss or misrepresented data?
|
||||
- [ ] Regression? Did this used to work or has it always been broken?
|
||||
- [ ] Is there a workaround available?
|
||||
- [ ] Does this impact a critical component?
|
||||
- [ ] Is this just a visual bug with no functional impact?
|
||||
- [ ] Does this block the execution of e2e tests?
|
||||
- [ ] Does this have an impact on Performance?
|
||||
|
||||
#### Additional Information
|
||||
<!--- Include any screenshots, gifs, or logs which will expedite triage -->
|
5
.github/ISSUE_TEMPLATE/config.yml
vendored
5
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,5 +0,0 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Discussions
|
||||
url: https://github.com/nasa/openmct/discussions
|
||||
about: Have a question about the project?
|
20
.github/ISSUE_TEMPLATE/enhancement-request.md
vendored
20
.github/ISSUE_TEMPLATE/enhancement-request.md
vendored
@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Enhancement request
|
||||
about: Suggest an enhancement or new improvement for this project
|
||||
title: ''
|
||||
labels: type:enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
11
.github/ISSUE_TEMPLATE/maintenance-type.md
vendored
11
.github/ISSUE_TEMPLATE/maintenance-type.md
vendored
@ -1,11 +0,0 @@
|
||||
---
|
||||
name: Maintenance
|
||||
about: Add, update or remove documentation, tests, or dependencies.
|
||||
title: ''
|
||||
labels: type:maintenance
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
#### Summary
|
||||
<!--- Generally describe the purpose of the change. -->
|
28
.github/PULL_REQUEST_TEMPLATE.md
vendored
28
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,28 +0,0 @@
|
||||
<!--- Note: Please open the PR in draft form until you are ready for active review. -->
|
||||
Closes <!--- Insert Issue Number(s) this PR addresses. Start by typing # will open a dropdown of recent issues. Note: this does not work on PRs which target release branches -->
|
||||
|
||||
### Describe your changes:
|
||||
<!--- Describe your changes and add any comments about your approach either here or inline if code comments aren't added -->
|
||||
|
||||
### All Submissions:
|
||||
|
||||
* [ ] Have you followed the guidelines in our [Contributing document](https://github.com/nasa/openmct/blob/master/CONTRIBUTING.md)?
|
||||
* [ ] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/nasa/openmct/pulls) for the same update/change?
|
||||
* [ ] Is this a [notable change](../docs/src/process/release.md) that will require a special callout in the release notes? For example, will this break compatibility with existing APIs or projects that consume these plugins?
|
||||
|
||||
### Author Checklist
|
||||
|
||||
* [ ] Changes address original issue?
|
||||
* [ ] Tests included and/or updated with changes?
|
||||
* [ ] Has this been smoke tested?
|
||||
* [ ] Have you associated this PR with a `type:` label? Note: this is not necessarily the same as the original issue.
|
||||
* [ ] Have you associated a milestone with this PR? Note: leave blank if unsure.
|
||||
* [ ] Testing instructions included in associated issue OR is this a dependency/testcase change?
|
||||
|
||||
### Reviewer Checklist
|
||||
|
||||
* [ ] Changes appear to address issue?
|
||||
* [ ] Reviewer has tested changes by following the provided instructions?
|
||||
* [ ] Changes appear not to be breaking changes?
|
||||
* [ ] Appropriate automated tests included?
|
||||
* [ ] Code style and in-line documentation are appropriate?
|
5
.github/codeql/codeql-config.yml
vendored
5
.github/codeql/codeql-config.yml
vendored
@ -1,5 +0,0 @@
|
||||
name: 'Custom CodeQL config'
|
||||
|
||||
paths-ignore:
|
||||
# Ignore e2e tests and framework
|
||||
- e2e
|
42
.github/dependabot.yml
vendored
42
.github/dependabot.yml
vendored
@ -1,42 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: 'npm'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
open-pull-requests-limit: 10
|
||||
rebase-strategy: 'disabled'
|
||||
labels:
|
||||
- 'pr:daveit'
|
||||
- 'pr:e2e'
|
||||
- 'type:maintenance'
|
||||
- 'dependencies'
|
||||
- 'pr:platform'
|
||||
ignore:
|
||||
#We have to source the playwright container which is not detected by Dependabot
|
||||
- dependency-name: '@playwright/test'
|
||||
- dependency-name: 'playwright-core'
|
||||
#Lots of noise in these type patch releases.
|
||||
- dependency-name: '@babel/eslint-parser'
|
||||
update-types: ['version-update:semver-patch']
|
||||
- dependency-name: 'eslint-plugin-vue'
|
||||
update-types: ['version-update:semver-patch']
|
||||
- dependency-name: 'babel-loader'
|
||||
update-types: ['version-update:semver-patch']
|
||||
- dependency-name: 'sinon'
|
||||
update-types: ['version-update:semver-patch']
|
||||
- dependency-name: 'moment-timezone'
|
||||
update-types: ['version-update:semver-patch']
|
||||
- dependency-name: '@types/lodash'
|
||||
update-types: ['version-update:semver-patch']
|
||||
- dependency-name: 'marked'
|
||||
update-types: ['version-update:semver-patch']
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
rebase-strategy: 'disabled'
|
||||
labels:
|
||||
- 'pr:daveit'
|
||||
- 'type:maintenance'
|
||||
- 'dependencies'
|
26
.github/release.yml
vendored
26
.github/release.yml
vendored
@ -1,26 +0,0 @@
|
||||
changelog:
|
||||
categories:
|
||||
- title: 💥 Notable Changes
|
||||
labels:
|
||||
- notable_change
|
||||
- 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:
|
||||
- "*"
|
44
.github/workflows/codeql-analysis.yml
vendored
44
.github/workflows/codeql-analysis.yml
vendored
@ -1,44 +0,0 @@
|
||||
name: 'CodeQL'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, 'release/*']
|
||||
pull_request:
|
||||
branches: [master, 'release/*']
|
||||
paths-ignore:
|
||||
- '**/*Spec.js'
|
||||
- '**/*.md'
|
||||
- '**/*.txt'
|
||||
- '**/*.yml'
|
||||
- '**/*.yaml'
|
||||
- '**/*.spec.js'
|
||||
- '**/*.config.js'
|
||||
schedule:
|
||||
- cron: '28 21 * * 3'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
languages: javascript
|
||||
queries: security-and-quality
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
88
.github/workflows/e2e-couchdb.yml
vendored
88
.github/workflows/e2e-couchdb.yml
vendored
@ -1,88 +0,0 @@
|
||||
name: 'e2e-couchdb'
|
||||
on:
|
||||
push:
|
||||
branches: master
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- opened
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
jobs:
|
||||
e2e-couchdb:
|
||||
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@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
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 ci --no-audit --progress=false
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- run: npx playwright@1.45.2 install
|
||||
|
||||
- name: Start CouchDB Docker Container and Init with Setup Scripts
|
||||
run: |
|
||||
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
|
||||
docker compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
|
||||
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
|
||||
env:
|
||||
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: always()
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo, number } = context.issue;
|
||||
const labelToRemove = 'pr:e2e:couchdb';
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: number,
|
||||
name: labelToRemove
|
||||
});
|
||||
} catch (error) {
|
||||
core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);
|
||||
}
|
61
.github/workflows/e2e-flakefinder.yml
vendored
61
.github/workflows/e2e-flakefinder.yml
vendored
@ -1,61 +0,0 @@
|
||||
name: 'pr:e2e:flakefinder'
|
||||
|
||||
on:
|
||||
# push:
|
||||
# branches: master
|
||||
workflow_dispatch:
|
||||
# pull_request:
|
||||
# types:
|
||||
# - labeled
|
||||
# - opened
|
||||
# schedule:
|
||||
# - cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
e2e-flakefinder:
|
||||
if: contains(github.event.pull_request.labels.*.name, 'pr:e2e:flakefinder') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event.action == 'opened'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
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.45.2 install
|
||||
- run: npm ci --no-audit --progress=false
|
||||
|
||||
- name: Run E2E Tests (Repeated 10 Times)
|
||||
run: npm run test:e2e:ci -- --retries=0 --repeat-each=10 --max-failures=50
|
||||
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: test-results
|
||||
|
||||
- name: Remove pr:e2e:flakefinder label (if present)
|
||||
if: always()
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo, number } = context.issue;
|
||||
const labelToRemove = 'pr:e2e:flakefinder';
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: number,
|
||||
name: labelToRemove
|
||||
});
|
||||
} catch (error) {
|
||||
core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);
|
||||
}
|
58
.github/workflows/e2e-perf.yml
vendored
58
.github/workflows/e2e-perf.yml
vendored
@ -1,58 +0,0 @@
|
||||
name: 'e2e-perf'
|
||||
on:
|
||||
push:
|
||||
branches: master
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- opened
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
jobs:
|
||||
e2e-full:
|
||||
if: contains(github.event.pull_request.labels.*.name, 'pr:e2e:perf') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
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.45.2 install
|
||||
- run: npm ci --no-audit --progress=false
|
||||
- run: npm run test:perf:localhost
|
||||
- run: npm run test:perf:contract
|
||||
- run: npm run test:perf:memory
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: test-results
|
||||
|
||||
- name: Remove pr:e2e:perf label (if present)
|
||||
if: always()
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo, number } = context.issue;
|
||||
const labelToRemove = 'pr:e2e:perf';
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: number,
|
||||
name: labelToRemove
|
||||
});
|
||||
} catch (error) {
|
||||
core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);
|
||||
}
|
68
.github/workflows/e2e-pr.yml
vendored
68
.github/workflows/e2e-pr.yml
vendored
@ -1,68 +0,0 @@
|
||||
name: 'e2e-pr'
|
||||
on:
|
||||
push:
|
||||
branches: master
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- opened
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
jobs:
|
||||
e2e-full:
|
||||
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:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
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.45.2 install
|
||||
- run: npx playwright install chrome-beta
|
||||
- run: npm ci --no-audit --progress=false
|
||||
- run: npm run test:e2e:full -- --max-failures=40
|
||||
- run: npm run cov:e2e:report || true
|
||||
- shell: bash
|
||||
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: Remove pr:e2e label (if present)
|
||||
if: always()
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo, number } = context.issue;
|
||||
const labelToRemove = 'pr:e2e';
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: number,
|
||||
name: labelToRemove
|
||||
});
|
||||
} catch (error) {
|
||||
core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);
|
||||
}
|
37
.github/workflows/npm-prerelease.yml
vendored
37
.github/workflows/npm-prerelease.yml
vendored
@ -1,37 +0,0 @@
|
||||
# This workflow will run tests using node and then publish a package to npmjs when a prerelease is created
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
|
||||
|
||||
name: npm_prerelease
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [prereleased]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/hydrogen
|
||||
- run: npm ci
|
||||
- run: |
|
||||
echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" >> ~/.npmrc
|
||||
npm whoami
|
||||
npm publish --access=public --tag unstable openmct
|
||||
# - run: npm test
|
||||
|
||||
publish-npm-prerelease:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/hydrogen
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- run: npm ci
|
||||
- run: npm publish --access=public --tag unstable
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
70
.github/workflows/pr-platform.yml
vendored
70
.github/workflows/pr-platform.yml
vendored
@ -1,70 +0,0 @@
|
||||
name: 'pr-platform'
|
||||
on:
|
||||
push:
|
||||
branches: master
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- opened
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
jobs:
|
||||
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:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- macos-latest
|
||||
- windows-latest
|
||||
node_version:
|
||||
- lts/iron
|
||||
- lts/hydrogen
|
||||
architecture:
|
||||
- x64
|
||||
|
||||
name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node_version }}
|
||||
architecture: ${{ matrix.architecture }}
|
||||
|
||||
- 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 ci --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}`);
|
||||
}
|
19
.github/workflows/prcop-config.json
vendored
19
.github/workflows/prcop-config.json
vendored
@ -1,19 +0,0 @@
|
||||
{
|
||||
"linters": [
|
||||
{
|
||||
"name": "descriptionRegexp",
|
||||
"config": {
|
||||
"regexp": "[x|X]] Testing instructions",
|
||||
"errorMessage": ":police_officer: PR Description does not confirm that associated issue(s) contain Testing instructions"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "descriptionMinWords",
|
||||
"config": {
|
||||
"minWordsCount": 160,
|
||||
"errorMessage": ":police_officer: Please, be sure to use existing PR template."
|
||||
}
|
||||
}
|
||||
],
|
||||
"disableWord": "pr:daveit"
|
||||
}
|
40
.github/workflows/prcop.yml
vendored
40
.github/workflows/prcop.yml
vendored
@ -1,40 +0,0 @@
|
||||
name: PRCop
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- unlabeled
|
||||
- milestoned
|
||||
- demilestoned
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- edited
|
||||
pull_request_review_comment:
|
||||
types:
|
||||
- created
|
||||
env:
|
||||
LABELS: ${{ join( github.event.pull_request.labels.*.name, ' ' ) }}
|
||||
jobs:
|
||||
prcop:
|
||||
runs-on: ubuntu-latest
|
||||
name: Template Check
|
||||
steps:
|
||||
- name: Linting Pull Request
|
||||
uses: makaroni4/prcop@v1.0.35
|
||||
with:
|
||||
config-file: '.github/workflows/prcop-config.json'
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
check-type-label:
|
||||
name: Check type Label
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- if: contains( env.LABELS, 'type:' ) == false
|
||||
run: exit 1
|
||||
check-milestone:
|
||||
name: Check Milestone
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- if: github.event.pull_request.milestone == null && contains( env.LABELS, 'no milestone' ) == false
|
||||
run: exit 1
|
25
.gitignore
vendored
25
.gitignore
vendored
@ -15,8 +15,7 @@
|
||||
*.idea
|
||||
*.iml
|
||||
|
||||
# VSCode
|
||||
.vscode/settings.json
|
||||
# External dependencies
|
||||
|
||||
# Build output
|
||||
target
|
||||
@ -25,28 +24,20 @@ dist
|
||||
# Mac OS X Finder
|
||||
.DS_Store
|
||||
|
||||
# Closed source libraries
|
||||
closed-lib
|
||||
|
||||
# Node, Bower dependencies
|
||||
node_modules
|
||||
bower_components
|
||||
|
||||
# Protractor logs
|
||||
protractor/logs
|
||||
|
||||
# npm-debug log
|
||||
npm-debug.log
|
||||
|
||||
# karma reports
|
||||
report.*.json
|
||||
|
||||
# e2e test artifacts
|
||||
test-results
|
||||
html-test-results
|
||||
|
||||
# couchdb scripting artifacts
|
||||
src/plugins/persistence/couch/.env.local
|
||||
index.html.bak
|
||||
|
||||
# codecov artifacts
|
||||
.nyc_output
|
||||
coverage
|
||||
codecov
|
||||
|
||||
# Don't commit MacOS screenshots
|
||||
*-darwin.png
|
||||
package-lock.json
|
||||
|
52
.npmignore
52
.npmignore
@ -1,27 +1,35 @@
|
||||
# Ignore everything first (will not ignore special files like LICENSE.md,
|
||||
# README.md, and package.json)...
|
||||
/**/*
|
||||
*.scssc
|
||||
*.zip
|
||||
*.gzip
|
||||
*.tgz
|
||||
*.DS_Store
|
||||
|
||||
# ...but include these folders...
|
||||
!/dist/**/*
|
||||
!/src/**/*
|
||||
*.sass-cache
|
||||
*COMPILE.css
|
||||
|
||||
# We might be able to remove this if it is not imported by any project directly.
|
||||
# https://github.com/nasa/openmct/issues/4992
|
||||
!/example/**/*
|
||||
# Intellij project configuration files
|
||||
*.idea
|
||||
*.iml
|
||||
|
||||
# ...except for these files in the above folders.
|
||||
/src/**/*Spec.js
|
||||
/src/**/test/
|
||||
# TODO move test utils into test/ folders
|
||||
/src/utils/testing.js
|
||||
# External dependencies
|
||||
|
||||
# Also include these special top-level files.
|
||||
!copyright-notice.js
|
||||
!copyright-notice.html
|
||||
!index.html
|
||||
!openmct.js
|
||||
!SECURITY.md
|
||||
# Build output
|
||||
target
|
||||
|
||||
# Dont include the example html
|
||||
dist/index.html
|
||||
# Mac OS X Finder
|
||||
.DS_Store
|
||||
|
||||
# Closed source libraries
|
||||
closed-lib
|
||||
|
||||
# Node, Bower dependencies
|
||||
node_modules
|
||||
bower_components
|
||||
|
||||
Procfile
|
||||
|
||||
# Protractor logs
|
||||
protractor/logs
|
||||
|
||||
# npm-debug log
|
||||
npm-debug.log
|
||||
|
4
.npmrc
4
.npmrc
@ -1,4 +0,0 @@
|
||||
loglevel=warn
|
||||
|
||||
#Prevent folks from ignoring an important error when building from source
|
||||
engine-strict=true
|
@ -1,27 +0,0 @@
|
||||
# Docs
|
||||
*.md
|
||||
|
||||
# Build output
|
||||
target
|
||||
dist
|
||||
|
||||
# Mac OS X Finder
|
||||
.DS_Store
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# npm-debug log
|
||||
npm-debug.log
|
||||
|
||||
# karma reports
|
||||
report.*.json
|
||||
|
||||
# e2e test artifacts
|
||||
test-results
|
||||
html-test-results
|
||||
|
||||
# codecov artifacts
|
||||
.nyc_output
|
||||
coverage
|
||||
codecov
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"trailingComma": "none",
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"endOfLine": "auto"
|
||||
}
|
13
.vscode/extensions.json
vendored
13
.vscode/extensions.json
vendored
@ -1,13 +0,0 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
|
||||
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
|
||||
|
||||
// List of extensions which should be recommended for users of this workspace.
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"rvest.vs-code-prettier-eslint"
|
||||
],
|
||||
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
|
||||
"unwantedRecommendations": ["octref.vetur"]
|
||||
}
|
@ -1,188 +0,0 @@
|
||||
/*
|
||||
This is the OpenMCT common webpack file. It is imported by the other three webpack configurations:
|
||||
- webpack.prod.mjs - the production configuration for OpenMCT (default)
|
||||
- webpack.dev.mjs - the development configuration for OpenMCT
|
||||
- webpack.coverage.mjs - imports webpack.dev.js and adds code coverage
|
||||
There are separate npm scripts to use these configurations, though simply running `npm install`
|
||||
will use the default production configuration.
|
||||
*/
|
||||
import { execSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import CopyWebpackPlugin from 'copy-webpack-plugin';
|
||||
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
||||
import { VueLoaderPlugin } from 'vue-loader';
|
||||
import webpack from 'webpack';
|
||||
import { merge } from 'webpack-merge';
|
||||
let gitRevision = 'error-retrieving-revision';
|
||||
let gitBranch = 'error-retrieving-branch';
|
||||
|
||||
const { version } = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url)));
|
||||
|
||||
try {
|
||||
gitRevision = execSync('git rev-parse HEAD').toString().trim();
|
||||
gitBranch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
}
|
||||
|
||||
const projectRootDir = fileURLToPath(new URL('../', import.meta.url));
|
||||
|
||||
/** @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',
|
||||
couchDBChangesFeed: './src/plugins/persistence/couch/CouchChangesFeed.js',
|
||||
inMemorySearchWorker: './src/api/objects/InMemorySearchWorker.js',
|
||||
espressoTheme: './src/plugins/themes/espresso-theme.scss',
|
||||
snowTheme: './src/plugins/themes/snow-theme.scss',
|
||||
darkmatterTheme: './src/plugins/themes/darkmatter-theme.scss'
|
||||
},
|
||||
output: {
|
||||
globalObject: 'this',
|
||||
filename: '[name].js',
|
||||
path: path.resolve(projectRootDir, 'dist'),
|
||||
library: {
|
||||
name: 'openmct',
|
||||
type: 'umd',
|
||||
export: 'default'
|
||||
},
|
||||
publicPath: '',
|
||||
hashFunction: 'xxhash64',
|
||||
clean: true
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.join(projectRootDir, 'src'),
|
||||
legacyRegistry: path.join(projectRootDir, 'src/legacyRegistry'),
|
||||
csv: 'comma-separated-values',
|
||||
bourbon: 'bourbon.scss',
|
||||
'plotly-basic': 'plotly.js-basic-dist-min',
|
||||
'plotly-gl2d': 'plotly.js-gl2d-dist-min',
|
||||
printj: 'printj/printj.mjs',
|
||||
styles: path.join(projectRootDir, 'src/styles'),
|
||||
MCT: path.join(projectRootDir, 'src/MCT'),
|
||||
testUtils: path.join(projectRootDir, 'src/utils/testUtils.js'),
|
||||
objectUtils: path.join(projectRootDir, 'src/api/objects/object-utils.js'),
|
||||
utils: path.join(projectRootDir, 'src/utils'),
|
||||
vue: 'vue/dist/vue.esm-bundler'
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
__OPENMCT_VERSION__: `'${version}'`,
|
||||
__OPENMCT_BUILD_DATE__: `'${new Date()}'`,
|
||||
__OPENMCT_REVISION__: `'${gitRevision}'`,
|
||||
__OPENMCT_BUILD_BRANCH__: `'${gitBranch}'`,
|
||||
__VUE_OPTIONS_API__: true, // enable/disable Options API support, default: true
|
||||
__VUE_PROD_DEVTOOLS__: false // enable/disable devtools support in production, default: false
|
||||
}),
|
||||
new VueLoaderPlugin(),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: 'src/images/favicons',
|
||||
to: 'favicons'
|
||||
},
|
||||
{
|
||||
from: './index.html',
|
||||
transform: function (content) {
|
||||
return content.toString().replace(/dist\//g, '');
|
||||
}
|
||||
},
|
||||
{
|
||||
from: 'src/plugins/imagery/layers',
|
||||
to: 'imagery'
|
||||
}
|
||||
]
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: '[name].css',
|
||||
chunkFilename: '[name].css'
|
||||
}),
|
||||
// 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: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(sc|sa|c)ss$/,
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
{
|
||||
loader: 'css-loader'
|
||||
},
|
||||
{
|
||||
loader: 'resolve-url-loader'
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: { sourceMap: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
compilerOptions: {
|
||||
hoistStatic: false,
|
||||
whitespace: 'preserve'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.html$/,
|
||||
type: 'asset/source'
|
||||
},
|
||||
{
|
||||
test: /\.(jpg|jpeg|png|svg)$/,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: 'images/[name][ext]'
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.ico$/,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: 'icons/[name][ext]'
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(woff|woff2?|eot|ttf)$/,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: 'fonts/[name][ext]'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
stats: 'errors-warnings',
|
||||
performance: {
|
||||
// We should eventually consider chunking to decrease
|
||||
// these values
|
||||
maxEntrypointSize: 27000000,
|
||||
maxAssetSize: 27000000
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
@ -1,31 +0,0 @@
|
||||
/*
|
||||
This file extends the webpack.dev.mjs config to add babel istanbul coverage.
|
||||
OpenMCT Continuous Integration servers use this configuration to add code coverage
|
||||
information to pull requests.
|
||||
*/
|
||||
|
||||
import config from './webpack.dev.mjs';
|
||||
|
||||
config.devtool = 'inline-source-map';
|
||||
config.devServer.hot = false;
|
||||
|
||||
config.module.rules.push({
|
||||
test: /\.js$/,
|
||||
exclude: /(Spec\.js$)|(node_modules)/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
retainLines: true,
|
||||
plugins: [
|
||||
[
|
||||
'babel-plugin-istanbul',
|
||||
{
|
||||
extension: ['.js', '.vue']
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default config;
|
@ -1,49 +0,0 @@
|
||||
/*
|
||||
This configuration should be used for development purposes. It contains full source map, a
|
||||
devServer (which be invoked using by `npm start`), and a non-minified Vue.js distribution.
|
||||
If OpenMCT is to be used for a production server, use webpack.prod.mjs instead.
|
||||
*/
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import path from 'path';
|
||||
import webpack from 'webpack';
|
||||
import { merge } from 'webpack-merge';
|
||||
|
||||
import common from './webpack.common.mjs';
|
||||
|
||||
export default merge(common, {
|
||||
mode: 'development',
|
||||
watchOptions: {
|
||||
// Since we use require.context, webpack is watching the entire directory.
|
||||
// We need to exclude any files we don't want webpack to watch.
|
||||
// See: https://webpack.js.org/configuration/watch/#watchoptions-exclude
|
||||
ignored: [
|
||||
'**/{node_modules,dist,docs,e2e}', // All files in node_modules, dist, docs, e2e,
|
||||
'**/{*.yml,Procfile,webpack*.js,babel*.js,package*.json,tsconfig.json}', // Config files
|
||||
'**/*.{sh,md,png,ttf,woff,svg}', // Non source files
|
||||
'**/.*' // dotfiles and dotfolders
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
__OPENMCT_ROOT_RELATIVE__: '"dist/"'
|
||||
})
|
||||
],
|
||||
devtool: 'eval-source-map',
|
||||
devServer: {
|
||||
devMiddleware: {
|
||||
writeToDisk: (filePathString) => {
|
||||
const filePath = path.parse(filePathString);
|
||||
const shouldWrite = !filePath.base.includes('hot-update');
|
||||
|
||||
return shouldWrite;
|
||||
}
|
||||
},
|
||||
watchFiles: ['src/**/*.css', 'example/**/*.css'],
|
||||
static: {
|
||||
directory: fileURLToPath(new URL('../dist', import.meta.url)),
|
||||
publicPath: '/dist',
|
||||
watch: false
|
||||
}
|
||||
}
|
||||
});
|
@ -1,19 +0,0 @@
|
||||
/*
|
||||
This configuration should be used for production installs.
|
||||
It is the default webpack configuration.
|
||||
*/
|
||||
|
||||
import webpack from 'webpack';
|
||||
import { merge } from 'webpack-merge';
|
||||
|
||||
import common from './webpack.common.mjs';
|
||||
|
||||
export default merge(common, {
|
||||
mode: 'production',
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
__OPENMCT_ROOT_RELATIVE__: '""'
|
||||
})
|
||||
],
|
||||
devtool: 'source-map'
|
||||
});
|
118
CONTRIBUTING.md
118
CONTRIBUTING.md
@ -10,7 +10,7 @@ accept changes from external contributors.
|
||||
|
||||
The short version:
|
||||
|
||||
1. Write your contribution or describe your idea in the form of a [GitHub issue](https://github.com/nasa/openmct/issues/new/choose) or [start a GitHub discussion](https://github.com/nasa/openmct/discussions).
|
||||
1. Write your contribution.
|
||||
2. Make sure your contribution meets code, test, and commit message
|
||||
standards as described below.
|
||||
3. Submit a pull request from a topic branch back to `master`. Include a check
|
||||
@ -18,13 +18,12 @@ The short version:
|
||||
for review.)
|
||||
4. Respond to any discussion. When the reviewer decides it's ready, they
|
||||
will merge back `master` and fill out their own check list.
|
||||
5. If you are a first-time contributor, please see [this discussion](https://github.com/nasa/openmct/discussions/3821) for further information.
|
||||
|
||||
## Contribution Process
|
||||
|
||||
Open MCT uses git for software version control, and for branching and
|
||||
merging. The central repository is at
|
||||
<https://github.com/nasa/openmct.git>.
|
||||
https://github.com/nasa/openmct.git.
|
||||
|
||||
### Roles
|
||||
|
||||
@ -116,8 +115,7 @@ the pull request containing the reviewer checklist (from below) and complete
|
||||
the merge back to the master branch.
|
||||
|
||||
Additionally:
|
||||
|
||||
* Every pull request must link to the issue that it addresses. Eg. “Addresses #1234” or “Closes #1234”. This is the responsibility of the pull request’s __author__. If no issue exists, [create one](https://github.com/nasa/openmct/issues/new/choose).
|
||||
* Every pull request must link to the issue that it addresses. Eg. “Addresses #1234” or “Closes #1234”. This is the responsibility of the pull request’s __author__. If no issue exists, create one.
|
||||
* Every __author__ must include testing instructions. These instructions should identify the areas of code affected, and some minimal test steps. If addressing a bug, reproduction steps should be included, if they were not included in the original issue. If reproduction steps were included on the original issue, and are sufficient, refer to them.
|
||||
* A pull request that closes an issue should say so in the description. Including the text “Closes #1234” will cause the linked issue to be automatically closed when the pull request is merged. This is the responsibility of the pull request’s __author__.
|
||||
* When a pull request is merged, and the corresponding issue closed, the __reviewer__ must add the tag “unverified” to the original issue. This will indicate that although the issue is closed, it has not been tested yet.
|
||||
@ -133,26 +131,25 @@ changes.
|
||||
|
||||
### Code Standards
|
||||
|
||||
JavaScript sources in Open MCT must satisfy the [ESLint](https://eslint.org/) rules defined in
|
||||
this repository. [Prettier](https://prettier.io/) is used in conjunction with ESLint to enforce code style
|
||||
via automated formatting. These are verified by the command line build.
|
||||
JavaScript sources in Open MCT must satisfy the ESLint rules defined in
|
||||
this repository. This is verified by the command line build.
|
||||
|
||||
#### Code Guidelines
|
||||
|
||||
The following guidelines are provided for anyone contributing source code to the Open MCT project:
|
||||
|
||||
1. Write clean code. Here’s a good summary - <https://github.com/ryanmcdermott/clean-code-javascript>.
|
||||
1. Write clean code. Here’s a good summary - https://github.com/ryanmcdermott/clean-code-javascript.
|
||||
1. Include JSDoc for any exposed API (e.g. public methods, classes).
|
||||
1. Include non-JSDoc comments as-needed for explaining private variables,
|
||||
methods, or algorithms when they are non-obvious. Otherwise code
|
||||
methods, or algorithms when they are non-obvious. Otherwise code
|
||||
should be self-documenting.
|
||||
1. Classes and Vue components should use camel case, first letter capitalized
|
||||
(e.g. SomeClassName).
|
||||
1. Methods, variables, fields, events, and function names should use camelCase,
|
||||
first letter lower-case (e.g. someVariableName).
|
||||
1. Source files that export functions should use camelCase, first letter lower-case (eg. testTools.js)
|
||||
1. Constants (variables or fields which are meant to be declared and
|
||||
initialized statically, and never changed) should use only capital
|
||||
1. Constants (variables or fields which are meant to be declared and
|
||||
initialized statically, and never changed) should use only capital
|
||||
letters, with underscores between words (e.g. SOME_CONSTANT). They should always be declared as `const`s
|
||||
1. File names should be the name of the exported class, plus a .js extension
|
||||
(e.g. SomeClassName.js).
|
||||
@ -161,25 +158,21 @@ The following guidelines are provided for anyone contributing source code to the
|
||||
(e.g. as arguments to a forEach call). Anonymous functions should always be arrow functions.
|
||||
1. Named functions are preferred over functions assigned to variables.
|
||||
eg.
|
||||
|
||||
```JavaScript
|
||||
function renameObject(object, newName) {
|
||||
Object.name = newName;
|
||||
}
|
||||
```
|
||||
|
||||
is preferable to
|
||||
|
||||
```JavaScript
|
||||
const rename = (object, newName) => {
|
||||
Object.name = newName;
|
||||
}
|
||||
```
|
||||
|
||||
1. Avoid deep nesting (especially of functions), except where necessary
|
||||
(e.g. due to closure scope).
|
||||
1. End with a single new-line character.
|
||||
1. Always use ES6 `Class`es and inheritance rather than the pre-ES6 prototypal
|
||||
1. Always use ES6 `Class`es and inheritence rather than the pre-ES6 prototypal
|
||||
pattern.
|
||||
1. Within a given function's scope, do not mix declarations and imperative
|
||||
code, and present these in the following order:
|
||||
@ -188,24 +181,19 @@ The following guidelines are provided for anyone contributing source code to the
|
||||
* Finally, the returned value. A single return statement at the end of the function should be used, except where an early return would improve code clarity.
|
||||
1. Avoid the use of "magic" values.
|
||||
eg.
|
||||
|
||||
```JavaScript
|
||||
const UNAUTHORIZED = 401;
|
||||
Const UNAUTHORIZED = 401
|
||||
if (responseCode === UNAUTHORIZED)
|
||||
```
|
||||
|
||||
is preferable to
|
||||
|
||||
```JavaScript
|
||||
if (responseCode === 401)
|
||||
```
|
||||
|
||||
1. Use the ternary operator only for simple cases such as variable assignment. Nested ternaries should be avoided in all cases.
|
||||
1. Unit Test specs should reside alongside the source code they test, not in a separate directory.
|
||||
1. Test specs should reside alongside the source code they test, not in a separate directory.
|
||||
1. Organize code by feature, not by type.
|
||||
eg.
|
||||
|
||||
```txt
|
||||
```
|
||||
- telemetryTable
|
||||
- row
|
||||
TableRow.js
|
||||
@ -217,10 +205,8 @@ The following guidelines are provided for anyone contributing source code to the
|
||||
plugin.js
|
||||
pluginSpec.js
|
||||
```
|
||||
|
||||
is preferable to
|
||||
|
||||
```txt
|
||||
```
|
||||
- telemetryTable
|
||||
- components
|
||||
TableRow.vue
|
||||
@ -232,10 +218,47 @@ The following guidelines are provided for anyone contributing source code to the
|
||||
plugin.js
|
||||
pluginSpec.js
|
||||
```
|
||||
|
||||
Deviations from Open MCT code style guidelines require two-party agreement,
|
||||
typically from the author of the change and its reviewer.
|
||||
|
||||
### Test Standards
|
||||
|
||||
Automated testing shall occur whenever changes are merged into the main
|
||||
development branch and must be confirmed alongside any pull request.
|
||||
|
||||
Automated tests are tests which exercise plugins, API, and utility classes.
|
||||
Tests are subject to code review along with the actual implementation, to
|
||||
ensure that tests are applicable and useful.
|
||||
|
||||
Examples of useful tests:
|
||||
* Tests which replicate bugs (or their root causes) to verify their
|
||||
resolution.
|
||||
* Tests which reflect details from software specifications.
|
||||
* Tests which exercise edge or corner cases among inputs.
|
||||
* Tests which verify expected interactions with other components in the
|
||||
system.
|
||||
|
||||
#### Guidelines
|
||||
* 100% statement coverage is achievable and desirable.
|
||||
* Do blackbox testing. Test external behaviors, not internal details. Write tests that describe what your plugin is supposed to do. How it does this doesn't matter, so don't test it.
|
||||
* Unit test specs for plugins should be defined at the plugin level. Start with one test spec per plugin named pluginSpec.js, and as this test spec grows too big, break it up into multiple test specs that logically group related tests.
|
||||
* Unit tests for API or for utility functions and classes may be defined at a per-source file level.
|
||||
* Wherever possible only use and mock public API, builtin functions, and UI in your test specs. Do not directly invoke any private functions. ie. only call or mock functions and objects exposed by openmct.* (eg. openmct.telemetry, openmct.objectView, etc.), and builtin browser functions (fetch, requestAnimationFrame, setTimeout, etc.).
|
||||
* Where builtin functions have been mocked, be sure to clear them between tests.
|
||||
* Test at an appropriate level of isolation. Eg.
|
||||
* If you’re testing a view, you do not need to test the whole application UI, you can just fetch the view provider using the public API and render the view into an element that you have created.
|
||||
* You do not need to test that the view switcher works, there should be separate tests for that.
|
||||
* You do not need to test that telemetry providers work, you can mock openmct.telemetry.request() to feed test data to the view.
|
||||
* Use your best judgement when deciding on appropriate scope.
|
||||
* Automated tests for plugins should start by actually installing the plugin being tested, and then test that installing the plugin adds the desired features and behavior to Open MCT, observing the above rules.
|
||||
* All variables used in a test spec, including any instances of the Open MCT API should be declared inside of an appropriate block scope (not at the root level of the source file), and should be initialized in the relevant beforeEach block. `beforeEach` is preferable to `beforeAll` to avoid leaking of state between tests.
|
||||
* A `afterEach` or `afterAll` should be used to do any clean up necessary to prevent leakage of state between test specs. This can happen when functions on `window` are wrapped, or when the URL is changed. [A convenience function](https://github.com/nasa/openmct/blob/master/src/utils/testing.js#L59) is provided for resetting the URL and clearing builtin spies between tests.
|
||||
* If writing unit tests for legacy Angular code be sure to follow [best practices in order to avoid memory leaks](https://www.thecodecampus.de/blog/avoid-memory-leaks-angularjs-unit-tests/).
|
||||
|
||||
#### Examples
|
||||
* [Example of an automated test spec for an object view plugin](https://github.com/nasa/openmct/blob/master/src/plugins/telemetryTable/pluginSpec.js)
|
||||
* [Example of an automated test spec for API](https://github.com/nasa/openmct/blob/master/src/api/time/TimeAPISpec.js)
|
||||
|
||||
### Commit Message Standards
|
||||
|
||||
Commit messages should:
|
||||
@ -271,14 +294,25 @@ these standards.
|
||||
|
||||
## Issue Reporting
|
||||
|
||||
Issues are tracked at <https://github.com/nasa/openmct/issues>.
|
||||
Issues are tracked at https://github.com/nasa/openmct/issues.
|
||||
|
||||
Issues should include:
|
||||
|
||||
* A short description of the issue encountered.
|
||||
* A longer-form description of the issue encountered. When possible, steps to
|
||||
reproduce the issue.
|
||||
* When possible, a description of the impact of the issue. What use case does
|
||||
it impede?
|
||||
* An assessment of the severity of the issue.
|
||||
|
||||
Issue severity is categorized as follows (in ascending order):
|
||||
|
||||
* _Trivial_: Minimal impact on the usefulness and functionality of the software; a "nice-to-have." Visual impact without functional impact,
|
||||
* _Medium_: Some impairment of use, but simple workarounds exist
|
||||
* _Critical_: Significant loss of functionality or impairment of use. Display of telemetry data is not affected though. Complex workarounds exist.
|
||||
* _Blocker_: Major functionality is impaired or lost, threatening mission success. Display of telemetry data is impaired or blocked by the bug, which could lead to loss of situational awareness.
|
||||
* _Trivial_: Minimal impact on the usefulness and functionality of the
|
||||
software; a "nice-to-have."
|
||||
* _(Unspecified)_: Major loss of functionality or impairment of use.
|
||||
* _Critical_: Large-scale loss of functionality or impairment of use,
|
||||
such that remaining utility becomes marginal.
|
||||
* _Blocker_: Harmful or otherwise unacceptable behavior. Must fix.
|
||||
|
||||
## Check Lists
|
||||
|
||||
@ -286,4 +320,18 @@ The following check lists should be completed and attached to pull requests
|
||||
when they are filed (author checklist) and when they are merged (reviewer
|
||||
checklist).
|
||||
|
||||
[Within PR Template](.github/PULL_REQUEST_TEMPLATE.md)
|
||||
### Author Checklist
|
||||
|
||||
1. Changes address original issue?
|
||||
2. Unit tests included and/or updated with changes?
|
||||
3. Command line build passes?
|
||||
4. Changes have been smoke-tested?
|
||||
5. Testing instructions included?
|
||||
|
||||
### Reviewer Checklist
|
||||
|
||||
1. Changes appear to address issue?
|
||||
2. Appropriate unit tests included?
|
||||
3. Code style and in-line documentation are appropriate?
|
||||
4. Commit messages meet standards?
|
||||
5. Has associated issue been labelled `unverified`? (only applicable if this PR closes the issue)
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Open MCT License
|
||||
|
||||
Open MCT, Copyright (c) 2014-2024, United States Government as represented by the Administrator of the National Aeronautics and Space Administration. All rights reserved.
|
||||
Open MCT, Copyright (c) 2014-2020, 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.
|
||||
|
||||
|
190
README.md
190
README.md
@ -1,47 +1,37 @@
|
||||
# Open MCT [](http://www.apache.org/licenses/LICENSE-2.0) [](https://codecov.io/gh/nasa/openmct) [](https://percy.io/b2e34b17/openmct) [](https://www.npmjs.com/package/openmct) 
|
||||
# Open MCT [](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
Open MCT (Open Mission Control Technologies) is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data.
|
||||
|
||||
> [!NOTE]
|
||||
> Please visit our [Official Site](https://nasa.github.io/openmct/) and [Getting Started Guide](https://nasa.github.io/openmct/getting-started/)
|
||||
Please visit our [Official Site](https://nasa.github.io/openmct/) and [Getting Started Guide](https://nasa.github.io/openmct/getting-started/)
|
||||
|
||||
Once you've created something amazing with Open MCT, showcase your work in our GitHub Discussions [Show and Tell](https://github.com/nasa/openmct/discussions/categories/show-and-tell) section. We love seeing unique and wonderful implementations of Open MCT!
|
||||
## See Open MCT in Action
|
||||
|
||||

|
||||
Try Open MCT now with our [live demo](https://openmct-demo.herokuapp.com/).
|
||||

|
||||
|
||||
## Building and Running Open MCT Locally
|
||||
|
||||
Building and running Open MCT in your local dev environment is very easy. Be sure you have [Git](https://git-scm.com/downloads) and [Node.js](https://nodejs.org/) installed, then follow the directions below. Need additional information? Check out the [Getting Started](https://nasa.github.io/openmct/getting-started/) page on our website.
|
||||
(These instructions assume you are installing as a non-root user; developers have [reported issues](https://github.com/nasa/openmct/issues/1151) running these steps with root privileges.)
|
||||
|
||||
1. Clone the source code:
|
||||
1. Clone the source code
|
||||
|
||||
```sh
|
||||
git clone https://github.com/nasa/openmct.git
|
||||
```
|
||||
`git clone https://github.com/nasa/openmct.git`
|
||||
|
||||
2. (Optional) Install the correct node version using [nvm](https://github.com/nvm-sh/nvm):
|
||||
2. Install development dependencies
|
||||
|
||||
```sh
|
||||
nvm install
|
||||
```
|
||||
`npm install`
|
||||
|
||||
3. Install development dependencies (Note: Check the `package.json` engine for our tested and supported node versions):
|
||||
3. Run a local development server
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
`npm start`
|
||||
|
||||
4. Run a local development server:
|
||||
Open MCT is now running, and can be accessed by pointing a web browser at [http://localhost:8080/](http://localhost:8080/)
|
||||
|
||||
```
|
||||
npm start
|
||||
```
|
||||
## Open MCT v1.0.0
|
||||
This represents a major overhaul of Open MCT with significant changes under the hood. We aim to maintain backward compatibility but if you do find compatibility issues, please let us know by filing an issue in this repository. If you are having major issues with v1.0.0 please check-out the v0.14.0 tag until we can resolve them for you.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Open MCT is now running, and can be accessed by pointing a web browser at [http://localhost:8080/](http://localhost:8080/)
|
||||
|
||||
Open MCT is built using [`npm`](http://npmjs.com/) and [`webpack`](https://webpack.js.org/).
|
||||
If you are migrating an application built with Open MCT as a dependency to v1.0.0 from an earlier version, please refer to [our migration guide](https://nasa.github.io/openmct/documentation/migration-guide).
|
||||
|
||||
## Documentation
|
||||
|
||||
@ -53,43 +43,29 @@ The clearest examples for developing Open MCT plugins are in the
|
||||
[tutorials](https://github.com/nasa/openmct-tutorial) provided in
|
||||
our documentation.
|
||||
|
||||
> [!NOTE]
|
||||
> We want Open MCT to be as easy to use, install, run, and develop for as
|
||||
> possible, and your feedback will help us get there!
|
||||
> Feedback can be provided via [GitHub issues](https://github.com/nasa/openmct/issues/new/choose),
|
||||
> [Starting a GitHub Discussion](https://github.com/nasa/openmct/discussions),
|
||||
> or by emailing us at [arc-dl-openmct@mail.nasa.gov](mailto:arc-dl-openmct@mail.nasa.gov).
|
||||
We want Open MCT to be as easy to use, install, run, and develop for as
|
||||
possible, and your feedback will help us get there! Feedback can be provided via [GitHub issues](https://github.com/nasa/openmct/issues), or by emailing us at [arc-dl-openmct@mail.nasa.gov](mailto:arc-dl-openmct@mail.nasa.gov).
|
||||
|
||||
## Developing Applications With Open MCT
|
||||
## Building Applications With Open MCT
|
||||
|
||||
For more on developing with Open MCT, see our documentation for a guide on [Developing Applications with Open MCT](./API.md#starting-an-open-mct-application).
|
||||
Open MCT is built using [`npm`](http://npmjs.com/) and [`webpack`](https://webpack.js.org/).
|
||||
|
||||
## Compatibility
|
||||
|
||||
This is a fast moving project and we do our best to test and support the widest possible range of browsers, operating systems, and NodeJS APIs. We have a published list of support available in our package.json's `browserslist` key.
|
||||
|
||||
The project utilizes `nvm` to maintain consistent node and npm versions across all projects. For UNIX, MacOS, Windows WSL, and other POSIX-compliant shell environments, click [here](https://github.com/nvm-sh/nvm). For Windows, check out [nvm-windows](https://github.com/coreybutler/nvm-windows).
|
||||
|
||||
If you encounter an issue with a particular browser, OS, or NodeJS API, please [file an issue](https://github.com/nasa/openmct/issues/new/choose).
|
||||
See our documentation for a guide on [building Applications with Open MCT](https://github.com/nasa/openmct/blob/master/API.md#starting-an-open-mct-application).
|
||||
|
||||
## Plugins
|
||||
|
||||
Open MCT can be extended via plugins that make calls to the Open MCT API. A plugin is a group
|
||||
Open MCT can be extended via plugins that make calls to the Open MCT API. A plugin is a group
|
||||
of software components (including source code and resources such as images and HTML templates)
|
||||
that is intended to be added or removed as a single unit.
|
||||
|
||||
As well as providing an extension mechanism, most of the core Open MCT codebase is also
|
||||
As well as providing an extension mechanism, most of the core Open MCT codebase is also
|
||||
written as plugins.
|
||||
|
||||
For information on writing plugins, please see [our API documentation](./API.md#plugins).
|
||||
For information on writing plugins, please see [our API documentation](https://github.com/nasa/openmct/blob/master/API.md#plugins).
|
||||
|
||||
## Tests
|
||||
|
||||
Our automated test coverage comes in the form of unit, e2e, visual, performance, and security tests.
|
||||
|
||||
### Unit Tests
|
||||
|
||||
Unit Tests are written for [Jasmine](https://jasmine.github.io/api/edge/global)
|
||||
Tests are written for [Jasmine 3](https://jasmine.github.io/api/3.1/global)
|
||||
and run by [Karma](http://karma-runner.github.io). To run:
|
||||
|
||||
`npm test`
|
||||
@ -98,93 +74,51 @@ The test suite is configured to load any scripts ending with `Spec.js` found
|
||||
in the `src` hierarchy. Full configuration details are found in
|
||||
`karma.conf.js`. By convention, unit test scripts should be located
|
||||
alongside the units that they test; for example, `src/foo/Bar.js` would be
|
||||
tested by `src/foo/BarSpec.js`.
|
||||
tested by `src/foo/BarSpec.js`. (For legacy reasons, some existing tests may
|
||||
be located in separate `test` folders near the units they test, but the
|
||||
naming convention is otherwise the same.)
|
||||
|
||||
### e2e, Visual, and Performance Testing
|
||||
### Test Reporting
|
||||
|
||||
Our e2e (end-to-end), Visual, and Performance tests leverage the Playwright framework and are executed using Playwright's test runner, [@playwright/test](https://playwright.dev/).
|
||||
When `npm test` is run, test results will be written as HTML to
|
||||
`dist/reports/tests/`. Code coverage information is written to `dist/reports/coverage`.
|
||||
|
||||
#### How to Run Tests
|
||||
|
||||
- **e2e Tests**: These tests are run on every commit. To run the tests locally, use:
|
||||
|
||||
```sh
|
||||
npm run test:e2e:ci
|
||||
```
|
||||
|
||||
- **Visual Tests**: For running the visual test suite, use:
|
||||
|
||||
```sh
|
||||
npm run test:e2e:visual
|
||||
```
|
||||
|
||||
- **Performance Tests**: To initiate the performance tests, enter:
|
||||
|
||||
```sh
|
||||
npm run test:perf
|
||||
```
|
||||
|
||||
All tests are located within the `e2e/tests/` directory and are identified by the `*.e2e.spec.js` filename pattern. For more information about the e2e test suite, refer to the [README](./e2e/README.md).
|
||||
|
||||
### Security Tests
|
||||
|
||||
Each commit is analyzed for known security vulnerabilities using [CodeQL](https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-javascript/). The list of CWE coverage items is available in the [CodeQL docs](https://codeql.github.com/codeql-query-help/javascript-cwe/). The CodeQL workflow is specified in the [CodeQL analysis file](./.github/workflows/codeql-analysis.yml) and the custom [CodeQL config](./.github/codeql/codeql-config.yml).
|
||||
|
||||
### Test Reporting and Code Coverage
|
||||
|
||||
Each test suite generates a report in CircleCI. For a complete overview of testing functionality, please see our [Circle CI Test Insights Dashboard](https://app.circleci.com/insights/github/nasa/openmct/workflows/the-nightly/overview?branch=master&reporting-window=last-30-days)
|
||||
|
||||
Our code coverage is generated during the runtime of our unit, e2e, and visual tests. The combination of those reports is published to [codecov.io](https://app.codecov.io/gh/nasa/openmct/)
|
||||
|
||||
For more on the specifics of our code coverage setup, [see](TESTING.md#code-coverage)
|
||||
|
||||
## Glossary
|
||||
# Glossary
|
||||
|
||||
Certain terms are used throughout Open MCT with consistent meanings
|
||||
or conventions. Any deviations from the below are issues and should be
|
||||
addressed (either by updating this glossary or changing code to reflect
|
||||
correct usage.) Other developer documentation, particularly in-line
|
||||
documentation, may presume an understanding of these terms.
|
||||
| Term | Definition |
|
||||
|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| _plugin_ | A removable, reusable grouping of software elements. The application is composed of plugins. |
|
||||
| _composition_ | In the context of a domain object, this term refers to the set of other domain objects that compose or are contained by that object. A domain object's composition is the set of domain objects that should appear immediately beneath it in a tree hierarchy. It is described in its model as an array of ids, providing a means to asynchronously retrieve the actual domain object instances associated with these identifiers. |
|
||||
| _description_ | When used as an object property, this term refers to the human-readable description of a thing, usually a single sentence or short paragraph. It is most often used in the context of extensions, domain object models, or other similar application-specific objects. |
|
||||
| _domain object_ | A meaningful object to the user and a distinct thing in the work supported by Open MCT. Anything that appears in the left-hand tree is a domain object. |
|
||||
| _identifier_ | A tuple consisting of a namespace and a key, which together uniquely identifies a domain object. |
|
||||
| _model_ | The persistent state associated with a domain object. A domain object's model is a JavaScript object that can be converted to JSON without losing information, meaning it contains no methods. |
|
||||
| _name_ | When used as an object property, this term refers to the human-readable name for a thing. It is most often used in the context of extensions, domain object models, or other similar application-specific objects. |
|
||||
| _navigation_ | This term refers to the current state of the application with respect to the user's expressed interest in a specific domain object. For example, when a user clicks on a domain object in the tree, they are navigating to it, and it is thereafter considered the navigated object until the user makes another such choice. |
|
||||
| _namespace_ | A name used to identify a persistence store. A running Open MCT application could potentially use multiple persistence stores. |
|
||||
|
||||
## Open MCT v2.0.0
|
||||
|
||||
Support for our legacy bundle-based API, and the libraries that it was built on (like Angular 1.x), have now been removed entirely from this repository.
|
||||
|
||||
For now if you have an Open MCT application that makes use of the legacy API, [a plugin](https://github.com/nasa/openmct-legacy-plugin) is provided that bootstraps the legacy bundling mechanism and API. This plugin will not be maintained over the long term however, and the legacy support plugin will not be tested for compatibility with future versions of Open MCT. It is provided for convenience only.
|
||||
|
||||
### How do I know if I am using legacy API?
|
||||
|
||||
You might still be using legacy API if your source code
|
||||
|
||||
- Contains files named bundle.js, or bundle.json,
|
||||
- Makes calls to `openmct.$injector()`, or `openmct.$angular`,
|
||||
- Makes calls to `openmct.legacyRegistry`, `openmct.legacyExtension`, or `openmct.legacyBundle`.
|
||||
|
||||
### What should I do if I am using legacy API?
|
||||
|
||||
Please refer to [the modern Open MCT API](https://nasa.github.io/openmct/documentation/). Post any questions to the [Discussions section](https://github.com/nasa/openmct/discussions) of the Open MCT GitHub repository.
|
||||
|
||||
## Related Repos
|
||||
|
||||
> [!NOTE]
|
||||
> Although Open MCT functions as a standalone project, it is primarily an extensible framework intended to be used as a dependency with users' own plugins and packaging. Furthermore, Open MCT is intended to be used with an HTTP server such as Apache or Nginx. A great example of hosting Open MCT with Apache is `openmct-quickstart` and can be found in the table below.
|
||||
|
||||
| Repository | Description |
|
||||
| --- | --- |
|
||||
| [openmct-tutorial](https://github.com/nasa/openmct-tutorial) | A great place for beginners to learn how to use and extend Open MCT. |
|
||||
| [openmct-quickstart](https://github.com/scottbell/openmct-quickstart) | A working example of Open MCT integrated with Apache HTTP server, YAMCS telemetry, and Couch DB for persistence.
|
||||
| [Open MCT YAMCS Plugin](https://github.com/akhenry/openmct-yamcs) | Plugin for integrating YAMCS telemetry and command server with Open MCT. |
|
||||
| [openmct-performance](https://github.com/unlikelyzero/openmct-performance) | Resources for performance testing Open MCT. |
|
||||
| [openmct-as-a-dependency](https://github.com/unlikelyzero/openmct-as-a-dependency) | An advanced guide for users on how to build, develop, and test Open MCT when it's used as a dependency. |
|
||||
|
||||
* _plugin_: A plugin is a removable, reusable grouping of software elements.
|
||||
The application is composed of plugins.
|
||||
* _composition_: In the context of a domain object, this refers to the set of
|
||||
other domain objects that compose or are contained by that object. A domain
|
||||
object's composition is the set of domain objects that should appear
|
||||
immediately beneath it in a tree hierarchy. A domain object's composition is
|
||||
described in its model as an array of id's; its composition capability
|
||||
provides a means to retrieve the actual domain object instances associated
|
||||
with these identifiers asynchronously.
|
||||
* _description_: When used as an object property, this refers to the human-readable
|
||||
description of a thing; usually a single sentence or short paragraph.
|
||||
(Most often used in the context of extensions, domain
|
||||
object models, or other similar application-specific objects.)
|
||||
* _domain object_: A meaningful object to the user; a distinct thing in
|
||||
the work support by Open MCT. Anything that appears in the left-hand
|
||||
tree is a domain object.
|
||||
* _identifier_: A tuple consisting of a namespace and a key, which together uniquely
|
||||
identifies a domain object.
|
||||
* _model_: The persistent state associated with a domain object. A domain
|
||||
object's model is a JavaScript object which can be converted to JSON
|
||||
without losing information (that is, it contains no methods.)
|
||||
* _name_: When used as an object property, this refers to the human-readable
|
||||
name for a thing. (Most often used in the context of extensions, domain
|
||||
object models, or other similar application-specific objects.)
|
||||
* _navigation_: Refers to the current state of the application with respect
|
||||
to the user's expressed interest in a specific domain object; e.g. when
|
||||
a user clicks on a domain object in the tree, they are _navigating_ to
|
||||
it, and it is thereafter considered the _navigated_ object (until the
|
||||
user makes another such choice.)
|
||||
* _namespace_: A name used to identify a persistence store. A running open MCT
|
||||
application could potentially use multiple persistence stores, with the
|
||||
|
29
SECURITY.md
29
SECURITY.md
@ -1,29 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
The Open MCT team secures our code base using a combination of code review, dependency review, and periodic security reviews. Static analysis performed during automated verification additionally safeguards against common coding errors which may result in vulnerabilities.
|
||||
|
||||
### Reporting a Vulnerability
|
||||
|
||||
For general defects, please for a [Bug Report](https://github.com/nasa/openmct/issues/new/choose)
|
||||
|
||||
To report a vulnerability for Open MCT please send a detailed report to [arc-dl-openmct](mailto:arc-dl-openmct@mail.nasa.gov).
|
||||
|
||||
See our [top-level security policy](https://github.com/nasa/openmct/security/policy) for additional information.
|
||||
|
||||
### CodeQL and LGTM
|
||||
|
||||
The [CodeQL GitHub Actions workflow](https://github.com/nasa/openmct/blob/master/.github/workflows/codeql-analysis.yml) is available to the public. To review the results, fork the repository and run the CodeQL workflow.
|
||||
|
||||
CodeQL is run for every pull-request in GitHub Actions.
|
||||
|
||||
### ESLint
|
||||
|
||||
Static analysis is run for every push on the master branch and every pull request on all branches in Github Actions.
|
||||
|
||||
For more information about ESLint, visit https://eslint.org/.
|
||||
|
||||
### General Support
|
||||
|
||||
For additional support, please open a [Github Discussion](https://github.com/nasa/openmct/discussions).
|
||||
|
||||
If you wish to report a cybersecurity incident or concern, please contact the NASA Security Operations Center either by phone at 1-877-627-2732 or via email address soc@nasa.gov.
|
122
TESTING.md
122
TESTING.md
@ -1,122 +0,0 @@
|
||||
# Testing
|
||||
Open MCT Testing is iterating and improving at a rapid pace. This document serves to capture and index existing testing documentation and house documentation which no other obvious location as our testing evolves.
|
||||
|
||||
## General Testing Process
|
||||
Documentation located [here](./docs/src/process/testing/plan.md)
|
||||
|
||||
## Unit Testing
|
||||
Unit testing is essential part of our test strategy and complements our e2e testing strategy.
|
||||
|
||||
#### Unit Test Guidelines
|
||||
* Unit Test specs should reside alongside the source code they test, not in a separate directory.
|
||||
* Unit test specs for plugins should be defined at the plugin level. Start with one test spec per plugin named pluginSpec.js, and as this test spec grows too big, break it up into multiple test specs that logically group related tests.
|
||||
* Unit tests for API or for utility functions and classes may be defined at a per-source file level.
|
||||
* Wherever possible only use and mock public API, builtin functions, and UI in your test specs. Do not directly invoke any private functions. ie. only call or mock functions and objects exposed by openmct.* (eg. openmct.telemetry, openmct.objectView, etc.), and builtin browser functions (fetch, requestAnimationFrame, setTimeout, etc.).
|
||||
* Where builtin functions have been mocked, be sure to clear them between tests.
|
||||
* Test at an appropriate level of isolation. Eg.
|
||||
* If you’re testing a view, you do not need to test the whole application UI, you can just fetch the view provider using the public API and render the view into an element that you have created.
|
||||
* You do not need to test that the view switcher works, there should be separate tests for that.
|
||||
* You do not need to test that telemetry providers work, you can mock openmct.telemetry.request() to feed test data to the view.
|
||||
* Use your best judgement when deciding on appropriate scope.
|
||||
* Automated tests for plugins should start by actually installing the plugin being tested, and then test that installing the plugin adds the desired features and behavior to Open MCT, observing the above rules.
|
||||
* All variables used in a test spec, including any instances of the Open MCT API should be declared inside of an appropriate block scope (not at the root level of the source file), and should be initialized in the relevant beforeEach block. `beforeEach` is preferable to `beforeAll` to avoid leaking of state between tests.
|
||||
* A `afterEach` or `afterAll` should be used to do any clean up necessary to prevent leakage of state between test specs. This can happen when functions on `window` are wrapped, or when the URL is changed. [A convenience function](https://github.com/nasa/openmct/blob/master/src/utils/testing.js#L59) is provided for resetting the URL and clearing builtin spies between tests.
|
||||
|
||||
#### Unit Test Examples
|
||||
* [Example of an automated test spec for an object view plugin](https://github.com/nasa/openmct/blob/master/src/plugins/telemetryTable/pluginSpec.js)
|
||||
* [Example of an automated test spec for API](https://github.com/nasa/openmct/blob/master/src/api/time/TimeAPISpec.js)
|
||||
|
||||
#### Unit Testing Execution
|
||||
|
||||
The unit tests can be executed in one of two ways:
|
||||
`npm run test` which runs the entire suite against headless chrome
|
||||
`npm run test:debug` for debugging the tests in realtime in an active chrome session.
|
||||
|
||||
## e2e, performance, and visual testing
|
||||
Documentation located [here](./e2e/README.md)
|
||||
|
||||
## Code Coverage
|
||||
|
||||
It's up to the individual developer as to whether they want to add line coverage in the form of a unit test or e2e test.
|
||||
|
||||
Line Code Coverage is generated by our unit tests and e2e tests, then combined by ([Codecov.io Flags](https://docs.codecov.com/docs/flags)), and finally reported in GitHub PRs by Codecov.io's PR Bot. This workflow gives a comprehensive (if flawed) view of line coverage.
|
||||
|
||||
### Karma-istanbul
|
||||
|
||||
Line coverage is generated by our `karma-coverage-istanbul-reporter` package as defined in our `karma.conf.js` file:
|
||||
|
||||
```js
|
||||
coverageIstanbulReporter: {
|
||||
fixWebpackSourcePaths: true,
|
||||
skipFilesWithNoCoverage: true,
|
||||
dir: 'coverage/unit', //Sets coverage file to be consumed by codecov.io
|
||||
reports: ['lcovonly']
|
||||
},
|
||||
```
|
||||
|
||||
Once the file is generated, it can be published to codecov with
|
||||
|
||||
```json
|
||||
"cov:unit:publish": "codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit",
|
||||
```
|
||||
|
||||
### e2e
|
||||
The e2e line coverage is a bit more complex than the karma implementation. This is the general sequence of events:
|
||||
|
||||
1. Each e2e suite will start webpack with the ```npm run start:coverage``` command with config `webpack.coverage.mjs` and the `babel-plugin-istanbul` plugin to generate code coverage during e2e test execution using our custom [baseFixture](./baseFixtures.js).
|
||||
1. During testcase execution, each e2e shard will generate its piece of the larger coverage suite. **This coverage file is not merged**. The raw coverage file is stored in a `.nyc_report` directory.
|
||||
1. [nyc](https://github.com/istanbuljs/nyc) converts this directory into a `lcov` file with the following command `npm run cov:e2e:report`
|
||||
1. Most of the tests focus on chrome/ubuntu at a single resolution. This coverage is published to codecov with `npm run cov:e2e:ci:publish`.
|
||||
1. The rest of our coverage only appears when run against persistent datastore (couchdb), non-ubuntu machines, and non-chrome browsers with the `npm run cov:e2e:full:publish` flag. Since this happens about once a day, we have leveraged codecov.io's carryforward flag to report on lines covered outside of each commit on an individual PR.
|
||||
|
||||
|
||||
### Limitations in our code coverage reporting
|
||||
Our code coverage implementation has some known limitations:
|
||||
- [Variability](https://github.com/nasa/openmct/issues/5811)
|
||||
- [Accuracy](https://github.com/nasa/openmct/issues/7015)
|
||||
- [Vue instrumentation gaps](https://github.com/nasa/openmct/issues/4973)
|
||||
|
||||
## Troubleshooting CI
|
||||
The following is an evolving guide to troubleshoot CI and PR issues.
|
||||
|
||||
### Github Checks failing
|
||||
There are a few reasons that your GitHub PR could be failing beyond simple failed tests.
|
||||
* Required Checks. We're leveraging required checks in GitHub so that we can quickly and precisely control what becomes and informational failure vs a hard requirement. The only way to determine the difference between a required vs information check is check for the `(Required)` emblem next to the step details in GitHub Checks.
|
||||
* Not all required checks are run per commit. You may need to manually trigger addition GitHub checks with a `pr:<label>` label added to your PR.
|
||||
|
||||
### Flaky tests
|
||||
|
||||
(CircleCI's test insights feature)[https://circleci.com/blog/introducing-test-insights-with-flaky-test-detection/] collects historical data about the individual test results for both unit and e2e tests. Note: only a 14 day window of flake is available.
|
||||
|
||||
### Local=Pass and CI=Fail
|
||||
Although rare, it is possible that your test can pass locally but fail in CI.
|
||||
|
||||
### Reset your workspace
|
||||
It's possible that you're running with dependencies or a local environment which is out of sync with the branch you're working on. Make sure to execute the following:
|
||||
|
||||
```sh
|
||||
nvm use
|
||||
npm run clean
|
||||
npm install
|
||||
```
|
||||
|
||||
#### Run tests in the same container as CI
|
||||
|
||||
In extreme cases, tests can fail due to the constraints of running within a container. To execute tests in exactly the same way as run in CircleCI.
|
||||
|
||||
```sh
|
||||
// Replace {X.X.X} with the current Playwright version
|
||||
// from our package.json or circleCI configuration file
|
||||
docker run --rm --network host --cpus="2" -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v{X.X.X}-focal /bin/bash
|
||||
npm install
|
||||
```
|
||||
|
||||
At this point, you're running inside the same container and with 2 cpu cores. You can specify the unit tests:
|
||||
```sh
|
||||
npm run test
|
||||
```
|
||||
or e2e tests:
|
||||
|
||||
```sh
|
||||
npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep <the testcase name>
|
||||
```
|
78
app.js
Normal file
78
app.js
Normal file
@ -0,0 +1,78 @@
|
||||
/*global require,process,console*/
|
||||
|
||||
/**
|
||||
* Usage:
|
||||
*
|
||||
* npm install minimist express
|
||||
* node app.js [options]
|
||||
*/
|
||||
|
||||
|
||||
const options = require('minimist')(process.argv.slice(2));
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const fs = require('fs');
|
||||
const request = require('request');
|
||||
|
||||
// Defaults
|
||||
options.port = options.port || options.p || 8080;
|
||||
options.host = options.host || 'localhost';
|
||||
options.directory = options.directory || options.D || '.';
|
||||
|
||||
// Show command line options
|
||||
if (options.help || options.h) {
|
||||
console.log("\nUsage: node app.js [options]\n");
|
||||
console.log("Options:");
|
||||
console.log(" --help, -h Show this message.");
|
||||
console.log(" --port, -p <number> Specify port.");
|
||||
console.log(" --directory, -D <bundle> Serve files from specified directory.");
|
||||
console.log("");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
app.disable('x-powered-by');
|
||||
|
||||
app.use('/proxyUrl', function proxyRequest(req, res, next) {
|
||||
console.log('Proxying request to: ', req.query.url);
|
||||
req.pipe(request({
|
||||
url: req.query.url,
|
||||
strictSSL: false
|
||||
}).on('error', next)).pipe(res);
|
||||
});
|
||||
|
||||
const webpack = require('webpack');
|
||||
const webpackConfig = require('./webpack.config.js');
|
||||
webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
|
||||
webpackConfig.plugins.push(function() { this.plugin('watch-run', function(watching, callback) { console.log('Begin compile at ' + new Date()); callback(); }) });
|
||||
|
||||
webpackConfig.entry.openmct = [
|
||||
'webpack-hot-middleware/client?reload=true',
|
||||
webpackConfig.entry.openmct
|
||||
];
|
||||
|
||||
const compiler = webpack(webpackConfig);
|
||||
|
||||
app.use(require('webpack-dev-middleware')(
|
||||
compiler,
|
||||
{
|
||||
publicPath: '/dist',
|
||||
logLevel: 'warn'
|
||||
}
|
||||
));
|
||||
|
||||
app.use(require('webpack-hot-middleware')(
|
||||
compiler,
|
||||
{
|
||||
|
||||
}
|
||||
));
|
||||
|
||||
// Expose index.html for development users.
|
||||
app.get('/', function (req, res) {
|
||||
fs.createReadStream('index.html').pipe(res);
|
||||
});
|
||||
|
||||
// Finally, open the HTTP server and log the instance to the console
|
||||
app.listen(options.port, options.host, function() {
|
||||
console.log('Open MCT application running at %s:%s', options.host, options.port)
|
||||
});
|
@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
#*****************************************************************************
|
||||
#* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
#* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
#* as represented by the Administrator of the National Aeronautics and Space
|
||||
#* Administration. All rights reserved.
|
||||
#*
|
||||
|
28
codecov.yml
28
codecov.yml
@ -1,28 +0,0 @@
|
||||
codecov:
|
||||
require_ci_to_pass: false #This setting will update the bot regardless of whether or not tests pass
|
||||
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
informational: true
|
||||
patch:
|
||||
default:
|
||||
informational: true
|
||||
precision: 2
|
||||
round: down
|
||||
range: "66...100"
|
||||
|
||||
flags:
|
||||
unit:
|
||||
carryforward: false
|
||||
e2e-ci:
|
||||
carryforward: false
|
||||
e2e-full:
|
||||
carryforward: true
|
||||
|
||||
comment:
|
||||
layout: "diff,flags,files,footer"
|
||||
behavior: default
|
||||
require_changes: false
|
||||
show_carryforward_flags: true
|
@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
|
3
docs/footer.html
Normal file
3
docs/footer.html
Normal file
@ -0,0 +1,3 @@
|
||||
<hr>
|
||||
</body>
|
||||
</html>
|
209
docs/gendocs.js
Normal file
209
docs/gendocs.js
Normal file
@ -0,0 +1,209 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*global require,process,__dirname,GLOBAL*/
|
||||
/*jslint nomen: false */
|
||||
|
||||
|
||||
// Usage:
|
||||
// node gendocs.js --in <source directory> --out <dest directory>
|
||||
|
||||
var CONSTANTS = {
|
||||
DIAGRAM_WIDTH: 800,
|
||||
DIAGRAM_HEIGHT: 500
|
||||
},
|
||||
TOC_HEAD = "# Table of Contents";
|
||||
|
||||
GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be defined
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var fs = require("fs"),
|
||||
mkdirp = require("mkdirp"),
|
||||
path = require("path"),
|
||||
glob = require("glob"),
|
||||
marked = require("marked"),
|
||||
split = require("split"),
|
||||
stream = require("stream"),
|
||||
nomnoml = require('nomnoml'),
|
||||
toc = require("markdown-toc"),
|
||||
Canvas = require('canvas'),
|
||||
header = fs.readFileSync(path.resolve(__dirname, 'header.html')),
|
||||
footer = fs.readFileSync(path.resolve(__dirname, 'footer.html')),
|
||||
options = require("minimist")(process.argv.slice(2));
|
||||
|
||||
// Convert from nomnoml source to a target PNG file.
|
||||
function renderNomnoml(source, target) {
|
||||
var canvas =
|
||||
new Canvas(CONSTANTS.DIAGRAM_WIDTH, CONSTANTS.DIAGRAM_HEIGHT);
|
||||
nomnoml.draw(canvas, source, 1.0);
|
||||
canvas.pngStream().pipe(fs.createWriteStream(target));
|
||||
}
|
||||
|
||||
// Stream transform.
|
||||
// Pulls out nomnoml diagrams from fenced code blocks and renders them
|
||||
// as PNG files in the output directory, prefixed with a provided name.
|
||||
// The fenced code blocks will be replaced with Markdown in the
|
||||
// output of this stream.
|
||||
function nomnomlifier(outputDirectory, prefix) {
|
||||
var transform = new stream.Transform({ objectMode: true }),
|
||||
isBuilding = false,
|
||||
counter = 1,
|
||||
outputPath,
|
||||
source = "";
|
||||
|
||||
transform._transform = function (chunk, encoding, done) {
|
||||
if (!isBuilding) {
|
||||
if (chunk.trim().indexOf("```nomnoml") === 0) {
|
||||
var outputFilename = prefix + '-' + counter + '.png';
|
||||
outputPath = path.join(outputDirectory, outputFilename);
|
||||
this.push([
|
||||
"\n\n\n"
|
||||
].join(""));
|
||||
isBuilding = true;
|
||||
source = "";
|
||||
counter += 1;
|
||||
} else {
|
||||
// Otherwise, pass through
|
||||
this.push(chunk + '\n');
|
||||
}
|
||||
} else {
|
||||
if (chunk.trim() === "```") {
|
||||
// End nomnoml
|
||||
renderNomnoml(source, outputPath);
|
||||
isBuilding = false;
|
||||
} else {
|
||||
source += chunk + '\n';
|
||||
}
|
||||
}
|
||||
done();
|
||||
};
|
||||
|
||||
return transform;
|
||||
}
|
||||
|
||||
// Convert from Github-flavored Markdown to HTML
|
||||
function gfmifier(renderTOC) {
|
||||
var transform = new stream.Transform({ objectMode: true }),
|
||||
markdown = "";
|
||||
transform._transform = function (chunk, encoding, done) {
|
||||
markdown += chunk;
|
||||
done();
|
||||
};
|
||||
transform._flush = function (done) {
|
||||
if (renderTOC){
|
||||
// Prepend table of contents
|
||||
markdown =
|
||||
[ TOC_HEAD, toc(markdown).content, "", markdown ].join("\n");
|
||||
}
|
||||
this.push(header);
|
||||
this.push(marked(markdown));
|
||||
this.push(footer);
|
||||
done();
|
||||
};
|
||||
return transform;
|
||||
}
|
||||
|
||||
// Custom renderer for marked; converts relative links from md to html,
|
||||
// and makes headings linkable.
|
||||
function CustomRenderer() {
|
||||
var renderer = new marked.Renderer(),
|
||||
customRenderer = Object.create(renderer);
|
||||
customRenderer.heading = function (text, level) {
|
||||
var escapedText = (text || "").trim().toLowerCase().replace(/\W/g, "-"),
|
||||
aOpen = "<a name=\"" + escapedText + "\" href=\"#" + escapedText + "\">",
|
||||
aClose = "</a>";
|
||||
return aOpen + renderer.heading.apply(renderer, arguments) + aClose;
|
||||
};
|
||||
// Change links to .md files to .html
|
||||
customRenderer.link = function (href, title, text) {
|
||||
// ...but only if they look like relative paths
|
||||
return (href || "").indexOf(":") === -1 && href[0] !== "/" ?
|
||||
renderer.link(href.replace(/\.md/, ".html"), title, text) :
|
||||
renderer.link.apply(renderer, arguments);
|
||||
};
|
||||
return customRenderer;
|
||||
}
|
||||
|
||||
options['in'] = options['in'] || options.i;
|
||||
options.out = options.out || options.o;
|
||||
|
||||
marked.setOptions({
|
||||
renderer: new CustomRenderer(),
|
||||
gfm: true,
|
||||
tables: true,
|
||||
breaks: false,
|
||||
pedantic: false,
|
||||
sanitize: true,
|
||||
smartLists: true,
|
||||
smartypants: false
|
||||
});
|
||||
|
||||
// Convert all markdown files.
|
||||
// First, pull out nomnoml diagrams.
|
||||
// Then, convert remaining Markdown to HTML.
|
||||
glob(options['in'] + "/**/*.md", {}, function (err, files) {
|
||||
files.forEach(function (file) {
|
||||
var destination = file.replace(options['in'], options.out)
|
||||
.replace(/md$/, "html"),
|
||||
destPath = path.dirname(destination),
|
||||
prefix = path.basename(destination).replace(/\.html$/, ""),
|
||||
//Determine whether TOC should be rendered for this file based
|
||||
//on regex provided as command line option
|
||||
renderTOC = file.match(options['suppress-toc'] || "") === null;
|
||||
|
||||
mkdirp(destPath, function (err) {
|
||||
fs.createReadStream(file, { encoding: 'utf8' })
|
||||
.pipe(split())
|
||||
.pipe(nomnomlifier(destPath, prefix))
|
||||
.pipe(gfmifier(renderTOC))
|
||||
.pipe(fs.createWriteStream(destination, {
|
||||
encoding: 'utf8'
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Also copy over all HTML, CSS, or PNG files
|
||||
glob(options['in'] + "/**/*.@(html|css|png)", {}, function (err, files) {
|
||||
files.forEach(function (file) {
|
||||
var destination = file.replace(options['in'], options.out),
|
||||
destPath = path.dirname(destination),
|
||||
streamOptions = {};
|
||||
if (file.match(/png$/)) {
|
||||
streamOptions.encoding = null;
|
||||
} else {
|
||||
streamOptions.encoding = 'utf8';
|
||||
}
|
||||
|
||||
mkdirp(destPath, function (err) {
|
||||
fs.createReadStream(file, streamOptions)
|
||||
.pipe(fs.createWriteStream(destination, streamOptions));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}());
|
9
docs/header.html
Normal file
9
docs/header.html
Normal file
@ -0,0 +1,9 @@
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet"
|
||||
href="//nasa.github.io/openmct/static/res/css/styles.css">
|
||||
<link rel="stylesheet"
|
||||
href="//nasa.github.io/openmct/static/res/css/documentation.css">
|
||||
</head>
|
||||
<body>
|
||||
|
232
docs/src/architecture/framework.md
Normal file
232
docs/src/architecture/framework.md
Normal file
@ -0,0 +1,232 @@
|
||||
# Overview
|
||||
|
||||
The framework layer's most basic responsibility is allowing individual
|
||||
software components to communicate. The software components it recognizes
|
||||
are:
|
||||
|
||||
* _Extensions_: Individual units of functionality that can be added to
|
||||
or removed from Open MCT. _Extension categories_ distinguish what
|
||||
type of functionality is being added/removed.
|
||||
* _Bundles_: A grouping of related extensions
|
||||
(named after an analogous concept from [OSGi](http://www.osgi.org/))
|
||||
that may be added or removed as a group.
|
||||
|
||||
The framework layer operates by taking a set of active bundles, and
|
||||
exposing extensions to one another as-needed, using
|
||||
[dependency injection](https://en.wikipedia.org/wiki/Dependency_injection).
|
||||
Extensions are responsible for declaring their dependencies in a
|
||||
manner which the framework layer can understand.
|
||||
|
||||
```nomnoml
|
||||
#direction: down
|
||||
[Open MCT|
|
||||
[Dependency injection framework]-->[Platform bundle #1]
|
||||
[Dependency injection framework]-->[Platform bundle #2]
|
||||
[Dependency injection framework]-->[Plugin bundle #1]
|
||||
[Dependency injection framework]-->[Plugin bundle #2]
|
||||
[Platform bundle #1|[Extensions]]
|
||||
[Platform bundle #2|[Extensions]]
|
||||
[Plugin bundle #1|[Extensions]]
|
||||
[Plugin bundle #2|[Extensions]]
|
||||
[Platform bundle #1]<->[Platform bundle #2]
|
||||
[Plugin bundle #1]<->[Platform bundle #2]
|
||||
[Plugin bundle #1]<->[Plugin bundle #2]
|
||||
]
|
||||
```
|
||||
|
||||
The "dependency injection framework" in this case is
|
||||
[AngularJS](https://angularjs.org/). Open MCT's framework layer
|
||||
is really just a thin wrapper over Angular that recognizes the
|
||||
concepts of bundles and extensions (as declared in JSON files) and
|
||||
registering extensions with Angular. It additionally acts as a
|
||||
mediator between Angular and [RequireJS](http://requirejs.org/),
|
||||
which is used to load JavaScript sources which implement
|
||||
extensions.
|
||||
|
||||
```nomnoml
|
||||
[Framework layer|
|
||||
[AngularJS]<-[Framework Component]
|
||||
[RequireJS]<-[Framework Component]
|
||||
[Framework Component]1o-*[Bundles]
|
||||
]
|
||||
```
|
||||
|
||||
It is worth noting that _no other components_ are "aware" of the
|
||||
framework component directly; Angular and Require are _used by_ the
|
||||
framework components, and extensions in various bundles will have
|
||||
their dependencies satisfied by Angular as a consequence of registration
|
||||
activities which were performed by the framework component.
|
||||
|
||||
|
||||
## Application Initialization
|
||||
|
||||
The framework component initializes an Open MCT application following
|
||||
a simple sequence of steps.
|
||||
|
||||
```nomnoml
|
||||
[<start> Start]->[<state> Load bundles.json]
|
||||
[Load bundles.json]->[<state> Load bundle.json files]
|
||||
[Load bundle.json files]->[<state> Resolve implementations]
|
||||
[Resolve implementations]->[<state> Register with Angular]
|
||||
[Register with Angular]->[<state> Bootstrap application]
|
||||
[Bootstrap application]->[<end> End]
|
||||
```
|
||||
|
||||
1. __Loading bundles.json.__ A file named `bundles.json` is loaded to determine
|
||||
which bundles to load. Bundles are given in this file as relative paths
|
||||
which point to bundle directories.
|
||||
2. __Load bundle.json files.__ Individual bundle definitions are loaded; a
|
||||
`bundle.json` file is expected in each bundle directory.
|
||||
2. __Resolving implementations.__ Any scripts which provide implementations for
|
||||
extensions exposed by bundles are loaded, using RequireJS.
|
||||
3. __Register with Angular.__ Resolved extensions are registered with Angular,
|
||||
such that they can be used by the application at run-time. This stage
|
||||
includes both registration of Angular built-ins (directives, controllers,
|
||||
routes, constants, and services) as well as registration of non-Angular
|
||||
extensions.
|
||||
4. __Bootstrap application.__ Once all extensions have been registered,
|
||||
the Angular application
|
||||
[is bootstrapped](https://docs.angularjs.org/guide/bootstrap).
|
||||
|
||||
## Architectural Paradigm
|
||||
|
||||
```nomnoml
|
||||
[Extension]
|
||||
[Extension]o->[Dependency #1]
|
||||
[Extension]o->[Dependency #2]
|
||||
[Extension]o->[Dependency #3]
|
||||
```
|
||||
|
||||
Open MCT's architecture relies on a simple premise: Individual units
|
||||
(extensions) only have access to the dependencies they declare that they
|
||||
need, and they acquire references to these dependencies via dependency
|
||||
injection. This has several desirable traits:
|
||||
|
||||
* Programming to an interface is enforced. Any given dependency can be
|
||||
swapped out for something which exposes an equivalent interface. This
|
||||
improves flexibility against refactoring, simplifies testing, and
|
||||
provides a common mechanism for extension and reconfiguration.
|
||||
* The dependencies of a unit must be explicitly defined. This means that
|
||||
it can be easily determined what a given unit's role is within the
|
||||
larger system, in terms of what other components it will interact with.
|
||||
It also helps to enforce good separation of concerns: When a set of
|
||||
declared dependencies becomes long it is obvious, and this is usually
|
||||
a sign that a given unit is involved in too many concerns and should
|
||||
be refactored into smaller pieces.
|
||||
* Individual units do not need to be aware of the framework; they need
|
||||
only be aware of the interfaces to the components they specifically
|
||||
use. This avoids introducing a ubiquitous dependency upon the framework
|
||||
layer itself; it is plausible to modify or replace the framework
|
||||
without making changes to individual software components which run upon
|
||||
the framework.
|
||||
|
||||
A drawback to this approach is that it makes it difficult to define
|
||||
"the architecture" of Open MCT, in terms of describing the specific
|
||||
units that interact at run-time. The run-time architecture is determined
|
||||
by the framework as the consequence of wiring together dependencies.
|
||||
As such, the specific architecture of any given application built on
|
||||
Open MCT can look very different.
|
||||
|
||||
Keeping that in mind, there are a few useful patterns supported by the
|
||||
framework that are useful to keep in mind.
|
||||
|
||||
The specific service infrastructure provided by the platform is described
|
||||
in the [Platform Architecture](platform.md).
|
||||
|
||||
## Extension Categories
|
||||
|
||||
One of the capabilities that the framework component layers on top of
|
||||
AngularJS is support for many-to-one dependencies. That is, a specific
|
||||
extension may declare a dependency to _all extensions of a specific
|
||||
category_, instead of being limited to declaring specific dependencies.
|
||||
|
||||
```nomnoml
|
||||
#direction: right
|
||||
[Specific Extension] 1 o-> * [Extension of Some Category]
|
||||
```
|
||||
|
||||
This is useful for introducing specific extension points to an application.
|
||||
Some unit of software will depend upon all extensions of a given category
|
||||
and integrate their behavior into the system in some fashion; plugin authors
|
||||
can then add new extensions of that category to augment existing behaviors.
|
||||
|
||||
Some developers may be familiar with the use of registries to achieve
|
||||
similar characteristics. This approach is similar, except that the registry
|
||||
is effectively implicit whenever a new extension category is used or
|
||||
depended-upon. This has some advantages over a more straightforward
|
||||
registry-based approach:
|
||||
|
||||
* These many-to-one relationships are expressed as dependencies; an
|
||||
extension category is registered as having dependencies on all individual
|
||||
extensions of this category. This avoids ordering issues that may occur
|
||||
with more conventional registries, which may be observed before all
|
||||
dependencies are resolved.
|
||||
* The need for service registries of specific types is removed, reducing
|
||||
the number of interfaces to manage within the system. Groups of
|
||||
extensions are provided as arrays.
|
||||
|
||||
## Composite Services
|
||||
|
||||
Composite services (registered via extension category `components`) are
|
||||
a pattern supported by the framework. These allow service instances to
|
||||
be built from multiple components at run-time; support for this pattern
|
||||
allows additional bundles to introduce or modify behavior associated
|
||||
with these services without modifying or replacing original service
|
||||
instances.
|
||||
|
||||
```nomnoml
|
||||
#direction: down
|
||||
[<abstract> FooService]
|
||||
[FooDecorator #1]--:>[FooService]
|
||||
[FooDecorator #n]--:>[FooService]
|
||||
[FooAggregator]--:>[FooService]
|
||||
[FooProvider #1]--:>[FooService]
|
||||
[FooProvider #n]--:>[FooService]
|
||||
|
||||
[FooDecorator #1]o->[<state> ...decorators...]
|
||||
[...decorators...]o->[FooDecorator #n]
|
||||
[FooDecorator #n]o->[FooAggregator]
|
||||
[FooAggregator]o->[FooProvider #1]
|
||||
[FooAggregator]o->[<state> ...providers...]
|
||||
[FooAggregator]o->[FooProvider #n]
|
||||
|
||||
[FooDecorator #1]--[<note> Exposed as fooService]
|
||||
```
|
||||
|
||||
In this pattern, components all implement an interface which is
|
||||
standardized for that service. Components additionally declare
|
||||
that they belong to one of three types:
|
||||
|
||||
* __Providers.__ A provider actually implements the behavior
|
||||
(satisfies the contract) for that kind of service. For instance,
|
||||
if a service is responsible for looking up documents by an identifier,
|
||||
one provider may do so by querying a database, while another may
|
||||
do so by reading a static JSON document. From the outside, either
|
||||
provider would look the same (they expose the same interface) and
|
||||
they could be swapped out easily.
|
||||
* __Aggregator.__ An aggregator takes many providers and makes them
|
||||
behave as one. Again, this implements the same interface as an
|
||||
individual provider, so users of the service do not need to be
|
||||
concerned about the difference between consulting many providers
|
||||
and consulting one. Continuing with the example of a service that
|
||||
looks up documents by identifiers, an aggregator here might consult
|
||||
all providers, and return any document is found (perhaps picking one
|
||||
over the other or merging documents if there are multiple matches.)
|
||||
* __Decorators.__ A decorator exposes the same interface as other
|
||||
components, but instead of fully implementing the behavior associated
|
||||
with that kind of service, it only acts as an intermediary, delegating
|
||||
the actual behavior to a different component. Decorators may transform
|
||||
inputs or outputs, or initiate some side effects associated with a
|
||||
service. This is useful if certain common behavior associated with a
|
||||
service (caching, for instance) may be useful across many different
|
||||
implementations of that same service.
|
||||
|
||||
The framework will register extensions in this category such that an
|
||||
aggregator will depend on all of its providers, and decorators will
|
||||
depend upon on one another in a chain. The result of this compositing step
|
||||
(the last decorator, if any; otherwise the aggregator, if any;
|
||||
otherwise a single provider) will be exposed as a single service that
|
||||
other extensions can acquire through dependency injection. Because all
|
||||
components of the same type of service expose the same interface, users
|
||||
of that service do not need to be aware that they are talking to an
|
||||
aggregator or a provider, for instance.
|
76
docs/src/architecture/index.md
Normal file
76
docs/src/architecture/index.md
Normal file
@ -0,0 +1,76 @@
|
||||
# Introduction
|
||||
|
||||
The purpose of this document is to familiarize developers with the
|
||||
overall architecture of Open MCT.
|
||||
|
||||
The target audience includes:
|
||||
|
||||
* _Platform maintainers_: Individuals involved in developing,
|
||||
extending, and maintaining capabilities of the platform.
|
||||
* _Integration developers_: Individuals tasked with integrated
|
||||
Open MCT into a larger system, who need to understand
|
||||
its inner workings sufficiently to complete this integration.
|
||||
|
||||
As the focus of this document is on architecture, whenever possible
|
||||
implementation details (such as relevant API or JSON syntax) have been
|
||||
omitted. These details may be found in the developer guide.
|
||||
|
||||
# Overview
|
||||
|
||||
Open MCT is client software: It runs in a web browser and
|
||||
provides a user interface, while communicating with various
|
||||
server-side resources through browser APIs.
|
||||
|
||||
```nomnoml
|
||||
#direction: right
|
||||
[Client|[Browser|[Open MCT]->[Browser APIs]]]
|
||||
[Server|[Web services]]
|
||||
[Client]<->[Server]
|
||||
```
|
||||
|
||||
While Open MCT can be configured to run as a standalone client,
|
||||
this is rarely very useful. Instead, it is intended to be used as a
|
||||
display and interaction layer for information obtained from a
|
||||
variety of back-end services. Doing so requires authoring or utilizing
|
||||
adapter plugins which allow Open MCT to interact with these services.
|
||||
|
||||
Typically, the pattern here is to provide a known interface that
|
||||
Open MCT can utilize, and implement it such that it interacts with
|
||||
whatever back-end provides the relevant information.
|
||||
Examples of back-ends that can be utilized in this fashion include
|
||||
databases for the persistence of user-created objects, or sources of
|
||||
telemetry data.
|
||||
|
||||
## Software Architecture
|
||||
|
||||
The simplest overview of Open MCT is to look at it as a "layered"
|
||||
architecture, where each layer more clearly specifies the behavior
|
||||
of the software.
|
||||
|
||||
```nomnoml
|
||||
#direction: down
|
||||
[Open MCT|
|
||||
[Platform]<->[Application]
|
||||
[Framework]->[Application]
|
||||
[Framework]->[Platform]
|
||||
]
|
||||
```
|
||||
|
||||
These layers are:
|
||||
|
||||
* [_Framework_](framework.md): The framework layer is responsible for
|
||||
managing the interactions between application components. It has no
|
||||
application-specific knowledge; at this layer, we have only
|
||||
established an abstraction by which different software components
|
||||
may communicate and/or interact.
|
||||
* [_Platform_](platform.md): The platform layer defines the general look,
|
||||
feel, and behavior of Open MCT. This includes user-facing components like
|
||||
Browse mode and Edit mode, as well as underlying elements of the
|
||||
information model and the general service infrastructure.
|
||||
* _Application_: The application layer defines specific features of
|
||||
an application built on Open MCT. This includes adapters to
|
||||
specific back-ends, new types of things for users to create, and
|
||||
new ways of visualizing objects within the system. This layer
|
||||
typically consists of a mix of custom plug-ins to Open MCT,
|
||||
as well as optional features (such as Plot view) included alongside
|
||||
the platform.
|
726
docs/src/architecture/platform.md
Normal file
726
docs/src/architecture/platform.md
Normal file
@ -0,0 +1,726 @@
|
||||
# Overview
|
||||
|
||||
The Open MCT platform utilizes the [framework layer](framework.md)
|
||||
to provide an extensible baseline for applications which includes:
|
||||
|
||||
* A common user interface (and user interface paradigm) for dealing with
|
||||
domain objects of various sorts.
|
||||
* A variety of extension points for introducing new functionality
|
||||
of various kinds within the context of the common user interface.
|
||||
* A service infrastructure to support building additional components.
|
||||
|
||||
## Platform Architecture
|
||||
|
||||
While the framework provides a more general architectural paradigm for
|
||||
building application, the platform adds more specificity by defining
|
||||
additional extension types and allowing for integration with back end
|
||||
components.
|
||||
|
||||
The run-time architecture of an Open MCT application can be categorized
|
||||
into certain high-level tiers:
|
||||
|
||||
```nomnoml
|
||||
[DOM]->[<state> AngularJS]
|
||||
[AngularJS]->[Presentation Layer]
|
||||
[Presentation Layer]->[Information Model]
|
||||
[Presentation Layer]->[Service Infrastructure]
|
||||
[Information Model]->[Service Infrastructure]
|
||||
[Service Infrastructure]->[<state> Browser APIs]
|
||||
[Browser APIs]->[Back-end]
|
||||
```
|
||||
|
||||
Applications built using Open MCT may add or configure functionality
|
||||
in __any of these tiers__.
|
||||
|
||||
* _DOM_: The rendered HTML document, composed from HTML templates which
|
||||
have been processed by AngularJS and will be updated by AngularJS
|
||||
to reflect changes from the presentation layer. User interactions
|
||||
are initiated from here and invoke behavior in the presentation layer. HTML
|
||||
templates are written in Angular’s template syntax; see the [Angular documentation on templates](https://docs.angularjs.org/guide/templates).
|
||||
These describe the page as actually seen by the user. Conceptually,
|
||||
stylesheets (controlling the look-and-feel of the rendered templates) belong
|
||||
in this grouping as well.
|
||||
* [_Presentation layer_](#presentation-layer): The presentation layer
|
||||
is responsible for updating (and providing information to update)
|
||||
the displayed state of the application. The presentation layer consists
|
||||
primarily of _controllers_ and _directives_. The presentation layer is
|
||||
concerned with inspecting the information model and preparing it for
|
||||
display.
|
||||
* [_Information model_](#information-model): Provides a common (within Open MCT
|
||||
Web) set of interfaces for dealing with “things” domain objects within the
|
||||
system. User-facing concerns in a Open MCT Web application are expressed as
|
||||
domain objects; examples include folders (used to organize other domain
|
||||
objects), layouts (used to build displays), or telemetry points (used as
|
||||
handles for streams of remote measurements.) These domain objects expose a
|
||||
common set of interfaces to allow reusable user interfaces to be built in the
|
||||
presentation and template tiers; the specifics of these behaviors are then
|
||||
mapped to interactions with underlying services.
|
||||
* [_Service infrastructure_](#service-infrastructure): The service
|
||||
infrastructure is responsible for providing the underlying general
|
||||
functionality needed to support the information model. This includes
|
||||
exposing underlying sets of extensions and mediating with the
|
||||
back-end.
|
||||
* _Back-end_: The back-end is out of the scope of Open MCT, except
|
||||
for the interfaces which are utilized by adapters participating in the
|
||||
service infrastructure. Includes the underlying persistence stores, telemetry
|
||||
streams, and so forth which the Open MCT Web client is being used to interact
|
||||
with.
|
||||
|
||||
## Application Start-up
|
||||
|
||||
Once the
|
||||
[application has been initialized](Framework.md#application-initialization)
|
||||
Open MCT primarily operates in an event-driven paradigm; various
|
||||
events (mouse clicks, timers firing, receiving responses to XHRs) trigger
|
||||
the invocation of functions, typically in the presentation layer for
|
||||
user actions or in the service infrastructure for server responses.
|
||||
|
||||
The "main point of entry" into an initialized Open MCT application
|
||||
is effectively the
|
||||
[route](https://docs.angularjs.org/api/ngRoute/service/$route#example)
|
||||
which is associated with the URL used to access Open MCT (or a
|
||||
default route.) This route will be associated with a template which
|
||||
will be displayed; this template will include references to directives
|
||||
and controllers which will be interpreted by Angular and used to
|
||||
initialize the state of the display in a manner which is backed by
|
||||
both the information model and the service infrastructure.
|
||||
|
||||
```nomnoml
|
||||
[<start> Start]->[<state> page load]
|
||||
[page load]->[<state> route selection]
|
||||
[route selection]->[<state> compile, display template]
|
||||
[compile, display template]->[Template]
|
||||
[Template]->[<state> use Controllers]
|
||||
[Template]->[<state> use Directives]
|
||||
[use Controllers]->[Controllers]
|
||||
[use Directives]->[Directives]
|
||||
[Controllers]->[<state> consult information model]
|
||||
[consult information model]->[<state> expose data]
|
||||
[expose data]->[Angular]
|
||||
[Angular]->[<state> update display]
|
||||
[Directives]->[<state> add event listeners]
|
||||
[Directives]->[<state> update display]
|
||||
[add event listeners]->[<end> End]
|
||||
[update display]->[<end> End]
|
||||
```
|
||||
|
||||
|
||||
# Presentation Layer
|
||||
|
||||
The presentation layer of Open MCT is responsible for providing
|
||||
information to display within templates, and for handling interactions
|
||||
which are initiated from templated DOM elements. AngularJS acts as
|
||||
an intermediary between the web page as the user sees it, and the
|
||||
presentation layer implemented as Open MCT extensions.
|
||||
|
||||
```nomnoml
|
||||
[Presentation Layer|
|
||||
[Angular built-ins|
|
||||
[routes]
|
||||
[controllers]
|
||||
[directives]
|
||||
[templates]
|
||||
]
|
||||
[Domain object representation|
|
||||
[views]
|
||||
[representations]
|
||||
[representers]
|
||||
[gestures]
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
## Angular built-ins
|
||||
|
||||
Several extension categories in the presentation layer map directly
|
||||
to primitives from AngularJS:
|
||||
|
||||
* [_Controllers_](https://docs.angularjs.org/guide/controller) provide
|
||||
data to templates, and expose functionality that can be called from
|
||||
templates.
|
||||
* [_Directives_](https://docs.angularjs.org/guide/directive) effectively
|
||||
extend HTML to provide custom behavior associated with specific
|
||||
attributes and tags.
|
||||
* [_Routes_](https://docs.angularjs.org/api/ngRoute/service/$route#example)
|
||||
are used to associate specific URLs (including the fragment identifier)
|
||||
with specific application states. (In Open MCT, these are used to
|
||||
describe the mode of usage - e.g. browse or edit - as well as to
|
||||
identify the object being used.)
|
||||
* [_Templates_](https://docs.angularjs.org/guide/templates) are partial
|
||||
HTML documents that will be rendered and kept up-to-date by AngularJS.
|
||||
Open MCT introduces a custom `mct-include` directive which acts
|
||||
as a wrapper around `ng-include` to allow templates to be referred
|
||||
to by symbolic names.
|
||||
|
||||
## Domain object representation
|
||||
|
||||
The remaining extension categories in the presentation layer are specific
|
||||
to displaying domain objects.
|
||||
|
||||
* _Representations_ are templates that will be used to display
|
||||
domain objects in specific ways (e.g. "as a tree node.")
|
||||
* _Views_ are representations which are exposed to the user as options
|
||||
for displaying domain objects.
|
||||
* _Representers_ are extensions which modify or augment the process
|
||||
of representing domain objects generally (e.g. by attaching
|
||||
gestures to them.)
|
||||
* _Gestures_ provide associations between specific user actions
|
||||
(expressed as DOM events) and resulting behavior upon domain objects
|
||||
(typically expressed as members of the `actions` extension category)
|
||||
that can be reused across domain objects. For instance, `drag` and
|
||||
`drop` are both gestures associated with using drag-and-drop to
|
||||
modify the composition of domain objects by interacting with their
|
||||
representations.
|
||||
|
||||
# Information Model
|
||||
|
||||
```nomnoml
|
||||
#direction: right
|
||||
[Information Model|
|
||||
[DomainObject|
|
||||
getId() : string
|
||||
getModel() : object
|
||||
getCapability(key : string) : Capability
|
||||
hasCapability(key : string) : boolean
|
||||
useCapability(key : string, args...) : *
|
||||
]
|
||||
[DomainObject] 1 +- 1 [Model]
|
||||
[DomainObject] 1 o- * [Capability]
|
||||
]
|
||||
```
|
||||
|
||||
Domain objects are the most fundamental component of Open MCT's
|
||||
information model. A domain object is some distinct thing relevant to a
|
||||
user's work flow, such as a telemetry channel, display, or similar.
|
||||
Open MCT is a tool for viewing, browsing, manipulating, and otherwise
|
||||
interacting with a graph of domain objects.
|
||||
|
||||
A domain object should be conceived of as the union of the following:
|
||||
|
||||
* _Identifier_: A machine-readable string that uniquely identifies the
|
||||
domain object within this application instance.
|
||||
* _Model_: The persistent state of the domain object. A domain object's
|
||||
model is a JavaScript object that can be losslessly converted to JSON.
|
||||
* _Capabilities_: Dynamic behavior associated with the domain object.
|
||||
Capabilities are JavaScript objects which provide additional methods
|
||||
for interacting with the domain objects which expose those capabilities.
|
||||
Not all domain objects expose all capabilities. The interface exposed
|
||||
by any given capability will depend on its type (as identified
|
||||
by the `key` argument.) For instance, a `persistence` capability
|
||||
has a different interface from a `telemetry` capability. Using
|
||||
capabilities requires some prior knowledge of their interface.
|
||||
|
||||
## Capabilities and Services
|
||||
|
||||
```nomnoml
|
||||
#direction: right
|
||||
[DomainObject]o-[FooCapability]
|
||||
[FooCapability]o-[FooService]
|
||||
[FooService]o-[foos]
|
||||
```
|
||||
|
||||
At run-time, the user is primarily concerned with interacting with
|
||||
domain objects. These interactions are ultimately supported via back-end
|
||||
services, but to allow customization per-object, these are often mediated
|
||||
by capabilities.
|
||||
|
||||
A common pattern that emerges in the Open MCT Platform is as follows:
|
||||
|
||||
* A `DomainObject` has some particular behavior that will be supported
|
||||
by a service.
|
||||
* A `Capability` of that domain object will define that behavior,
|
||||
_for that domain object_, supported by a service.
|
||||
* A `Service` utilized by that capability will perform the actual behavior.
|
||||
* An extension category will be utilized by that capability to determine
|
||||
the set of possible behaviors.
|
||||
|
||||
Concrete examples of capabilities which follow this pattern
|
||||
(or a subset of this pattern) include:
|
||||
|
||||
```nomnoml
|
||||
#direction: right
|
||||
[DomainObject]1 o- *[Capability]
|
||||
[Capability]<:--[TypeCapability]
|
||||
[Capability]<:--[ActionCapability]
|
||||
[Capability]<:--[PersistenceCapability]
|
||||
[Capability]<:--[TelemetryCapability]
|
||||
[TypeCapability]o-[TypeService]
|
||||
[TypeService]o-[types]
|
||||
[ActionCapability]o-[ActionService]
|
||||
[ActionService]o-[actions]
|
||||
[PersistenceCapability]o-[PersistenceService]
|
||||
[TelemetryCapability]o-[TelemetryService]
|
||||
```
|
||||
|
||||
# Service Infrastructure
|
||||
|
||||
Most services exposed by the Open MCT platform follow the
|
||||
[composite services](Framework.md#composite-services) to permit
|
||||
a higher degree of flexibility in how a service can be modified
|
||||
or customized for specific applications.
|
||||
|
||||
To simplify usage for plugin developers, the platform also usually
|
||||
includes a provider implementation for these service type that consumes
|
||||
some extension category. For instance, an `ActionService` provider is
|
||||
included which depends upon extension category `actions`, and exposes
|
||||
all actions declared as such to the system. As such, plugin developers
|
||||
can simply implement the new actions they wish to be made available without
|
||||
worrying about the details of composite services or implementing a new
|
||||
`ActionService` provider; however, the ability to implement a new provider
|
||||
remains useful when the expressive power of individual extensions is
|
||||
insufficient.
|
||||
|
||||
```nomnoml
|
||||
[ Service Infrastructure |
|
||||
[ObjectService]->[ModelService]
|
||||
[ModelService]->[PersistenceService]
|
||||
[ObjectService]->[CapabilityService]
|
||||
[CapabilityService]->[capabilities]
|
||||
[capabilities]->[TelemetryService]
|
||||
[capabilities]->[PersistenceService]
|
||||
[capabilities]->[TypeService]
|
||||
[capabilities]->[ActionService]
|
||||
[capabilities]->[ViewService]
|
||||
[PersistenceService]->[<database> Document store]
|
||||
[TelemetryService]->[<database> Telemetry source]
|
||||
[ActionService]->[actions]
|
||||
[ActionService]->[PolicyService]
|
||||
[ViewService]->[PolicyService]
|
||||
[ViewService]->[views]
|
||||
[PolicyService]->[policies]
|
||||
[TypeService]->[types]
|
||||
]
|
||||
```
|
||||
|
||||
A short summary of the roles of these services:
|
||||
|
||||
* _[ObjectService](#object-service)_: Allows retrieval of domain objects by
|
||||
their identifiers; in practice, often the main point of entry into the
|
||||
[information model](#information-model).
|
||||
* _[ModelService](#model-service)_: Provides domain object models, retrieved
|
||||
by their identifier.
|
||||
* _[CapabilityService](#capability-service)_: Provides capabilities, as they
|
||||
apply to specific domain objects (as judged from their model.)
|
||||
* _[TelemetryService](#telemetry-service)_: Provides access to historical
|
||||
and real-time telemetry data.
|
||||
* _[PersistenceService](#persistence-service)_: Provides the ability to
|
||||
store and retrieve documents (such as domain object models.)
|
||||
* _[ActionService](#action-service)_: Provides distinct user actions that
|
||||
can take place within the system (typically, upon or using domain objects.)
|
||||
* _[ViewService](#view-service)_: Provides views for domain objects. A view
|
||||
is a user-selectable representation of a domain object (in practice, an
|
||||
HTML template.)
|
||||
* _[PolicyService](#policy-service)_: Handles decisions about which
|
||||
behavior are allowed within certain specific contexts.
|
||||
* _[TypeService](#type-service)_: Provides information to distinguish
|
||||
different types of domain objects from one another within the system.
|
||||
|
||||
## Object Service
|
||||
|
||||
```nomnoml
|
||||
#direction: right
|
||||
[<abstract> ObjectService|
|
||||
getObjects(ids : Array.<string>) : Promise.<object.<string, DomainObject>>
|
||||
]
|
||||
[DomainObjectProvider]--:>[ObjectService]
|
||||
[DomainObjectProvider]o-[ModelService]
|
||||
[DomainObjectProvider]o-[CapabilityService]
|
||||
```
|
||||
|
||||
As domain objects are central to Open MCT's information model,
|
||||
acquiring domain objects is equally important.
|
||||
|
||||
```nomnoml
|
||||
#direction: right
|
||||
[<start> Start]->[<state> Look up models]
|
||||
[<state> Look up models]->[<state> Look up capabilities]
|
||||
[<state> Look up capabilities]->[<state> Instantiate DomainObject]
|
||||
[<state> Instantiate DomainObject]->[<end> End]
|
||||
```
|
||||
|
||||
Open MCT includes an implementation of an `ObjectService` which
|
||||
satisfies this capability by:
|
||||
|
||||
* Consulting the [Model Service](#model-service) to acquire domain object
|
||||
models by identifier.
|
||||
* Passing these models to a [Capability Service](#capability-service) to
|
||||
determine which capabilities are applicable.
|
||||
* Combining these results together as [DomainObject](#information-model)
|
||||
instances.
|
||||
|
||||
## Model Service
|
||||
|
||||
```nomnoml
|
||||
#direction: down
|
||||
[<abstract> ModelService|
|
||||
getModels(ids : Array.<string>) : Promise.<object.<string, object>>
|
||||
]
|
||||
[StaticModelProvider]--:>[ModelService]
|
||||
[RootModelProvider]--:>[ModelService]
|
||||
[PersistedModelProvider]--:>[ModelService]
|
||||
[ModelAggregator]--:>[ModelService]
|
||||
[CachingModelDecorator]--:>[ModelService]
|
||||
[MissingModelDecorator]--:>[ModelService]
|
||||
|
||||
[MissingModelDecorator]o-[CachingModelDecorator]
|
||||
[CachingModelDecorator]o-[ModelAggregator]
|
||||
[ModelAggregator]o-[StaticModelProvider]
|
||||
[ModelAggregator]o-[RootModelProvider]
|
||||
[ModelAggregator]o-[PersistedModelProvider]
|
||||
|
||||
[PersistedModelProvider]o-[PersistenceService]
|
||||
[RootModelProvider]o-[roots]
|
||||
[StaticModelProvider]o-[models]
|
||||
```
|
||||
|
||||
The platform's model service is responsible for providing domain object
|
||||
models (effectively, JSON documents describing the persistent state
|
||||
associated with domain objects.) These are retrieved by identifier.
|
||||
|
||||
The platform includes multiple components of this variety:
|
||||
|
||||
* `PersistedModelProvider` looks up domain object models from
|
||||
a persistence store (the [`PersistenceService`](#persistence-service));
|
||||
this is how user-created and user-modified
|
||||
domain object models are retrieved.
|
||||
* `RootModelProvider` provides domain object models that have been
|
||||
declared via the `roots` extension category. These will appear at the
|
||||
top level of the tree hierarchy in the user interface.
|
||||
* `StaticModelProvider` provides domain object models that have been
|
||||
declared via the `models` extension category. This is useful for
|
||||
allowing plugins to expose new domain objects declaratively.
|
||||
* `ModelAggregator` merges together the results from multiple providers.
|
||||
If multiple providers return models for the same domain object,
|
||||
the most recently modified version (as determined by the `modified`
|
||||
property of the model) is chosen.
|
||||
* `CachingModelDecorator` caches model instances in memory. This
|
||||
ensures that only a single instance of a domain object model is
|
||||
present at any given time within the application, and prevent
|
||||
redundant retrievals.
|
||||
* `MissingModelDecorator` adds in placeholders when no providers
|
||||
have returned domain object models for a specific identifier. This
|
||||
allows the user to easily see that something was expected to be
|
||||
present, but wasn't.
|
||||
|
||||
## Capability Service
|
||||
|
||||
```nomnoml
|
||||
#direction: down
|
||||
[<abstract> CapabilityService|
|
||||
getCapabilities(model : object) : object.<string, Function>
|
||||
]
|
||||
[CoreCapabilityProvider]--:>[CapabilityService]
|
||||
[QueuingPersistenceCapabilityDecorator]--:>[CapabilityService]
|
||||
|
||||
[CoreCapabilityProvider]o-[capabilities]
|
||||
[QueuingPersistenceCapabilityDecorator]o-[CoreCapabilityProvider]
|
||||
```
|
||||
|
||||
The capability service is responsible for determining which capabilities
|
||||
are applicable for a given domain object, based on its model. Primarily,
|
||||
this is handled by the `CoreCapabilityProvider`, which examines
|
||||
capabilities exposed via the `capabilities` extension category.
|
||||
|
||||
Additionally, `platform/persistence/queue` decorates the persistence
|
||||
capability specifically to batch persistence attempts among multiple
|
||||
objects (this allows failures to be recognized and handled in groups.)
|
||||
|
||||
## Telemetry Service
|
||||
|
||||
```nomnoml
|
||||
[<abstract> TelemetryService|
|
||||
requestData(requests : Array.<TelemetryRequest>) : Promise.<object>
|
||||
subscribe(requests : Array.<TelemetryRequest>) : Function
|
||||
]<--:[TelemetryAggregator]
|
||||
```
|
||||
|
||||
The telemetry service is responsible for acquiring telemetry data.
|
||||
|
||||
Notably, the platform does not include any providers for
|
||||
`TelemetryService`; applications built on Open MCT will need to
|
||||
implement a provider for this service if they wish to expose telemetry
|
||||
data. This is usually the most important step for integrating Open MCT
|
||||
into an existing telemetry system.
|
||||
|
||||
Requests for telemetry data are usually initiated in the
|
||||
[presentation layer](#presentation-layer) by some `Controller` referenced
|
||||
from a view. The `telemetryHandler` service is most commonly used (although
|
||||
one could also use an object's `telemetry` capability directly) as this
|
||||
handles capability delegation, by which a domain object such as a Telemetry
|
||||
Panel can declare that its `telemetry` capability should be handled by the
|
||||
objects it contains. Ultimately, the request for historical data and the
|
||||
new subscriptions will reach the `TelemetryService`, and, by way of the
|
||||
provider(s) which are present for that `TelemetryService`, will pass the
|
||||
same requests to the back-end.
|
||||
|
||||
```nomnoml
|
||||
[<start> Start]->[Controller]
|
||||
[Controller]->[<state> declares object of interest]
|
||||
[declares object of interest]->[TelemetryHandler]
|
||||
[TelemetryHandler]->[<state> requests telemetry from capabilities]
|
||||
[TelemetryHandler]->[<state> subscribes to telemetry using capabilities]
|
||||
[requests telemetry from capabilities]->[TelemetryCapability]
|
||||
[subscribes to telemetry using capabilities]->[TelemetryCapability]
|
||||
[TelemetryCapability]->[<state> requests telemetry]
|
||||
[TelemetryCapability]->[<state> subscribes to telemetry]
|
||||
[requests telemetry]->[TelemetryService]
|
||||
[subscribes to telemetry]->[TelemetryService]
|
||||
[TelemetryService]->[<state> issues request]
|
||||
[TelemetryService]->[<state> updates subscriptions]
|
||||
[TelemetryService]->[<state> listens for real-time data]
|
||||
[issues request]->[<database> Telemetry Back-end]
|
||||
[updates subscriptions]->[Telemetry Back-end]
|
||||
[listens for real-time data]->[Telemetry Back-end]
|
||||
[Telemetry Back-end]->[<end> End]
|
||||
```
|
||||
|
||||
The back-end, in turn, is expected to provide whatever historical
|
||||
telemetry is available to satisfy the request that has been issue.
|
||||
|
||||
```nomnoml
|
||||
[<start> Start]->[<database> Telemetry Back-end]
|
||||
[Telemetry Back-end]->[<state> transmits historical telemetry]
|
||||
[transmits historical telemetry]->[TelemetryService]
|
||||
[TelemetryService]->[<state> packages telemetry, fulfills requests]
|
||||
[packages telemetry, fulfills requests]->[TelemetryCapability]
|
||||
[TelemetryCapability]->[<state> unpacks telemetry per-object, fulfills request]
|
||||
[unpacks telemetry per-object, fulfills request]->[TelemetryHandler]
|
||||
[TelemetryHandler]->[<state> exposes data]
|
||||
[TelemetryHandler]->[<state> notifies controller]
|
||||
[exposes data]->[Controller]
|
||||
[notifies controller]->[Controller]
|
||||
[Controller]->[<state> prepares data for template]
|
||||
[prepares data for template]->[Template]
|
||||
[Template]->[<state> displays data]
|
||||
[displays data]->[<end> End]
|
||||
```
|
||||
|
||||
One peculiarity of this approach is that we package many responses
|
||||
together at once in the `TelemetryService`, then unpack these in the
|
||||
`TelemetryCapability`, then repackage these in the `TelemetryHandler`.
|
||||
The rationale for this is as follows:
|
||||
|
||||
* In the `TelemetryService`, we want to have the ability to combine
|
||||
multiple requests into one call to the back-end, as many back-ends
|
||||
will support this. It follows that we give the response as a single
|
||||
object, packages in a manner that allows responses to individual
|
||||
requests to be easily identified.
|
||||
* In the `TelemetryCapability`, we want to provide telemetry for a
|
||||
_single object_, so the telemetry data gets unpacked. This allows
|
||||
for the unpacking of data to be handled in a single place, and
|
||||
also permits a flexible substitution method; domain objects may have
|
||||
implementations of the `telemetry` capability that do not use the
|
||||
`TelemetryService` at all, while still maintaining compatibility
|
||||
with any presentation layer code written to utilize this capability.
|
||||
(This is true of capabilities generally.)
|
||||
* In the `TelemetryHandler`, we want to group multiple responses back
|
||||
together again to make it easy for the presentation layer to consume.
|
||||
In this case, the grouping is different from what may have occurred
|
||||
in the `TelemetryService`; this grouping is based on what is expected
|
||||
to be useful _in a specific view_. The `TelemetryService`
|
||||
may be receiving requests from multiple views.
|
||||
|
||||
```nomnoml
|
||||
[<start> Start]->[<database> Telemetry Back-end]
|
||||
[Telemetry Back-end]->[<state> notifies client of new data]
|
||||
[notifies client of new data]->[TelemetryService]
|
||||
[TelemetryService]->[<choice> relevant subscribers?]
|
||||
[relevant subscribers?] yes ->[<state> notify subscribers]
|
||||
[relevant subscribers?] no ->[<state> ignore]
|
||||
[ignore]->[<end> Ignored]
|
||||
[notify subscribers]->[TelemetryCapability]
|
||||
[TelemetryCapability]->[<state> notify listener]
|
||||
[notify listener]->[TelemetryHandler]
|
||||
[TelemetryHandler]->[<state> exposes data]
|
||||
[TelemetryHandler]->[<state> notifies controller]
|
||||
[exposes data]->[Controller]
|
||||
[notifies controller]->[Controller]
|
||||
[Controller]->[<state> prepares data for template]
|
||||
[prepares data for template]->[Template]
|
||||
[Template]->[<state> displays data]
|
||||
[displays data]->[<end> End]
|
||||
```
|
||||
|
||||
The flow of real-time data is similar, and is handled by a sequence
|
||||
of callbacks between the presentation layer component which is
|
||||
interested in data and the telemetry service. Providers in the
|
||||
telemetry service listen to the back-end for new data (via whatever
|
||||
mechanism their specific back-end supports), package this data in
|
||||
the same manner as historical data, and pass that to the callbacks
|
||||
which are associated with relevant requests.
|
||||
|
||||
## Persistence Service
|
||||
|
||||
```nomnoml
|
||||
#direction: right
|
||||
[<abstract> PersistenceService|
|
||||
listSpaces() : Promise.<Array.<string>>
|
||||
listObjects() : Promise.<Array.<string>>
|
||||
createObject(space : string, key : string, document : object) : Promise.<boolean>
|
||||
readObject(space : string, key : string, document : object) : Promise.<object>
|
||||
updateObject(space : string, key : string, document : object) : Promise.<boolean>
|
||||
deleteObject(space : string, key : string, document : object) : Promise.<boolean>
|
||||
]
|
||||
|
||||
[ElasticPersistenceProvider]--:>[PersistenceService]
|
||||
[ElasticPersistenceProvider]->[<database> ElasticSearch]
|
||||
|
||||
[CouchPersistenceProvider]--:>[PersistenceService]
|
||||
[CouchPersistenceProvider]->[<database> CouchDB]
|
||||
```
|
||||
|
||||
Closely related to the notion of domain objects models is their
|
||||
persistence. The `PersistenceService` allows these to be saved
|
||||
and loaded. (Currently, this capability is only used for domain
|
||||
object models, but the interface has been designed without this idea
|
||||
in mind; other kinds of documents could be saved and loaded in the
|
||||
same manner.)
|
||||
|
||||
There is no single definitive implementation of a `PersistenceService` in
|
||||
the platform. Optional adapters are provided to store and load documents
|
||||
from CouchDB and ElasticSearch, respectively; plugin authors may also
|
||||
write additional adapters to utilize different back end technologies.
|
||||
|
||||
## Action Service
|
||||
|
||||
```nomnoml
|
||||
[ActionService|
|
||||
getActions(context : ActionContext) : Array.<Action>
|
||||
]
|
||||
[ActionProvider]--:>[ActionService]
|
||||
[CreateActionProvider]--:>[ActionService]
|
||||
[ActionAggregator]--:>[ActionService]
|
||||
[LoggingActionDecorator]--:>[ActionService]
|
||||
[PolicyActionDecorator]--:>[ActionService]
|
||||
|
||||
[LoggingActionDecorator]o-[PolicyActionDecorator]
|
||||
[PolicyActionDecorator]o-[ActionAggregator]
|
||||
[ActionAggregator]o-[ActionProvider]
|
||||
[ActionAggregator]o-[CreateActionProvider]
|
||||
|
||||
[ActionProvider]o-[actions]
|
||||
[CreateActionProvider]o-[TypeService]
|
||||
[PolicyActionDecorator]o-[PolicyService]
|
||||
```
|
||||
|
||||
Actions are discrete tasks or behaviors that can be initiated by a user
|
||||
upon or using a domain object. Actions may appear as menu items or
|
||||
buttons in the user interface, or may be triggered by certain gestures.
|
||||
|
||||
Responsibilities of platform components of the action service are as
|
||||
follows:
|
||||
|
||||
* `ActionProvider` exposes actions registered via extension category
|
||||
`actions`, supporting simple addition of new actions. Actions are
|
||||
filtered down to match action contexts based on criteria defined as
|
||||
part of an action's extension definition.
|
||||
* `CreateActionProvider` provides the various Create actions which
|
||||
populate the Create menu. These are driven by the available types,
|
||||
so do not map easily to extension category `actions`; instead, these
|
||||
are generated after looking up which actions are available from the
|
||||
[`TypeService`](#type-service).
|
||||
* `ActionAggregator` merges together actions from multiple providers.
|
||||
* `PolicyActionDecorator` enforces the `action` policy category by
|
||||
filtering out actions which violate this policy, as determined by
|
||||
consulting the [`PolicyService`](#policy-service).
|
||||
* `LoggingActionDecorator` wraps exposed actions and writes to the
|
||||
console when they are performed.
|
||||
|
||||
## View Service
|
||||
|
||||
```nomnoml
|
||||
[ViewService|
|
||||
getViews(domainObject : DomainObject) : Array.<View>
|
||||
]
|
||||
[ViewProvider]--:>[ViewService]
|
||||
[PolicyViewDecorator]--:>[ViewService]
|
||||
|
||||
[ViewProvider]o-[views]
|
||||
[PolicyViewDecorator]o-[ViewProvider]
|
||||
```
|
||||
|
||||
The view service provides views that are relevant to a specified domain
|
||||
object. A "view" is a user-selectable visualization of a domain object.
|
||||
|
||||
The responsibilities of components of the view service are as follows:
|
||||
|
||||
* `ViewProvider` exposes views registered via extension category
|
||||
`views`, supporting simple addition of new views. Views are
|
||||
filtered down to match domain objects based on criteria defined as
|
||||
part of a view's extension definition.
|
||||
* `PolicyViewDecorator` enforces the `view` policy category by
|
||||
filtering out views which violate this policy, as determined by
|
||||
consulting the [`PolicyService`](#policy-service).
|
||||
|
||||
## Policy Service
|
||||
|
||||
```nomnoml
|
||||
[PolicyService|
|
||||
allow(category : string, candidate : object, context : object, callback? : Function) : boolean
|
||||
]
|
||||
[PolicyProvider]--:>[PolicyService]
|
||||
[PolicyProvider]o-[policies]
|
||||
```
|
||||
|
||||
The policy service provides a general-purpose extensible decision-making
|
||||
mechanism; plugins can add new extensions of category `policies` to
|
||||
modify decisions of a known category.
|
||||
|
||||
Often, the policy service is referenced from a decorator for another
|
||||
service, to filter down the results of using that service based on some
|
||||
appropriate policy category.
|
||||
|
||||
The policy provider works by looking up all registered policy extensions
|
||||
which are relevant to a particular _category_, then consulting each in
|
||||
order to see if they allow a particular _candidate_ in a particular
|
||||
_context_; the types for the `candidate` and `context` arguments will
|
||||
vary depending on the `category`. Any one policy may disallow the
|
||||
decision as a whole.
|
||||
|
||||
|
||||
```nomnoml
|
||||
[<start> Start]->[<state> is something allowed?]
|
||||
[is something allowed?]->[PolicyService]
|
||||
[PolicyService]->[<state> look up relevant policies by category]
|
||||
[look up relevant policies by category]->[<state> consult policy #1]
|
||||
[consult policy #1]->[Policy #1]
|
||||
[Policy #1]->[<choice> policy #1 allows?]
|
||||
[policy #1 allows?] no ->[<state> decision disallowed]
|
||||
[policy #1 allows?] yes ->[<state> consult policy #2]
|
||||
[consult policy #2]->[Policy #2]
|
||||
[Policy #2]->[<choice> policy #2 allows?]
|
||||
[policy #2 allows?] no ->[<state> decision disallowed]
|
||||
[policy #2 allows?] yes ->[<state> consult policy #3]
|
||||
[consult policy #3]->[<state> ...]
|
||||
[...]->[<state> consult policy #n]
|
||||
[consult policy #n]->[Policy #n]
|
||||
[Policy #n]->[<choice> policy #n allows?]
|
||||
[policy #n allows?] no ->[<state> decision disallowed]
|
||||
[policy #n allows?] yes ->[<state> decision allowed]
|
||||
[decision disallowed]->[<end> Disallowed]
|
||||
[decision allowed]->[<end> Allowed]
|
||||
```
|
||||
|
||||
The policy decision is effectively an "and" operation over the individual
|
||||
policy decisions: That is, all policies must agree to allow a particular
|
||||
policy decision, and the first policy to disallow a decision will cause
|
||||
the entire decision to be disallowed. As a consequence of this, policies
|
||||
should generally be written with a default behavior of "allow", and
|
||||
should only disallow the specific circumstances they are intended to
|
||||
disallow.
|
||||
|
||||
## Type Service
|
||||
|
||||
```nomnoml
|
||||
[TypeService|
|
||||
listTypes() : Array.<Type>
|
||||
getType(key : string) : Type
|
||||
]
|
||||
[TypeProvider]--:>[TypeService]
|
||||
[TypeProvider]o-[types]
|
||||
```
|
||||
|
||||
The type service provides metadata about the different types of domain
|
||||
objects that exist within an Open MCT application. The platform
|
||||
implementation reads these types in from extension category `types`
|
||||
and wraps them in a JavaScript interface.
|
3
docs/src/design/index.md
Normal file
3
docs/src/design/index.md
Normal file
@ -0,0 +1,3 @@
|
||||
Design proposals:
|
||||
|
||||
* [API Redesign](proposals/APIRedesign.md)
|
338
docs/src/design/planning/APIRefactor.md
Normal file
338
docs/src/design/planning/APIRefactor.md
Normal file
@ -0,0 +1,338 @@
|
||||
# API Refactoring
|
||||
|
||||
This document summarizes a path toward implementing API changes
|
||||
from the [API Redesign](../proposals/APIRedesign.md) for Open MCT
|
||||
v1.0.0.
|
||||
|
||||
# Goals
|
||||
|
||||
These plans are intended to minimize:
|
||||
|
||||
* Waste; avoid allocating effort to temporary changes.
|
||||
* Downtime; avoid making changes in large increments that blocks
|
||||
delivery of new features for substantial periods of time.
|
||||
* Risk; ensure that changes can be validated quickly, avoid putting
|
||||
large effort into changes that have not been validated.
|
||||
|
||||
# Plan
|
||||
|
||||
```nomnoml
|
||||
#comment: This diagram is in nomnoml syntax and should be rendered.
|
||||
#comment: See https://github.com/nasa/openmctweb/issues/264#issuecomment-167166471
|
||||
|
||||
|
||||
[<start> Start]->[<state> Imperative bundle registration]
|
||||
|
||||
[<state> Imperative bundle registration]->[<state> Build and packaging]
|
||||
[<state> Imperative bundle registration]->[<state> Refactor API]
|
||||
|
||||
[<state> Build and packaging |
|
||||
[<start> Start]->[<state> Incorporate a build step]
|
||||
[<state> Incorporate a build step |
|
||||
[<start> Start]->[<state> Choose package manager]
|
||||
[<start> Start]->[<state> Choose build system]
|
||||
[<state> Choose build system]<->[<state> Choose package manager]
|
||||
[<state> Choose package manager]->[<state> Implement]
|
||||
[<state> Choose build system]->[<state> Implement]
|
||||
[<state> Implement]->[<end> End]
|
||||
]->[<state> Separate repositories]
|
||||
[<state> Separate repositories]->[<end> End]
|
||||
]->[<state> Release candidacy]
|
||||
|
||||
|
||||
[<start> Start]->[<state> Design registration API]
|
||||
|
||||
[<state> Design registration API |
|
||||
[<start> Start]->[<state> Decide on role of Angular]
|
||||
[<state> Decide on role of Angular]->[<state> Design API]
|
||||
[<state> Design API]->[<choice> Passes review?]
|
||||
[<choice> Passes review?] no ->[<state> Design API]
|
||||
[<choice> Passes review?]-> yes [<end> End]
|
||||
]->[<state> Refactor API]
|
||||
|
||||
[<state> Refactor API |
|
||||
[<start> Start]->[<state> Imperative extension registration]
|
||||
[<state> Imperative extension registration]->[<state> Refactor individual extensions]
|
||||
|
||||
[<state> Refactor individual extensions |
|
||||
[<start> Start]->[<state> Prioritize]
|
||||
[<state> Prioritize]->[<choice> Sufficient value added?]
|
||||
[<choice> Sufficient value added?] no ->[<end> End]
|
||||
[<choice> Sufficient value added?] yes ->[<state> Design]
|
||||
[<state> Design]->[<choice> Passes review?]
|
||||
[<choice> Passes review?] no ->[<state> Design]
|
||||
[<choice> Passes review?]-> yes [<state> Implement]
|
||||
[<state> Implement]->[<end> End]
|
||||
]->[<state> Remove legacy bundle support]
|
||||
|
||||
[<state> Remove legacy bundle support]->[<end> End]
|
||||
]->[<state> Release candidacy]
|
||||
|
||||
[<state> Release candidacy |
|
||||
[<start> Start]->[<state> Verify |
|
||||
[<start> Start]->[<choice> API well-documented?]
|
||||
[<start> Start]->[<choice> API well-tested?]
|
||||
[<choice> API well-documented?]-> no [<state> Write documentation]
|
||||
[<choice> API well-documented?] yes ->[<end> End]
|
||||
[<state> Write documentation]->[<choice> API well-documented?]
|
||||
[<choice> API well-tested?]-> no [<state> Write test cases]
|
||||
[<choice> API well-tested?]-> yes [<end> End]
|
||||
[<state> Write test cases]->[<choice> API well-tested?]
|
||||
]
|
||||
[<start> Start]->[<state> Validate |
|
||||
[<start> Start]->[<choice> Passes review?]
|
||||
[<start> Start]->[<state> Use internally]
|
||||
[<state> Use internally]->[<choice> Proves useful?]
|
||||
[<choice> Passes review?]-> no [<state> Address feedback]
|
||||
[<state> Address feedback]->[<choice> Passes review?]
|
||||
[<choice> Passes review?] yes -> [<end> End]
|
||||
[<choice> Proves useful?] yes -> [<end> End]
|
||||
[<choice> Proves useful?] no -> [<state> Fix problems]
|
||||
[<state> Fix problems]->[<state> Use internally]
|
||||
]
|
||||
[<state> Validate]->[<end> End]
|
||||
[<state> Verify]->[<end> End]
|
||||
]->[<state> Release]
|
||||
|
||||
[<state> Release]->[<end> End]
|
||||
```
|
||||
|
||||
## Step 1. Imperative bundle registration
|
||||
|
||||
Register whole bundles imperatively, using their current format.
|
||||
|
||||
For example, in each bundle add a `bundle.js` file:
|
||||
|
||||
```js
|
||||
define([
|
||||
'mctRegistry',
|
||||
'json!bundle.json'
|
||||
], function (mctRegistry, bundle) {
|
||||
mctRegistry.install(bundle, "path/to/bundle");
|
||||
});
|
||||
```
|
||||
|
||||
Where `mctRegistry.install` is placeholder API that wires into the
|
||||
existing bundle registration mechanisms. The main point of entry
|
||||
would need to be adapted to clearly depend on these bundles
|
||||
(in the require sense of a dependency), and the framework layer
|
||||
would need to implement and integrate with this transitional
|
||||
API.
|
||||
|
||||
Benefits:
|
||||
|
||||
* Achieves an API Redesign goal with minimal immediate effort.
|
||||
* Conversion to an imperative syntax may be trivially automated.
|
||||
* Minimal change; reuse existing bundle definitions, primarily.
|
||||
* Allows early validation of switch to imperative; unforeseen
|
||||
consequences of the change may be detected at this point.
|
||||
* Allows implementation effort to progress in parallel with decisions
|
||||
about API changes, including fundamental ones such as the role of
|
||||
Angular. May act in some sense as a prototype to inform those
|
||||
decisions.
|
||||
* Creates a location (framework layer) where subsequent changes to
|
||||
the manner in which extensions are registered may be centralized.
|
||||
When there is a one-to-one correspondence between the existing
|
||||
form of an extension and its post-refactor form, adapters can be
|
||||
written here to defer the task of making changes ubiquitously
|
||||
throughout bundles, allowing for earlier validation and
|
||||
verification of those changes, and avoiding ubiquitous changes
|
||||
which might require us to go dark. (Mitigates
|
||||
["greenfield paradox"](http://stepaheadsoftware.blogspot.com/2012/09/greenfield-or-refactor-legacy-code-base.html);
|
||||
want to add value with new API but don't want to discard value
|
||||
of tested/proven legacy codebase.)
|
||||
|
||||
Detriments:
|
||||
|
||||
* Requires transitional API to be implemented/supported; this is
|
||||
waste. May mitigate this by time-bounding the effort put into
|
||||
this step to ensure that waste is minimal.
|
||||
|
||||
Note that API changes at this point do not meaningfully reflect
|
||||
the desired 1.0.0 API, so no API reviews are necessary.
|
||||
|
||||
## Step 2. Incorporate a build step
|
||||
|
||||
After the previous step is completed, there should be a
|
||||
straightforward dependency graph among AMD modules, and an
|
||||
imperative (albeit transitional) API allowing for other plugins
|
||||
to register themselves. This should allow for a build step to
|
||||
be included in a straightforward fashion.
|
||||
|
||||
Some goals for this build step:
|
||||
|
||||
* Compile (and, preferably, optimize/minify) Open MCT
|
||||
sources into a single `.js` file.
|
||||
* It is desirable to do the same for HTML sources, but
|
||||
may wish to defer this until a subsequent refactoring
|
||||
step if appropriate.
|
||||
* Provide non-code assets in a format that can be reused by
|
||||
derivative projects in a straightforward fashion.
|
||||
|
||||
Should also consider which dependency/packaging manager should
|
||||
be used by dependent projects to obtain Open MCT. Approaches
|
||||
include:
|
||||
|
||||
1. Plain `npm`. Dependents then declare their dependency with
|
||||
`npm` and utilize built sources and assets in a documented
|
||||
fashion. (Note that there are
|
||||
[documented challenges](http://blog.npmjs.org/post/101775448305/npm-and-front-end-packaging)
|
||||
in using `npm` in this fashion.)
|
||||
2. Build with `npm`, but recommend dependents install using
|
||||
`bower`, as this is intended for front-end development. This may
|
||||
require checking in built products, however, which
|
||||
we wish to avoid (this could be solved by maintaining
|
||||
a separate repository for built products.)
|
||||
|
||||
In all cases, there is a related question of which build system
|
||||
to use for asset generation/management and compilation/minification/etc.
|
||||
|
||||
1. [`webpack`](https://webpack.github.io/)
|
||||
is well-suited in principle, as it is specifically
|
||||
designed for modules with non-JS dependencies. However,
|
||||
there may be limitations and/or undesired behavior here
|
||||
(for instance, CSS dependencies get in-lined as style tags,
|
||||
removing our ability to control ordering) so it may
|
||||
2. `gulp` or `grunt`. Commonplace, but both still require
|
||||
non-trivial coding and/or configuration in order to produce
|
||||
appropriate build artifacts.
|
||||
3. [Just `npm`](http://blog.keithcirkel.co.uk/how-to-use-npm-as-a-build-tool/).
|
||||
Reduces the amount of tooling being used, but may introduce
|
||||
some complexity (e.g. custom scripts) to the build process,
|
||||
and may reduce portability.
|
||||
|
||||
## Step 3. Separate repositories
|
||||
|
||||
Refactor existing applications built on Open MCT such that they
|
||||
are no longer forks, but instead separate projects with a dependency
|
||||
on the built artifacts from Step 2.
|
||||
|
||||
Note that this is achievable already using `bower` (see `warp-bower`
|
||||
branch at http://developer.nasa.gov/mct/warp for an example.)
|
||||
However, changes involved in switching to an imperative API and
|
||||
introducing a build process may change (and should simplify) the
|
||||
approach used to utilize Open MCT as a dependency, so these
|
||||
changes should be introduced first.
|
||||
|
||||
## Step 4. Design registration API
|
||||
|
||||
Design the registration API that will replace declarative extension
|
||||
categories and extensions (including Angular built-ins and composite
|
||||
services.)
|
||||
|
||||
This may occur in parallel with implementation steps.
|
||||
|
||||
It will be necessary
|
||||
to have a decision about the role of Angular at this point; are extensions
|
||||
registered via provider configuration (Angular), or directly in some
|
||||
exposed registry?
|
||||
|
||||
Success criteria here should be based on peer review. Scope of peer
|
||||
review should be based on perceived risk/uncertainty surrounding
|
||||
proposed changes, to avoid waste; may wish to limit this review to
|
||||
the internal team. (The extent to which external
|
||||
feedback is available is limited, but there is an inherent timeliness
|
||||
to external review; need to balance this.)
|
||||
|
||||
Benefits:
|
||||
|
||||
* Solves the "general case" early, allowing for early validation.
|
||||
|
||||
Note that in specific cases, it may be desirable to refactor some
|
||||
current "extension category" in a manner that will not appear as
|
||||
registries, _or_ to locate these in different
|
||||
namespaces, _or_ to remove/replace certain categories entirely.
|
||||
This work is deferred intentionally to allow for a solution of the
|
||||
general case.
|
||||
|
||||
## Step 5. Imperative extension registration
|
||||
|
||||
Register individual extensions imperatively, implementing API changes
|
||||
from the previous step. At this stage, _usage_ of the API may be confined
|
||||
to a transitional adapter in the framework layer; bundles may continue
|
||||
to utilize the transitional API for registering extensions in the
|
||||
legacy format.
|
||||
|
||||
An important, ongoing sub-task here will be to discover and define dependencies
|
||||
among bundles. Composite services and extension categories are presently
|
||||
"implicit"; after the API redesign, these will become "explicit", insofar
|
||||
as some specific component will be responsible for creating any registries.
|
||||
As such, "bundles" which _use_ specific registries will need to have an
|
||||
enforceable dependency (e.g. require) upon those "bundles" which
|
||||
_declare_ those registries.
|
||||
|
||||
## Step 6. Refactor individual extensions
|
||||
|
||||
Refactor individual extension categories and/or services that have
|
||||
been identified as needing changes. This includes, but is not
|
||||
necessarily limited to:
|
||||
|
||||
* Views/Representations/Templates (refactored into "components.")
|
||||
* Capabilities (refactored into "roles", potentially.)
|
||||
* Telemetry (from `TelemetrySeries` to `TelemetryService`.)
|
||||
|
||||
Changes should be made one category at a time (either serially
|
||||
or separately in parallel) and should involve a tight cycle of:
|
||||
|
||||
1. Prioritization/reprioritization; highest-value API improvements
|
||||
should be done first.
|
||||
2. Design.
|
||||
3. Review. Refactoring individual extensions will require significant
|
||||
effort (likely the most significant effort in the process) so changes
|
||||
should be validated early to minimize risk/waste.
|
||||
4. Implementation. These changes will not have a one-to-one relationship
|
||||
with existing extensions, so changes cannot be centralized; usages
|
||||
will need to be updated across all "bundles" instead of centralized
|
||||
in a legacy adapter. If changes are of sufficient complexity, some
|
||||
planning should be done to spread out the changes incrementally.
|
||||
|
||||
By necessity, these changes may break functionality in applications
|
||||
built using Open MCT. On a case-by-case basis, should consider
|
||||
providing temporary "legacy support" to allow downstream updates
|
||||
to occur as a separate task; the relevant trade here is between
|
||||
waste/effort required to maintain legacy support, versus the
|
||||
downtime which may be introduced by making these changes simultaneously
|
||||
across several repositories.
|
||||
|
||||
|
||||
## Step 7. Remove legacy bundle support
|
||||
|
||||
Update bundles to remove any usages of legacy support for bundles
|
||||
(including that used by dependent projects.) Then, remove legacy
|
||||
support from Open MCT.
|
||||
|
||||
## Step 8. Release candidacy
|
||||
|
||||
Once API changes are complete, Open MCT should enter a release
|
||||
candidacy cycle. Important things to look at here:
|
||||
|
||||
* Are changes really complete?
|
||||
* Are they sufficiently documented?
|
||||
* Are they sufficiently tested?
|
||||
* Are changes really sufficient?
|
||||
* Do reviewers think they are usable?
|
||||
* Does the development team find them useful in practice? This
|
||||
will require calendar time to ascertain; should allocate time
|
||||
for this, particularly in alignment with the sprint/release
|
||||
cycle.
|
||||
* Has learning curve been measurably decreased? Comparing a to-do
|
||||
list tutorial to [other examples(http://todomvc.com/) could
|
||||
provide an empirical basis to this. How much code is required?
|
||||
How much explanation is required? How many dependencies must
|
||||
be installed before initial setup?
|
||||
* Does the API offer sufficient power to implement the extensions we
|
||||
anticipate?
|
||||
* Any open API-related issues which should block a 1.0.0 release?
|
||||
|
||||
Any problems identified during release candidacy will require
|
||||
subsequent design changes and planning.
|
||||
|
||||
## Step 9. Release
|
||||
|
||||
Once API changes have been verified and validated, proceed
|
||||
with release, including:
|
||||
|
||||
* Tagging as version 1.0.0 (at an appropriate time in the
|
||||
sprint/release cycle.)
|
||||
* Close any open issues which have been resolved (or made obsolete)
|
||||
by API changes.
|
1282
docs/src/design/proposals/APIRedesign.md
Normal file
1282
docs/src/design/proposals/APIRedesign.md
Normal file
File diff suppressed because it is too large
Load Diff
251
docs/src/design/proposals/APIRedesign_PeteRichards.md
Normal file
251
docs/src/design/proposals/APIRedesign_PeteRichards.md
Normal file
@ -0,0 +1,251 @@
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
|
||||
|
||||
- [Reducing interface depth (the bundle.json version)](#reducing-interface-depth-the-bundlejson-version)
|
||||
- [Imperitive component registries](#imperative-component-registries)
|
||||
- [Get rid of "extension category" concept.](#get-rid-of-extension-category-concept)
|
||||
- [Reduce number and depth of extension points](#reduce-number-and-depth-of-extension-points)
|
||||
- [Composite services should not be the default](#composite-services-should-not-be-the-default)
|
||||
- [Get rid of views, representations, and templates.](#get-rid-of-views-representations-and-templates)
|
||||
- [Reducing interface depth (The angular discussion)](#reducing-interface-depth-the-angular-discussion)
|
||||
- [More angular: for all services](#more-angular-for-all-services)
|
||||
- [Less angular: only for views](#less-angular-only-for-views)
|
||||
- [Standard packaging and build system](#standard-packaging-and-build-system)
|
||||
- [Use systemjs for module loading](#use-systemjs-for-module-loading)
|
||||
- [Use gulp or grunt for standard tooling](#use-gulp-or-grunt-for-standard-tooling)
|
||||
- [Package openmctweb as single versioned file.](#package-openmctweb-as-single-versioned-file)
|
||||
- [Misc Improvements](#misc-improvements)
|
||||
- [Refresh on navigation](#refresh-on-navigation)
|
||||
- [Move persistence adapter to promise rejection.](#move-persistence-adapter-to-promise-rejection)
|
||||
- [Remove bulk requests from providers](#remove-bulk-requests-from-providers)
|
||||
- [Notes on current API proposals:](#notes-on-current-api-proposals)
|
||||
- [[1] Footnote: The angular debacle](#1-footnote-the-angular-debacle)
|
||||
- ["Do or do not, there is no try"](#do-or-do-not-there-is-no-try)
|
||||
- [A lack of commitment](#a-lack-of-commitment)
|
||||
- [Commitment is good!](#commitment-is-good)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
|
||||
# Reducing interface depth (the bundle.json version)
|
||||
|
||||
## Imperative component registries
|
||||
|
||||
Transition component registries to javascript, get rid of bundle.json and bundles.json. Prescribe a method for application configuration, but allow flexibility in how application configuration is defined.
|
||||
|
||||
Register components in an imperative fashion, see angularApp.factory, angularApp.controller, etc. Alternatively, implement our own application object with new registries and it's own form of registering objects.
|
||||
|
||||
## Get rid of "extension category" concept.
|
||||
|
||||
The concept of an "extension category" is itself an extraneous concept-- an extra layer of interface depth, an extra thing to learn before you can say "hello world". Extension points should be clearly supported and documented with whatever interfaces make sense. Developers who wish to add something that is conceptually equivalent to an extension category can do so directly, in the manner that suites their needs, without us forcing a common method on them.
|
||||
|
||||
## Reduce number and depth of extension points
|
||||
|
||||
Clearly specify supported extension points (e.g. persistence, model providers, telemetry providers, routes, time systems), but don't claim that the system has a clear and perfect repeatable solution for unknown extension types. New extension categories can be implemented in whatever way makes sense, without prescribing "the one and only system for managing extensions".
|
||||
|
||||
The underlying problem here is we are predicting needs for extension points where none exist-- if we try and design the extension system before we know how it is used, we design the wrong thing and have to rewrite it later.
|
||||
|
||||
## Composite services should not be the default
|
||||
|
||||
Understanding composite services, and describing services as composite services can confuse developers. Aggregators are implemented once and forgotten, while decorators tend to be hacky, brittle solutions that are generally needed to avoid circular imports. While composite services are a useful construct, it reduces interface depth to implement them as registries + typed providers.
|
||||
|
||||
You can write a provider (provides "thing x" for "inputs y") with a simple interface. A provider has two or more methods:
|
||||
* a method which takes "inputs y" and returns True if it knows how to provide "thing x", false otherwise.
|
||||
* one or more methods which provide "thing x" for objects of "inputs y".
|
||||
|
||||
Actually checking whether a provider can respond to a request before asking it to do work allows for faster failure and clearer errors when no providers match the request.
|
||||
|
||||
## Get rid of views, representations, and templates.
|
||||
|
||||
Templates are an implementation detail that should be handled by module loaders. Views and representations become "components," and a new concept, "routes", is used to exposing specific views to end users.
|
||||
|
||||
`components` - building blocks for views, have clear inputs and outputs, and can be coupled to other components when it makes sense. (e.g. parent-child components such as menu and menu item), but should have ZERO knowledge of our data models or telemetry apis. They should define data models that enable them to do their job well while still being easy to test.
|
||||
|
||||
`routes` - a view type for a given domain object, e.g. a plot, table, display layout, etc. Can be described as "whatever shows in the main screen when you are viewing an object." Handle loading of data from a domain object and passing that data to the view components. Routes should support editing as it makes sense in their own context.
|
||||
|
||||
To facilitate testing:
|
||||
|
||||
* routes should be testable without having to test the actual view.
|
||||
* components should be independently testable with zero knowledge of our data models or telemetry APIs.
|
||||
|
||||
Component code should be organized side by side, such as:
|
||||
|
||||
```
|
||||
app
|
||||
|- components
|
||||
|- productDetail
|
||||
| |- productDetail.js
|
||||
| |- productDetail.css
|
||||
| |- productDetail.html
|
||||
| |- productDetailSpec.js
|
||||
|- productList
|
||||
|- checkout
|
||||
|- wishlist
|
||||
```
|
||||
|
||||
Components are not always reusable, and we shouldn't be overly concerned with making them so. If components are heavily reused, they should either be moved to a platform feature (e.g. notifications, indicators), or broken off as an external dependency (e.g. publish mct-plot as mct-plot.js).
|
||||
|
||||
|
||||
# Reducing interface depth (The angular discussion)
|
||||
|
||||
Two options here: use more angular, use less angular. Wrapping angular methods does not reduce interface depth and must be avoided.
|
||||
|
||||
The primary issue with angular is duplications of concerns-- both angular and the openmctweb platform implement the same tools side by side and it can be hard to comprehend-- it increases interface depth. For other concerns, see footnotes[1].
|
||||
|
||||
Wrapping angular methods for non-view related code is confusing to developers because of the random constraints angular places on these items-- developers ultimately have to understand both angular DI and our framework. For example, it's not possible to name the topic service "topicService" because angular expects Services to be implemented by Providers, which is different than our expectation.
|
||||
|
||||
To reduce interface depth, we can replace our own provider and registry patterns with angular patterns, or we can only utilize angular view logic, and only use our own DI patterns.
|
||||
|
||||
## More angular: for all services
|
||||
|
||||
Increasing our commitment to angular would mean using more of the angular factories, services, etc, and less of our home grown tools. We'd implement our services and extension points as angular providers, and make them configurable via app.config.
|
||||
|
||||
As an example, registering a specific type of model provider in angular would look like:
|
||||
|
||||
```javascript
|
||||
mct.provider('model', modelProvider() { /* implementation */});
|
||||
|
||||
mct.config(['modelProvider', function (modelProvider) {
|
||||
modelProvider.providers.push(RootModelProvider);
|
||||
}]);
|
||||
```
|
||||
|
||||
## Less angular: only for views
|
||||
|
||||
If we wish to use less angular, I would recommend discontinuing use of all angular components that are not view related-- services, factories, $http, etc, and implementing them in our own paradigm. Otherwise, we end up with layered interfaces-- one of the goals we would like to avoid.
|
||||
|
||||
|
||||
# Standard packaging and build system
|
||||
|
||||
Standardize the packaging and build system, and completely separate the core platform from deployments. Prescribe a starting point for deployments, but allow flexibility.
|
||||
|
||||
## Use systemjs for module loading
|
||||
|
||||
Allow developers to use whatever module loading system they'd like to use, while still supporting all standard cases. We should also use this system for loading assets (css, scss, html templates), which makes it easier to implement a single file deployment using standard build tooling.
|
||||
|
||||
## Use gulp or grunt for standard tooling
|
||||
|
||||
Using gulp or grunt as a task runner would bring us in line with standard web developer workflows and help standardize rendering, deployment, and packaging. Additional tools can be added to the workflow at low cost, simplifying the setup of developer environments.
|
||||
|
||||
Gulp and grunt provide useful developer tooling such as live reload, automatic scss/less/etc compilation, and ease of extensibility for standard production build processes. They're key in decoupling code.
|
||||
|
||||
## Package openmctweb as single versioned file.
|
||||
|
||||
Deployments should depend on a specific version of openmctweb, but otherwise be allowed to have their own deployment and development toolsets.
|
||||
|
||||
Customizations and deployments of openmctweb should not use the same build tooling as the core platform; instead they should be free to use their own build tools as they wish. (We would provide a template for an application, based on our experience with warp-for-rp and vista)
|
||||
|
||||
Installation and utilization of openmctweb should be as simple as downloading the js file, including it in your own html page, and then initializing an app and running it. If a developer would prefer, they could use bower or npm to handle installation.
|
||||
|
||||
Then, if we're using imperative methods for extending the application we can use the following for basic customization:
|
||||
|
||||
```html
|
||||
<script src="//localhost/openmctweb.js"></script>
|
||||
<script>
|
||||
// can configure from object
|
||||
var myApp = new OpenMCTWeb({
|
||||
persitence: {
|
||||
providers: [
|
||||
{
|
||||
type: 'elastic',
|
||||
uri: 'http://someElasticHost/'
|
||||
} // ...
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// alternative configurations
|
||||
myApp.persistence.addProvider(MyPersistenceAdapter);
|
||||
myApp.model.addProvider(someProviderObject);
|
||||
|
||||
// Removing via method
|
||||
myApp.persistence.removeProvider('some method for removing functionality');
|
||||
// directly mutating providers
|
||||
myApp.persistence.providers = [ThisProviderStandsAlone];
|
||||
//
|
||||
myApp.run();
|
||||
</script>
|
||||
```
|
||||
|
||||
This packaging reduces the complexity of managing multiple deployed versions, and also allows us to provide users with incredibly simple tutorials-- they can use whatever tooling they like. For instance, a hello world tutorial may take the option of "exposing a new object in the tree".
|
||||
|
||||
```javascript
|
||||
var myApp = new OpenMCTWeb();
|
||||
myApp.roots.addRoot({
|
||||
id: 'myRoot',
|
||||
name: 'Hello World!',
|
||||
});
|
||||
myApp.routes.setDefault('myRoot');
|
||||
myApp.run();
|
||||
```
|
||||
|
||||
# Misc Improvements
|
||||
|
||||
## Refresh on navigation
|
||||
In cases where navigation events change the entire screen, we should be using routes and location changes to navigate between objects. We should be using href for all navigation events.
|
||||
|
||||
At the same time, navigating should refresh state of every visible object. A properly configured persistence store will handle caching with standard cache headers and 304 not modified responses, which will provide good performance of object reloads, while helping us ensure that objects are always in sync between clients.
|
||||
|
||||
View state (say, the expanded tree nodes) should not be tied to caching of data-- it should be something we intentionally persist and restore with each navigation. Data (such as object definitions) should be reloaded from server as necessary to restore state.
|
||||
|
||||
## Move persistence adapter to promise rejection.
|
||||
Simple: reject on fail, resolve on success.
|
||||
|
||||
## Remove bulk requests from providers
|
||||
|
||||
Aggregators can request multiple things at once, but individual providers should only have to implement handling at the level of a single request. Each provider can implement it's own internal batching, but it should support making requests at a finer level of detail.
|
||||
|
||||
Excessive wrapping of code with $q.all causes additional digest cycles and decreased performance.
|
||||
|
||||
For example, instead of every telemetry provider responding to a given telemetry request, aggregators should route each request to the first provider that can fulfill that request.
|
||||
|
||||
|
||||
# Notes on current API proposals:
|
||||
|
||||
* [RequireJS for Dependency Injection](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#requirejs-as-dependency-injector): requires other topics to be discussed first.
|
||||
* [Arbitrary HTML Views](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#arbitrary-html-views): think there is a place for it, requires other topics to be discussed first.
|
||||
* [Wrap Angular Services](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#wrap-angular-services): No, this is bad.
|
||||
* [Bundle definitions in Javascript](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#bundle-declarations-in-javascript): Points to a solution, but ultimately requires more discussion.
|
||||
* [pass around a dependency injector](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#pass-around-a-dependency-injector): No.
|
||||
* [remove partial constructors](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#remove-partial-constructors): Yes, this should be superseded by another proposal though. The entire concept was a messy solution to dependency injection issues caused by declarative syntax.
|
||||
* [Rename views to applications](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#rename-views-to-applications): Points to a problem that needs to be solved but I think the name is bad.
|
||||
* [Provide classes for extensions](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#provide-classes-for-extensions): Yes, in specific places
|
||||
* [Normalize naming conventions](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#normalize-naming-conventions): Yes.
|
||||
* [Expose no third-party APIs](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#expose-no-third-party-apis): Completely disagree, points to a real problem with poor angular integration.
|
||||
* [Register Extensions as Instances instead of Constructors](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#register-extensions-as-instances-instead-of-constructors): Superseded by the fact that we should not hope to implement a generic construct.
|
||||
* [Remove capability delegation](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#remove-capability-delegation): Yes.
|
||||
* [Nomenclature Change](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#nomenclature-change): Yes, hope to discuss the implications of this more clearly in other proposals.
|
||||
* [Capabilities as mixins](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#capabilities-as-mixins): Yes.
|
||||
* [Remove appliesTo methods](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#remove-applies-to-methods): No-- I think some level of this is necessary. I think a more holistic approach to policy is needed. it's a rather complicated system.
|
||||
* [Revise telemetry API](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#revise-telemetry-api): If we can rough out and agree to the specifics, then Yes. Needs discussion.
|
||||
* [Allow composite services to fail gracefully](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#allow-composite-services-to-fail-gracefully): No. As mentioned above, I think composite services themselves should be eliminated for a more purpose bound tool.
|
||||
* [Plugins as angular modules](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#plugins-as-angular-modules): Should we decide to embrace Angular completely, I would support this. Otherwise, no.
|
||||
* [Contextual Injection](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#contextual-injection): No, don't see a need.
|
||||
* [Add New Abstractions for Actions](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#add-new-abstractions-for-actions): Worth a discussion.
|
||||
* [Add gesture handlers](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#add-gesture-handlers): Yes if we can agree on details. We need a platform implementation that is easy to use, but we should not reinvent the wheel.
|
||||
|
||||
|
||||
|
||||
# [1] Footnote: The angular debacle
|
||||
|
||||
## "Do or do not, there is no try"
|
||||
|
||||
A commonly voiced concern of embracing angular is the possibility of becoming dependent on a third party framework. This concern is itself detrimental-- if we're afraid of becoming dependent on a third party framework, then we will do a bad job of using the framework, and inevitably will want to stop using it.
|
||||
|
||||
If we're using a framework, we need to use it fully, or not use it at all.
|
||||
|
||||
## A lack of commitment
|
||||
|
||||
A number of the concerns we heard from developers and interns can be attributed to the tenuous relationship between the OpenMCTWeb platform and angular. We claimed to be angular, but we weren't really angular. Instead, we are caught between our incomplete framework paradigm and the angular paradigm. In many cases we reinvented the wheel or worked around functionality that angular provides, and ended up in a more confusing state.
|
||||
|
||||
## Commitment is good!
|
||||
|
||||
We could just be an application that is built with angular.
|
||||
|
||||
An application that is modular and extensible not because it reinvents tools for providing modularity and extensibility, but because it reuses existing tools for modularity and extensibility.
|
||||
|
||||
There are benefits to buying into the angular paradigm: shift documentation burden to external project, engage a larger talent pool available both as voluntary open source contributors and as experienced developers for hire, and gain access to an ecosystem of tools that we can use to increase the speed of development.
|
||||
|
||||
There are negatives too: Angular is a monolith, it has performance concerns, and an unclear future. If we can't live with it, we should look at alternatives.
|
||||
|
164
docs/src/design/proposals/ImperativePlugins.md
Normal file
164
docs/src/design/proposals/ImperativePlugins.md
Normal file
@ -0,0 +1,164 @@
|
||||
# Imperative Plugins
|
||||
|
||||
This is a design proposal for handling
|
||||
[bundle declarations in JavaScript](
|
||||
APIRedesign.md#bundle-declarations-in-javascript).
|
||||
|
||||
## Developer Use Cases
|
||||
|
||||
Developers will want to use bundles/plugins to (in rough order
|
||||
of occurrence):
|
||||
|
||||
1. Add new extension instances.
|
||||
2. Use existing services
|
||||
3. Add new service implementations.
|
||||
4. Decorate service implementations.
|
||||
5. Decorate extension instances.
|
||||
6. Add new types of services.
|
||||
7. Add new extension categories.
|
||||
|
||||
Notably, bullets 4 and 5 above are currently handled implicitly,
|
||||
which has been cited as a source of confusion.
|
||||
|
||||
## Interfaces
|
||||
|
||||
Two base classes may be used to satisfy these use cases:
|
||||
|
||||
* The `CompositeServiceFactory` provides composite service instances.
|
||||
Decorators may be added; the approach used for compositing may be
|
||||
modified; and individual services may be registered to support compositing.
|
||||
* The `ExtensionRegistry` allows for the simpler case where what is desired
|
||||
is an array of all instances of some kind of thing within the system.
|
||||
|
||||
Note that additional developer use cases may be supported by using the
|
||||
more general-purpose `Registry`
|
||||
|
||||
```nomnoml
|
||||
[Factory.<T, V>
|
||||
|
|
||||
- factoryFn : function (V) : T
|
||||
|
|
||||
+ decorate(decoratorFn : function (T, V) : T, options? : RegistrationOptions)
|
||||
]-:>[function (V) : T]
|
||||
|
||||
[RegistrationOptions |
|
||||
+ priority : number or string
|
||||
]
|
||||
|
||||
[Registry.<T, V>
|
||||
|
|
||||
- compositorFn : function (Array.<T>) : V
|
||||
|
|
||||
+ register(item : T, options? : RegistrationOptions)
|
||||
+ composite(compositorFn : function (Array.<T>) : V, options? : RegistrationOptions)
|
||||
]-:>[Factory.<V, Void>]
|
||||
[Factory.<V, Void>]-:>[Factory.<T, V>]
|
||||
|
||||
[ExtensionRegistry.<T>]-:>[Registry.<T, Array.<T>>]
|
||||
[Registry.<T, Array.<T>>]-:>[Registry.<T, V>]
|
||||
|
||||
[CompositeServiceFactory.<T>]-:>[Registry.<T, T>]
|
||||
[Registry.<T, T>]-:>[Registry.<T, V>]
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### 1. Add new extension instances.
|
||||
|
||||
```js
|
||||
// Instance-style registration
|
||||
mct.types.register(new mct.Type({
|
||||
key: "timeline",
|
||||
name: "Timeline",
|
||||
description: "A container for activities ordered in time."
|
||||
});
|
||||
|
||||
// Factory-style registration
|
||||
mct.actions.register(function (domainObject) {
|
||||
return new RemoveAction(domainObject);
|
||||
}, { priority: 200 });
|
||||
```
|
||||
|
||||
### 2. Use existing services
|
||||
|
||||
```js
|
||||
mct.actions.register(function (domainObject) {
|
||||
var dialogService = mct.ui.dialogServiceFactory();
|
||||
return new PropertiesAction(dialogService, domainObject);
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Add new service implementations
|
||||
|
||||
```js
|
||||
// Instance-style registration
|
||||
mct.persistenceServiceFactory.register(new LocalPersistenceService());
|
||||
|
||||
// Factory-style registration
|
||||
mct.persistenceServiceFactory.register(function () {
|
||||
var $http = angular.injector(['ng']).get('$http');
|
||||
return new LocalPersistenceService($http);
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Decorate service implementations
|
||||
|
||||
```js
|
||||
mct.modelServiceFactory.decorate(function (modelService) {
|
||||
return new CachingModelDecorator(modelService);
|
||||
}, { priority: 100 });
|
||||
```
|
||||
|
||||
### 5. Decorate extension instances
|
||||
|
||||
```js
|
||||
mct.capabilities.decorate(function (capabilities) {
|
||||
return capabilities.map(decorateIfApplicable);
|
||||
});
|
||||
```
|
||||
|
||||
This use case is not well-supported by these API changes. The most
|
||||
common case for decoration is capabilities, which are under reconsideration;
|
||||
should consider handling decoration of capabilities in a different way.
|
||||
|
||||
### 6. Add new types of services
|
||||
|
||||
```js
|
||||
myModule.myServiceFactory = new mct.CompositeServiceFactory();
|
||||
|
||||
// In cases where a custom composition strategy is desired
|
||||
myModule.myServiceFactory.composite(function (services) {
|
||||
return new MyServiceCompositor(services);
|
||||
});
|
||||
```
|
||||
|
||||
### 7. Add new extension categories.
|
||||
|
||||
```js
|
||||
myModule.hamburgers = new mct.ExtensionRegistry();
|
||||
```
|
||||
|
||||
## Evaluation
|
||||
|
||||
### Benefits
|
||||
|
||||
* Encourages separation of registration from declaration (individual
|
||||
components are decoupled from the manner in which they are added
|
||||
to the architecture.)
|
||||
* Minimizes "magic." Dependencies are acquired, managed, and exposed
|
||||
using plain-old-JavaScript without any dependency injector present
|
||||
to obfuscate what is happening.
|
||||
* Offers comparable expressive power to existing APIs; can still
|
||||
extend the behavior of platform components in a variety of ways.
|
||||
* Does not force or limit formalisms to use;
|
||||
|
||||
### Detriments
|
||||
|
||||
* Does not encourage separation of dependency acquisition from
|
||||
declaration; that is, it would be quite natural using this API
|
||||
to acquire references to services during the constructor call
|
||||
to an extension or service. But, passing these in as constructor
|
||||
arguments is preferred (to separate implementation from architecture.)
|
||||
* Adds (negligible?) boilerplate relative to declarative syntax.
|
||||
* Relies on factories, increasing number of interfaces to be concerned
|
||||
with.
|
138
docs/src/design/proposals/Roles.md
Normal file
138
docs/src/design/proposals/Roles.md
Normal file
@ -0,0 +1,138 @@
|
||||
# Roles
|
||||
|
||||
Roles are presented as an alternative formulation to capabilities
|
||||
(dynamic behavior associated with individual domain objects.)
|
||||
|
||||
Specific goals here:
|
||||
|
||||
* Dependencies of individual scripts should be clear.
|
||||
* Domain objects should be able to selectively exhibit a wide
|
||||
variety of behaviors.
|
||||
|
||||
## Developer Use Cases
|
||||
|
||||
1. Checking for the existence of behavior.
|
||||
2. Using behavior.
|
||||
3. Augmenting existing behaviors.
|
||||
4. Overriding existing behaviors.
|
||||
5. Adding new behaviors.
|
||||
|
||||
## Overview of Proposed Solution
|
||||
|
||||
Remove `getCapability` from domain objects; add roles as external
|
||||
services which can be applied to domain objects.
|
||||
|
||||
## Interfaces
|
||||
|
||||
```nomnoml
|
||||
[Factory.<T, V>
|
||||
|
|
||||
- factoryFn : function (V) : T
|
||||
|
|
||||
+ decorate(decoratorFn : function (T, V) : T, options? : RegistrationOptions)
|
||||
]-:>[function (V) : T]
|
||||
|
||||
[RegistrationOptions |
|
||||
+ priority : number or string
|
||||
]<:-[RoleOptions |
|
||||
+ validate : function (DomainObject) : boolean
|
||||
]
|
||||
|
||||
[Role.<T> |
|
||||
+ validate(domainObject : DomainObject) : boolean
|
||||
+ decorate(decoratorFn : function (T, V) : T, options? : RoleOptions)
|
||||
]-:>[Factory.<T, DomainObject>]
|
||||
[Factory.<T, DomainObject>]-:>[Factory.<T, V>]
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### 1. Checking for the existence of behavior
|
||||
|
||||
```js
|
||||
function PlotViewPolicy(telemetryRole) {
|
||||
this.telemetryRole = telemetryRole;
|
||||
}
|
||||
PlotViewPolicy.prototype.allow = function (view, domainObject) {
|
||||
return this.telemetryRole.validate(domainObject);
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Using behavior
|
||||
|
||||
```js
|
||||
PropertiesAction.prototype.perform = function () {
|
||||
var mutation = this.mutationRole(this.domainObject);
|
||||
return this.showDialog.then(function (newModel) {
|
||||
return mutation.mutate(function () {
|
||||
return newModel;
|
||||
});
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Augmenting existing behaviors
|
||||
|
||||
```js
|
||||
// Non-Angular style
|
||||
mct.roles.persistenceRole.decorate(function (persistence) {
|
||||
return new DecoratedPersistence(persistence);
|
||||
});
|
||||
|
||||
// Angular style
|
||||
myModule.decorate('persistenceRole', ['$delegate', function ($delegate) {
|
||||
return new DecoratedPersistence(persistence);
|
||||
}]);
|
||||
```
|
||||
|
||||
### 4. Overriding existing behaviors
|
||||
|
||||
```js
|
||||
// Non-Angular style
|
||||
mct.roles.persistenceRole.decorate(function (persistence, domainObject) {
|
||||
return domainObject.getModel().type === 'someType' ?
|
||||
new DifferentPersistence(domainObject) :
|
||||
persistence;
|
||||
}, {
|
||||
validate: function (domainObject, next) {
|
||||
return domainObject.getModel().type === 'someType' || next();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 5. Adding new behaviors
|
||||
|
||||
```js
|
||||
function FooRole() {
|
||||
mct.Role.apply(this, [function (domainObject) {
|
||||
return new Foo(domainObject);
|
||||
}]);
|
||||
}
|
||||
|
||||
FooRole.prototype = Object.create(mct.Role.prototype);
|
||||
|
||||
FooRole.prototype.validate = function (domainObject) {
|
||||
return domainObject.getModel().type === 'some-type';
|
||||
};
|
||||
|
||||
//
|
||||
myModule.roles.fooRole = new FooRole();
|
||||
```
|
||||
|
||||
|
||||
## Evaluation
|
||||
|
||||
### Benefits
|
||||
|
||||
* Simplifies/standardizes augmentation or replacement of behavior associated
|
||||
with specific domain objects.
|
||||
* Minimizes number of abstractions; roles are just factories.
|
||||
* Clarifies dependencies; roles used must be declared/acquired in the
|
||||
same manner as services.
|
||||
|
||||
### Detriments
|
||||
|
||||
* Externalizes functionality which is conceptually associated with a
|
||||
domain object.
|
||||
* Relies on factories, increasing number of interfaces to be concerned
|
||||
with.
|
2456
docs/src/guide/index.md
Normal file
2456
docs/src/guide/index.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -14,13 +14,24 @@
|
||||
extend the platform are provided in the following documentation.
|
||||
|
||||
## Sections
|
||||
|
||||
* The [API](api/) uses inline documentation.
|
||||
using [TypeScript](https://www.typescriptlang.org) and some legacy [JSDoc](https://jsdoc.app/). It describes the JavaScript objects and
|
||||
|
||||
* The [API](api/) document is generated from inline documentation
|
||||
using [JSDoc](http://usejsdoc.org/), and describes the JavaScript objects and
|
||||
functions that make up the software platform.
|
||||
|
||||
* The [Development Process](process/) document describes the
|
||||
* The [Development Process](process/) document describes the
|
||||
Open MCT software development cycle.
|
||||
|
||||
* The [tutorial](https://github.com/nasa/openmct-tutorial) and [plugin template](https://github.com/nasa/openmct-hello) give examples of extending the platform to add
|
||||
functionality and integrate with data sources.
|
||||
## Legacy Documentation
|
||||
|
||||
As we transition to a new API, the following documentation for the old API
|
||||
(which is supported during the transtion) may be useful as well:
|
||||
|
||||
* The [Architecture Overview](architecture/) describes the concepts used
|
||||
throughout Open MCT, and gives a high level overview of the platform's design.
|
||||
|
||||
* The [Developer's Guide](guide/) goes into more detail about how to use the
|
||||
platform and the functionality that it provides.
|
||||
|
||||
* The [Tutorials](https://github.com/nasa/openmct-tutorial) give examples of extending the platform to add
|
||||
functionality, and integrate with data sources.
|
||||
|
@ -73,11 +73,11 @@ acceptance testing (e.g. by resolving any blockers found); any
|
||||
resources not needed for this effort should be used to begin work
|
||||
for the subsequent sprint.
|
||||
|
||||
| Week | Mon | Tue | Wed | Thu | Fri |
|
||||
|:-----:|:-------------------------:|:------:|:---:|:----------------------------:|:-------------------------------------:|
|
||||
| __1__ | Sprint plan | Tag-up | | | |
|
||||
| __2__ | | Tag-up | | | Code freeze and sprint branch |
|
||||
| __3__ | Per-sprint testing | Triage | | _Per-sprint testing*_ | Ship and merge sprint branch to master|
|
||||
| Week | Mon | Tue | Wed | Thu | Fri |
|
||||
|:-----:|:-------------------------:|:------:|:---:|:----------------------------:|:-----------:|
|
||||
| __1__ | Sprint plan | Tag-up | | | |
|
||||
| __2__ | | Tag-up | | | Code freeze |
|
||||
| __3__ | Per-sprint testing | Triage | | _Per-sprint testing*_ | Ship |
|
||||
|
||||
* If necessary.
|
||||
|
||||
@ -105,20 +105,14 @@ emphasis on testing.
|
||||
that team may begin work for that sprint during the
|
||||
third week, since testing and blocker resolution is unlikely
|
||||
to require all available resources.
|
||||
* Testing success criteria identified per issue (where necessary). This could be in the form of acceptance tests on the issue or detailing performance tests, for example.
|
||||
* __Tag-up.__ Check in and status update among development team.
|
||||
May amend plan for sprint as-needed.
|
||||
* __Code freeze.__ Any new work from this sprint
|
||||
(features, bug fixes, enhancements) must be integrated by the
|
||||
end of the second week of the sprint. After code freeze, a sprint
|
||||
branch will be created (and until the end of the sprint) the only
|
||||
changes that should be merged into the sprint branch should
|
||||
directly address issues needed to pass acceptance testing.
|
||||
During this time, any other feature development will continue to
|
||||
be merged into the master branch for the next sprint.
|
||||
* __Sprint branch merge to master.__ After acceptance testing, the sprint branch
|
||||
will be merged back to the master branch. Any code conflicts that
|
||||
arise will be resolved by the team.
|
||||
end of the second week of the sprint. After code freeze
|
||||
(and until the end of the sprint) the only changes that should be
|
||||
merged into the master branch should directly address issues
|
||||
needed to pass acceptance testing.
|
||||
* [__Per-release Testing.__](testing/plan.md#per-release-testing)
|
||||
Structured testing with predefined
|
||||
success criteria. No release should ship without passing
|
||||
@ -132,8 +126,8 @@ emphasis on testing.
|
||||
* [__Testathon.__](testing/plan.md#user-testing)
|
||||
Multi-user testing, involving as many users as
|
||||
is feasible, plus development team. Open-ended; should verify
|
||||
completed work from this sprint using the sprint branch, test
|
||||
exploratory for regressions, et cetera.
|
||||
completed work from this sprint, test exploratorily for
|
||||
regressions, et cetera.
|
||||
* [__Long-Duration Test.__](testing/plan.md#long-duration-testing) A
|
||||
test to verify that the software remains
|
||||
stable after running for longer durations. May include some
|
||||
@ -149,7 +143,7 @@ emphasis on testing.
|
||||
Subset of Pre-release Testing
|
||||
which should be performed before shipping at the end of any
|
||||
sprint. Time is allocated for a second round of
|
||||
Pre-release Testing if the first round is not passed. Smoke tests collected from issues/PRs
|
||||
Pre-release Testing if the first round is not passed.
|
||||
* __Triage.__ Team reviews issues from acceptance testing and uses
|
||||
success criteria to determine whether or not they should block
|
||||
release, then formulates a plan to address these issues before
|
||||
|
@ -7,5 +7,9 @@ documents:
|
||||
process points are repeated during development.
|
||||
* The [Version Guide](version.md) describes version numbering for
|
||||
Open MCT (both semantics and process.)
|
||||
* The [Test Plan](testing/plan.md) summarizes the approaches used
|
||||
to test Open MCT.
|
||||
* Testing is described in two documents:
|
||||
* The [Test Plan](testing/plan.md) summarizes the approaches used
|
||||
to test Open MCT.
|
||||
* The [Test Procedures](testing/procedures.md) document what
|
||||
specific tests are performed to verify correctness, and how
|
||||
they should be carried out.
|
||||
|
@ -1,30 +0,0 @@
|
||||
|
||||
# Release of NASA Open MCT NPM Package
|
||||
|
||||
This document outlines the process and key considerations for releasing a new version of the NASA Open MCT project as an NPM (Node Package Manager) package.
|
||||
|
||||
## 1. Pre-requisites
|
||||
|
||||
Before releasing a new version of the NASA Open MCT NPM package, ensure all dependencies are updated, and comprehensive tests are performed. This ensures compatibility and performance of the Open MCT within the Node.js ecosystem.
|
||||
|
||||
## 2. Versioning
|
||||
|
||||
Versioning is a critical step for package release. The Open MCT team follows [Semantic Versioning (SemVer)](https://semver.org) that consists of three major components: MAJOR.MINOR.PATCH. These ensure a structured process for updating, bug fixes, backward compatibility, and software progress.
|
||||
|
||||
## 3. Changelog Maintenance
|
||||
|
||||
A comprehensive changelog file, `CHANGELOG.md`, documents any changes, adding a high level of transparencies for anyone desiring to look into the status of new and past progress. It includes the summation of any major new enhancements, changes, bug fixes, and the credits to the users responsible for each unique progress.
|
||||
|
||||
## 4. Notable Changes Labels on GitHub PRs
|
||||
|
||||
For the Open MCT package, we leverage GitHub's Pull Request (PR) mechanisms extensively, with three important PR labels dedicated to signifying 'notable_changes':
|
||||
|
||||
- **Breaking Change** Highlights the integration of changes that are suspected to break, or without a doubt will break, backward compatibility. These should signal to users the upgrade might be seamless only if dependency and integration factors are properly managed, if not, one should expect to manage atypical technical snags.
|
||||
- **API change** Signifies when a contribution makes any complete or under layer changes to the communication or its supporting access processes. This label flags required see-through insight on how the web-based control panel sees and manipulates any value and or network logs.
|
||||
- **Default Behavior Change:** In the incident an update either adjusts a form to or integrates a not previously kept setting or plugin. i.e. autoscale is enabled by default when working with plots.
|
||||
|
||||
## 6. Community & Contributions
|
||||
|
||||
A flat community and the rounded center are kept in continuous celebration, with the given station open for two open-specifying dialogues, research, and all-for development probing. State the ownership for a handed looped, a welcome for even structure-core and architectural draft and impend.
|
||||
|
||||
Thank you for your collaboration and commitment to moving the project onto a text big club.
|
@ -19,7 +19,7 @@ Testing for Open MCT includes:
|
||||
|
||||
Manual, non-rigorous testing of the software and/or specific features
|
||||
of interest. Verifies that the software runs and that basic functionality
|
||||
is present. The outcome of Smoke Testing should be a simplified list of Acceptance Tests which could be executed by another team member with sufficient context.
|
||||
is present.
|
||||
|
||||
### Unit Testing
|
||||
|
||||
@ -49,7 +49,7 @@ User testing will focus on the following activities:
|
||||
* General "trying to break things."
|
||||
|
||||
During user testing, users will
|
||||
[report issues](https://github.com/nasa/openmct/issues/new/choose)
|
||||
[report issues](https://github.com/nasa/openmctweb/blob/master/CONTRIBUTING.md#issue-reporting)
|
||||
as they are encountered.
|
||||
|
||||
Desired outcomes of user testing are:
|
||||
@ -71,7 +71,7 @@ usage. After twenty-four hours, the software is evaluated for:
|
||||
at the start of the test? Is it as responsive?
|
||||
|
||||
Any defects or unexpected behavior identified during testing should be
|
||||
[reported as issues](https://github.com/nasa/openmct/issues/new/choose)
|
||||
[reported as issues](https://github.com/nasa/openmctweb/blob/master/CONTRIBUTING.md#issue-reporting)
|
||||
and reviewed for severity.
|
||||
|
||||
## Test Performance
|
||||
|
169
docs/src/process/testing/procedures.md
Normal file
169
docs/src/process/testing/procedures.md
Normal file
@ -0,0 +1,169 @@
|
||||
# Test Procedures
|
||||
|
||||
## Introduction
|
||||
|
||||
This document is intended to be used:
|
||||
|
||||
* By testers, to verify that Open MCT behaves as specified.
|
||||
* By the development team, to document new test cases and to provide
|
||||
guidance on how to author these.
|
||||
|
||||
## Writing Procedures
|
||||
|
||||
### Template
|
||||
|
||||
Procedures for individual tests should use the following template,
|
||||
adapted from [https://swehb.nasa.gov/display/7150/SWE-114]().
|
||||
|
||||
Property | Value
|
||||
---------------|---------------------------------------------------------------
|
||||
Test ID |
|
||||
Relevant reqs. |
|
||||
Prerequisites |
|
||||
Test input |
|
||||
Instructions |
|
||||
Expectation |
|
||||
Eval. criteria |
|
||||
|
||||
For multi-line descriptions, use an asterisk or similar indicator to refer
|
||||
to a longer-form description below.
|
||||
|
||||
#### Example Procedure - Edit a Layout
|
||||
|
||||
Property | Value
|
||||
---------------|---------------------------------------------------------------
|
||||
Test ID | MCT-TEST-000X - Edit a layout
|
||||
Relevant reqs. | MCT-EDIT-000Y
|
||||
Prerequisites | Create a layout, as in MCT-TEST-000Z
|
||||
Test input | Domain object database XYZ
|
||||
Instructions | See below *
|
||||
Expectation | Change to editing context †
|
||||
Eval. criteria | Visual inspection
|
||||
|
||||
* Follow the following steps:
|
||||
|
||||
1. Verify that the created layout is currently navigated-to,
|
||||
as in MCT-TEST-00ZZ.
|
||||
2. Click the Edit button, identified by a pencil icon and the text "Edit"
|
||||
displayed on hover.
|
||||
|
||||
† Right-hand viewing area should be surrounded by a dashed
|
||||
blue border when a domain object is being edited.
|
||||
|
||||
### Guidelines
|
||||
|
||||
Test procedures should be written assuming minimal prior knowledge of the
|
||||
application: Non-standard terms should only be used when they are documented
|
||||
in [the glossary](#glossary), and shorthands used for user actions should
|
||||
be accompanied by useful references to test procedures describing those
|
||||
actions (when available) or descriptions in user documentation.
|
||||
|
||||
Test cases should be narrow in scope; if a list of steps is excessively
|
||||
long (or must be written vaguely to be kept short) it should be broken
|
||||
down into multiple tests which reference one another.
|
||||
|
||||
All requirements satisfied by Open MCT should be verifiable using
|
||||
one or more test procedures.
|
||||
|
||||
## Glossary
|
||||
|
||||
This section will contain terms used in test procedures. This may link to
|
||||
a common glossary, to avoid replication of content.
|
||||
|
||||
## Procedures
|
||||
|
||||
This section will contain specific test procedures. Presently, procedures
|
||||
are placeholders describing general patterns for setting up and conducting
|
||||
testing.
|
||||
|
||||
### User Testing Setup
|
||||
|
||||
These procedures describes a general pattern for setting up for user
|
||||
testing. Specific deployments should customize this pattern with
|
||||
relevant data and any additional steps necessary.
|
||||
|
||||
Property | Value
|
||||
---------------|---------------------------------------------------------------
|
||||
Test ID | MCT-TEST-SETUP0 - User Testing Setup
|
||||
Relevant reqs. | TBD
|
||||
Prerequisites | Build of relevant components
|
||||
Test input | Exemplary database; exemplary telemetry data set
|
||||
Instructions | See below
|
||||
Expectation | Able to load application in a web browser (Google Chrome)
|
||||
Eval. criteria | Visual inspection
|
||||
|
||||
Instructions:
|
||||
|
||||
1. Start telemetry server.
|
||||
2. Start ElasticSearch.
|
||||
3. Restore database snapshot to ElasticSearch.
|
||||
4. Start telemetry playback.
|
||||
5. Start HTTP server for client sources.
|
||||
|
||||
### User Test Procedures
|
||||
|
||||
Specific user test cases have not yet been authored. In their absence,
|
||||
user testing is conducted by:
|
||||
|
||||
* Reviewing the text of issues from the issue tracker to understand the
|
||||
desired behavior, and exercising this behavior in the running application.
|
||||
(For instance, by following steps to reproduce from the original issue.)
|
||||
* Issues which appear to be resolved should be marked as such with comments
|
||||
on the original issue (e.g. "verified during user testing MM/DD/YYYY".)
|
||||
* Issues which appear not to have been resolved should be reopened with an
|
||||
explanation of what unexpected behavior has been observed.
|
||||
* In cases where an issue appears resolved as-worded but other related
|
||||
undesirable behavior is observed during testing, a new issue should be
|
||||
opened, and linked to from a comment in the original issues.
|
||||
* General usage of new features and/or existing features which have undergone
|
||||
recent changes. Defects or problems with usability should be documented
|
||||
by filing issues in the issue tracker.
|
||||
* Open-ended testing to discover defects, identify usability issues, and
|
||||
generate feature requests.
|
||||
|
||||
### Long-Duration Testing
|
||||
|
||||
The purpose of long-duration testing is to identify performance issues
|
||||
and/or other defects which are sensitive to the amount of time the
|
||||
application is kept running. (Memory leaks, for instance.)
|
||||
|
||||
Property | Value
|
||||
---------------|---------------------------------------------------------------
|
||||
Test ID | MCT-TEST-LDT0 - Long-duration Testing
|
||||
Relevant reqs. | TBD
|
||||
Prerequisites | MCT-TEST-SETUP0
|
||||
Test input | (As for test setup.)
|
||||
Instructions | See "Instructions" below *
|
||||
Expectation | See "Expectations" below †
|
||||
Eval. criteria | Visual inspection
|
||||
|
||||
* Instructions:
|
||||
|
||||
1. Start `top` or a similar tool to measure CPU usage and memory utilization.
|
||||
2. Open several user-created displays (as many as would be realistically
|
||||
opened during actual usage in a stressing case) in some combination of
|
||||
separate tabs and windows (approximately as many tabs-per-window as
|
||||
total windows.)
|
||||
3. Ensure that playback data is set to run continuously for at least 24 hours
|
||||
(e.g. on a loop.)
|
||||
4. Record CPU usage and memory utilization.
|
||||
5. In at least one tab, try some general user interface gestures and make
|
||||
notes about the subjective experience of using the application. (Particularly,
|
||||
the degree of responsiveness.)
|
||||
6. Leave client displays open for 24 hours.
|
||||
7. Record CPU usage and memory utilization again.
|
||||
8. Make additional notes about the subjective experience of using the
|
||||
application (again, particularly responsiveness.)
|
||||
9. Check logs for any unexpected warnings or errors.
|
||||
|
||||
† Expectations:
|
||||
|
||||
* At the end of the test, CPU usage and memory usage should both be similar
|
||||
to their levels at the start of the test.
|
||||
* At the end of the test, subjective usage of the application should not
|
||||
be observably different from the way it was at the start of the test.
|
||||
(In particular, responsiveness should not decrease.)
|
||||
* Logs should not contain any unexpected warnings or errors ("expected"
|
||||
warnings or errors are those that have been documented and prioritized
|
||||
as known issues, or those that are explained by transient conditions
|
||||
external to the software, such as network outages.)
|
@ -53,7 +53,7 @@ requirements.
|
||||
|
||||
Additionally, the following project-specific standards will be used:
|
||||
|
||||
* During development, a "-next" suffix shall be appended to the
|
||||
* During development, a "-SNAPSHOT" suffix shall be appended to the
|
||||
version number. The version number before the suffix shall reflect
|
||||
the next expected version number for release.
|
||||
* Prior to a 1.0.0 release, the _minor_ version will be incremented
|
||||
@ -92,8 +92,8 @@ should update (or delegate the task of updating) Open MCT version
|
||||
numbers by the following process:
|
||||
|
||||
1. Update version number in `package.json`
|
||||
1. Checkout branch created for the last sprint that has been successfully tested.
|
||||
2. Remove a `-next` suffix from the version in `package.json`.
|
||||
1. Create a new branch off the `master` branch.
|
||||
2. Remove `-SNAPSHOT` suffix from the version in `package.json`.
|
||||
3. Verify that resulting version number meets semantic versioning
|
||||
requirements relative to previous stable version. Increment the
|
||||
version number if necessary.
|
||||
@ -131,14 +131,13 @@ numbers by the following process:
|
||||
3. In `package.json` change package to be public (private: false)
|
||||
4. Test the package before publishing by doing `npm publish --dry-run`
|
||||
if necessary.
|
||||
5. Publish the package to the npmjs registry (e.g. `npm publish --access public`)
|
||||
NOTE: Use the `--tag unstable` flag to the npm publish if this is a prerelease.
|
||||
5. Publish the package to the npmjs registry (e.g. `npm publish --access public`)
|
||||
6. Confirm the package has been published (e.g. `https://www.npmjs.com/package/openmct`)
|
||||
5. Update snapshot status in `package.json`
|
||||
1. Create a new branch off the `master` branch.
|
||||
2. Remove any suffix from the version number,
|
||||
or increment the _patch_ version if there is no suffix.
|
||||
3. Append a `-next` suffix.
|
||||
3. Append a `-SNAPSHOT` suffix.
|
||||
4. Commit changes to `package.json` on the `master` branch.
|
||||
The commit message should reference the sprint being opened,
|
||||
preferably by a URL reference to the associated Milestone in
|
||||
@ -150,6 +149,6 @@ numbers by the following process:
|
||||
Projects dependent on Open MCT being co-developed by the Open MCT
|
||||
team should follow a similar process, except that they should
|
||||
additionally update their dependency on Open MCT to point to the
|
||||
latest archive when removing their `-next` status, and
|
||||
latest archive when removing their `-SNAPSHOT` status, and
|
||||
that they should be pointed back to the `master` branch after
|
||||
this has completed.
|
||||
|
@ -1,32 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
module.exports = {
|
||||
extends: ['plugin:playwright/recommended'],
|
||||
rules: {
|
||||
'playwright/max-nested-describe': ['error', { max: 1 }],
|
||||
'playwright/expect-expect': 'off'
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
//Apply Best Practices to externalFixtures and exampleTemplate.e2e.spec.js
|
||||
files: [
|
||||
'appActions.js',
|
||||
'baseFixtures.js',
|
||||
'pluginFixtures.js',
|
||||
'**/exampleTemplate.e2e.spec.js'
|
||||
],
|
||||
rules: {
|
||||
'playwright/no-raw-locators': 'error',
|
||||
'playwright/no-nth-methods': 'error',
|
||||
'playwright/no-get-by-title': 'error',
|
||||
'playwright/prefer-comparison-matcher': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
// Disable no-raw-locators for .contract.perf.spec.js files until https://github.com/grafana/xk6-browser/issues/1226
|
||||
files: ['**/*.contract.perf.spec.js'],
|
||||
rules: {
|
||||
'playwright/no-raw-locators': 'off'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
@ -1,7 +0,0 @@
|
||||
*
|
||||
!appActions.js
|
||||
!baseFixtures.js
|
||||
!pluginFixtures.js
|
||||
!avpFixtures.js
|
||||
!index.js
|
||||
!*.md
|
@ -1,44 +0,0 @@
|
||||
version: 2
|
||||
snapshot:
|
||||
widths: [1024]
|
||||
min-height: 1440 # px
|
||||
percyCSS: |
|
||||
/* Clock indicator... your days are numbered */
|
||||
.t-indicator-clock > .label {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
.c-input--datetime {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Timer object text */
|
||||
.c-ne__time-and-creator {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Time Conductor ticks */
|
||||
div.c-conductor-axis.c-conductor__ticks > svg {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Embedded timestamp in notebooks */
|
||||
.c-ne__embed__time{
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Time Conductor Start Time */
|
||||
.c-compact-tc__setting-value{
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Chart Area for Plots */
|
||||
.gl-plot-chart-area{
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* SWG Time values on plot */
|
||||
.gl-plot-x{
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Notification Time in modal */
|
||||
.c-ne__time{
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Snapshot name with embedded time */
|
||||
.l-browse-bar__snapshot-datetime{
|
||||
opacity: 0 !important;
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
version: 2
|
||||
snapshot:
|
||||
widths: [1024, 2000]
|
||||
min-height: 1440 # px
|
||||
percyCSS: |
|
||||
/* Clock indicator... your days are numbered */
|
||||
.t-indicator-clock > .label {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
.c-input--datetime {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Timer object text */
|
||||
.c-ne__time-and-creator {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Time Conductor ticks */
|
||||
div.c-conductor-axis.c-conductor__ticks > svg {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Embedded timestamp in notebooks */
|
||||
.c-ne__embed__time{
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Time Conductor Start Time */
|
||||
.c-compact-tc__setting-value{
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Chart Area for Plots */
|
||||
.gl-plot-chart-area{
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* SWG Time values on plot */
|
||||
.gl-plot-x{
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Notification Time in modal */
|
||||
.c-ne__time{
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Snapshot name with embedded time */
|
||||
.l-browse-bar__snapshot-datetime{
|
||||
opacity: 0 !important;
|
||||
}
|
646
e2e/README.md
646
e2e/README.md
@ -1,646 +0,0 @@
|
||||
# e2e testing
|
||||
|
||||
This document captures information specific to the e2e testing of Open MCT. For general information about testing, please see [the Open MCT README](https://github.com/nasa/openmct/blob/master/README.md#tests).
|
||||
|
||||
## Table of Contents
|
||||
|
||||
This document is designed to capture on the What, Why, and How's of writing and running e2e tests in Open MCT. Please use the built-in Github Table of Contents functionality at the top left of this page or the markup.
|
||||
|
||||
1. [Getting Started](#getting-started)
|
||||
2. [Types of Testing](#types-of-e2e-testing)
|
||||
3. [Architecture](#test-architecture-and-ci)
|
||||
|
||||
## Getting Started
|
||||
|
||||
While our team does our best to lower the barrier to entry to working with our e2e framework and Open MCT, there is a bit of work required to get from 0 to 1 test contributed.
|
||||
|
||||
### Getting started with Playwright
|
||||
|
||||
If this is your first time ever using the Playwright framework, we recommend going through the [Getting Started Guide](https://playwright.dev/docs/next/intro) which can be completed in about 15 minutes. This will give you a concise tour of Playwright's functionality and an understanding of the official Playwright documentation which we leverage in Open MCT.
|
||||
|
||||
### Getting started with Open MCT's implementation of Playwright
|
||||
|
||||
Once you've got an understanding of Playwright, you'll need a baseline understanding of Open MCT:
|
||||
|
||||
1. Follow the steps [Building and Running Open MCT Locally](../README.md#building-and-running-open-mct-locally)
|
||||
2. Once you're serving Open MCT locally, create a 'Display Layout' object. Save it.
|
||||
3. Create a 'Plot' Object (e.g.: 'Stacked Plot')
|
||||
4. Create an Example Telemetry Object (e.g.: 'Sine Wave Generator')
|
||||
5. Expand the Tree and note the hierarchy of objects which were created.
|
||||
6. Navigate to the Demo Display Layout Object to edit and modify the embedded plot.
|
||||
7. Modify the embedded plot with Telemetry Data.
|
||||
|
||||
What you've created is a display which mimics the display that a mission control operator might use to understand and model telemetry data.
|
||||
|
||||
Recreate the steps above with Playwright's codegen tool:
|
||||
|
||||
1. `npm run start` in a terminal window to serve Open MCT locally
|
||||
2. `npx @playwright/test install` to install playwright and dependencies
|
||||
3. Open another terminal window and start the Playwright codegen application `npx playwright codegen`
|
||||
4. Navigate the browser to `http://localhost:8080`
|
||||
5. Click the Create button and notice how your actions in the browser are being recorded in the Playwright Inspector
|
||||
6. Continue through the steps 2-6 above
|
||||
|
||||
What you've created is an automated test which mimics the creation of a mission control display.
|
||||
|
||||
Next, you should walk through our implementation of Playwright in Open MCT:
|
||||
|
||||
1. Close any terminals which are serving up a local instance of Open MCT
|
||||
2. Run our 'Getting Started' test in debug mode with `npm run test:e2e:local -- exampleTemplate --debug`
|
||||
3. Step through each test step in the Playwright Inspector to see how we leverage Playwright's capabilities to test Open MCT
|
||||
|
||||
## Types of e2e Testing
|
||||
|
||||
e2e testing describes the layer at which a test is performed without prescribing the assertions which are made. Generally, when writing an e2e test, we have five choices to make on an assertion strategy:
|
||||
|
||||
1. Functional - Verifies the functional correctness of the application. Sometimes interchanged with e2e or regression testing.
|
||||
2. Visual - Verifies the "look and feel" of the application and can only detect _undesirable changes when compared to a previous baseline_.
|
||||
3. Snapshot - Similar to Visual in that it captures the "look" of the application and can only detect _undesirable changes when compared to a previous baseline_. **Generally not preferred due to advanced setup necessary.**
|
||||
4. Accessibility - Verifies that the application meets the accessibility standards defined by the [WCAG organization](https://www.w3.org/WAI/standards-guidelines/wcag/).
|
||||
5. Performance - Verifies that application provides a performant experience. Like Snapshot testing, these tests are generally not recommended due to their difficulty in providing a consistent result.
|
||||
|
||||
When choosing between the different testing strategies, think only about the assertion that is made at the end of the series of test steps. "I want to verify that the Timer plugin functions correctly" vs "I want to verify that the Timer plugin does not look different than originally designed".
|
||||
|
||||
We do not want to interleave visual and functional testing inside the same suite because visual test verification of correctness must happen with a 3rd party service. This service is not available when executing these tests in other contexts (i.e. VIPER).
|
||||
|
||||
### Functional Testing
|
||||
|
||||
The bulk of our e2e coverage lies in "functional" test coverage which verifies that Open MCT is functionally correct as well as defining _how we expect it to behave_. This enables us to test the application exactly as a user would, while prescribing exactly how a user can interact with the application via a web browser.
|
||||
|
||||
### Visual Testing
|
||||
|
||||
Visual Testing is an essential part of our e2e strategy as it ensures that the application _appears_ correctly to a user while it compliments the functional e2e suite. It would be impractical to make thousands of assertions functional assertions on the look and feel of the application. Visual testing is interested in getting the DOM into a specified state and then comparing that it has not changed against a baseline.
|
||||
|
||||
For a better understanding of the visual issues which affect Open MCT, please see our bug tracker with the `label:visual` filter applied [here](https://github.com/nasa/openmct/issues?q=label%3Abug%3Avisual+)
|
||||
To read about how to write a good visual test, please see [How to write a great Visual Test](#how-to-write-a-great-visual-test).
|
||||
|
||||
`npm run test:e2e:visual` commands will run all of the visual tests against a local instance of Open MCT. If no `PERCY_TOKEN` API key is found in the terminal or command line environment variables, no visual comparisons will be made.
|
||||
|
||||
- `npm run test:e2e:visual: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).
|
||||
|
||||
At present, we are using percy with two configuration files: `./e2e/.percy.nightly.yml` and `./e2e/.percy.ci.yml`. This is mainly to reduce the number of snapshots.
|
||||
|
||||
### Advanced: Snapshot Testing (Not Recommended)
|
||||
|
||||
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.
|
||||
|
||||
#### Open MCT's implementation
|
||||
|
||||
- Our Snapshot tests receive a `@snapshot` tag.
|
||||
- Snapshots need to be executed within the official Playwright container to ensure we're using the exact rendering platform in CI and locally. To do a valid comparison locally:
|
||||
|
||||
```sh
|
||||
// Replace {X.X.X} with the current Playwright version
|
||||
// from our package.json or circleCI configuration file
|
||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v{X.X.X}-focal /bin/bash
|
||||
npm install
|
||||
npm run test:e2e:checksnapshots
|
||||
```
|
||||
|
||||
### Updating Snapshots
|
||||
|
||||
When the `@snapshot` tests fail, they will need to be evaluated to determine if the failure is an acceptable and desireable or an unintended regression.
|
||||
|
||||
To compare a snapshot, run a test and open the html report with the 'Expected' vs 'Actual' screenshot. If the actual screenshot is preferred, then the source-controlled 'Expected' snapshots will need to be updated with the following scripts.
|
||||
|
||||
```sh
|
||||
// Replace {X.X.X} with the current Playwright version
|
||||
// from our package.json or circleCI configuration file
|
||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v{X.X.X}-focal /bin/bash
|
||||
npm install
|
||||
npm run test:e2e:updatesnapshots
|
||||
```
|
||||
|
||||
Once that's done, you'll need to run the following to verify that the changes do not cause more problems:
|
||||
|
||||
```sh
|
||||
npm run test:e2e:checksnapshots
|
||||
```
|
||||
|
||||
## Automated Accessibility (a11y) Testing
|
||||
|
||||
Open MCT incorporates accessibility testing through two primary methods to ensure its compliance with accessibility standards:
|
||||
|
||||
1. **Usage of Playwright's Locator Strategy**: Open MCT utilizes Playwright's locator strategy, specifically the [page.getByRole('') function](https://playwright.dev/docs/api/class-framelocator#frame-locator-get-by-role), to ensure that web elements are accessible via assistive technologies. This approach focuses on the accessibility of elements rather than full adherence to a11y guidelines, which is covered in the second method.
|
||||
|
||||
2. **Enforcing a11y Guidelines with Playwright Axe Plugin**: To rigorously enforce a11y guideline compliance, Open MCT employs the [playwright axe plugin](https://playwright.dev/docs/accessibility-testing). This is achieved through the `scanForA11yViolations` function within the visual testing suite. This method not only benefits from the existing coverage of the visual tests but also targets specific a11y issues, such as `color-contrast` violations, which are particularly pertinent in the context of visual testing.
|
||||
|
||||
### a11y Standards (WCAG and Section 508)
|
||||
|
||||
Playwright axe supports a wide range of [WCAG Standards](https://playwright.dev/docs/accessibility-testing#scanning-for-wcag-violations) to test against. Open MCT is testing against the [Section 508](https://www.section508.gov/test/testing-overview/) accessibility guidelines with the intent to support higher standards over time. As of 2024, Section508 requirements now map completely to WCAG 2.0 AA. In the future, Section 508 requirements may map to WCAG 2.1 AA.
|
||||
|
||||
### Reading an a11y test failure
|
||||
|
||||
When an a11y test fails, the result must be interpreted in the html test report or the a11y report json artifact stored in the `/test-results/` folder. The json structure should be parsed for `"violations"` by `"id"` and identified `"target"`. Example provided for the 'color-contrast-enhanced' violation.
|
||||
|
||||
```json
|
||||
"violations":
|
||||
{
|
||||
"id": "color-contrast-enhanced",
|
||||
"impact": "serious",
|
||||
"html": "<span class=\"label c-indicator__label\">0 Snapshots <button aria-label=\"Show Snapshots\">Show</button></span>",
|
||||
"target": [
|
||||
".s-status-off > .label.c-indicator__label"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 6.51 (foreground color: #aaaaaa, background color: #262626, font size: 8.1pt (10.8px), font weight: normal). Expected contrast ratio of 7:1"
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Testing
|
||||
|
||||
The open source performance tests function in three ways which match their naming and folder structure:
|
||||
|
||||
`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.
|
||||
`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.
|
||||
`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.
|
||||
|
||||
In addition to the explicit definition of performance tests, we also ensure that our test timeout timing is "tight" to catch performance regressions detectable by action timeouts. i.e. [Notebooks load much slower than they used to #6459](https://github.com/nasa/openmct/issues/6459)
|
||||
|
||||
## Test Architecture and CI
|
||||
|
||||
### Architecture
|
||||
|
||||
### File Structure
|
||||
|
||||
Our file structure follows the type of type of testing being exercised at the e2e layer and files containing test suites which matcher application behavior or our `src` and `example` layout. This area is not well refined as we figure out what works best for closed source and downstream projects. This may change altogether if we move `e2e` to it's own npm package.
|
||||
|
||||
|File Path|Description|
|
||||
|:-:|-|
|
||||
|`./helper` | Contains helper functions or scripts which are leveraged directly within the test suites (e.g.: non-default plugin scripts injected into the DOM)|
|
||||
|`./test-data` | Contains test data which is leveraged or generated in the functional, performance, or visual test suites (e.g.: localStorage data).|
|
||||
|`./tests/functional` | The bulk of the tests are contained within this folder to verify the functionality of Open MCT.|
|
||||
|`./tests/functional/example/` | Tests which specifically verify the example plugins (e.g.: Sine Wave Generator).|
|
||||
|`./tests/functional/plugins/` | Tests which loosely test each plugin. This folder is the most likely to change. Note: some `@snapshot` tests are still contained within this structure.|
|
||||
|`./tests/framework/` | Tests which verify that our testing framework's functionality and assumptions will continue to work based on further refactoring or Playwright version changes (e.g.: verifying custom fixtures and appActions).|
|
||||
|`./tests/performance/` | Performance tests which should be run on every commit.|
|
||||
|`./tests/performance/contract/` | A subset of performance tests which are designed to provide a contract between the open source tests which are run on every commit and the downstream tests which are run post merge and with other frameworks.|
|
||||
|`./tests/performance/memory` | A subset of performance tests which are designed to test for memory leaks.|
|
||||
|`./tests/visual-a11y/` | Visual tests and accessibility tests.|
|
||||
|`./tests/visual-a11y/component/` | Visual and accessibility tests which are only run against a single component.|
|
||||
|`./appActions.js` | Contains common methods which can be leveraged by test case authors to quickly move through the application when writing new tests.|
|
||||
|`./baseFixture.js` | Contains base fixtures which only extend default `@playwright/test` functionality. The expectation is that these fixtures will be removed as the native Playwright API improves|
|
||||
|
||||
Our functional tests end in `*.e2e.spec.js`, visual tests in `*.visual.spec.js` and performance tests in `*.perf.spec.js`.
|
||||
|
||||
### Configuration
|
||||
|
||||
Where possible, we try to run Open MCT without modification or configuration change so that the Open MCT doesn't fail exclusively in "test mode" or in "production mode".
|
||||
|
||||
Open MCT is leveraging the [config file](https://playwright.dev/docs/test-configuration) pattern to describe the capabilities of Open MCT e2e _where_ it's run
|
||||
|
||||
|Config File|Description|
|
||||
|:-:|-|
|
||||
|`./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-a11y.config.js` | Used to run the visual and a11y tests in CI or locally|
|
||||
|
||||
#### Test Tags
|
||||
|
||||
Test tags are a great way of organizing tests outside of a file structure. To learn more see the official documentation [here](https://playwright.dev/docs/test-annotations#tag-tests).
|
||||
|
||||
Current list of test tags:
|
||||
|
||||
|Test Tag|Description|
|
||||
|:-:|-|
|
||||
|`@mobile` | Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no create button).|
|
||||
|`@a11y` | Test case or test suite to execute playwright-axe accessibility checks and generate a11y reports.|
|
||||
|`@addInit` | Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of `npm start`.|
|
||||
|`@localStorage` | Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB). See [note](#utilizing-localstorage)|
|
||||
|`@snapshot` | Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of the playwright container.|
|
||||
|`@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.|
|
||||
|`@clock` | A test which modifies the clock. These have expanded out of the visual tests and into the functional tests.
|
||||
|`@framework` | A test for open mct e2e capabilities. This is primarily to ensure we don't break projects which depend on sourcing this project's fixtures like appActions.js.
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
The cheapest time to catch a bug is pre-merge. Unfortunately, this is the most expensive time to run all of the tests since each merge event can consist of hundreds of commits. For this reason, we're selective in _what we run_ as much as _when we run it_.
|
||||
|
||||
We leverage CircleCI to run tests against each commit and inject the Test Reports which are generated by Playwright so that they team can keep track of flaky and [historical test trends](https://app.circleci.com/insights/github/nasa/openmct/workflows/overall-circleci-commit-status/tests?branch=master&reporting-window=last-30-days)
|
||||
|
||||
We leverage Github Actions / Workflows to execute tests as it gives us the ability to run against multiple operating systems with greater control over git event triggers (i.e. Run on a PR Comment event).
|
||||
|
||||
Our CI environment consists of 3 main modes of operation:
|
||||
|
||||
#### 1. Per-Commit Testing
|
||||
|
||||
CircleCI
|
||||
|
||||
- e2e tests against ubuntu and chrome
|
||||
- Performance tests against ubuntu and chrome
|
||||
- e2e tests are linted
|
||||
- Visual and a11y tests are run in a single resolution on the default `espresso` theme
|
||||
|
||||
#### 2. Per-Merge Testing
|
||||
|
||||
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'
|
||||
|
||||
#### 3. Scheduled / Batch Testing
|
||||
|
||||
Nightly Testing in Circle CI
|
||||
|
||||
- Full e2e suite against ubuntu and chrome, firefox, and an MMOC resolution profile
|
||||
- Performance tests against ubuntu and chrome
|
||||
- CouchDB suite
|
||||
- Visual and a11y Tests are run in the full profile
|
||||
|
||||
Github Actions / Workflow
|
||||
|
||||
- None at the moment
|
||||
|
||||
#### Parallelism and Fast Feedback
|
||||
|
||||
In order to provide fast feedback in the Per-Commit context, we try to keep total test feedback at 5 minutes or less. That is to say, A developer should have a pass/fail result in under 5 minutes.
|
||||
|
||||
Playwright has native support for semi-intelligent sharding. Read about it [here](https://playwright.dev/docs/test-parallel#shard-tests-between-multiple-machines).
|
||||
|
||||
We will be adjusting the parallelization of the Per-Commit tests to keep below the 5 minute total runtime threshold.
|
||||
|
||||
In addition to the Parallelization of Test Runners (Sharding), we're also running two concurrent threads on every Shard. This is the functional limit of what CircleCI Agents can support from a memory and CPU resource constraint.
|
||||
|
||||
So for every commit, Playwright is effectively running 4 x 2 concurrent browsercontexts to keep the overall runtime to a miminum.
|
||||
|
||||
At the same time, we don't want to waste CI resources on parallel runs, so we've configured each shard to fail after 5 test failures. Test failure logs are recorded and stored to allow fast triage.
|
||||
|
||||
### Cross-browser and Cross-operating system
|
||||
|
||||
#### **What's supported:**
|
||||
|
||||
We are leveraging the `browserslist` project to declare our supported list of browsers. We support macOS, Windows, and ubuntu 20+.
|
||||
|
||||
#### **Where it's tested:**
|
||||
|
||||
We lint on `browserslist` to ensure that we're not implementing deprecated browser APIs and are aware of browser API improvements over time.
|
||||
|
||||
We also have the need to execute our e2e tests across this published list of browsers. Our browsers and browser version matrix is found inside of our `./playwright-*.config.js`, but mostly follows in order of bleeding edge to stable:
|
||||
|
||||
- `playwright-chromium channel:beta`
|
||||
- A beta version of Chromium from official chromium channels. As close to the bleeding edge as we can get.
|
||||
- `playwright-chromium`
|
||||
- A stable version of Chromium from the official chromium channels. This is always at least 1 version ahead of desktop chrome.
|
||||
- `playwright-chrome`
|
||||
- The stable channel of Chrome from the official chrome channels. This is always 2 versions behind chromium.
|
||||
- `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 a Mission-need to support iPad and mobile devices. To run our test suites with mobile devices, please see our `playwright-mobile.config.js` projects.
|
||||
|
||||
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). To bypass the object creation, we leverage the `storageState` properties for starting the mobile tests with localstorage.
|
||||
|
||||
For now, the mobile tests will exist in the /tests/mobile/ suites and be executed with the
|
||||
|
||||
```sh
|
||||
npm run test:e2e:mobile
|
||||
```
|
||||
|
||||
command.
|
||||
|
||||
#### **Skipping or executing tests based on browser, os, and/os browser version:**
|
||||
|
||||
Conditionally skipping tests based on browser (**RECOMMENDED**):
|
||||
|
||||
```js
|
||||
test('Can adjust image brightness/contrast by dragging the sliders', async ({ page, browserName }) => {
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
Conditionally skipping tests based on OS:
|
||||
|
||||
```js
|
||||
test('Can adjust image brightness/contrast by dragging the sliders', async ({ page }) => {
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip(process.platform === 'darwin', 'This test needs to be updated to work with MacOS');
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
Skipping based on browser version (Rarely used): <https://github.com/microsoft/playwright/discussions/17318>
|
||||
|
||||
## Test Design, Best Practices, and Tips & Tricks
|
||||
|
||||
### Test Design
|
||||
|
||||
#### Test as the User
|
||||
|
||||
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 to application changes
|
||||
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' });
|
||||
|
||||
// ...and use its `url` property to navigate directly to it later in the test:
|
||||
await page.goto(clock.url);
|
||||
```
|
||||
|
||||
1. Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });`
|
||||
- Initial navigation should _almost_ always use the `{ waitUntil: 'domcontentloaded' }` option.
|
||||
1. Avoid repeated setup to test a single assertion. Write longer tests with multiple soft assertions.
|
||||
This ensures that your changes will be picked up with large refactors.
|
||||
1. Use [user-facing locators](https://playwright.dev/docs/best-practices#use-locators) (Now a eslint rule!)
|
||||
|
||||
```js
|
||||
page.getByRole('button', { name: 'Create' } )
|
||||
```
|
||||
Instead of
|
||||
```js
|
||||
page.locator('.c-create-button')
|
||||
```
|
||||
Note: `page.locator()` can be used in performance tests as xk6-browser does not yet support the new `page.getBy` pattern and css lookups can be [1.5x faster](https://serpapi.com/blog/css-selectors-faster-than-getbyrole-playwright/)
|
||||
|
||||
##### Utilizing LocalStorage
|
||||
|
||||
1. In order to save test runtime in the case of tests that require a decent amount of initial setup (such as in the case of testing complex displays), you may use [Playwright's `storageState` feature](https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state) to generate and load localStorage states.
|
||||
1. To generate a localStorage state to be used in a test:
|
||||
- Add an e2e test to our generateLocalStorageData suite which sets the initial state (creating/configuring objects, etc.), saving it in the `test-data` folder:
|
||||
```js
|
||||
// Save localStorage for future test execution
|
||||
await context.storageState({
|
||||
path: path.join(__dirname, '../../../e2e/test-data/display_layout_with_child_layouts.json')
|
||||
});
|
||||
```
|
||||
- Load the state from file at the beginning of the desired test suite (within the `test.describe()`). (NOTE: the storage state will be used for each test in the suite, so you may need to create a new suite):
|
||||
```js
|
||||
const LOCALSTORAGE_PATH = path.resolve(
|
||||
__dirname,
|
||||
'../../../../test-data/display_layout_with_child_layouts.json'
|
||||
);
|
||||
test.use({
|
||||
storageState: path.resolve(__dirname, LOCALSTORAGE_PATH)
|
||||
});
|
||||
```
|
||||
|
||||
### How to write a great test
|
||||
|
||||
- Avoid using css locators to find elements to the page. Use modern web accessible locators like `getByRole`
|
||||
- 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`:
|
||||
|
||||
```js
|
||||
// Fill the "Notes" section with information about the
|
||||
// currently running test and its project.
|
||||
const { testNotes } = page;
|
||||
const notesInput = page.locator('form[name="mctForm"] #notes-textarea');
|
||||
await notesInput.fill(testNotes);
|
||||
```
|
||||
|
||||
#### How to Write a Great Visual Test
|
||||
|
||||
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 unless explicitly testing realtime clock
|
||||
- Employ the `createExampleTelemetryObject` appAction to source telemetry and specify a `name` to avoid autogenerated names.
|
||||
- Avoid creating objects with a time component like timers and clocks.
|
||||
- Utilize the playwright clock() API. See @clock Annotations for examples.
|
||||
|
||||
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-a11y/component/` folder and limit the scope of the comparison to that component. For instance:
|
||||
|
||||
```js
|
||||
await percySnapshot(page, `Tree Pane w/ single level expanded (theme: ${theme})`, {
|
||||
scope: treePane
|
||||
});
|
||||
```
|
||||
|
||||
- Note: The `scope` variable can be any valid CSS selector.
|
||||
|
||||
7. **Write many `percySnapshot` commands in a single test**: In line with our approach to longer functional tests, we recommend that many test percySnapshots are taken in a single test. For instance:
|
||||
|
||||
```js
|
||||
//<Some interesting state>
|
||||
await percySnapshot(page, `Before object expanded (theme: ${theme})`);
|
||||
//<Click on object>
|
||||
await percySnapshot(page, `object expanded (theme: ${theme})`);
|
||||
//Select from object
|
||||
await percySnapshot(page, `object selected (theme: ${theme})`)
|
||||
```
|
||||
|
||||
#### How to write a great network test
|
||||
|
||||
- Where possible, it is best to mock out third-party network activity to ensure we are testing application behavior of Open MCT.
|
||||
- It is best to be as specific as possible about the expected network request/response structures in creating your mocks.
|
||||
- Make sure to only mock requests which are relevant to the specific behavior being tested.
|
||||
- Where possible, network requests and responses should be treated in an order-agnostic manner, as the order in which certain requests/responses happen is dynamic and subject to change.
|
||||
|
||||
Some examples of mocking network responses in regards to CouchDB can be found in our [couchdb.e2e.spec.js](./tests/functional/couchdb.e2e.spec.js) test file.
|
||||
|
||||
### Best Practices
|
||||
|
||||
For now, our best practices exist as self-tested, living documentation in our [exampleTemplate.e2e.spec.js](./tests/framework/exampleTemplate.e2e.spec.js) file.
|
||||
|
||||
For best practices with regards to mocking network responses, see our [couchdb.e2e.spec.js](./tests/functional/couchdb.e2e.spec.js) file.
|
||||
|
||||
### Tips & Tricks
|
||||
|
||||
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. Use the `page.clock()` API as such:
|
||||
|
||||
```js
|
||||
import { test, expect } from '../../pluginFixtures.js';
|
||||
|
||||
test.describe('foo test suite @clock', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Set clock time
|
||||
await page.clock.install({ time: MISSION_TIME });
|
||||
await page.clock.resume();
|
||||
//Navigate to page with new clock
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
|
||||
test('bar here', async ({ page }) => {
|
||||
/// ...
|
||||
});
|
||||
```
|
||||
|
||||
- Working with multiple pages
|
||||
There are instances where multiple browser pages will needed to verify multi-page or multi-tab application behavior. Make sure to use the `@2p` annotation as well as name each page appropriately: i.e. `page1` and `page2` or `tab1` and `tab2` depending on the intended use case. Generally pages should be used unless testing `sharedWorker` code, specifically.
|
||||
|
||||
- Working with file downloads and JSON data
|
||||
Open MCT has the capability of exporting certain objects in the form of a JSON file handled by the chrome browser. The best example of this type of test can be found in the exportAsJson test.
|
||||
|
||||
```js
|
||||
const [download] = await Promise.all([
|
||||
page.waitForEvent('download'), // Waits for the download event
|
||||
page.getByLabel('Export as JSON').click() // Triggers the download
|
||||
]);
|
||||
|
||||
// Wait for the download process to complete
|
||||
const path = await download.path();
|
||||
|
||||
// Read the contents of the downloaded file using readFile from fs/promises
|
||||
const fileContents = await fs.readFile(path, 'utf8');
|
||||
const jsonData = JSON.parse(fileContents);
|
||||
|
||||
// Use the function to retrieve the key
|
||||
const key = getFirstKeyFromOpenMctJson(jsonData);
|
||||
|
||||
// Verify the contents of the JSON file
|
||||
expect(jsonData.openmct[key]).toHaveProperty('name', 'e2e folder');
|
||||
```
|
||||
|
||||
### Reporting
|
||||
|
||||
Test Reporting is done through official Playwright reporters and the CI Systems which execute them.
|
||||
|
||||
We leverage the following official Playwright reporters:
|
||||
|
||||
- HTML
|
||||
- junit
|
||||
- github annotations
|
||||
- Tracefile
|
||||
- Screenshots
|
||||
|
||||
When running the tests locally with the `npm run test:e2e:local` command, the html report will open automatically on failure. Inside this HTML report will be a complete summary of the finished tests. If the tests failed, you'll see embedded links to screenshot failure, execution logs, and the Tracefile.
|
||||
|
||||
When looking at the reports run in CI, you'll leverage this same HTML Report which is hosted either in CircleCI or Github Actions as a build artifact.
|
||||
|
||||
### e2e Code Coverage
|
||||
|
||||
Our e2e code coverage is captured and combined with our unit test coverage. For more information, please see our [code coverage documentation](../TESTING.md)
|
||||
|
||||
#### Generating e2e code coverage
|
||||
|
||||
Please read more about our code coverage [here](../TESTING.md#code-coverage)
|
||||
|
||||
## Other
|
||||
|
||||
### About e2e testing
|
||||
|
||||
e2e testing is an industry-standard approach to automating the testing of web-based UIs such as Open MCT. Broadly speaking, e2e tests differentiate themselves from unit tests by preferring replication of real user interactions over execution of raw JavaScript functions.
|
||||
|
||||
Historically, the abstraction necessary to replicate real user behavior meant that:
|
||||
|
||||
- e2e tests were "expensive" due to how much code each test executed. The closer a test replicates the user, the more code is needed run during test execution. Unit tests could run smaller units of code more efficiently.
|
||||
- e2e tests were flaky due to network conditions or the underlying protocols associated with testing a browser.
|
||||
- e2e frameworks relied on a browser communication standard which lacked the observability and controls necessary needed to reach the code paths possible with unit and integration tests.
|
||||
- e2e frameworks provided insufficient debug information on test failure
|
||||
|
||||
However, as the web ecosystem has matured to the point where mission-critical UIs can be written for the web (Open MCT), the e2e testing tools have matured as well. There are now fewer "trade-offs" when choosing to write an e2e test over any other type of test.
|
||||
|
||||
Modern e2e frameworks:
|
||||
|
||||
- Bypass the surface layer of the web-application-under-test and use a raw debugging protocol to observe and control application and browser state.
|
||||
- These new browser-internal protocols enable near-instant, bi-directional communication between test code and the browser, speeding up test execution and making the tests as reliable as the application itself.
|
||||
- Provide test debug tooling which enables developers to pinpoint failure
|
||||
|
||||
Furthermore, the abstraction necessary to run e2e tests as a user enables them to be extended to run within a variety of contexts. This matches the extensible design of Open MCT.
|
||||
|
||||
A single e2e test in Open MCT is extended to run:
|
||||
|
||||
- Against a matrix of browser versions.
|
||||
- Against a matrix of OS platforms.
|
||||
- Against a local development version of Open MCT.
|
||||
- A version of Open MCT loaded as a dependency (VIPER, VISTA, etc)
|
||||
- Against a variety of data sources or telemetry endpoints.
|
||||
|
||||
### Why Playwright?
|
||||
|
||||
[Playwright](https://playwright.dev/) was chosen as our e2e framework because it solves a few VIPER Mission needs:
|
||||
|
||||
1. First-class support for Automated Performance Testing
|
||||
2. Official Chrome, Chrome Canary, and iPad Capabilities
|
||||
3. Support for Browserless.io to run tests in a "hermetically sealed" environment
|
||||
4. Ability to generate code coverage reports
|
||||
|
||||
### FAQ
|
||||
|
||||
- How does this help NASA missions?
|
||||
- When should I write an e2e test instead of a unit test?
|
||||
- When should I write a functional vs visual test?
|
||||
- How is Open MCT extending default Playwright functionality?
|
||||
- What about Component Testing?
|
||||
|
||||
### Writing Tests
|
||||
|
||||
Playwright provides 3 supported methods of debugging and authoring tests:
|
||||
|
||||
- A 'watch mode' for running tests locally and debugging on the fly
|
||||
- A 'debug mode' for debugging tests and writing assertions against tests
|
||||
- A 'VSCode plugin' for debugging tests within the VSCode IDE.
|
||||
|
||||
Generally, we encourage folks to use the watch mode and provide a script `npm run test:e2e:watch` which launches the launch mode ui and enables hot reloading on the dev server.
|
||||
|
||||
### e2e Troubleshooting
|
||||
|
||||
Please follow the general guide troubleshooting in [the general troubleshooting doc](../TESTING.md#troubleshooting-ci)
|
||||
|
||||
- Tests won't start because 'Error: <http://localhost:8080/># is already used...'
|
||||
This error will appear when running the tests locally. Sometimes, the webserver is left in an orphaned state and needs to be cleaned up. To clear up the orphaned webserver, execute the following from your Terminal:
|
||||
```lsof -n -i4TCP:8080 | awk '{print$2}' | tail -1 | xargs kill -9```
|
||||
|
||||
### Upgrading Playwright
|
||||
|
||||
In order to upgrade from one version of Playwright to another, the version should be updated in several places in both `openmct` and `openmct-yamcs` repos. An easy way to identify these locations is to search for the current version in all files and find/replace.
|
||||
|
||||
For reference, all of the locations where the version should be updated are listed below:
|
||||
|
||||
#### **In `openmct`:**
|
||||
|
||||
- `package.json`
|
||||
- Both packages `@playwright/test` and `playwright-core` should be updated to the same target version.
|
||||
- `.circleci/config.yml`
|
||||
- `.github/workflows/e2e-couchdb.yml`
|
||||
- `.github/workflows/e2e-pr.yml`
|
||||
|
||||
#### **In `openmct-yamcs`:**
|
||||
|
||||
- `package.json`
|
||||
- `@playwright/test` should be updated to the target version.
|
||||
- `.github/workflows/yamcs-quickstart-e2e.yml`
|
@ -1,648 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* The fixtures in this file are to be used to consolidate common actions performed by the
|
||||
* various test suites. The goal is only to avoid duplication of code across test suites and not to abstract
|
||||
* away the underlying functionality of the application. For more about the App Action pattern, see /e2e/README.md)
|
||||
*
|
||||
* For example, if two functions are nearly identical in
|
||||
* timer.e2e.spec.js and notebook.e2e.spec.js, that function should be generalized and moved into this file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Defines parameters to be used in the creation of a domain object.
|
||||
* @typedef {Object} CreateObjectOptions
|
||||
* @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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Contains information about the newly created domain object.
|
||||
* @typedef {Object} CreatedObjectInfo
|
||||
* @property {string} name the name of the created object
|
||||
* @property {string} uuid the uuid of the created object
|
||||
* @property {string} url the relative url to the object (for use with `page.goto()`)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Defines parameters to be used in the creation of a notification.
|
||||
* @typedef {Object} CreateNotificationOptions
|
||||
* @property {string} message the message
|
||||
* @property {'info' | 'alert' | 'error'} severity the severity
|
||||
* @property {import('../src/api/notifications/NotificationAPI').NotificationOptions} [notificationOptions] additional options
|
||||
*/
|
||||
|
||||
import { expect } from '@playwright/test';
|
||||
import { Buffer } from 'buffer';
|
||||
import { v4 as genUuid } from 'uuid';
|
||||
|
||||
/**
|
||||
* This common function creates a domain object with the default options. It is the preferred way of creating objects
|
||||
* in the e2e suite when uninterested in properties of the objects themselves.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page - The Playwright page object.
|
||||
* @param {Object} options - Options for creating the domain object.
|
||||
* @param {string} options.type - The type of domain object to create (e.g., "Sine Wave Generator").
|
||||
* @param {string} [options.name] - The desired name of the created domain object.
|
||||
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} [options.parent='mine'] - The Identifier or uuid of the parent object. Defaults to 'mine' folder
|
||||
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
|
||||
*/
|
||||
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) {
|
||||
if (!name) {
|
||||
name = `${type}:${genUuid()}`;
|
||||
}
|
||||
|
||||
const parentUrl = await getHashUrlToDomainObject(page, parent);
|
||||
|
||||
// Navigate to the parent object. This is necessary to create the object
|
||||
// in the correct location, such as a folder, layout, or plot.
|
||||
await page.goto(parentUrl);
|
||||
|
||||
// Click the Create button
|
||||
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
|
||||
// Click the object specified by 'type'-- case insensitive
|
||||
await page.getByRole('menuitem', { name: new RegExp(`^${type}$`, 'i') }).click();
|
||||
|
||||
// Fill in the name of the object
|
||||
await page.getByLabel('Title', { exact: true }).fill('');
|
||||
await page.getByLabel('Title', { exact: true }).fill(name);
|
||||
|
||||
if (page.testNotes) {
|
||||
// Fill the "Notes" section with information about the
|
||||
// currently running test and its project.
|
||||
// eslint-disable-next-line playwright/no-raw-locators
|
||||
await page.locator('#notes-textarea').fill(page.testNotes);
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
// Wait until the URL is updated
|
||||
await page.waitForURL(`**/${parent}/*`);
|
||||
const uuid = await getFocusedObjectUuid(page);
|
||||
const objectUrl = await getHashUrlToDomainObject(page, uuid);
|
||||
|
||||
if (await _isInEditMode(page, uuid)) {
|
||||
// Save (exit edit mode)
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
uuid,
|
||||
url: objectUrl
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a notification with the given options.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {CreateNotificationOptions} createNotificationOptions
|
||||
*/
|
||||
async function createNotification(page, createNotificationOptions) {
|
||||
await page.evaluate((_createNotificationOptions) => {
|
||||
const { message, severity, options } = _createNotificationOptions;
|
||||
const notificationApi = window.openmct.notifications;
|
||||
if (severity === 'info') {
|
||||
notificationApi.info(message, options);
|
||||
} else if (severity === 'alert') {
|
||||
notificationApi.alert(message, options);
|
||||
} else {
|
||||
notificationApi.error(message, options);
|
||||
}
|
||||
}, createNotificationOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Plan object from JSON with the provided options. Must be used with a json based plan.
|
||||
* Please check appActions.e2e.spec.js for an example of how to use this function.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} name
|
||||
* @param {Object} json
|
||||
* @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 newly created domain object.
|
||||
*/
|
||||
async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
|
||||
const parentUrl = await getHashUrlToDomainObject(page, parent);
|
||||
|
||||
// Navigate to the parent object. This is necessary to create the object
|
||||
// in the correct location, such as a folder, layout, or plot.
|
||||
await page.goto(`${parentUrl}`);
|
||||
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Plan' }).click();
|
||||
|
||||
// Fill in the name of the object or generate a random one
|
||||
if (!name) {
|
||||
name = `Plan:${genUuid()}`;
|
||||
}
|
||||
await page.getByLabel('Title', { exact: true }).fill('');
|
||||
await page.getByLabel('Title', { exact: true }).fill(name);
|
||||
|
||||
// Upload buffer from memory
|
||||
await page.getByLabel('Select File...').setInputFiles({
|
||||
name: 'plan.txt',
|
||||
mimeType: 'text/plain',
|
||||
buffer: Buffer.from(JSON.stringify(json))
|
||||
});
|
||||
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
// Wait until the URL is updated
|
||||
await page.waitForURL(`**/${parent}/*`);
|
||||
const uuid = await getFocusedObjectUuid(page);
|
||||
const objectUrl = await getHashUrlToDomainObject(page, uuid);
|
||||
|
||||
return {
|
||||
uuid,
|
||||
name,
|
||||
url: objectUrl
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
await page.goto(`${parentUrl}`);
|
||||
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Sine Wave Generator' }).click();
|
||||
|
||||
const name = 'VIPER Rover Heading';
|
||||
await page.getByLabel('Title', { exact: true }).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. Note: does not set
|
||||
* default view type.
|
||||
*
|
||||
* @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}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates directly to a given object url, in real-time mode. Note: does not set
|
||||
* default view type.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} url The url to the domainObject
|
||||
* @param {string | number} start The start offset in milliseconds
|
||||
* @param {string | number} end The end offset in milliseconds
|
||||
*/
|
||||
async function navigateToObjectWithRealTime(page, url, start = '1800000', end = '30000') {
|
||||
await page.goto(
|
||||
`${url}?tc.mode=local&tc.startDelta=${start}&tc.endDelta=${end}&tc.timeSystem=utc`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands the entire object tree (every expandable tree item). Can be used to
|
||||
* ensure that the tree is fully expanded before performing actions on objects.
|
||||
* Can be applied to either the main tree or the create modal tree.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {"Main Tree" | "Create Modal Tree"} [treeName="Main Tree"]
|
||||
*/
|
||||
async function expandEntireTree(page, treeName = 'Main Tree') {
|
||||
const treeLocator = page.getByRole('tree', {
|
||||
name: treeName
|
||||
});
|
||||
const collapsedTreeItems = treeLocator
|
||||
.getByRole('treeitem', {
|
||||
expanded: false
|
||||
})
|
||||
.getByLabel(/Expand/);
|
||||
|
||||
while ((await collapsedTreeItems.count()) > 0) {
|
||||
//eslint-disable-next-line playwright/no-nth-methods
|
||||
await collapsedTreeItems.nth(0).click();
|
||||
|
||||
// FIXME: Replace hard wait with something event-driven.
|
||||
// Without the wait, this fails periodically due to a race condition
|
||||
// with Vue rendering (loop exits prematurely).
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(200);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the UUID of the currently focused object by parsing the current URL
|
||||
* and returning the last UUID in the path.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @returns {Promise<string>} the uuid of the focused object
|
||||
*/
|
||||
async function getFocusedObjectUuid(page) {
|
||||
const UUIDv4Regexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi;
|
||||
const focusedObjectUuid = await page.evaluate((regexp) => {
|
||||
return window.location.href.split('?')[0].match(regexp).at(-1);
|
||||
}, UUIDv4Regexp);
|
||||
|
||||
return focusedObjectUuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hashUrl to the domainObject given its uuid.
|
||||
* Useful for directly navigating to the given domainObject.
|
||||
*
|
||||
* URLs returned will be of the form `'./browse/#/mine/<uuid0>/<uuid1>/...'`
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string | 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, identifier) {
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
const hashUrl = await page.evaluate(async (objectIdentifier) => {
|
||||
const path = await window.openmct.objects.getOriginalPath(objectIdentifier);
|
||||
let url =
|
||||
'./#/browse/' +
|
||||
[...path]
|
||||
.reverse()
|
||||
.map((object) => window.openmct.objects.makeKeyString(object.identifier))
|
||||
.join('/');
|
||||
|
||||
// Drop the vestigial '/ROOT' if it exists
|
||||
if (url.includes('/ROOT')) {
|
||||
url = url.split('/ROOT').join('');
|
||||
}
|
||||
|
||||
return url;
|
||||
}, identifier);
|
||||
|
||||
return hashUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
// eslint-disable-next-line no-return-await
|
||||
return await page.evaluate(() => window.openmct.editor.isEditing());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time conductor mode to either fixed timespan or realtime mode.
|
||||
* @private
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true
|
||||
*/
|
||||
async function _setTimeConductorMode(page, isFixedTimespan = true) {
|
||||
// Click 'mode' button
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode Menu' }).click();
|
||||
// Switch time conductor mode. Note, need to wait here for URL to update as the router is debounced.
|
||||
if (isFixedTimespan) {
|
||||
await page.getByRole('menuitem', { name: /Fixed Timespan/ }).click();
|
||||
await page.waitForURL(/tc\.mode=fixed/);
|
||||
} else {
|
||||
await page.getByRole('menuitem', { name: /Real-Time/ }).click();
|
||||
await page.waitForURL(/tc\.mode=local/);
|
||||
}
|
||||
//dismiss the time conductor popup
|
||||
await page.getByLabel('Discard changes and close time popup').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time conductor to fixed timespan mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function setFixedTimeMode(page) {
|
||||
await _setTimeConductorMode(page, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time conductor to realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function setRealTimeMode(page) {
|
||||
await _setTimeConductorMode(page, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} OffsetValues
|
||||
* @property {string | undefined} startHours
|
||||
* @property {string | undefined} startMins
|
||||
* @property {string | undefined} startSecs
|
||||
* @property {string | undefined} endHours
|
||||
* @property {string | undefined} endMins
|
||||
* @property {string | undefined} endSecs
|
||||
*/
|
||||
|
||||
/**
|
||||
* Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {OffsetValues} offset - Object containing offset values
|
||||
* @param {boolean} [offset.submitChanges=true] - If true, submit the offset changes; otherwise, discard them
|
||||
*/
|
||||
async function setTimeConductorOffset(
|
||||
page,
|
||||
{ startHours, startMins, startSecs, endHours, endMins, endSecs, submitChanges = true }
|
||||
) {
|
||||
if (startHours) {
|
||||
await page.getByLabel('Start offset hours').fill(startHours);
|
||||
}
|
||||
|
||||
if (startMins) {
|
||||
await page.getByLabel('Start offset minutes').fill(startMins);
|
||||
}
|
||||
|
||||
if (startSecs) {
|
||||
await page.getByLabel('Start offset seconds').fill(startSecs);
|
||||
}
|
||||
|
||||
if (endHours) {
|
||||
await page.getByLabel('End offset hours').fill(endHours);
|
||||
}
|
||||
|
||||
if (endMins) {
|
||||
await page.getByLabel('End offset minutes').fill(endMins);
|
||||
}
|
||||
|
||||
if (endSecs) {
|
||||
await page.getByLabel('End offset seconds').fill(endSecs);
|
||||
}
|
||||
|
||||
// Click the check button
|
||||
if (submitChanges) {
|
||||
await page.getByLabel('Submit time offsets').click();
|
||||
} else {
|
||||
await page.getByLabel('Discard changes and close time popup').click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the values (hours, mins, secs) for the start time offset when in realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {OffsetValues} offset
|
||||
* @param {boolean} [submit=true] If true, submit the offset changes; otherwise, discard them
|
||||
*/
|
||||
async function setStartOffset(page, { submitChanges = true, ...offset }) {
|
||||
// Click 'mode' button
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||
await setTimeConductorOffset(page, { submitChanges, ...offset });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the values (hours, mins, secs) for the end time offset when in realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {OffsetValues} offset
|
||||
* @param {boolean} [submit=true] If true, submit the offset changes; otherwise, discard them
|
||||
*/
|
||||
async function setEndOffset(page, { submitChanges = true, ...offset }) {
|
||||
// Click 'mode' button
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||
await setTimeConductorOffset(page, { submitChanges, ...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 {Object} bounds - The time conductor bounds
|
||||
* @param {string} [bounds.startDate] - The start date in YYYY-MM-DD format
|
||||
* @param {string} [bounds.startTime] - The start time in HH:mm:ss format
|
||||
* @param {string} [bounds.endDate] - The end date in YYYY-MM-DD format
|
||||
* @param {string} [bounds.endTime] - The end time in HH:mm:ss format
|
||||
* @param {boolean} [bounds.submitChanges=true] - If true, submit the changes; otherwise, discard them.
|
||||
*/
|
||||
async function setTimeConductorBounds(page, { submitChanges = true, ...bounds }) {
|
||||
const { startDate, endDate, startTime, endTime } = bounds;
|
||||
|
||||
// Open the time conductor popup
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||
|
||||
if (startDate) {
|
||||
await page.getByLabel('Start date').fill(startDate);
|
||||
}
|
||||
|
||||
if (startTime) {
|
||||
await page.getByLabel('Start time').fill(startTime);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
await page.getByLabel('End date').fill(endDate);
|
||||
}
|
||||
|
||||
if (endTime) {
|
||||
await page.getByLabel('End time').fill(endTime);
|
||||
}
|
||||
|
||||
if (submitChanges) {
|
||||
await page.getByLabel('Submit time bounds').click();
|
||||
} else {
|
||||
await page.getByLabel('Discard changes and close time popup').click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the bounds of the visible conductor in fixed time mode.
|
||||
* Requires that page already has an independent time conductor in view.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} start - The start date in 'YYYY-MM-DD HH:mm:ss.SSSZ' format
|
||||
* @param {string} end - The end date in 'YYYY-MM-DD HH:mm:ss.SSSZ' format
|
||||
*/
|
||||
async function setFixedIndependentTimeConductorBounds(page, { start, end }) {
|
||||
// Activate Independent Time Conductor
|
||||
await page.getByLabel('Enable Independent Time Conductor').click();
|
||||
|
||||
// Bring up the time conductor popup
|
||||
await page.getByLabel('Independent Time Conductor Settings').click();
|
||||
await expect(page.getByLabel('Time Conductor Options')).toBeInViewport();
|
||||
await _setTimeBounds(page, start, end);
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the bounds of the visible conductor in fixed time mode
|
||||
* @private
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} start - The start date in 'YYYY-MM-DD HH:mm:ss.SSSZ' format
|
||||
* @param {string} end - The end date in 'YYYY-MM-DD HH:mm:ss.SSSZ' format
|
||||
*/
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits and asserts that all plot series data on the page
|
||||
* is loaded and drawn.
|
||||
*
|
||||
* In lieu of a better way to detect when a plot is done rendering,
|
||||
* we [attach a class to the '.gl-plot' element](https://github.com/nasa/openmct/blob/5924d7ea95a0c2d4141c602a3c7d0665cb91095f/src/plugins/plot/MctPlot.vue#L27)
|
||||
* once all pending series data has been loaded. The following appAction retrieves
|
||||
* all plots on the page and waits up to the default timeout for the class to be
|
||||
* attached to each plot.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} [timeout] Provide a custom timeout in milliseconds to override the default timeout
|
||||
*/
|
||||
async function waitForPlotsToRender(page, { timeout } = {}) {
|
||||
//eslint-disable-next-line playwright/no-raw-locators
|
||||
const plotLocator = page.locator('.gl-plot');
|
||||
for (const plot of await plotLocator.all()) {
|
||||
await expect(plot).toHaveClass(/js-series-data-loaded/, { timeout });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} PlotPixel
|
||||
* @property {number} r The value of the red channel (0-255)
|
||||
* @property {number} g The value of the green channel (0-255)
|
||||
* @property {number} b The value of the blue channel (0-255)
|
||||
* @property {number} a The value of the alpha channel (0-255)
|
||||
* @property {string} strValue The rgba string value of the pixel
|
||||
*/
|
||||
|
||||
/**
|
||||
* Wait for all plots to render and then retrieve and return an array
|
||||
* of canvas plot pixel data (RGBA values).
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} canvasSelector The selector for the canvas element
|
||||
* @return {Promise<PlotPixel[]>}
|
||||
*/
|
||||
async function getCanvasPixels(page, canvasSelector) {
|
||||
const canvasHandle = await page.evaluateHandle(
|
||||
(canvas) => document.querySelector(canvas),
|
||||
canvasSelector
|
||||
);
|
||||
const canvasContextHandle = await page.evaluateHandle(
|
||||
(canvas) => canvas.getContext('2d'),
|
||||
canvasHandle
|
||||
);
|
||||
|
||||
await waitForPlotsToRender(page);
|
||||
return page.evaluate(
|
||||
([canvas, ctx]) => {
|
||||
// The document canvas is where the plot points and lines are drawn.
|
||||
// The only way to access the canvas is using document (using page.evaluate)
|
||||
/** @type {ImageData} */
|
||||
const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
|
||||
/** @type {number[]} */
|
||||
const imageDataValues = Object.values(data);
|
||||
/** @type {PlotPixel[]} */
|
||||
const plotPixels = [];
|
||||
// Each pixel consists of four values within the ImageData.data array. The for loop iterates by multiples of four.
|
||||
// The values associated with each pixel are R (red), G (green), B (blue), and A (alpha), in that order.
|
||||
for (let i = 0; i < imageDataValues.length; ) {
|
||||
if (imageDataValues[i] > 0) {
|
||||
plotPixels.push({
|
||||
r: imageDataValues[i],
|
||||
g: imageDataValues[i + 1],
|
||||
b: imageDataValues[i + 2],
|
||||
a: imageDataValues[i + 3],
|
||||
strValue: `rgb(${imageDataValues[i]}, ${imageDataValues[i + 1]}, ${
|
||||
imageDataValues[i + 2]
|
||||
}, ${imageDataValues[i + 3]})`
|
||||
});
|
||||
}
|
||||
|
||||
i = i + 4;
|
||||
}
|
||||
|
||||
return plotPixels;
|
||||
},
|
||||
[canvasHandle, canvasContextHandle]
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
createDomainObjectWithDefaults,
|
||||
createExampleTelemetryObject,
|
||||
createNotification,
|
||||
createPlanFromJSON,
|
||||
expandEntireTree,
|
||||
getCanvasPixels,
|
||||
navigateToObjectWithFixedTimeBounds,
|
||||
navigateToObjectWithRealTime,
|
||||
setEndOffset,
|
||||
setFixedIndependentTimeConductorBounds,
|
||||
setFixedTimeMode,
|
||||
setRealTimeMode,
|
||||
setStartOffset,
|
||||
setTimeConductorBounds,
|
||||
waitForPlotsToRender
|
||||
};
|
@ -1,161 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* avpFixtures.js
|
||||
*
|
||||
* @file This module provides custom fixtures specifically tailored for Accessibility, Visual, and Performance (AVP) tests.
|
||||
* These fixtures extend the base functionality of the Playwright fixtures and appActions, and are designed to be
|
||||
* generalized across all plugins. They offer functionalities like scanning for accessibility violations, integrating
|
||||
* with axe-core, and more.
|
||||
*
|
||||
* IMPORTANT NOTE: This fixture file is not intended to be extended further by other fixtures. If you find yourself
|
||||
* needing to do so, please consult the documentation and consider creating a specialized fixture or modifying the
|
||||
* existing ones.
|
||||
*/
|
||||
|
||||
import AxeBuilder from '@axe-core/playwright';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { expect, test } from './pluginFixtures.js';
|
||||
// Constants for repeated values
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const TEST_RESULTS_DIR = path.join(__dirname, './test-results');
|
||||
|
||||
const extendedTest = test.extend({
|
||||
/**
|
||||
* Overrides the default screenshot function to apply default options that should apply to all
|
||||
* screenshots taken in the AVP tests.
|
||||
*
|
||||
* @param {import('@playwright/test').PlaywrightTestArgs} args - The Playwright test arguments.
|
||||
* @param {Function} use - The function to use the page object.
|
||||
* Defaults:
|
||||
* - Disables animations
|
||||
* - Masks the clock indicator
|
||||
* - Masks the time conductor last update time in realtime mode
|
||||
* - Masks the time conductor start bounds in fixed mode
|
||||
* - Masks the time conductor end bounds in fixed mode
|
||||
*/
|
||||
page: async ({ page }, use) => {
|
||||
const playwrightScreenshot = page.screenshot;
|
||||
|
||||
/**
|
||||
* Override the screenshot function to always mask a given set of locators which will always
|
||||
* show variance across screenshots. Defaults may be overridden by passing in options to the
|
||||
* screenshot function.
|
||||
* @param {import('@playwright/test').PageScreenshotOptions} options - The options for the screenshot.
|
||||
* @returns {Promise<Buffer>} Returns the screenshot as a buffer.
|
||||
*/
|
||||
page.screenshot = async function (options = {}) {
|
||||
const mask = [
|
||||
this.getByLabel('Clock Indicator'), // Mask the clock indicator
|
||||
this.getByLabel('Last update'), // Mask the time conductor last update time in realtime mode
|
||||
this.getByLabel('Start bounds'), // Mask the time conductor start bounds in fixed mode
|
||||
this.getByLabel('End bounds') // Mask the time conductor end bounds in fixed mode
|
||||
];
|
||||
|
||||
const result = await playwrightScreenshot.call(this, {
|
||||
animations: 'disabled',
|
||||
mask,
|
||||
...options // Pass through or override any options
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
await use(page);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Writes the accessibility report to the specified path.
|
||||
*
|
||||
* @param {string} reportPath - The path to write the report to.
|
||||
* @param {Object} accessibilityScanResults - The results of the accessibility scan.
|
||||
* @returns {Promise<Object>} The accessibility scan results.
|
||||
* @throws Will throw an error if writing the report fails.
|
||||
*/
|
||||
async function writeAccessibilityReport(reportPath, accessibilityScanResults) {
|
||||
try {
|
||||
await fs.mkdir(path.dirname(reportPath), { recursive: true });
|
||||
const data = JSON.stringify(accessibilityScanResults, null, 2);
|
||||
await fs.writeFile(reportPath, data);
|
||||
console.log(`Accessibility report with violations saved successfully as ${reportPath}`);
|
||||
return accessibilityScanResults;
|
||||
} catch (err) {
|
||||
console.error(`Error writing the accessibility report to file ${reportPath}:`, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans for accessibility violations on a page and writes a report to disk if violations are found.
|
||||
* Automatically asserts that no violations should be present.
|
||||
*
|
||||
* @param {import('playwright').Page} page - The page object from Playwright.
|
||||
* @param {string} testCaseName - The name of the test case.
|
||||
* @param {{ reportName?: string }} [options={}] - The options for the report generation.
|
||||
* @returns {Promise<Object|null>} Returns the accessibility scan results if violations are found, otherwise returns null.
|
||||
*/
|
||||
|
||||
export async function scanForA11yViolations(page, testCaseName, options = {}) {
|
||||
const builder = new AxeBuilder({ page });
|
||||
builder.withTags(['wcag2aa']);
|
||||
// https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md
|
||||
const accessibilityScanResults = await builder.analyze();
|
||||
|
||||
// Assert that no violations should be present
|
||||
expect
|
||||
.soft(
|
||||
accessibilityScanResults.violations,
|
||||
`Accessibility violations found in test case: ${testCaseName}`
|
||||
)
|
||||
.toEqual([]);
|
||||
|
||||
// Check if there are any violations
|
||||
if (accessibilityScanResults.violations.length > 0) {
|
||||
const reportName = options.reportName || testCaseName;
|
||||
const sanitizedReportName = reportName.replace(/\//g, '_');
|
||||
const reportPath = path.join(
|
||||
TEST_RESULTS_DIR,
|
||||
'a11y-json-reports',
|
||||
`${sanitizedReportName}.json`
|
||||
);
|
||||
|
||||
try {
|
||||
await page.screenshot({
|
||||
path: path.join(TEST_RESULTS_DIR, 'a11y-screenshots', `${sanitizedReportName}.png`)
|
||||
});
|
||||
|
||||
return await writeAccessibilityReport(reportPath, accessibilityScanResults);
|
||||
} catch (err) {
|
||||
console.error(`Error writing the accessibility report to file ${reportPath}:`, err);
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
console.log('No accessibility violations found, no report generated.');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export { expect, extendedTest as test };
|
@ -1,129 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* This file is dedicated to extending the base functionality of the `@playwright/test` framework.
|
||||
* The functions in this file should be viewed as temporary or a shim to be removed as the RFEs in
|
||||
* the Playwright GitHub repo are implemented. Functions which serve those RFEs are marked with corresponding
|
||||
* GitHub issues.
|
||||
*/
|
||||
|
||||
import { expect, request, test } from '@playwright/test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
/**
|
||||
* Takes a `ConsoleMessage` and returns a formatted string. Used to enable console log error detection.
|
||||
* @see {@link https://github.com/microsoft/playwright/discussions/11690 Github Discussion}
|
||||
* @private
|
||||
* @param {import('@playwright/test').ConsoleMessage} msg
|
||||
* @returns {string} formatted string with message type, text, url, and line and column numbers
|
||||
*/
|
||||
function _consoleMessageToString(msg) {
|
||||
const { url, lineNumber, columnNumber } = msg.location();
|
||||
|
||||
return `[${msg.type()}] ${msg.text()} at (${url} ${lineNumber}:${columnNumber})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for all animations within the given element and subtrees to finish. Useful when
|
||||
* verifying that css transitions have completed.
|
||||
* @see {@link https://github.com/microsoft/playwright/issues/15660 Github RFE}
|
||||
* @param {import('@playwright/test').Locator} locator
|
||||
* @return {Promise<Animation[]>}
|
||||
*/
|
||||
function waitForAnimations(locator) {
|
||||
return locator.evaluate((element) =>
|
||||
Promise.all(element.getAnimations({ subtree: true }).map((animation) => animation.finished))
|
||||
);
|
||||
}
|
||||
|
||||
const istanbulCLIOutput = fileURLToPath(new URL('.nyc_output', import.meta.url));
|
||||
|
||||
const extendedTest = test.extend({
|
||||
/**
|
||||
* Path to output raw coverage files. Can be overridden in Playwright config file.
|
||||
* @see {@link https://github.com/mxschmitt/playwright-test-coverage Github Example Project}
|
||||
* @constant {string}
|
||||
*/
|
||||
|
||||
coveragePath: [istanbulCLIOutput, { option: true }],
|
||||
/**
|
||||
* Extends the base context class to add codecoverage shim.
|
||||
* @see {@link https://github.com/mxschmitt/playwright-test-coverage Github Project}
|
||||
*/
|
||||
context: async ({ context, coveragePath }, use) => {
|
||||
await context.addInitScript(() =>
|
||||
window.addEventListener('beforeunload', () =>
|
||||
window.collectIstanbulCoverage(JSON.stringify(window.__coverage__))
|
||||
)
|
||||
);
|
||||
await fs.promises.mkdir(coveragePath, { recursive: true });
|
||||
await context.exposeFunction('collectIstanbulCoverage', (coverageJSON) => {
|
||||
if (coverageJSON) {
|
||||
fs.writeFileSync(
|
||||
path.join(coveragePath, `playwright_coverage_${uuid()}.json`),
|
||||
coverageJSON
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await use(context);
|
||||
for (const page of context.pages()) {
|
||||
await page.evaluate(() => {
|
||||
window.collectIstanbulCoverage(JSON.stringify(window.__coverage__));
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* If true, will assert against any console.error calls that occur during the test. Assertions occur
|
||||
* during test teardown (after the test has completed).
|
||||
*
|
||||
* Default: `true`
|
||||
*/
|
||||
failOnConsoleError: [true, { option: true }],
|
||||
/**
|
||||
* 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) => {
|
||||
// Capture any console errors during test execution
|
||||
const messages = [];
|
||||
page.on('console', (msg) => messages.push(msg));
|
||||
|
||||
await use(page);
|
||||
|
||||
// Assert against console errors during teardown
|
||||
if (failOnConsoleError) {
|
||||
messages.forEach((msg) =>
|
||||
// eslint-disable-next-line playwright/no-standalone-expect
|
||||
expect
|
||||
.soft(msg.type(), `Console error detected: ${_consoleMessageToString(msg)}`)
|
||||
.not.toEqual('error')
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export { expect, request, extendedTest as test, waitForAnimations };
|
@ -1,36 +0,0 @@
|
||||
/**
|
||||
* Constants which may be used across all e2e tests.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Time Constants
|
||||
* - Used for overriding the browser clock in tests.
|
||||
*/
|
||||
export const MISSION_TIME = 1732413600000; // Saturday, November 23, 2024 6:00:00 PM GMT-08:00 (Thanksgiving Dinner Time)
|
||||
// Subtracting 30 minutes from MISSION_TIME
|
||||
export const MISSION_TIME_FIXED_START = 1732413600000 - 1800000; // 1732411800000
|
||||
|
||||
// Adding 1 minute to MISSION_TIME
|
||||
export const MISSION_TIME_FIXED_END = 1732413600000 + 60000; // 1732413660000
|
||||
/**
|
||||
* URL Constants
|
||||
* These constants are used for initial navigation in visual tests, in either fixed or realtime mode.
|
||||
* They navigate to the 'My Items' folder at MISSION_TIME.
|
||||
* They set the following url parameters:
|
||||
* - tc.mode - The time conductor mode ('fixed' or 'local')
|
||||
* - tc.startBound - The time conductor start bound (when in fixed mode)
|
||||
* - tc.endBound - The time conductor end bound (when in fixed mode)
|
||||
* - tc.startDelta - The time conductor start delta (when in realtime mode)
|
||||
* - tc.endDelta - The time conductor end delta (when in realtime mode)
|
||||
* - tc.timeSystem - The time conductor time system ('utc')
|
||||
* - view - The view to display ('grid')
|
||||
* - hideInspector - Whether to hide the inspector (true)
|
||||
* - hideTree - Whether to hide the tree (true)
|
||||
* @typedef {string} VisualUrl
|
||||
*/
|
||||
|
||||
/** @type {VisualUrl} */
|
||||
export const VISUAL_FIXED_URL = `./#/browse/mine?tc.mode=fixed&tc.startBound=${MISSION_TIME_FIXED_START}&tc.endBound=${MISSION_TIME_FIXED_END}&tc.timeSystem=utc&view=grid&hideInspector=true&hideTree=true`;
|
||||
/** @type {VisualUrl} */
|
||||
export const VISUAL_REALTIME_URL =
|
||||
'./#/browse/mine?tc.mode=local&tc.timeSystem=utc&view=grid&tc.startDelta=1800000&tc.endDelta=30000&hideTree=true&hideInspector=true';
|
@ -1,71 +0,0 @@
|
||||
class DomainObjectViewProvider {
|
||||
constructor(openmct) {
|
||||
this.key = 'doViewProvider';
|
||||
this.name = 'Domain Object View Provider';
|
||||
this.openmct = openmct;
|
||||
}
|
||||
|
||||
canView(domainObject) {
|
||||
return domainObject.type === 'imageFileInput' || domainObject.type === 'jsonFileInput';
|
||||
}
|
||||
|
||||
view(domainObject, objectPath) {
|
||||
let content;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
const body = domainObject.selectFile.body;
|
||||
const type = typeof body;
|
||||
|
||||
content = document.createElement('div');
|
||||
content.id = 'file-input-type';
|
||||
content.textContent = JSON.stringify(type);
|
||||
element.appendChild(content);
|
||||
},
|
||||
destroy: function (element) {
|
||||
element.removeChild(content);
|
||||
content = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const openmct = window.openmct;
|
||||
|
||||
openmct.types.addType('jsonFileInput', {
|
||||
key: 'jsonFileInput',
|
||||
name: 'JSON File Input Object',
|
||||
creatable: true,
|
||||
form: [
|
||||
{
|
||||
name: 'Upload File',
|
||||
key: 'selectFile',
|
||||
control: 'file-input',
|
||||
required: true,
|
||||
text: 'Select File...',
|
||||
type: 'application/json',
|
||||
property: ['selectFile']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
openmct.types.addType('imageFileInput', {
|
||||
key: 'imageFileInput',
|
||||
name: 'Image File Input Object',
|
||||
creatable: true,
|
||||
form: [
|
||||
{
|
||||
name: 'Upload File',
|
||||
key: 'selectFile',
|
||||
control: 'file-input',
|
||||
required: true,
|
||||
text: 'Select File...',
|
||||
type: 'image/*',
|
||||
property: ['selectFile']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
openmct.objectViews.addProvider(new DomainObjectViewProvider(openmct));
|
||||
});
|
@ -1,30 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
// This should be used to install the non-default Restricted Notebook plugin since it is not installed by default.
|
||||
// e.g.
|
||||
// await page.addInitScript({ path: path.join(__dirname, 'addInitRestrictedNotebook.js') });
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const openmct = window.openmct;
|
||||
openmct.install(openmct.plugins.RestrictedNotebook('CUSTOM_NAME'));
|
||||
});
|
@ -1,27 +0,0 @@
|
||||
(function () {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const PERSISTENCE_KEY = 'persistence-tests';
|
||||
const openmct = window.openmct;
|
||||
|
||||
openmct.objects.addRoot({
|
||||
namespace: PERSISTENCE_KEY,
|
||||
key: PERSISTENCE_KEY
|
||||
});
|
||||
|
||||
openmct.objects.addProvider(PERSISTENCE_KEY, {
|
||||
get(identifier) {
|
||||
if (identifier.key !== PERSISTENCE_KEY) {
|
||||
return undefined;
|
||||
} else {
|
||||
return Promise.resolve({
|
||||
identifier,
|
||||
type: 'folder',
|
||||
name: 'Persistence Testing',
|
||||
location: 'ROOT',
|
||||
composition: []
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
@ -1,280 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { expect } from '../pluginFixtures.js';
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function navigateToFaultManagementWithExample(page) {
|
||||
await page.addInitScript({
|
||||
path: fileURLToPath(new URL('./addInitExampleFaultProvider.js', import.meta.url))
|
||||
});
|
||||
|
||||
await navigateToFaultItemInTree(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function navigateToFaultManagementWithStaticExample(page) {
|
||||
await page.addInitScript({
|
||||
path: fileURLToPath(new URL('./addInitExampleFaultProviderStatic.js', import.meta.url))
|
||||
});
|
||||
|
||||
await navigateToFaultItemInTree(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function navigateToFaultManagementWithoutExample(page) {
|
||||
await page.addInitScript({
|
||||
path: fileURLToPath(new URL('./addInitFaultManagementPlugin.js', import.meta.url))
|
||||
});
|
||||
|
||||
await navigateToFaultItemInTree(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function navigateToFaultItemInTree(page) {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForURL('**/#/browse/mine?**');
|
||||
|
||||
const faultManagementTreeItem = page
|
||||
.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
})
|
||||
.getByRole('treeitem', {
|
||||
name: 'Fault Management'
|
||||
});
|
||||
|
||||
// Navigate to "Fault Management" from the tree
|
||||
await faultManagementTreeItem.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function acknowledgeFault(page, rowNumber) {
|
||||
await openFaultRowMenu(page, rowNumber);
|
||||
await page.getByLabel('Acknowledge', { exact: true }).click();
|
||||
await page.getByLabel('Save').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function shelveMultipleFaults(page, ...nums) {
|
||||
const selectRows = nums.map((num) => {
|
||||
return selectFaultItem(page, num);
|
||||
});
|
||||
await Promise.all(selectRows);
|
||||
|
||||
await page.getByLabel('Shelve selected faults').click();
|
||||
await page.getByLabel('Save').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function acknowledgeMultipleFaults(page, ...nums) {
|
||||
const selectRows = nums.map((num) => {
|
||||
return selectFaultItem(page, num);
|
||||
});
|
||||
await Promise.all(selectRows);
|
||||
|
||||
await page.locator('button:has-text("Acknowledge")').click();
|
||||
await page.getByLabel('Save').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function shelveFault(page, rowNumber) {
|
||||
await openFaultRowMenu(page, rowNumber);
|
||||
await page.locator('.c-menu >> text="Shelve"').click();
|
||||
// Click [aria-label="Save"]
|
||||
await page.getByLabel('Save').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function changeViewTo(page, view) {
|
||||
await page.locator('.c-fault-mgmt__search-row select').first().selectOption(view);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function sortFaultsBy(page, sort) {
|
||||
await page.locator('.c-fault-mgmt__list-header-sortButton select').selectOption(sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function enterSearchTerm(page, term) {
|
||||
await page.locator('.c-fault-mgmt-search [aria-label="Search Input"]').fill(term);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function clearSearch(page) {
|
||||
await enterSearchTerm(page, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function selectFaultItem(page, rowNumber) {
|
||||
await page
|
||||
.getByLabel('Select fault')
|
||||
.nth(rowNumber - 1)
|
||||
.check({
|
||||
// Need force here because checkbox state is changed by an event emitted by the checkbox
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
force: true
|
||||
});
|
||||
await expect(page.getByLabel('Select fault').nth(rowNumber - 1)).toBeChecked();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function getHighestSeverity(page) {
|
||||
const criticalCount = await page.locator('[title=CRITICAL]').count();
|
||||
const warningCount = await page.locator('[title=WARNING]').count();
|
||||
|
||||
if (criticalCount > 0) {
|
||||
return 'CRITICAL';
|
||||
} else if (warningCount > 0) {
|
||||
return 'WARNING';
|
||||
}
|
||||
|
||||
return 'WATCH';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function getLowestSeverity(page) {
|
||||
const warningCount = await page.locator('[title=WARNING]').count();
|
||||
const watchCount = await page.locator('[title=WATCH]').count();
|
||||
|
||||
if (watchCount > 0) {
|
||||
return 'WATCH';
|
||||
} else if (warningCount > 0) {
|
||||
return 'WARNING';
|
||||
}
|
||||
|
||||
return 'CRITICAL';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function getFaultResultCount(page) {
|
||||
const count = await page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').count();
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export function getFault(page, rowNumber) {
|
||||
const fault = page.locator(
|
||||
`.c-faults-list-view-item-body > .c-fault-mgmt__list >> nth=${rowNumber - 1}`
|
||||
);
|
||||
|
||||
return fault;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export function getFaultByName(page, name) {
|
||||
const fault = page.locator(`.c-fault-mgmt__list-faultname:has-text("${name}")`);
|
||||
|
||||
return fault;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function getFaultName(page, rowNumber) {
|
||||
const faultName = await page
|
||||
.locator(`.c-fault-mgmt__list-faultname >> nth=${rowNumber - 1}`)
|
||||
.textContent();
|
||||
|
||||
return faultName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function getFaultSeverity(page, rowNumber) {
|
||||
const faultSeverity = await page
|
||||
.locator(`.c-faults-list-view-item-body .c-fault-mgmt__list-severity >> nth=${rowNumber - 1}`)
|
||||
.getAttribute('title');
|
||||
|
||||
return faultSeverity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function getFaultNamespace(page, rowNumber) {
|
||||
const faultNamespace = await page
|
||||
.locator(`.c-fault-mgmt__list-path >> nth=${rowNumber - 1}`)
|
||||
.textContent();
|
||||
|
||||
return faultNamespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function getFaultTriggerTime(page, rowNumber) {
|
||||
const faultTriggerTime = await page
|
||||
.locator(`.c-fault-mgmt__list-trigTime >> nth=${rowNumber - 1} >> .c-fault-mgmt-item__value`)
|
||||
.textContent();
|
||||
|
||||
return faultTriggerTime.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function openFaultRowMenu(page, rowNumber) {
|
||||
// select
|
||||
await page
|
||||
.getByLabel('Disposition actions')
|
||||
.nth(rowNumber - 1)
|
||||
.click();
|
||||
}
|
@ -1,164 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { createDomainObjectWithDefaults } from '../appActions.js';
|
||||
|
||||
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
|
||||
const CUSTOM_NAME = 'CUSTOM_NAME';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} text
|
||||
*/
|
||||
async function enterTextEntry(page, text) {
|
||||
await addNotebookEntry(page);
|
||||
await enterTextInLastEntry(page, text);
|
||||
await commitEntry(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function addNotebookEntry(page) {
|
||||
await page.locator(NOTEBOOK_DROP_AREA).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function enterTextInLastEntry(page, text) {
|
||||
await page.getByLabel('Notebook Entry Input').last().fill(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function dragAndDropEmbed(page, notebookObject) {
|
||||
// Create example telemetry object
|
||||
const swg = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator'
|
||||
});
|
||||
// Navigate to notebook
|
||||
await page.goto(notebookObject.url);
|
||||
// Expand the tree to reveal the notebook
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
// Drag and drop the SWG into the notebook
|
||||
await page.getByLabel(`Navigate to ${swg.name}`).dragTo(page.locator(NOTEBOOK_DROP_AREA));
|
||||
await commitEntry(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function commitEntry(page) {
|
||||
//Click the Commit Entry button
|
||||
await page.locator('.c-ne__save-button > button').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function startAndAddRestrictedNotebookObject(page) {
|
||||
await page.addInitScript({
|
||||
path: fileURLToPath(new URL('./addInitRestrictedNotebook.js', import.meta.url))
|
||||
});
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForURL('**/browse/mine?**');
|
||||
|
||||
return createDomainObjectWithDefaults(page, {
|
||||
type: CUSTOM_NAME,
|
||||
name: 'Restricted Test Notebook'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function lockPage(page) {
|
||||
// Click the Commit Entries button
|
||||
await page.getByLabel('Commit Entries').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 page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export {
|
||||
addNotebookEntry,
|
||||
commitEntry,
|
||||
createNotebookAndEntry,
|
||||
createNotebookEntryAndTags,
|
||||
dragAndDropEmbed,
|
||||
enterTextEntry,
|
||||
enterTextInLastEntry,
|
||||
lockPage,
|
||||
startAndAddRestrictedNotebookObject
|
||||
};
|
@ -1,238 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../appActions.js';
|
||||
import { expect } from '../pluginFixtures.js';
|
||||
|
||||
/**
|
||||
* Asserts that the number of activities in the plan view matches the number of
|
||||
* activities in the plan data within the specified time bounds. Performs an assertion
|
||||
* for each activity in the plan data per group, using the earliest activity's
|
||||
* start time as the start bound and the current activity's end time as the end bound.
|
||||
* @param {import('@playwright/test').Page} page the page
|
||||
* @param {Object} plan The raw plan json to assert against
|
||||
* @param {string} planObjectUrl The URL of the object to assert against (plan or gantt chart)
|
||||
*/
|
||||
export async function assertPlanActivities(page, plan, planObjectUrl) {
|
||||
const groups = Object.keys(plan);
|
||||
for (const group of groups) {
|
||||
for (let i = 0; i < plan[group].length; i++) {
|
||||
// Set the startBound to the start time of the first activity in the group
|
||||
const startBound = plan[group][0].start;
|
||||
// Set the endBound to the end time of the current activity
|
||||
let endBound = plan[group][i].end;
|
||||
if (endBound === startBound) {
|
||||
// Prevent oddities with setting start and end bound equal
|
||||
// via URL params
|
||||
endBound += 1;
|
||||
}
|
||||
|
||||
// Switch to fixed time mode with all plan events within the bounds
|
||||
await page.goto(
|
||||
`${planObjectUrl}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=plan.view`
|
||||
);
|
||||
|
||||
// Assert that the number of activities in the plan view matches the number of
|
||||
// activities in the plan data within the specified time bounds
|
||||
await expect(page.locator('.activity-bounds')).toHaveCount(
|
||||
Object.values(plan)
|
||||
.flat()
|
||||
.filter((event) =>
|
||||
activitiesWithinTimeBounds(event.start, event.end, startBound, endBound)
|
||||
).length
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the activities time bounds overlap, false otherwise.
|
||||
* @param {number} start1 the start time of the first activity
|
||||
* @param {number} end1 the end time of the first activity
|
||||
* @param {number} start2 the start time of the second activity
|
||||
* @param {number} end2 the end time of the second activity
|
||||
* @returns {boolean} true if the activities overlap, false otherwise
|
||||
*/
|
||||
function activitiesWithinTimeBounds(start1, end1, start2, end2) {
|
||||
return (
|
||||
(start1 >= start2 && start1 <= end2) ||
|
||||
(end1 >= start2 && end1 <= end2) ||
|
||||
(start2 >= start1 && start2 <= end1) ||
|
||||
(end2 >= start1 && end2 <= end1)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the swim lanes / groups in the plan view matches the order of
|
||||
* groups in the plan data.
|
||||
* @param {import('@playwright/test').Page} page the page
|
||||
* @param {Object} plan The raw plan json to assert against
|
||||
* @param {string} objectUrl The URL of the object to assert against (plan or gantt chart)
|
||||
*/
|
||||
export async function assertPlanOrderedSwimLanes(page, plan, objectUrl) {
|
||||
// Switch to the plan view
|
||||
await page.goto(`${objectUrl}?view=plan.view`);
|
||||
const planGroups = await page
|
||||
.locator('.c-plan__contents > div > .c-swimlane__lane-label .c-object-label__name')
|
||||
.all();
|
||||
|
||||
const groups = plan.Groups;
|
||||
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
// Assert that the order of groups in the plan view matches the order of
|
||||
// groups in the plan data
|
||||
const groupName = planGroups[i];
|
||||
await expect(groupName).toHaveText(groups[i].name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the plan view, switch to fixed time mode,
|
||||
* and set the bounds to span all activities.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {Object} planJson
|
||||
* @param {string} planObjectUrl
|
||||
*/
|
||||
export async function setBoundsToSpanAllActivities(page, planJson, planObjectUrl) {
|
||||
// Get the earliest start value
|
||||
const start = getEarliestStartTime(planJson);
|
||||
// Get the latest end value
|
||||
const end = getLatestEndTime(planJson);
|
||||
// Set the start and end bounds to the earliest start and latest end
|
||||
await page.goto(
|
||||
`${planObjectUrl}?tc.mode=fixed&tc.startBound=${start}&tc.endBound=${end}&tc.timeSystem=utc&view=plan.view`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} planJson
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getEarliestStartTime(planJson) {
|
||||
const activities = Object.values(planJson).flat();
|
||||
return Math.min(...activities.map((activity) => activity.start));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} planJson
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getLatestEndTime(planJson) {
|
||||
const activities = Object.values(planJson).flat();
|
||||
return Math.max(...activities.map((activity) => activity.end));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} planJson
|
||||
* @returns {object}
|
||||
*/
|
||||
export function getFirstActivity(planJson) {
|
||||
const groups = Object.keys(planJson);
|
||||
const firstGroupKey = groups[0];
|
||||
const firstGroupItems = planJson[firstGroupKey];
|
||||
return firstGroupItems[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the Open MCT API to set the status of a plan to 'draft'.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {import('../../appActions').CreatedObjectInfo} plan
|
||||
*/
|
||||
export async function setDraftStatusForPlan(page, plan) {
|
||||
await page.evaluate(async (planObject) => {
|
||||
await window.openmct.status.set(planObject.uuid, 'draft');
|
||||
}, plan);
|
||||
}
|
||||
|
||||
export async function addPlanGetInterceptor(page) {
|
||||
await page.waitForLoadState('load');
|
||||
await page.evaluate(async () => {
|
||||
await window.openmct.objects.addGetInterceptor({
|
||||
appliesTo: (identifier, domainObject) => {
|
||||
return domainObject && domainObject.type === 'plan';
|
||||
},
|
||||
invoke: (identifier, object) => {
|
||||
if (object) {
|
||||
object.sourceMap = {
|
||||
orderedGroups: 'Groups'
|
||||
};
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Plan from JSON and add it to a Timelist and Navigate to the Plan view
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function createTimelistWithPlanAndSetActivityInProgress(page, planJson) {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const timelist = await createDomainObjectWithDefaults(page, {
|
||||
name: 'Time List',
|
||||
type: 'Time List'
|
||||
});
|
||||
|
||||
await createPlanFromJSON(page, {
|
||||
name: 'Test Plan',
|
||||
json: planJson,
|
||||
parent: timelist.uuid
|
||||
});
|
||||
|
||||
// Ensure that all activities are shown in the expanded view
|
||||
const groups = Object.keys(planJson);
|
||||
const firstGroupKey = groups[0];
|
||||
const firstGroupItems = planJson[firstGroupKey];
|
||||
const firstActivityForPlan = firstGroupItems[0];
|
||||
const lastActivity = firstGroupItems[firstGroupItems.length - 1];
|
||||
const startBound = firstActivityForPlan.start;
|
||||
const endBound = lastActivity.end;
|
||||
|
||||
// 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`
|
||||
);
|
||||
|
||||
// Change the object to edit mode
|
||||
await page.getByRole('button', { name: 'Edit Object' }).click();
|
||||
|
||||
// Find the display properties section in the inspector
|
||||
await page.getByRole('tab', { name: 'View Properties' }).click();
|
||||
// Switch to expanded view and save the setting
|
||||
await page.getByLabel('Display Style').selectOption({ label: 'Expanded' });
|
||||
|
||||
// Click on the "Save" button
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
const anActivity = page.getByRole('row').nth(0);
|
||||
|
||||
// Set the activity to in progress
|
||||
await anActivity.click();
|
||||
await page.getByRole('tab', { name: 'Activity' }).click();
|
||||
await page.getByLabel('Activity Status', { exact: true }).selectOption({ label: 'In progress' });
|
||||
}
|
@ -1,189 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { waitForPlotsToRender } from '../appActions.js';
|
||||
import { expect } from '../pluginFixtures.js';
|
||||
|
||||
/**
|
||||
* Given a canvas and a set of points, tags the points on the canvas.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {HTMLCanvasElement} canvas a telemetry item with a plot
|
||||
* @param {number} xEnd a telemetry item with a plot
|
||||
* @param {number} yEnd a telemetry item with a plot
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function createTags({ page, canvas, xEnd = 700, yEnd = 520 }) {
|
||||
await canvas.hover({ trial: true });
|
||||
|
||||
//Alt+Shift Drag Start to select some points to tag
|
||||
await page.keyboard.down('Alt');
|
||||
await page.keyboard.down('Shift');
|
||||
|
||||
await canvas.dragTo(canvas, {
|
||||
sourcePosition: {
|
||||
x: 1,
|
||||
y: 1
|
||||
},
|
||||
targetPosition: {
|
||||
x: xEnd,
|
||||
y: yEnd
|
||||
}
|
||||
});
|
||||
|
||||
//Alt Drag End
|
||||
await page.keyboard.up('Alt');
|
||||
await page.keyboard.up('Shift');
|
||||
|
||||
//Wait for canvas to stabilize.
|
||||
await canvas.hover({ trial: true });
|
||||
|
||||
// add some tags
|
||||
await page.getByText('Annotations').click();
|
||||
await page.getByRole('button', { name: /Add Tag/ }).click();
|
||||
await page.getByPlaceholder('Type to select tag').click();
|
||||
await page.getByText('Driving').click();
|
||||
|
||||
await page.getByRole('button', { name: /Add Tag/ }).click();
|
||||
await page.getByPlaceholder('Type to select tag').click();
|
||||
await page.getByText('Science').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a telemetry item (e.g., a Sine Wave Generator) with a plot, tests that the plot can be tagged.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {import('../../../../appActions').CreatedObjectInfo} telemetryItem a telemetry item with a plot
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function testTelemetryItem(page, telemetryItem) {
|
||||
// Check that telemetry item also received the tag
|
||||
await page.goto(telemetryItem.url);
|
||||
|
||||
await expect(page.getByText('No tags to display for this item')).toBeVisible();
|
||||
|
||||
const canvas = page.locator('canvas').nth(1);
|
||||
//Wait for canvas to stabilize.
|
||||
await waitForPlotsToRender(page);
|
||||
|
||||
await expect(canvas).toBeInViewport();
|
||||
await canvas.hover({ trial: true });
|
||||
|
||||
// click on the tagged plot point
|
||||
await canvas.click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 100
|
||||
}
|
||||
});
|
||||
|
||||
await expect(page.getByText('Science')).toBeVisible();
|
||||
await expect(page.getByText('Driving')).toBeHidden();
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a page, tests that tags are searchable, deletable, and persist across reloads.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function basicTagsTests(page) {
|
||||
// Search for Driving
|
||||
await page.getByRole('searchbox', { name: 'Search Input' }).click();
|
||||
|
||||
// Clicking elsewhere should cause annotation selection to be cleared
|
||||
await expect(page.getByText('No tags to display for this item')).toBeVisible();
|
||||
//
|
||||
await page.getByRole('searchbox', { name: 'Search Input' }).fill('driv');
|
||||
|
||||
// Always click on the first Sine Wave result
|
||||
await page
|
||||
.getByLabel('Search Result')
|
||||
.getByText(/Sine Wave/)
|
||||
.first()
|
||||
.click();
|
||||
|
||||
// Delete Driving Tag
|
||||
await page.hover('[aria-label="Tag"]:has-text("Driving")');
|
||||
await page.locator('[aria-label="Remove tag Driving"]').click();
|
||||
|
||||
// Search for Science Tag
|
||||
await page.getByRole('searchbox', { name: 'Search Input' }).click();
|
||||
await page.getByRole('searchbox', { name: 'Search Input' }).fill('sc');
|
||||
|
||||
//Expect Science Tag to be present and and Driving Tags to be deleted
|
||||
await expect(page.getByLabel('Search Result').first()).toContainText('Science');
|
||||
await expect(page.getByLabel('Search Result').first()).not.toContainText('Driving');
|
||||
|
||||
// Search for Driving Tag and expect nothing found
|
||||
await page.getByRole('searchbox', { name: 'Search Input' }).click();
|
||||
await page.getByRole('searchbox', { name: 'Search Input' }).fill('driv');
|
||||
await expect(page.getByText('No results found')).toBeVisible();
|
||||
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
|
||||
await waitForPlotsToRender(page);
|
||||
|
||||
//Navigate to the Inspector and check that all tags have been removed
|
||||
await expect(page.getByRole('tab', { name: 'Annotations' })).not.toHaveClass(/is-current/);
|
||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
await expect(page.getByRole('tab', { name: 'Annotations' })).toHaveClass(/is-current/);
|
||||
await expect(page.getByText('No tags to display for this item')).toBeVisible();
|
||||
|
||||
const canvas = page.locator('canvas').nth(1);
|
||||
// click on the tagged plot point
|
||||
await canvas.click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 100
|
||||
}
|
||||
});
|
||||
|
||||
//Expect Science to be visible but Driving to be hidden
|
||||
await expect(page.getByText('Science')).toBeVisible();
|
||||
await expect(page.getByText('Driving')).toBeHidden();
|
||||
|
||||
//Click elsewhere
|
||||
await page.locator('body').click();
|
||||
//Click on tagged plot point again
|
||||
await canvas.click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 100
|
||||
}
|
||||
});
|
||||
|
||||
// Add Driving Tag again
|
||||
await page.getByText('Annotations').click();
|
||||
await page.getByRole('button', { name: /Add Tag/ }).click();
|
||||
await page.getByPlaceholder('Type to select tag').click();
|
||||
await page.getByText('Driving').click();
|
||||
|
||||
//Science and Driving Tags should be visible
|
||||
await expect(page.getByText('Science')).toBeVisible();
|
||||
await expect(page.getByText('Driving')).toBeVisible();
|
||||
|
||||
// Delete Driving Tag again
|
||||
await page.hover('[aria-label="Tag"]:has-text("Driving")');
|
||||
await page.locator('[aria-label="Remove tag Driving"]').click();
|
||||
|
||||
//Science Tag should be visible and Driving Tag should be hidden
|
||||
await expect(page.getByText('Science')).toBeVisible();
|
||||
await expect(page.getByText('Driving')).toBeHidden();
|
||||
}
|
@ -1,104 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { expect } from '../pluginFixtures.js';
|
||||
|
||||
/**
|
||||
* Converts a hex color value to its RGB equivalent.
|
||||
*
|
||||
* @param {string} hex - The hex color value. i.e. '#5b0f00'
|
||||
* @returns {string} The RGB equivalent of the hex color.
|
||||
*/
|
||||
function hexToRGB(hex) {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result
|
||||
? `rgb(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)})`
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the background and text color of a given element.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page - The Playwright page object.
|
||||
* @param {string} borderColorHex - The hex value of the border color to set, or 'No Style'.
|
||||
* @param {string} backgroundColorHex - The hex value of the background color to set, or 'No Style'.
|
||||
* @param {string} textColorHex - The hex value of the text color to set, or 'No Style'.
|
||||
* @param {import('@playwright/test').Locator} locator - The Playwright locator for the element whose style is to be set.
|
||||
*/
|
||||
async function setStyles(page, borderColorHex, backgroundColorHex, textColorHex, locator) {
|
||||
await locator.click(); // Assuming the locator is clickable and opens the style setting UI
|
||||
await page.getByLabel('Set border color').click();
|
||||
await page.getByLabel(borderColorHex).click();
|
||||
await page.getByLabel('Set background color').click();
|
||||
await page.getByLabel(backgroundColorHex).click();
|
||||
await page.getByLabel('Set text color').click();
|
||||
await page.getByLabel(textColorHex).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the styles of an element match the expected values.
|
||||
*
|
||||
* @param {string} expectedBorderColor - The expected border color in RGB format. Default is '#e6b8af' or 'rgb(230, 184, 175)'
|
||||
* @param {string} expectedBackgroundColor - The expected background color in RGB format.
|
||||
* @param {string} expectedTextColor - The expected text color in RGB format. Default is #aaaaaa or 'rgb(170, 170, 170)'
|
||||
* @param {import('@playwright/test').Locator} locator - The Playwright locator for the element whose style is to be checked.
|
||||
*/
|
||||
async function checkStyles(
|
||||
expectedBorderColor,
|
||||
expectedBackgroundColor,
|
||||
expectedTextColor,
|
||||
locator
|
||||
) {
|
||||
const layoutStyles = await locator.evaluate((el) => {
|
||||
return {
|
||||
border: window.getComputedStyle(el).getPropertyValue('border-top-color'), //infer the left, right, and bottom
|
||||
background: window.getComputedStyle(el).getPropertyValue('background-color'),
|
||||
fontColor: window.getComputedStyle(el).getPropertyValue('color')
|
||||
};
|
||||
});
|
||||
expect(layoutStyles.border).toContain(expectedBorderColor);
|
||||
expect(layoutStyles.background).toContain(expectedBackgroundColor);
|
||||
expect(layoutStyles.fontColor).toContain(expectedTextColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the font Styles of an element match the expected values.
|
||||
*
|
||||
* @param {string} expectedFontSize - The expected font size in '72px' format. Default is 'Default'
|
||||
* @param {string} expectedFontWeight - The expected font Type. Format as '700' for bold. Default is 'Default'
|
||||
* @param {string} expectedFontFamily - The expected font Type. Format as "\"Andale Mono\", sans-serif". Default is 'Default'
|
||||
* @param {import('@playwright/test').Locator} locator - The Playwright locator for the element whose style is to be checked.
|
||||
*/
|
||||
async function checkFontStyles(expectedFontSize, expectedFontWeight, expectedFontFamily, locator) {
|
||||
const layoutStyles = await locator.evaluate((el) => {
|
||||
return {
|
||||
fontSize: window.getComputedStyle(el).getPropertyValue('font-size'),
|
||||
fontWeight: window.getComputedStyle(el).getPropertyValue('font-weight'),
|
||||
fontFamily: window.getComputedStyle(el).getPropertyValue('font-family')
|
||||
};
|
||||
});
|
||||
expect(layoutStyles.fontSize).toContain(expectedFontSize);
|
||||
expect(layoutStyles.fontWeight).toContain(expectedFontWeight);
|
||||
expect(layoutStyles.fontFamily).toContain(expectedFontFamily);
|
||||
}
|
||||
|
||||
export { checkFontStyles, checkStyles, hexToRGB, setStyles };
|
@ -1,29 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
// This should be used to install the Darkmatter theme for Open MCT.
|
||||
// e.g.
|
||||
// await page.addInitScript({ path: path.join(__dirname, 'useDarkmatterTheme.js') });
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const openmct = window.openmct;
|
||||
openmct.install(openmct.plugins.DarkmatterTheme());
|
||||
});
|
@ -1,8 +0,0 @@
|
||||
// Import everything from the specific fixture files
|
||||
import * as appActions from './appActions.js';
|
||||
import * as avpFixtures from './avpFixtures.js';
|
||||
import * as baseFixtures from './baseFixtures.js';
|
||||
import * as pluginFixtures from './pluginFixtures.js';
|
||||
|
||||
// Export these as named exports
|
||||
export { appActions, avpFixtures, baseFixtures, pluginFixtures };
|
1449
e2e/package-lock.json
generated
1449
e2e/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,27 +0,0 @@
|
||||
{
|
||||
"name": "openmct-e2e",
|
||||
"version": "4.0.0-next",
|
||||
"description": "The Open MCT e2e framework",
|
||||
"type": "module",
|
||||
"module": "index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "npx playwright test",
|
||||
"test:visual": "percy exec"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@percy/cli": "1.27.4",
|
||||
"@percy/playwright": "1.0.4",
|
||||
"@playwright/test": "1.45.2",
|
||||
"@axe-core/playwright": "4.8.5"
|
||||
},
|
||||
"author": {
|
||||
"name": "National Aeronautics and Space Administration",
|
||||
"url": "https://www.nasa.gov"
|
||||
},
|
||||
"license": "Apache-2.0"
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { devices } from '@playwright/test';
|
||||
import { fileURLToPath } from 'url';
|
||||
const MAX_FAILURES = 5;
|
||||
const NUM_WORKERS = 2;
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 2, //Retries 2 times for a total of 3 runs. When running sharded and with max-failures=5, this should ensure that flake is managed without failing the full suite
|
||||
testDir: 'tests',
|
||||
grepInvert: /@mobile/, //Ignore mobile tests
|
||||
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-performance.config.js
|
||||
timeout: 60 * 1000,
|
||||
webServer: {
|
||||
command: 'npm run start:coverage',
|
||||
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.
|
||||
},
|
||||
maxFailures: MAX_FAILURES, //Limits failures to 5 to reduce CI Waste
|
||||
workers: NUM_WORKERS, //Limit to 2 for CircleCI Agent
|
||||
use: {
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: true,
|
||||
ignoreHTTPSErrors: true,
|
||||
screenshot: 'only-on-failure',
|
||||
trace: 'on-first-retry',
|
||||
video: 'off',
|
||||
// @ts-ignore - custom configuration option for nyc codecoverage output path
|
||||
coveragePath: fileURLToPath(new URL('../.nyc_output', import.meta.url))
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chrome',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
use: {
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'MMOC',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
viewport: {
|
||||
width: 2560,
|
||||
height: 1440
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'firefox'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'chrome-beta', //Only Chrome Beta is available on ubuntu -- not chrome canary
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome-beta'
|
||||
}
|
||||
}
|
||||
],
|
||||
reporter: [
|
||||
['list'],
|
||||
[
|
||||
'html',
|
||||
{
|
||||
open: 'never',
|
||||
outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
|
||||
}
|
||||
],
|
||||
['junit', { outputFile: '../test-results/results.xml' }]
|
||||
]
|
||||
};
|
||||
|
||||
export default config;
|
@ -1,89 +0,0 @@
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
import { fileURLToPath } from 'url';
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 0,
|
||||
testDir: 'tests',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
testIgnore: '**/*.perf.spec.js',
|
||||
timeout: 30 * 1000,
|
||||
webServer: {
|
||||
command: 'npm run start:coverage',
|
||||
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 120 * 1000,
|
||||
reuseExistingServer: true
|
||||
},
|
||||
workers: 1,
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: false,
|
||||
ignoreHTTPSErrors: true,
|
||||
screenshot: 'only-on-failure',
|
||||
trace: 'retain-on-failure',
|
||||
video: 'off'
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chrome',
|
||||
use: {
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'MMOC',
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
viewport: {
|
||||
width: 2560,
|
||||
height: 1440
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'safari',
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'webkit'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'firefox'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'canary',
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome-canary' //Note this is not available in ubuntu/CircleCI
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'chrome-beta',
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome-beta'
|
||||
}
|
||||
}
|
||||
],
|
||||
reporter: [
|
||||
['list'],
|
||||
[
|
||||
'html',
|
||||
{
|
||||
open: 'on-failure',
|
||||
outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
|
||||
}
|
||||
]
|
||||
]
|
||||
};
|
||||
|
||||
export default config;
|
@ -1,72 +0,0 @@
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
import { devices } from '@playwright/test';
|
||||
const MAX_FAILURES = 5;
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 1, //Retries 2 times for a total of 3 runs. When running sharded and with max-failures=5, this should ensure that flake is managed without failing the full suite
|
||||
testDir: 'tests',
|
||||
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-performance.config.js
|
||||
timeout: 30 * 1000,
|
||||
webServer: {
|
||||
command: 'npm run start:coverage',
|
||||
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.
|
||||
},
|
||||
maxFailures: MAX_FAILURES, //Limits failures to 5 to reduce CI Waste
|
||||
workers: 1, //Limit to 1 due to resource constraints similar to https://github.com/percy/cli/discussions/1067
|
||||
|
||||
use: {
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: true,
|
||||
ignoreHTTPSErrors: true,
|
||||
screenshot: 'only-on-failure',
|
||||
trace: 'on-first-retry',
|
||||
video: 'off',
|
||||
// @ts-ignore - custom configuration option for nyc codecoverage output path
|
||||
coveragePath: fileURLToPath(new URL('../.nyc_output', import.meta.url))
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'ipad',
|
||||
grep: /@mobile/,
|
||||
use: {
|
||||
storageState: fileURLToPath(
|
||||
new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)
|
||||
),
|
||||
browserName: 'webkit',
|
||||
...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'iphone',
|
||||
grep: /@mobile/,
|
||||
use: {
|
||||
storageState: fileURLToPath(
|
||||
new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)
|
||||
),
|
||||
browserName: 'webkit',
|
||||
...devices['iPhone 14 Pro'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
|
||||
}
|
||||
}
|
||||
],
|
||||
reporter: [
|
||||
['list'],
|
||||
[
|
||||
'html',
|
||||
{
|
||||
open: 'never',
|
||||
outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
|
||||
}
|
||||
],
|
||||
['junit', { outputFile: '../test-results/results.xml' }]
|
||||
]
|
||||
};
|
||||
|
||||
export default config;
|
@ -1,43 +0,0 @@
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
import { fileURLToPath } from 'url';
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 1, //Only for debugging purposes for trace: 'on-first-retry'
|
||||
testDir: 'tests/performance/',
|
||||
testMatch: '*.contract.perf.spec.js', //Run everything except contract tests which require marks in dev mode
|
||||
timeout: 60 * 1000,
|
||||
workers: 1, //Only run in serial with 1 worker
|
||||
webServer: {
|
||||
command: 'npm run start', //need development mode for performance.marks and others
|
||||
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: false
|
||||
},
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: true,
|
||||
ignoreHTTPSErrors: false, //HTTP performance varies!
|
||||
screenshot: 'off',
|
||||
trace: 'on-first-retry',
|
||||
video: 'off'
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chrome',
|
||||
testIgnore: '*.memory.perf.spec.js', //Do not run memory tests without proper flags. Shouldn't get here
|
||||
use: {
|
||||
browserName: 'chromium'
|
||||
}
|
||||
}
|
||||
],
|
||||
reporter: [
|
||||
['list'],
|
||||
['junit', { outputFile: '../test-results/results.xml' }],
|
||||
['json', { outputFile: '../test-results/results.json' }]
|
||||
]
|
||||
};
|
||||
|
||||
export default config;
|
@ -1,60 +0,0 @@
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
import { fileURLToPath } from 'url';
|
||||
/** @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
|
||||
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
|
||||
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' }]
|
||||
]
|
||||
};
|
||||
|
||||
export default config;
|
@ -1,61 +0,0 @@
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
import { fileURLToPath } from 'url';
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig<{ theme: string }>} */
|
||||
const config = {
|
||||
retries: 0, // Visual tests should never retry due to snapshot comparison errors. Leaving as a shim
|
||||
testDir: 'tests/visual-a11y',
|
||||
testMatch: '**/*.visual.spec.js', // only run visual tests
|
||||
timeout: 60 * 1000,
|
||||
workers: 1, //Lower stress on Circle CI Agent for Visual tests https://github.com/percy/cli/discussions/1067
|
||||
webServer: {
|
||||
command: 'npm run start:coverage',
|
||||
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.
|
||||
},
|
||||
use: {
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: true, // this needs to remain headless to avoid visual changes due to GPU rendering in headed browsers
|
||||
ignoreHTTPSErrors: true,
|
||||
screenshot: 'only-on-failure',
|
||||
trace: 'on-first-retry',
|
||||
video: 'off'
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chrome',
|
||||
use: {
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'chrome-snow-theme', //Runs the same visual tests but with snow-theme enabled
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
theme: 'snow'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'darkmatter-theme', //Runs the same visual tests but with darkmatter-theme
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
theme: 'darkmatter'
|
||||
}
|
||||
}
|
||||
],
|
||||
reporter: [
|
||||
['list'],
|
||||
['junit', { outputFile: '../test-results/results.xml' }],
|
||||
[
|
||||
'html',
|
||||
{
|
||||
open: 'on-failure',
|
||||
outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
|
||||
}
|
||||
]
|
||||
]
|
||||
};
|
||||
|
||||
export default config;
|
@ -1,71 +0,0 @@
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
import { devices } from '@playwright/test';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 0, //Retries are not needed with watch mode
|
||||
testDir: 'tests',
|
||||
timeout: 60 * 1000,
|
||||
webServer: {
|
||||
command: 'npm run start', //Start in dev mode for hot reloading
|
||||
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.
|
||||
},
|
||||
workers: '75%', //Limit to 75% of the CPU to support running with dev server
|
||||
use: {
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: true,
|
||||
ignoreHTTPSErrors: true,
|
||||
screenshot: 'only-on-failure',
|
||||
trace: 'on-first-retry',
|
||||
video: 'off'
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chrome',
|
||||
testMatch: '**/*.spec.js', // run all tests
|
||||
use: {
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'ipad',
|
||||
grep: /@mobile/,
|
||||
use: {
|
||||
storageState: fileURLToPath(
|
||||
new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)
|
||||
),
|
||||
browserName: 'webkit',
|
||||
...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'iphone',
|
||||
grep: /@mobile/,
|
||||
use: {
|
||||
storageState: fileURLToPath(
|
||||
new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)
|
||||
),
|
||||
browserName: 'webkit',
|
||||
...devices['iPhone 14 Pro'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
|
||||
}
|
||||
}
|
||||
],
|
||||
reporter: [
|
||||
['list'],
|
||||
[
|
||||
'html',
|
||||
{
|
||||
open: 'never',
|
||||
outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
|
||||
}
|
||||
],
|
||||
['junit', { outputFile: '../test-results/results.xml' }]
|
||||
]
|
||||
};
|
||||
|
||||
export default config;
|
@ -1,165 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* The file contains custom fixtures which extend the base functionality of the Playwright fixtures
|
||||
* and appActions. These fixtures should be generalized across all plugins.
|
||||
*/
|
||||
|
||||
// import { createDomainObjectWithDefaults } from './appActions.js';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { expect, request, test } from './baseFixtures.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} ObjectCreateOptions
|
||||
* @property {string} type
|
||||
* @property {string} name
|
||||
*/
|
||||
|
||||
/**
|
||||
* **NOTE: This feature is a work-in-progress and should not currently be used.**
|
||||
*
|
||||
* Used to create a new domain object as a part of getOrCreateDomainObject.
|
||||
* @type {Map<string, string>}
|
||||
*/
|
||||
// const createdObjects = new Map();
|
||||
|
||||
/**
|
||||
* 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
|
||||
* about object creation, while providing a consistent interface to retrieving objects in a persistentContext.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {ObjectCreateOptions} options
|
||||
* @returns {Promise<string>} uuid of the domain object
|
||||
*/
|
||||
// async function getOrCreateDomainObject(page, options) {
|
||||
// const { type, name } = options;
|
||||
// const objectName = name ? `${type}:${name}` : type;
|
||||
|
||||
// if (createdObjects.has(objectName)) {
|
||||
// return createdObjects.get(objectName);
|
||||
// }
|
||||
|
||||
// await createDomainObjectWithDefaults(page, type, name);
|
||||
|
||||
// const uuid = getHashUrlToDomainObject(page);
|
||||
|
||||
// createdObjects.set(objectName, uuid);
|
||||
|
||||
// return uuid;
|
||||
// }
|
||||
|
||||
/**
|
||||
* **NOTE: This feature is a work-in-progress and should not currently be used.**
|
||||
*
|
||||
* If provided, these options will be used to get or create the desired domain object before
|
||||
* any tests or test hooks have run.
|
||||
* The `uuid` of the `domainObject` will then be available to use within the scoped tests.
|
||||
*
|
||||
* ### Example:
|
||||
* ```js
|
||||
* test.describe("My test suite", () => {
|
||||
* test.use({ objectCreateOptions: { type: "Telemetry Table", name: "My Telemetry Table" }});
|
||||
* test("'My Telemetry Table' is created and provides a uuid", async ({ page, domainObject }) => {
|
||||
* const { uuid } = domainObject;
|
||||
* expect(uuid).toBeDefined();
|
||||
* }))
|
||||
* });
|
||||
* ```
|
||||
* @type {ObjectCreateOptions}
|
||||
*/
|
||||
// const objectCreateOptions = null;
|
||||
|
||||
/**
|
||||
* The default theme for VIPER and Open MCT is the 'espresso' theme. Overriding this value with 'snow' in our playwright config.js
|
||||
* will override the default theme by injecting the 'snow' theme on launch.
|
||||
*
|
||||
* ### Example:
|
||||
* ```js
|
||||
* projects: [
|
||||
* {
|
||||
* name: 'chrome-snow-theme',
|
||||
* use: {
|
||||
* browserName: 'chromium',
|
||||
* theme: 'snow'
|
||||
* ```
|
||||
* @type {'snow' | 'espresso'}
|
||||
*/
|
||||
const theme = 'espresso';
|
||||
|
||||
/**
|
||||
* The name of the "My Items" folder in the domain object tree.
|
||||
*
|
||||
* Default: `"My Items"`
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
const myItemsFolderName = 'My Items';
|
||||
|
||||
const extendedTest = test.extend({
|
||||
// This should follow in the Project's configuration. Can be set to 'snow' in playwright config.js
|
||||
theme: [theme, { option: true }],
|
||||
// eslint-disable-next-line no-shadow
|
||||
page: async ({ page, theme }, use, testInfo) => {
|
||||
if (theme === 'snow') {
|
||||
//inject snow theme
|
||||
await page.addInitScript({
|
||||
path: fileURLToPath(new URL('./helper/useSnowTheme.js', import.meta.url))
|
||||
});
|
||||
} else if (theme === 'darkmatter') {
|
||||
//inject darkmatter theme
|
||||
await page.addInitScript({
|
||||
path: fileURLToPath(new URL('./helper/useDarkmatterTheme.js', import.meta.url))
|
||||
});
|
||||
}
|
||||
|
||||
// Attach info about the currently running test and its project.
|
||||
// This will be used by appActions to fill in the created
|
||||
// domain object's notes.
|
||||
page.testNotes = [`${testInfo.titlePath.join('\n')}`, `${testInfo.project.name}`].join('\n');
|
||||
|
||||
await use(page);
|
||||
},
|
||||
myItemsFolderName: [myItemsFolderName, { option: true }],
|
||||
// eslint-disable-next-line no-shadow
|
||||
openmctConfig: async ({ myItemsFolderName }, use) => {
|
||||
await use({ myItemsFolderName });
|
||||
}
|
||||
});
|
||||
|
||||
export { expect, request, extendedTest as test };
|
||||
|
||||
/**
|
||||
* Takes a readable stream and returns a string.
|
||||
* @param {ReadableStream} readable - the readable stream
|
||||
* @return {Promise<String>} the stringified stream
|
||||
*/
|
||||
export async function streamToString(readable) {
|
||||
let result = '';
|
||||
for await (const chunk of readable) {
|
||||
result += chunk;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -1,90 +0,0 @@
|
||||
{
|
||||
"openmct": {
|
||||
"b3cee102-86dd-4c0a-8eec-4d5d276f8691": {
|
||||
"identifier": { "key": "b3cee102-86dd-4c0a-8eec-4d5d276f8691", "namespace": "" },
|
||||
"name": "Performance Display Layout",
|
||||
"type": "layout",
|
||||
"composition": [{ "key": "9666e7b4-be0c-47a5-94b8-99accad7155e", "namespace": "" }],
|
||||
"configuration": {
|
||||
"items": [
|
||||
{
|
||||
"width": 32,
|
||||
"height": 18,
|
||||
"x": 12,
|
||||
"y": 9,
|
||||
"identifier": { "key": "9666e7b4-be0c-47a5-94b8-99accad7155e", "namespace": "" },
|
||||
"hasFrame": true,
|
||||
"fontSize": "default",
|
||||
"font": "default",
|
||||
"type": "subobject-view",
|
||||
"id": "23ca351d-a67d-46aa-a762-290eb742d2f1"
|
||||
}
|
||||
],
|
||||
"layoutGrid": [10, 10]
|
||||
},
|
||||
"modified": 1654299875432,
|
||||
"location": "mine",
|
||||
"persisted": 1654299878751
|
||||
},
|
||||
"9666e7b4-be0c-47a5-94b8-99accad7155e": {
|
||||
"identifier": { "key": "9666e7b4-be0c-47a5-94b8-99accad7155e", "namespace": "" },
|
||||
"name": "Performance Example Imagery",
|
||||
"type": "example.imagery",
|
||||
"configuration": {
|
||||
"imageLocation": "",
|
||||
"imageLoadDelayInMilliSeconds": 20000,
|
||||
"imageSamples": [],
|
||||
"layers": [
|
||||
{
|
||||
"source": "dist/imagery/example-imagery-layer-16x9.png",
|
||||
"name": "16:9",
|
||||
"visible": false
|
||||
},
|
||||
{
|
||||
"source": "dist/imagery/example-imagery-layer-safe.png",
|
||||
"name": "Safe",
|
||||
"visible": false
|
||||
},
|
||||
{
|
||||
"source": "dist/imagery/example-imagery-layer-scale.png",
|
||||
"name": "Scale",
|
||||
"visible": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"telemetry": {
|
||||
"values": [
|
||||
{ "name": "Name", "key": "name" },
|
||||
{ "name": "Time", "key": "utc", "format": "utc", "hints": { "domain": 2 } },
|
||||
{
|
||||
"name": "Local Time",
|
||||
"key": "local",
|
||||
"format": "local-format",
|
||||
"hints": { "domain": 1 }
|
||||
},
|
||||
{
|
||||
"name": "Image",
|
||||
"key": "url",
|
||||
"format": "image",
|
||||
"hints": { "image": 1 },
|
||||
"layers": [
|
||||
{ "source": "dist/imagery/example-imagery-layer-16x9.png", "name": "16:9" },
|
||||
{ "source": "dist/imagery/example-imagery-layer-safe.png", "name": "Safe" },
|
||||
{ "source": "dist/imagery/example-imagery-layer-scale.png", "name": "Scale" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Image Download Name",
|
||||
"key": "imageDownloadName",
|
||||
"format": "imageDownloadName",
|
||||
"hints": { "imageDownloadName": 1 }
|
||||
}
|
||||
]
|
||||
},
|
||||
"modified": 1654299840077,
|
||||
"location": "b3cee102-86dd-4c0a-8eec-4d5d276f8691",
|
||||
"persisted": 1654299840078
|
||||
}
|
||||
},
|
||||
"rootId": "b3cee102-86dd-4c0a-8eec-4d5d276f8691"
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
{
|
||||
"openmct": {
|
||||
"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d": {
|
||||
"identifier": { "key": "6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d", "namespace": "" },
|
||||
"name": "Performance Notebook",
|
||||
"type": "notebook",
|
||||
"configuration": {
|
||||
"defaultSort": "oldest",
|
||||
"entries": {
|
||||
"3e31c412-33ba-4757-8ade-e9821f6ba321": {
|
||||
"8c8f6035-631c-45af-8c24-786c60295335": [
|
||||
{
|
||||
"id": "entry-1652815305457",
|
||||
"createdOn": 1652815305457,
|
||||
"createdBy": "",
|
||||
"text": "Existing Entry 1",
|
||||
"embeds": []
|
||||
},
|
||||
{
|
||||
"id": "entry-1652815313465",
|
||||
"createdOn": 1652815313465,
|
||||
"createdBy": "",
|
||||
"text": "Existing Entry 2",
|
||||
"embeds": []
|
||||
},
|
||||
{
|
||||
"id": "entry-1652815399955",
|
||||
"createdOn": 1652815399955,
|
||||
"createdBy": "",
|
||||
"text": "Existing Entry 3",
|
||||
"embeds": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"imageMigrationVer": "v1",
|
||||
"pageTitle": "Page",
|
||||
"sections": [
|
||||
{
|
||||
"id": "3e31c412-33ba-4757-8ade-e9821f6ba321",
|
||||
"isDefault": false,
|
||||
"isSelected": false,
|
||||
"name": "Section1",
|
||||
"pages": [
|
||||
{
|
||||
"id": "8c8f6035-631c-45af-8c24-786c60295335",
|
||||
"isDefault": false,
|
||||
"isSelected": false,
|
||||
"name": "Page1",
|
||||
"pageTitle": "Page"
|
||||
},
|
||||
{
|
||||
"id": "36555942-c9aa-439c-bbdb-0aaf50db50f5",
|
||||
"isDefault": false,
|
||||
"isSelected": false,
|
||||
"name": "Page2",
|
||||
"pageTitle": "Page"
|
||||
}
|
||||
],
|
||||
"sectionTitle": "Section"
|
||||
},
|
||||
{
|
||||
"id": "dab0bd1d-2c5a-405c-987f-107123d6189a",
|
||||
"isDefault": false,
|
||||
"isSelected": true,
|
||||
"name": "Section2",
|
||||
"pages": [
|
||||
{
|
||||
"id": "f625a86a-cb99-4898-8082-80543c8de534",
|
||||
"isDefault": false,
|
||||
"isSelected": false,
|
||||
"name": "Page1",
|
||||
"pageTitle": "Page"
|
||||
},
|
||||
{
|
||||
"id": "e77ef810-f785-42a7-942e-07e999b79c59",
|
||||
"isDefault": false,
|
||||
"isSelected": true,
|
||||
"name": "Page2",
|
||||
"pageTitle": "Page"
|
||||
}
|
||||
],
|
||||
"sectionTitle": "Section"
|
||||
}
|
||||
],
|
||||
"sectionTitle": "Section",
|
||||
"type": "General",
|
||||
"showTime": "0"
|
||||
},
|
||||
"modified": 1652815915219,
|
||||
"location": "mine",
|
||||
"persisted": 1652815915222
|
||||
}
|
||||
},
|
||||
"rootId": "6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d"
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,54 +0,0 @@
|
||||
{
|
||||
"Groups": [
|
||||
{
|
||||
"name": "Group 1"
|
||||
},
|
||||
{
|
||||
"name": "Group 2"
|
||||
}
|
||||
],
|
||||
"Group 2": [
|
||||
{
|
||||
"name": "Past event 3",
|
||||
"start": 1660493208000,
|
||||
"end": 1660503981000,
|
||||
"type": "Group 2",
|
||||
"color": "orange",
|
||||
"textColor": "white"
|
||||
},
|
||||
{
|
||||
"name": "Past event 4",
|
||||
"start": 1660579608000,
|
||||
"end": 1660624108000,
|
||||
"type": "Group 2",
|
||||
"color": "orange",
|
||||
"textColor": "white"
|
||||
},
|
||||
{
|
||||
"name": "Past event 5",
|
||||
"start": 1660666008000,
|
||||
"end": 1660681529000,
|
||||
"type": "Group 2",
|
||||
"color": "orange",
|
||||
"textColor": "white"
|
||||
}
|
||||
],
|
||||
"Group 1": [
|
||||
{
|
||||
"name": "Past event 1",
|
||||
"start": 1660320408000,
|
||||
"end": 1660343797000,
|
||||
"type": "Group 1",
|
||||
"color": "orange",
|
||||
"textColor": "white"
|
||||
},
|
||||
{
|
||||
"name": "Past event 2",
|
||||
"start": 1660406808000,
|
||||
"end": 1660429160000,
|
||||
"type": "Group 1",
|
||||
"color": "orange",
|
||||
"textColor": "white"
|
||||
}
|
||||
]
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user