mirror of
https://github.com/nasa/openmct.git
synced 2025-06-25 10:44:21 +00:00
Compare commits
752 Commits
v1.2-RC4
...
5211-tests
Author | SHA1 | Date | |
---|---|---|---|
7b88655373 | |||
c83ee95d9c | |||
9c54a9f862 | |||
161fca98d5 | |||
f493f031df | |||
029605ebdc | |||
60b59d6bd7 | |||
9de84df6a9 | |||
71c43dee65 | |||
c10e9ad14c | |||
075ca1f87d | |||
7e1337447f | |||
7e854f078e | |||
2cfd7ac5b6 | |||
6c90798905 | |||
2fa053e80c | |||
488cd82ae1 | |||
d85be3b88e | |||
4d48cf3180 | |||
413cb13edf | |||
70115be727 | |||
97f5528dfc | |||
0e1cc5dc30 | |||
0062191416 | |||
eedc523078 | |||
db97acb61e | |||
43a4bf9606 | |||
0f352087f5 | |||
8ce15521de | |||
c0b0c44dc2 | |||
b9a644cd4f | |||
1afc5ef245 | |||
34442c53c6 | |||
451ca075fe | |||
84f1a61a8d | |||
ea041aaaf9 | |||
ac9420bfa1 | |||
0ebab10578 | |||
cbdb9fc437 | |||
63d2246345 | |||
78002f0a24 | |||
f08fd58486 | |||
730272e165 | |||
0f0c6a7b17 | |||
370e6a0c37 | |||
815506cf17 | |||
bdb1867c73 | |||
e288fdffea | |||
194060f30a | |||
45bc317a59 | |||
e103ea44d8 | |||
d13d7dc8f3 | |||
05e3303828 | |||
aa0fc70e54 | |||
9fbb695379 | |||
584d11a2ef | |||
162cc6bc77 | |||
111b0d0d68 | |||
59c0da1b57 | |||
3c70cf1767 | |||
2aec1ee854 | |||
ab60e3c3bd | |||
4445d7116a | |||
93abc18001 | |||
7fb37de721 | |||
1c525f50c8 | |||
40a7451064 | |||
04ee6f49d6 | |||
f5796c984e | |||
50b642fabe | |||
dfb726b924 | |||
8d761f729b | |||
d88ead502c | |||
c46849b166 | |||
6c71fa01f5 | |||
c56d458ecb | |||
f74a35f45a | |||
d9ac0182c3 | |||
7bb108c36b | |||
77804cff75 | |||
2d73296b36 | |||
405418b9d5 | |||
f999b9e12b | |||
664ba399ea | |||
c6078a234a | |||
17c16eba50 | |||
9f9c69ee68 | |||
037886aa01 | |||
48916564e4 | |||
1ca5271c3e | |||
6521b888d6 | |||
85fce3c456 | |||
8d577a8958 | |||
9c8ee09960 | |||
9568da9d5f | |||
2aa3b810ba | |||
1cdbb34e21 | |||
95299336d0 | |||
b8ff5c7f33 | |||
9ede023cfa | |||
308e621b5d | |||
e6b5870234 | |||
03e7d912be | |||
09da373d1c | |||
b8d9e41c01 | |||
815e7d169c | |||
58387e0902 | |||
0a0826f87e | |||
e063442d8c | |||
6a5823ab5c | |||
0493e5ae3c | |||
24f13b6249 | |||
221fb4d6bf | |||
257742b45b | |||
44edec4f04 | |||
ab4d0dd37f | |||
c089a4760d | |||
b77a4066f2 | |||
20d7e80502 | |||
d63fec51a7 | |||
d30c4fcb53 | |||
fff3ce0acf | |||
db5cb2517f | |||
5236f1c796 | |||
1ed253cb07 | |||
a6553ba010 | |||
cf6bc5be2d | |||
a53a3a0297 | |||
402cd15726 | |||
a5580912e3 | |||
54d1b8991c | |||
7b6acee793 | |||
04e1c60e5c | |||
91bcd78d40 | |||
a3c0e073c8 | |||
21ae9f45c1 | |||
0a40c8dd0b | |||
ef1ea8e712 | |||
5c4fad77ff | |||
dbac9e6cd2 | |||
4b7bcf9c89 | |||
2b42abd495 | |||
1f2102b845 | |||
2ccb90aa41 | |||
525496fbca | |||
47099786cb | |||
3a11291a3b | |||
476f1b2579 | |||
6153ad8e1e | |||
77c0b16050 | |||
d19088cec6 | |||
43afb39e56 | |||
cd8c332fb5 | |||
b899475939 | |||
cc1f7659f9 | |||
0d5539be96 | |||
0a511e6155 | |||
47b6d19de8 | |||
3fd93f47bc | |||
651e61954c | |||
d30ec4c757 | |||
3c24733476 | |||
04d00fac3d | |||
150909d4b9 | |||
2b599a7ff4 | |||
824a597825 | |||
cf5edf2db0 | |||
0705d321da | |||
dad0768d57 | |||
9643dbe918 | |||
594f9d3e9a | |||
0f9e727675 | |||
0cf30940c8 | |||
3a2381b90b | |||
94def56bcb | |||
93a796353d | |||
18ff16052a | |||
f1c7fa337d | |||
557f29f9bb | |||
255ebc9a37 | |||
ca7fbe58e3 | |||
b347f35892 | |||
28d5d72834 | |||
0df33730f7 | |||
7b2ff8fa15 | |||
d80b692354 | |||
4205abdc80 | |||
492ff2fa64 | |||
93a81a1369 | |||
482d8f392c | |||
67234c70a4 | |||
e9680e975f | |||
e691a89682 | |||
bcd668594d | |||
5471e13d9e | |||
dae446808e | |||
ee9e47f487 | |||
003b1ffede | |||
16cb5f3911 | |||
74b83903c9 | |||
6a470fba1a | |||
4e7debabb1 | |||
384e36920c | |||
d4429f9686 | |||
0b2d08078b | |||
3bbc9e1582 | |||
af0420361b | |||
896f0ca3f4 | |||
28c5405a01 | |||
d114353556 | |||
f40398807e | |||
81f440e1e6 | |||
c6d6400131 | |||
55828af1ec | |||
34b951f4c6 | |||
a7d4006fee | |||
a71485f820 | |||
058259278c | |||
345285b966 | |||
ce98c10145 | |||
22604220de | |||
548afda6d7 | |||
88c96675e4 | |||
10b97d83ab | |||
7c48ff9088 | |||
839e210ac1 | |||
7209104d05 | |||
869c2ab16c | |||
e434b294b9 | |||
48b66e0af2 | |||
54ef9a830e | |||
cd38545267 | |||
8aed21d670 | |||
b27f24cc30 | |||
a7df8bf168 | |||
ff269ac390 | |||
cdee5e8102 | |||
159457a52d | |||
d637420da1 | |||
eb4da293c6 | |||
738fac64b8 | |||
7703ec0a61 | |||
52466999b8 | |||
45373c56f7 | |||
91e909bb4a | |||
556f762d20 | |||
e14b7cd0e2 | |||
b1cffd0df3 | |||
09f25bc525 | |||
bd406d1a73 | |||
03380e1846 | |||
dcaad40064 | |||
cf47f7c1b2 | |||
ba2a6030c2 | |||
384af89d4c | |||
324318793d | |||
f211d3fab0 | |||
6aaf4a2c06 | |||
4a9744e916 | |||
05b73bb654 | |||
19b3be7ec0 | |||
c39593b065 | |||
f62d834384 | |||
7a7202f75b | |||
6bcc9bfd84 | |||
f42f291790 | |||
176e8167f0 | |||
87d58904b4 | |||
ae629a6c8b | |||
3f575f0ec0 | |||
b3ab56cb57 | |||
f6934a43c9 | |||
22a7537974 | |||
3620760991 | |||
88a94c80be | |||
2fc0d34b8f | |||
d53ca3ec9a | |||
86e5d10fc1 | |||
936b88363c | |||
38fec73a33 | |||
43c2c8543e | |||
e8e719e7f7 | |||
26e70d82b7 | |||
3a65f75d21 | |||
51e4c0c836 | |||
bb9c225f23 | |||
19ec98af79 | |||
23ead2ceaa | |||
6a8f4b5d9c | |||
464bb3b885 | |||
4775c88909 | |||
722e2e2bb1 | |||
333aa1d6db | |||
5e92c69fe2 | |||
8ddba2b06f | |||
6f9241c0b1 | |||
d84808aa68 | |||
0df672e470 | |||
c9bc390355 | |||
5b1664f073 | |||
e634e09e32 | |||
8e3947e48d | |||
b1ea6efd45 | |||
70f2fad243 | |||
2d64813a4f | |||
fd0e89ca05 | |||
01d02642e8 | |||
1f588a2a6e | |||
e18c7562ae | |||
08b1c4ae74 | |||
2488072d6b | |||
82ea23e20c | |||
a0b02c9684 | |||
bba29b083f | |||
7c4258d720 | |||
95d30f75ac | |||
db3cfd33af | |||
85fe838c04 | |||
6bdea20f9a | |||
159563ce5c | |||
0d88bc8531 | |||
8acbcadd5d | |||
e20c7a17d6 | |||
035a1be57d | |||
09b49f31ab | |||
e1e2cf9be8 | |||
84e82d3bda | |||
7b53cad2c5 | |||
420edb75f8 | |||
749e84611e | |||
8eba3a81f3 | |||
63fabe576c | |||
11c96796c0 | |||
5caa5e1a50 | |||
b9b0cfe0ef | |||
bee0527acb | |||
ed3c2f6a56 | |||
b1c97e51e5 | |||
16786a64c1 | |||
e3ad2c98ca | |||
0a0d3b20c2 | |||
02c2fb5655 | |||
0b02b083c3 | |||
25b3431131 | |||
c8723da098 | |||
933ce7aa3c | |||
8ef3869325 | |||
53f5fdabe5 | |||
d10561fc7f | |||
566469e691 | |||
6d4a324fca | |||
b8fabb7e73 | |||
1680c3cc1b | |||
61dd85c704 | |||
980777691a | |||
a50c1704e5 | |||
98080784a3 | |||
bf7b672957 | |||
5dadf6f6fc | |||
555db60bf7 | |||
1972ca9ea7 | |||
1e11bbc2ad | |||
c4a87ae5d7 | |||
4e325fb165 | |||
e59e4efdf0 | |||
f17fda53a0 | |||
f2dbe6d816 | |||
510d3bd333 | |||
a908eb1d65 | |||
c0bda64927 | |||
d0c5731287 | |||
5eaf222f88 | |||
0249ab4df5 | |||
4f8cba160d | |||
c269e089da | |||
4873f40614 | |||
10bb9173ec | |||
ea8c9c7cc8 | |||
4c9c084eec | |||
b64ee10812 | |||
ee1ecf43db | |||
4d8db8eb7c | |||
1b13965200 | |||
38db8f7fe5 | |||
4ba8f893a6 | |||
c4b9be18f1 | |||
eabdf6cd04 | |||
e56c673005 | |||
dad9f12a5c | |||
aa5edb0b83 | |||
b315803180 | |||
b27317631b | |||
953a9daafb | |||
63f9cd449f | |||
54220f547b | |||
93d967c2b3 | |||
1226459c6f | |||
d7c9c9cb98 | |||
2131ef2397 | |||
48c22369a1 | |||
6506077f4d | |||
b1b4266ff3 | |||
42b0148f93 | |||
9461ad8edd | |||
40055ba955 | |||
9cb85ad176 | |||
f2b2953a5d | |||
62de310686 | |||
4b9ff67e49 | |||
d5e32ec494 | |||
38880ba3d1 | |||
a99ce7733c | |||
9f48764210 | |||
a1aaa0dd41 | |||
bee15e98c8 | |||
092bbe547d | |||
6cbe05317c | |||
3b92fcdf6c | |||
6dde54bd25 | |||
359e7377ac | |||
9f4190f781 | |||
f3fc991a74 | |||
2564e75fc9 | |||
f42fe78acf | |||
fe928a1386 | |||
b329ed6ed5 | |||
9b7a0d7e4c | |||
5c15e53abb | |||
f58b3881f2 | |||
071a13b219 | |||
ca66898e51 | |||
94c7b2343a | |||
c397c336ab | |||
eea23f2caf | |||
6665641c02 | |||
c3ebf52dd2 | |||
f8f2e7da9b | |||
240f58b2d0 | |||
7d3baee7b5 | |||
1f5cb7ca42 | |||
4a7ebe326c | |||
10da314a4a | |||
b3ceccd7fb | |||
1bde4c9a0c | |||
4b85360446 | |||
41b860a547 | |||
254b3db966 | |||
cbb3f32d1e | |||
e3bf72e77f | |||
0b63b782cf | |||
da39fd0c70 | |||
96dd581a67 | |||
2a1e322230 | |||
300b98bd54 | |||
c946609d13 | |||
7ca559fbe4 | |||
71392915c1 | |||
2889e88a97 | |||
d56d176aac | |||
925518c83f | |||
fa5aceb7b3 | |||
6755ef4641 | |||
333e8b5583 | |||
9d8a8b36d2 | |||
b484a4a959 | |||
64e7c62d98 | |||
6483fe2402 | |||
a123889d6a | |||
a40867d544 | |||
dbed9262c0 | |||
43ac66233e | |||
04e85c176a | |||
8274c23129 | |||
5fafde5f23 | |||
80a6e7f719 | |||
2c13aeecce | |||
ac015c3e45 | |||
ae1a4bcc6a | |||
e1e0eeac56 | |||
c90dfb2a1f | |||
1dfa5e5b8c | |||
99896b72ea | |||
979ba77c8e | |||
aebb5df611 | |||
605eeff9d7 | |||
a83ee1f90f | |||
fe899cbcc8 | |||
633bac2ed5 | |||
dacec48aec | |||
3ca133c782 | |||
12416b8079 | |||
9920e67c83 | |||
0e80a5b8a0 | |||
05f9202fe4 | |||
0da35a44b0 | |||
2305cd2e49 | |||
b30b6bc94e | |||
564f254652 | |||
9fa71244ea | |||
8157cdc7e9 | |||
721bdd737a | |||
1b57999059 | |||
b7460cef41 | |||
46f7f6dd04 | |||
cf2b8b3c8a | |||
ac0e1d3161 | |||
f9bd31deee | |||
cf3566742b | |||
b4a87dd07f | |||
e83cfa24c2 | |||
86ebd14ae7 | |||
b042d9098d | |||
325f2c4860 | |||
74a516aa9e | |||
28e26461cc | |||
cfaaf6b1fe | |||
bffe79ecbd | |||
94d9852339 | |||
905e397d3f | |||
e70a636073 | |||
03abb5e5de | |||
ac20c01233 | |||
b8ded0a16e | |||
b68f79f427 | |||
221d10d3e6 | |||
22d32eed1d | |||
5d656f0963 | |||
201d622b85 | |||
3571004f5c | |||
16249c3790 | |||
5377f0d0b3 | |||
15778b00a0 | |||
169eec0a51 | |||
f789775b1c | |||
fc59a4dce4 | |||
29128a891d | |||
dd3d4c8c3a | |||
4047c888be | |||
1499286bee | |||
6226763c37 | |||
7623a0648f | |||
b7085f7f62 | |||
55c851873c | |||
2b143dfc0f | |||
9405272f3b | |||
a9be9f1827 | |||
abb1a5c75b | |||
5e2fe7dc42 | |||
e4d6e90c35 | |||
84d9a525a9 | |||
0aca0ce6a6 | |||
c0742d521c | |||
92737b43af | |||
8b0f6885ee | |||
9af2d15cef | |||
e60d8d08a4 | |||
3e9b567fce | |||
6f51de85db | |||
f202ae19cb | |||
668bd75025 | |||
e6e07cf959 | |||
2f8431905f | |||
23aba14dfe | |||
b0fa955914 | |||
98207a3e0d | |||
26b81345f2 | |||
ac2034b243 | |||
351848ad56 | |||
cbac495f93 | |||
15ef5b7623 | |||
46c7ac944f | |||
aa4bfab462 | |||
f5cbb37e5a | |||
8d9079984a | |||
41783d8939 | |||
441ad58fe7 | |||
06a6a3f773 | |||
52fab78625 | |||
5eb6c15959 | |||
ce8c31cfa4 | |||
d80c0eef8e | |||
55829dcf05 | |||
d78956327c | |||
4a0728a55b | |||
2e1d57aa8c | |||
1c2b0678be | |||
6f810add43 | |||
12727adb16 | |||
9da750c3bb | |||
176226ddef | |||
acea18fa70 | |||
d1656f8561 | |||
87751e882c | |||
dff393a714 | |||
fd9c9aee03 | |||
59bf981fb0 | |||
4bbdac759f | |||
13fe7509de | |||
6fd8f6cd43 | |||
6375ecda34 | |||
d232dacc65 | |||
59946e89ef | |||
d75c4b4049 | |||
30ca4b707d | |||
27704c9a48 | |||
b0203f2272 | |||
77b720d00d | |||
ba982671b2 | |||
5df7d92d64 | |||
a8228406de | |||
2401473012 | |||
e502fb88fa | |||
37a52cb011 | |||
04fb4e8a82 | |||
5646a252f7 | |||
0e6ce7f58b | |||
8cd6a4c6a3 | |||
02fc162197 | |||
84d21a3695 | |||
1a6369c2b9 | |||
463c44679d | |||
c1f3ea4e61 | |||
142b767470 | |||
184b716b53 | |||
e53399495b | |||
d27f73579b | |||
1ae8199e89 | |||
2deb4e8474 | |||
7f10681424 | |||
c756adad6f | |||
f3d593bc1e | |||
b637307de6 | |||
b6e0208e71 | |||
631876cab3 | |||
a192d46c2b | |||
6923f17645 | |||
87a45de05b | |||
ab76451360 | |||
a91179091f | |||
5f7e34ce6c | |||
db33f0538a | |||
257a8e2e2d | |||
baa8078d23 | |||
ee60013f45 | |||
505796d9f0 | |||
56120ba1bb | |||
225b235059 | |||
de614ff606 | |||
7879752f47 | |||
08b2940eb6 | |||
67749dd2bb | |||
56c203d9ca | |||
54ce86eff3 | |||
9713ca74c7 | |||
81ed9169f1 | |||
e60462c221 | |||
09c4e610af | |||
4a576321e3 | |||
24c5dc03de | |||
d8291ddc17 | |||
1b75d828d8 | |||
966e583978 | |||
e52f60fdf0 | |||
b4c44c6d49 | |||
0f6215da9d | |||
7d0b323dab | |||
14114781fc | |||
97694fa29c | |||
9f6bfa2351 | |||
b842cbc83d | |||
0da5409092 | |||
888afd88b8 | |||
35692ae4b9 | |||
18b2a270c9 | |||
6f2d8e6ce3 | |||
91d1681698 | |||
efd97de743 | |||
c4cd725c9a | |||
23e5efbb19 | |||
4e995c10da | |||
b5cf157c40 | |||
ac082e9f58 | |||
9f7799b44f | |||
cdf9c50b8a | |||
7c4e9cef87 | |||
ffb3b302c7 | |||
278f48f65c | |||
4801dc4f32 | |||
9e8f845fbe | |||
b4d1cdaae8 | |||
f9d3af2724 | |||
ac6ed5dd01 | |||
6e0c1cc674 | |||
33c37ebf19 | |||
c33c29b6f3 | |||
37debefadc | |||
891fe0a36b | |||
4b6de59b03 | |||
6a5b902365 | |||
270f07ebd5 | |||
52500f6097 | |||
2fa8a8d61d | |||
69a6cd20af | |||
28eb268af1 | |||
045bb443cb | |||
c6ca912f2b | |||
78c7484d54 | |||
4d560086dd | |||
ef965ebdfd | |||
0b4a843617 | |||
7e7141a5a0 | |||
b3fd6c9b85 | |||
deb0511b43 | |||
e32f465f7a | |||
abc458cef4 | |||
2198a4780b | |||
b76d4b76cb | |||
9ebb013f22 | |||
5a4cba0226 | |||
f03bfdebb4 | |||
7c1550ced7 | |||
a09da30768 | |||
66bb938fc4 | |||
0d478d5dfc | |||
2a9b431c50 | |||
756e7e5372 | |||
573a63d359 | |||
3f2ad2be0c | |||
fd3be63805 | |||
ad8ed419de | |||
8b4f07c2e0 | |||
9198effafe | |||
de6eceb98b | |||
72f7ec1481 | |||
13ff0c368d | |||
5e21f98309 | |||
26069dac79 | |||
b7d2402434 | |||
68eb5e6286 | |||
d2b9a7754f | |||
70125afa9d | |||
551364ad8c | |||
7f7518bb9d | |||
8c8a14af7b | |||
3735a85c69 | |||
79c4dc9272 | |||
d7c266d70f | |||
e1d0c22071 | |||
7dee6344b0 | |||
731ab89561 | |||
6bd0246db6 | |||
99aa5c7b7b |
@ -1,36 +1,194 @@
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
version: 2.1
|
||||
executors:
|
||||
pw-focal-development:
|
||||
docker:
|
||||
- image: circleci/node:13-browsers
|
||||
environment:
|
||||
CHROME_BIN: "/usr/bin/google-chrome"
|
||||
- image: mcr.microsoft.com/playwright:v1.23.0-focal
|
||||
environment:
|
||||
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
|
||||
parameters:
|
||||
BUST_CACHE:
|
||||
description: "Set this with the CircleCI UI Trigger Workflow button (boolean = true) to bust the cache!"
|
||||
default: false
|
||||
type: boolean
|
||||
commands:
|
||||
build_and_install:
|
||||
description: "All steps used to build and install. Will use cache if found"
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Update npm
|
||||
command: 'sudo npm install -g npm@latest'
|
||||
- restore_cache:
|
||||
key: dependency-cache-13-{{ checksum "package.json" }}
|
||||
- run:
|
||||
name: Installing dependencies (npm install)
|
||||
command: npm install
|
||||
- save_cache:
|
||||
key: dependency-cache-13-{{ 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
|
||||
|
||||
- checkout
|
||||
- restore_cache_cmd:
|
||||
node-version: << parameters.node-version >>
|
||||
- node/install:
|
||||
install-npm: true
|
||||
node-version: << parameters.node-version >>
|
||||
- run: npm install --prefer-offline --no-audit --progress=false
|
||||
restore_cache_cmd:
|
||||
description: "Custom command for restoring cache with the ability to bust cache. When BUST_CACHE is set to true, jobs will not restore cache"
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
steps:
|
||||
- when:
|
||||
condition:
|
||||
equal: [false, << pipeline.parameters.BUST_CACHE >> ]
|
||||
steps:
|
||||
- restore_cache:
|
||||
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
|
||||
save_cache_cmd:
|
||||
description: "Custom command for saving cache."
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
steps:
|
||||
- save_cache:
|
||||
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
|
||||
paths:
|
||||
- ~/.npm
|
||||
- node_modules
|
||||
generate_and_store_version_and_filesystem_artifacts:
|
||||
description: "Track important packages and files"
|
||||
steps:
|
||||
- run: |
|
||||
mkdir /tmp/artifacts
|
||||
printenv NODE_ENV >> /tmp/artifacts/NODE_ENV.txt
|
||||
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
|
||||
- run: npm run cov:e2e:<<parameters.suite>>:publish
|
||||
orbs:
|
||||
node: circleci/node@4.9.0
|
||||
browser-tools: circleci/browser-tools@1.3.0
|
||||
jobs:
|
||||
npm-audit:
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
executor: pw-focal-development
|
||||
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: npm run test
|
||||
- run: npm run cov:unit:publish
|
||||
- save_cache_cmd:
|
||||
node-version: <<parameters.node-version>>
|
||||
- store_test_results:
|
||||
path: dist/reports/tests/
|
||||
- store_artifacts:
|
||||
path: coverage
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
e2e-test:
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
suite: #stable or full
|
||||
type: string
|
||||
executor: pw-focal-development
|
||||
parallelism: 4
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: <<parameters.node-version>>
|
||||
- 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: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
|
||||
- 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
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
perf-test:
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
executor: pw-focal-development
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: <<parameters.node-version>>
|
||||
- run: npm run test:perf
|
||||
- store_test_results:
|
||||
path: test-results/results.xml
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
path: html-test-results
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
workflows:
|
||||
version: 2
|
||||
test:
|
||||
overall-circleci-commit-status: #These jobs run on every commit
|
||||
jobs:
|
||||
- build
|
||||
- lint:
|
||||
name: node14-lint
|
||||
node-version: lts/fermium
|
||||
- unit-test:
|
||||
name: node18-chrome
|
||||
node-version: "18"
|
||||
- e2e-test:
|
||||
name: e2e-stable
|
||||
node-version: lts/gallium
|
||||
suite: stable
|
||||
- perf-test:
|
||||
node-version: lts/gallium
|
||||
the-nightly: #These jobs do not run on PRs, but against master at night
|
||||
jobs:
|
||||
- unit-test:
|
||||
name: node14-chrome-nightly
|
||||
node-version: lts/fermium
|
||||
- unit-test:
|
||||
name: node16-chrome-nightly
|
||||
node-version: lts/gallium
|
||||
- unit-test:
|
||||
name: node18-chrome
|
||||
node-version: "18"
|
||||
- npm-audit:
|
||||
node-version: lts/gallium
|
||||
- e2e-test:
|
||||
name: e2e-full-nightly
|
||||
node-version: lts/gallium
|
||||
suite: full
|
||||
triggers:
|
||||
- schedule:
|
||||
cron: "0 0 * * *"
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
|
130
.eslintrc.js
130
.eslintrc.js
@ -1,4 +1,4 @@
|
||||
const LEGACY_FILES = ["platform/**", "example/**"];
|
||||
const LEGACY_FILES = ["example/**"];
|
||||
module.exports = {
|
||||
"env": {
|
||||
"browser": true,
|
||||
@ -11,12 +11,14 @@ module.exports = {
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:compat/recommended",
|
||||
"plugin:vue/recommended",
|
||||
"plugin:you-dont-need-lodash-underscore/compatible"
|
||||
],
|
||||
"parser": "vue-eslint-parser",
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint",
|
||||
"parser": "@babel/eslint-parser",
|
||||
"requireConfigFile": false,
|
||||
"allowImportExportEverywhere": true,
|
||||
"ecmaVersion": 2015,
|
||||
"ecmaFeatures": {
|
||||
@ -27,6 +29,7 @@ module.exports = {
|
||||
"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",
|
||||
@ -35,7 +38,6 @@ module.exports = {
|
||||
"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",
|
||||
@ -54,7 +56,7 @@ module.exports = {
|
||||
{
|
||||
"anonymous": "always",
|
||||
"asyncArrow": "always",
|
||||
"named": "never",
|
||||
"named": "never"
|
||||
}
|
||||
],
|
||||
"array-bracket-spacing": "error",
|
||||
@ -120,6 +122,100 @@ module.exports = {
|
||||
"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",
|
||||
@ -145,34 +241,30 @@ module.exports = {
|
||||
],
|
||||
"vue/max-attributes-per-line": ["error", {
|
||||
"singleline": 1,
|
||||
"multiline": {
|
||||
"max": 1,
|
||||
"allowFirstLine": true
|
||||
}
|
||||
"multiline": 1,
|
||||
}],
|
||||
"vue/first-attribute-linebreak": "error",
|
||||
"vue/multiline-html-element-content-newline": "off",
|
||||
"vue/singleline-html-element-content-newline": "off"
|
||||
"vue/singleline-html-element-content-newline": "off",
|
||||
"vue/multi-word-component-names": "off", // TODO enable, align with conventions
|
||||
"vue/no-mutating-props": "off"
|
||||
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*Spec.js"],
|
||||
"files": LEGACY_FILES,
|
||||
"rules": {
|
||||
"no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"vars": "all",
|
||||
"args": "none",
|
||||
"varsIgnorePattern": "controller",
|
||||
|
||||
"varsIgnorePattern": "controller"
|
||||
}
|
||||
]
|
||||
}
|
||||
}, {
|
||||
"files": LEGACY_FILES,
|
||||
"rules": {
|
||||
// https://eslint.org/docs/rules/no-nested-ternary
|
||||
],
|
||||
"no-nested-ternary": "off",
|
||||
"no-var": "off"
|
||||
"no-var": "off",
|
||||
"one-var": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
47
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
47
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
---
|
||||
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
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
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
Normal file
20
.github/ISSUE_TEMPLATE/enhancement-request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
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
Normal file
11
.github/ISSUE_TEMPLATE/maintenance-type.md
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
name: Maintenance
|
||||
about: Add, update or remove documentation, tests, or dependencies.
|
||||
title: ''
|
||||
labels: type:maintenance
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
#### Summary
|
||||
<!--- Generally describe the purpose of the change. -->
|
29
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
29
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
<!--- 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 change backwards compatible? For example, developers won't need to change how they are calling the API or how they've extended core plugins such as Tables or Plots.
|
||||
|
||||
### Author Checklist
|
||||
|
||||
* [ ] Changes address original issue?
|
||||
* [ ] Tests included and/or updated with changes?
|
||||
* [ ] Command line build passes?
|
||||
* [ ] Has this been smoke tested?
|
||||
* [ ] Testing instructions included in associated issue OR is this a dependency/testcase change?
|
||||
|
||||
### Reviewer Checklist
|
||||
|
||||
* [ ] Changes appear to address issue?
|
||||
* [ ] Changes appear not to be breaking changes?
|
||||
* [ ] Appropriate unit tests included?
|
||||
* [ ] Code style and in-line documentation are appropriate?
|
||||
* [ ] Commit messages meet standards?
|
||||
* [ ] Has associated issue been labelled unverified? (only applicable if this PR closes the issue)
|
||||
* [ ] Has associated issue been labelled bug? (only applicable if this PR is for a bug fix)
|
24
.github/dependabot.yml
vendored
Normal file
24
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
- "type:maintenance"
|
||||
- "dependencies"
|
||||
- "pr:e2e"
|
||||
- "pr:daveit"
|
||||
- "pr:visual"
|
||||
- "pr:platform"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- "type:maintenance"
|
||||
- "dependencies"
|
||||
- "pr:daveit"
|
43
.github/workflows/codeql-analysis.yml
vendored
Normal file
43
.github/workflows/codeql-analysis.yml
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
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@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: javascript
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
62
.github/workflows/e2e-pr.yml
vendored
Normal file
62
.github/workflows/e2e-pr.yml
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
name: "e2e-pr"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- opened
|
||||
|
||||
jobs:
|
||||
e2e-full:
|
||||
if: ${{ github.event.label.name == 'pr:e2e' }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
steps:
|
||||
- name: Trigger Success
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: "nasa",
|
||||
repo: "openmct",
|
||||
body: 'Started e2e Run. Follow along: https://github.com/nasa/openmct/actions/runs/' + context.runId
|
||||
})
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: npx playwright@1.23.0 install
|
||||
- run: npx playwright install chrome-beta
|
||||
- run: npm install
|
||||
- run: npm run test:e2e:full
|
||||
- name: Archive test results
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: test-results
|
||||
- name: Test success
|
||||
if: ${{ success() }}
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: "nasa",
|
||||
repo: "openmct",
|
||||
body: 'Success ✅ ! Build artifacts are here: https://github.com/nasa/openmct/actions/runs/' + context.runId
|
||||
})
|
||||
- name: Test failure
|
||||
if: ${{ failure() }}
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: "nasa",
|
||||
repo: "openmct",
|
||||
body: 'Failure ❌ ! Build artifacts are here: https://github.com/nasa/openmct/actions/runs/' + context.runId
|
||||
})
|
25
.github/workflows/e2e-visual.yml
vendored
Normal file
25
.github/workflows/e2e-visual.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: "e2e-visual"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- opened
|
||||
schedule:
|
||||
- cron: '28 21 * * 1-5'
|
||||
|
||||
jobs:
|
||||
e2e-visual:
|
||||
if: ${{ github.event.label.name == 'pr:visual' }} || ${{ github.event.workflow_dispatch }} || ${{ github.event.schedule }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: npx playwright@1.23.0 install
|
||||
- run: npm install
|
||||
- name: Run the e2e visual tests
|
||||
run: npm run test:e2e:visual
|
||||
env:
|
||||
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
|
21
.github/workflows/e2e.yml
vendored
Normal file
21
.github/workflows/e2e.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
name: "e2e"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Which branch do you want to test?' # Limited to branch for now
|
||||
required: false
|
||||
default: 'master'
|
||||
jobs:
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.inputs.version }}
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: npm install
|
||||
- name: Run the e2e tests
|
||||
run: npm run test:e2e:ci
|
98
.github/workflows/lighthouse.yml
vendored
Normal file
98
.github/workflows/lighthouse.yml
vendored
Normal file
@ -0,0 +1,98 @@
|
||||
name: lighthouse
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Which branch do you want to test?' # Limited to branch for now
|
||||
required: false
|
||||
default: 'master'
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
jobs:
|
||||
lighthouse-pr:
|
||||
if: ${{ github.event.label.name == 'pr:lighthouse' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Master for Baseline
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: master #explicitly checkout master for baseline
|
||||
- name: Install Node 16
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
- name: npm install with lighthouse cli
|
||||
run: npm install && npm install -g @lhci/cli
|
||||
- name: Run lhci against master to generate baseline and ignore exit codes
|
||||
run: lhci autorun || true
|
||||
- name: Perform clean checkout of PR
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: true
|
||||
- name: Install Node version which is compatible with PR
|
||||
uses: actions/setup-node@v3
|
||||
- name: npm install with lighthouse cli
|
||||
run: npm install && npm install -g @lhci/cli
|
||||
- name: Run lhci with PR
|
||||
run: lhci autorun
|
||||
env:
|
||||
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
|
||||
lighthouse-nightly:
|
||||
if: ${{ github.event.schedule }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Node 16
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
- name: npm install with lighthouse cli
|
||||
run: npm install && npm install -g @lhci/cli
|
||||
- name: Run lhci against master to generate baseline
|
||||
run: lhci autorun
|
||||
env:
|
||||
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
|
||||
lighthouse-dispatch:
|
||||
if: ${{ github.event.workflow_dispatch }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.inputs.version }}
|
||||
- name: Install Node 14
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
- name: npm install with lighthouse cli
|
||||
run: npm install && npm install -g @lhci/cli
|
||||
- name: Run lhci against master to generate baseline
|
||||
run: lhci autorun
|
||||
|
33
.github/workflows/npm-prerelease.yml
vendored
Normal file
33
.github/workflows/npm-prerelease.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
# 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@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- run: npm install
|
||||
- run: npm test
|
||||
|
||||
publish-npm-prerelease:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- run: npm install
|
||||
- run: npm publish --access public --tag unstable
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
34
.github/workflows/pr-platform.yml
vendored
Normal file
34
.github/workflows/pr-platform.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
name: "pr-platform"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types: [ labeled ]
|
||||
|
||||
jobs:
|
||||
e2e-full:
|
||||
if: ${{ github.event.label.name == 'pr:platform' }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- macos-latest
|
||||
- windows-latest
|
||||
node_version:
|
||||
- 14
|
||||
- 16
|
||||
- 18
|
||||
architecture:
|
||||
- x64
|
||||
name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node_version }}
|
||||
architecture: ${{ matrix.architecture }}
|
||||
- run: npm install
|
||||
- run: npm test
|
||||
- run: npm run lint -- --quiet
|
19
.github/workflows/prcop-config.json
vendored
Normal file
19
.github/workflows/prcop-config.json
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"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"
|
||||
}
|
26
.github/workflows/prcop.yml
vendored
Normal file
26
.github/workflows/prcop.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
name: PRCop
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- edited
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
- review_requested
|
||||
- review_request_removed
|
||||
pull_request_review_comment:
|
||||
types:
|
||||
- created
|
||||
|
||||
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 }}
|
18
.gitignore
vendored
18
.gitignore
vendored
@ -15,8 +15,6 @@
|
||||
*.idea
|
||||
*.iml
|
||||
|
||||
# External dependencies
|
||||
|
||||
# Build output
|
||||
target
|
||||
dist
|
||||
@ -24,20 +22,24 @@ 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
|
||||
|
||||
# codecov artifacts
|
||||
.nyc_output
|
||||
coverage
|
||||
codecov
|
||||
|
||||
# :(
|
||||
package-lock.json
|
||||
|
52
.npmignore
52
.npmignore
@ -1,35 +1,27 @@
|
||||
*.scssc
|
||||
*.zip
|
||||
*.gzip
|
||||
*.tgz
|
||||
*.DS_Store
|
||||
# Ignore everything first (will not ignore special files like LICENSE.md,
|
||||
# README.md, and package.json)...
|
||||
/**/*
|
||||
|
||||
*.sass-cache
|
||||
*COMPILE.css
|
||||
# ...but include these folders...
|
||||
!/dist/**/*
|
||||
!/src/**/*
|
||||
|
||||
# Intellij project configuration files
|
||||
*.idea
|
||||
*.iml
|
||||
# We might be able to remove this if it is not imported by any project directly.
|
||||
# https://github.com/nasa/openmct/issues/4992
|
||||
!/example/**/*
|
||||
|
||||
# External dependencies
|
||||
# We will remove this in https://github.com/nasa/openmct/issues/4922
|
||||
!/app.js
|
||||
|
||||
# Build output
|
||||
target
|
||||
# ...except for these files in the above folders.
|
||||
/src/**/*Spec.js
|
||||
/src/**/test/
|
||||
# TODO move test utils into test/ folders
|
||||
/src/utils/testing.js
|
||||
|
||||
# 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
|
||||
# Also include these special top-level files.
|
||||
!copyright-notice.js
|
||||
!copyright-notice.html
|
||||
!index.html
|
||||
!openmct.js
|
||||
!SECURITY.md
|
4
.npmrc
Normal file
4
.npmrc
Normal file
@ -0,0 +1,4 @@
|
||||
loglevel=warn
|
||||
|
||||
#Prevent folks from ignoring an important error when building from source
|
||||
engine-strict=true
|
71
API.md
71
API.md
@ -27,7 +27,7 @@
|
||||
- [Request Strategies **draft**](#request-strategies-draft)
|
||||
- [`latest` request strategy](#latest-request-strategy)
|
||||
- [`minmax` request strategy](#minmax-request-strategy)
|
||||
- [Telemetry Formats **draft**](#telemetry-formats-draft)
|
||||
- [Telemetry Formats](#telemetry-formats)
|
||||
- [Registering Formats](#registering-formats)
|
||||
- [Telemetry Data](#telemetry-data)
|
||||
- [Telemetry Datums](#telemetry-datums)
|
||||
@ -52,6 +52,8 @@
|
||||
- [The URL Status Indicator](#the-url-status-indicator)
|
||||
- [Creating a Simple Indicator](#creating-a-simple-indicator)
|
||||
- [Custom Indicators](#custom-indicators)
|
||||
- [Priority API](#priority-api)
|
||||
- [Priority Types](#priority-types)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
@ -247,16 +249,24 @@ To do so, use the `addRoot` method of the object API.
|
||||
eg.
|
||||
```javascript
|
||||
openmct.objects.addRoot({
|
||||
namespace: "example.namespace",
|
||||
key: "my-key"
|
||||
});
|
||||
namespace: "example.namespace",
|
||||
key: "my-key"
|
||||
},
|
||||
openmct.priority.HIGH);
|
||||
```
|
||||
|
||||
The `addRoot` function takes a single [object identifier](#domain-objects-and-identifiers)
|
||||
as an argument.
|
||||
The `addRoot` function takes a two arguments, the first can be an [object identifier](#domain-objects-and-identifiers) for a root level object, or an array of identifiers for root
|
||||
level objects, or a function that returns a promise for an identifier or an array of root level objects, the second is a [priority](#priority-api) or numeric value.
|
||||
|
||||
Root objects are loaded just like any other objects, i.e. via an object
|
||||
provider.
|
||||
When using the `getAll` method of the object API, they will be returned in order of priority.
|
||||
|
||||
eg.
|
||||
```javascript
|
||||
openmct.objects.addRoot(identifier, openmct.priority.LOW); // low = -1000, will appear last in composition or tree
|
||||
openmct.objects.addRoot(otherIdentifier, openmct.priority.HIGH); // high = 1000, will appear first in composition or tree
|
||||
```
|
||||
|
||||
Root objects are loaded just like any other objects, i.e. via an object provider.
|
||||
|
||||
## Object Providers
|
||||
|
||||
@ -423,13 +433,14 @@ attribute | type | flags | notes
|
||||
|
||||
###### Value Hints
|
||||
|
||||
Each telemetry value description has an object defining hints. Keys in this this object represent the hint itself, and the value represents the weight of that hint. A lower weight means the hint has a higher priority. For example, multiple values could be hinted for use as the y-axis of a plot (raw, engineering), but the highest priority would be the default choice. Likewise, a table will use hints to determine the default order of columns.
|
||||
Each telemetry value description has an object defining hints. Keys in this object represent the hint itself, and the value represents the weight of that hint. A lower weight means the hint has a higher priority. For example, multiple values could be hinted for use as the y-axis of a plot (raw, engineering), but the highest priority would be the default choice. Likewise, a table will use hints to determine the default order of columns.
|
||||
|
||||
Known hints:
|
||||
|
||||
* `domain`: Values with a `domain` hint will be used for the x-axis of a plot, and tables will render columns for these values first.
|
||||
* `range`: Values with a `range` hint will be used as the y-axis on a plot, and tables will render columns for these values after the `domain` values.
|
||||
* `image`: Indicates that the value may be interpreted as the URL to an image file, in which case appropriate views will be made available.
|
||||
* `imageDownloadName`: Indicates that the value may be interpreted as the name of the image file.
|
||||
|
||||
##### The Time Conductor and Telemetry
|
||||
|
||||
@ -480,6 +491,8 @@ In this case, the `domain` is the currently selected time-system, and the start
|
||||
|
||||
A telemetry provider's `request` method should return a promise for an array of telemetry datums. These datums must be sorted by `domain` in ascending order.
|
||||
|
||||
The telemetry provider's `request` method will also return an object `signal` with an `aborted` property with a value `true` if the request has been aborted by user navigation. This can be used to trigger actions when a request has been aborted.
|
||||
|
||||
#### Request Strategies **draft**
|
||||
|
||||
To improve performance views may request a certain strategy for data reduction. These are intended to improve visualization performance by reducing the amount of data needed to be sent to the client. These strategies will be indicated by additional parameters in the request options. You may choose to handle them or ignore them.
|
||||
@ -522,7 +535,7 @@ example:
|
||||
|
||||
MinMax queries are issued by plots, and may be issued by other types as well. The aim is to reduce the amount of data returned but still faithfully represent the full extent of the data. In order to do this, the view calculates the maximum data resolution it can display (i.e. the number of horizontal pixels in a plot) and sends that as the `size`. The response should include at least one minimum and one maximum value per point of resolution.
|
||||
|
||||
#### Telemetry Formats **draft**
|
||||
#### Telemetry Formats
|
||||
|
||||
Telemetry format objects define how to interpret and display telemetry data.
|
||||
They have a simple structure:
|
||||
@ -594,9 +607,17 @@ section.
|
||||
|
||||
#### Limit Evaluators **draft**
|
||||
|
||||
Limit evaluators allow a telemetry integrator to define how limits should be
|
||||
applied to telemetry from a given domain object. For an example of a limit
|
||||
evaluator, take a look at `examples/generator/SinewaveLimitProvider.js`.
|
||||
Limit evaluators allow a telemetry integrator to define which limits exist for a
|
||||
telemetry endpoint and how limits should be applied to telemetry from a given domain object.
|
||||
|
||||
A limit evaluator can implement the `evalute` method which is used to define how limits
|
||||
should be applied to telemetry and the `getLimits` method which is used to specify
|
||||
what the limit values are for different limit levels.
|
||||
|
||||
Limit levels can be mapped to one of 5 colors for visualization:
|
||||
`purple`, `red`, `orange`, `yellow` and `cyan`.
|
||||
|
||||
For an example of a limit evaluator, take a look at `examples/generator/SinewaveLimitProvider.js`.
|
||||
|
||||
### Telemetry Consumer APIs **draft**
|
||||
|
||||
@ -987,7 +1008,7 @@ reveal additional information when the mouse cursor is hovered over it.
|
||||
A common use case for indicators is to convey the state of some external system such as a
|
||||
persistence backend or HTTP server. So long as this system is accessible via HTTP request,
|
||||
Open MCT provides a general purpose indicator to show whether the server is available and
|
||||
returing a 2xx status code. The URL Status Indicator is made available as a default plugin. See
|
||||
returning a 2xx status code. The URL Status Indicator is made available as a default plugin. See
|
||||
the [documentation](./src/plugins/URLIndicatorPlugin) for details on how to install and configure the
|
||||
URL Status Indicator.
|
||||
|
||||
@ -1040,3 +1061,25 @@ A completely custom indicator can be added by simply providing a DOM element to
|
||||
element: domNode
|
||||
});
|
||||
```
|
||||
|
||||
## Priority API
|
||||
|
||||
Open MCT provides some built-in priority values that can be used in the application for view providers, indicators, root object order, and more.
|
||||
|
||||
### Priority Types
|
||||
|
||||
Currently, the Open MCT Priority API provides (type: numeric value):
|
||||
- HIGH: 1000
|
||||
- Default: 0
|
||||
- LOW: -1000
|
||||
|
||||
View provider Example:
|
||||
|
||||
``` javascript
|
||||
class ViewProvider {
|
||||
...
|
||||
priority() {
|
||||
return openmct.priority.HIGH;
|
||||
}
|
||||
}
|
||||
```
|
@ -10,7 +10,7 @@ accept changes from external contributors.
|
||||
|
||||
The short version:
|
||||
|
||||
1. Write your contribution.
|
||||
1. Write your contribution or describe your idea in the form of an [GitHub issue](https://github.com/nasa/openmct/issues/new/choose) or [Starting a GitHub Discussion](https://github.com/nasa/openmct/discussions)
|
||||
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,6 +18,7 @@ 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
|
||||
|
||||
@ -115,7 +116,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.
|
||||
* 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 __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.
|
||||
@ -182,7 +183,7 @@ The following guidelines are provided for anyone contributing source code to the
|
||||
1. Avoid the use of "magic" values.
|
||||
eg.
|
||||
```JavaScript
|
||||
Const UNAUTHORIZED = 401
|
||||
const UNAUTHORIZED = 401;
|
||||
if (responseCode === UNAUTHORIZED)
|
||||
```
|
||||
is preferable to
|
||||
@ -296,23 +297,12 @@ these standards.
|
||||
|
||||
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."
|
||||
* _(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.
|
||||
* _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.
|
||||
* _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.
|
||||
|
||||
## Check Lists
|
||||
|
||||
@ -322,16 +312,20 @@ checklist).
|
||||
|
||||
### 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?
|
||||
[Within PR Template](.github/PULL_REQUEST_TEMPLATE.md)
|
||||
|
||||
### 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)
|
||||
* [ ] Changes appear to address issue?
|
||||
* [ ] Changes appear not to be breaking changes?
|
||||
* [ ] Appropriate unit tests included?
|
||||
* [ ] Code style and in-line documentation are appropriate?
|
||||
* [ ] Commit messages meet standards?
|
||||
* [ ] Has associated issue been labelled `unverified`? (only applicable if this PR closes the issue)
|
||||
* [ ] Has associated issue been labelled `bug`? (only applicable if this PR is for a bug fix)
|
||||
* [ ] List of Acceptance Tests Performed.
|
||||
|
||||
Write out a small list of tests performed with just enough detail for another developer on the team
|
||||
to execute.
|
||||
|
||||
i.e. ```When Clicking on Add button, new `object` appears in dropdown.```
|
@ -1,6 +1,6 @@
|
||||
# Open MCT License
|
||||
|
||||
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, Copyright (c) 2014-2022, United States Government as represented by the Administrator of the National Aeronautics and Space Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
|
71
README.md
71
README.md
@ -1,9 +1,11 @@
|
||||
# Open MCT [](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
# Open MCT [](http://www.apache.org/licenses/LICENSE-2.0) [](https://lgtm.com/projects/g/nasa/openmct/context:javascript) [](https://codecov.io/gh/nasa/openmct) [](https://percy.io/b2e34b17/openmct) [](https://www.npmjs.com/package/openmct)
|
||||
|
||||
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.
|
||||
|
||||
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/).
|
||||
@ -18,7 +20,7 @@ Building and running Open MCT in your local dev environment is very easy. Be sur
|
||||
|
||||
`git clone https://github.com/nasa/openmct.git`
|
||||
|
||||
2. Install development dependencies
|
||||
2. Install development dependencies. Note: Check the package.json engine for our tested and supported node versions.
|
||||
|
||||
`npm install`
|
||||
|
||||
@ -28,11 +30,6 @@ Building and running Open MCT in your local dev environment is very easy. Be sur
|
||||
|
||||
Open MCT is now running, and can be accessed by pointing a web browser at [http://localhost:8080/](http://localhost:8080/)
|
||||
|
||||
## 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.
|
||||
|
||||
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
|
||||
|
||||
Documentation is available on the [Open MCT website](https://nasa.github.io/openmct/documentation/).
|
||||
@ -44,7 +41,7 @@ The clearest examples for developing Open MCT plugins are in the
|
||||
our documentation.
|
||||
|
||||
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).
|
||||
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).
|
||||
|
||||
## Building Applications With Open MCT
|
||||
|
||||
@ -52,6 +49,12 @@ Open MCT is built using [`npm`](http://npmjs.com/) and [`webpack`](https://webpa
|
||||
|
||||
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).
|
||||
|
||||
## 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.
|
||||
|
||||
If you encounter an issue with a particular browser, OS, or nodejs API, please file a [GitHub issue](https://github.com/nasa/openmct/issues/new/choose)
|
||||
|
||||
## Plugins
|
||||
|
||||
Open MCT can be extended via plugins that make calls to the Open MCT API. A plugin is a group
|
||||
@ -65,7 +68,10 @@ For information on writing plugins, please see [our API documentation](https://g
|
||||
|
||||
## Tests
|
||||
|
||||
Tests are written for [Jasmine 3](https://jasmine.github.io/api/3.1/global)
|
||||
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)
|
||||
and run by [Karma](http://karma-runner.github.io). To run:
|
||||
|
||||
`npm test`
|
||||
@ -74,14 +80,33 @@ 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`. (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.)
|
||||
tested by `src/foo/BarSpec.js`.
|
||||
|
||||
### Test Reporting
|
||||
### e2e, Visual, and Performance tests
|
||||
The e2e, Visual, and Performance tests are written for playwright and run by playwright's new 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`.
|
||||
To run the e2e tests which are part of every commit:
|
||||
|
||||
`npm run test:e2e:stable`
|
||||
|
||||
To run the visual test suite:
|
||||
|
||||
`npm run test:e2e:visual`
|
||||
|
||||
To run the performance tests:
|
||||
|
||||
`npm run test:perf`
|
||||
|
||||
The test suite is configured to all tests localed in `e2e/tests/` ending in `*.e2e.spec.js`. For more about the e2e test suite, please see 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/) and our overall security report is available on [LGTM](https://lgtm.com/projects/g/nasa/openmct/)
|
||||
|
||||
### 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/)
|
||||
|
||||
# Glossary
|
||||
|
||||
@ -122,3 +147,19 @@ documentation, may presume an understanding of these terms.
|
||||
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
|
||||
|
||||
## 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.
|
||||
|
31
SECURITY.md
Normal file
31
SECURITY.md
Normal file
@ -0,0 +1,31 @@
|
||||
# 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.
|
||||
|
||||
The project is also monitored by [LGTM](https://lgtm.com/projects/g/nasa/openmct/) and is available to public.
|
||||
|
||||
### 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.
|
52
app.js
52
app.js
@ -1,4 +1,4 @@
|
||||
/*global require,process,console*/
|
||||
/*global process*/
|
||||
|
||||
/**
|
||||
* Usage:
|
||||
@ -7,12 +7,12 @@
|
||||
* 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');
|
||||
const __DEV__ = !process.env.NODE_ENV || process.env.NODE_ENV === 'development';
|
||||
|
||||
// Defaults
|
||||
options.port = options.port || options.p || 8080;
|
||||
@ -40,15 +40,28 @@ app.use('/proxyUrl', function proxyRequest(req, res, next) {
|
||||
}).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(); }) });
|
||||
class WatchRunPlugin {
|
||||
apply(compiler) {
|
||||
compiler.hooks.emit.tapAsync('WatchRunPlugin', (compilation, callback) => {
|
||||
console.log('Begin compile at ' + new Date());
|
||||
callback();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
webpackConfig.entry.openmct = [
|
||||
'webpack-hot-middleware/client?reload=true',
|
||||
webpackConfig.entry.openmct
|
||||
];
|
||||
const webpack = require('webpack');
|
||||
let webpackConfig;
|
||||
if (__DEV__) {
|
||||
webpackConfig = require('./webpack.dev');
|
||||
webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
|
||||
webpackConfig.entry.openmct = [
|
||||
'webpack-hot-middleware/client?reload=true',
|
||||
webpackConfig.entry.openmct
|
||||
];
|
||||
webpackConfig.plugins.push(new WatchRunPlugin());
|
||||
} else {
|
||||
webpackConfig = require('./webpack.coverage');
|
||||
}
|
||||
|
||||
const compiler = webpack(webpackConfig);
|
||||
|
||||
@ -56,16 +69,16 @@ app.use(require('webpack-dev-middleware')(
|
||||
compiler,
|
||||
{
|
||||
publicPath: '/dist',
|
||||
logLevel: 'warn'
|
||||
stats: 'errors-warnings'
|
||||
}
|
||||
));
|
||||
|
||||
app.use(require('webpack-hot-middleware')(
|
||||
compiler,
|
||||
{
|
||||
|
||||
}
|
||||
));
|
||||
if (__DEV__) {
|
||||
app.use(require('webpack-hot-middleware')(
|
||||
compiler,
|
||||
{}
|
||||
));
|
||||
}
|
||||
|
||||
// Expose index.html for development users.
|
||||
app.get('/', function (req, res) {
|
||||
@ -73,6 +86,7 @@ app.get('/', function (req, 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)
|
||||
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-2017, United States Government
|
||||
#* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
#* as represented by the Administrator of the National Aeronautics and Space
|
||||
#* Administration. All rights reserved.
|
||||
#*
|
||||
|
28
codecov.yml
Normal file
28
codecov.yml
Normal file
@ -0,0 +1,28 @@
|
||||
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: true
|
||||
e2e-ci:
|
||||
carryforward: true
|
||||
e2e-full:
|
||||
carryforward: true
|
||||
|
||||
comment:
|
||||
layout: "reach,diff,flags,files,footer"
|
||||
behavior: default
|
||||
require_changes: false
|
||||
show_carryforward_flags: true
|
@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
Open MCT, Copyright (c) 2014-2022, 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-2020, United States Government
|
||||
* Open MCT, Copyright (c) 2014-2022, 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-2017, United States Government
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
|
@ -1,232 +0,0 @@
|
||||
# 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.
|
@ -1,76 +0,0 @@
|
||||
# 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.
|
@ -1,726 +0,0 @@
|
||||
# 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.
|
@ -1,3 +0,0 @@
|
||||
Design proposals:
|
||||
|
||||
* [API Redesign](proposals/APIRedesign.md)
|
@ -1,338 +0,0 @@
|
||||
# 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.
|
File diff suppressed because it is too large
Load Diff
@ -1,251 +0,0 @@
|
||||
<!-- 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.
|
||||
|
@ -1,164 +0,0 @@
|
||||
# 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.
|
@ -1,138 +0,0 @@
|
||||
# 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.
|
File diff suppressed because it is too large
Load Diff
@ -22,16 +22,5 @@
|
||||
* The [Development Process](process/) document describes the
|
||||
Open MCT software development cycle.
|
||||
|
||||
## 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 |
|
||||
| __3__ | Per-sprint testing | Triage | | _Per-sprint testing*_ | Ship |
|
||||
| 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|
|
||||
|
||||
* If necessary.
|
||||
|
||||
@ -105,14 +105,20 @@ 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
|
||||
(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.
|
||||
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.
|
||||
* [__Per-release Testing.__](testing/plan.md#per-release-testing)
|
||||
Structured testing with predefined
|
||||
success criteria. No release should ship without passing
|
||||
@ -126,8 +132,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, test exploratorily for
|
||||
regressions, et cetera.
|
||||
completed work from this sprint using the sprint branch, 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
|
||||
@ -143,7 +149,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.
|
||||
Pre-release Testing if the first round is not passed. Smoke tests collected from issues/PRs
|
||||
* __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,9 +7,5 @@ documents:
|
||||
process points are repeated during development.
|
||||
* The [Version Guide](version.md) describes version numbering for
|
||||
Open MCT (both semantics and process.)
|
||||
* 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.
|
||||
* The [Test Plan](testing/plan.md) summarizes the approaches used
|
||||
to test Open MCT.
|
||||
|
@ -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.
|
||||
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.
|
||||
|
||||
### 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/openmctweb/blob/master/CONTRIBUTING.md#issue-reporting)
|
||||
[report issues](https://github.com/nasa/openmct/issues/new/choose)
|
||||
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/openmctweb/blob/master/CONTRIBUTING.md#issue-reporting)
|
||||
[reported as issues](https://github.com/nasa/openmct/issues/new/choose)
|
||||
and reviewed for severity.
|
||||
|
||||
## Test Performance
|
||||
|
@ -1,169 +0,0 @@
|
||||
# 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.)
|
@ -92,47 +92,60 @@ should update (or delegate the task of updating) Open MCT version
|
||||
numbers by the following process:
|
||||
|
||||
1. Update version number in `package.json`
|
||||
1. Remove `-SNAPSHOT` suffix.
|
||||
2. Verify that resulting version number meets semantic versioning
|
||||
requirements relative to previous stable version. Increment if
|
||||
necessary.
|
||||
3. If version is considered unstable (which may be the case during
|
||||
1. Checkout branch created for the last sprint that has been successfully tested.
|
||||
2. Remove a `-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.
|
||||
4. If version is considered unstable (which may be the case during
|
||||
the first three sprints of a release), apply a new suffix per
|
||||
[Version Numbering](#version-numbering) guidance above.
|
||||
2. Tag the release.
|
||||
1. Commit changes to `package.json` on the `master` branch.
|
||||
1. Commit changes to `package.json` on the new branch created in
|
||||
the previous step.
|
||||
The commit message should reference the sprint being closed,
|
||||
preferably by a URL reference to the associated Milestone in
|
||||
GitHub.
|
||||
2. Verify that build still completes, that application passes
|
||||
smoke-testing, and that only differences from tested versions
|
||||
are the changes to version number above.
|
||||
3. Push the `master` branch.
|
||||
3. Push the new branch.
|
||||
4. Tag this commit with the version number, prepending the letter "v".
|
||||
(e.g. `git tag v0.9.3-alpha`)
|
||||
5. Push the tag to GitHub. (e.g. `git push origin v0.9.3-alpha`).
|
||||
3. Upload a release archive.
|
||||
1. Run `npm pack` to generate the archive.
|
||||
2. Use the [GitHub release interface](https://github.com/nasa/openmct/releases)
|
||||
1. Use the [GitHub release interface](https://github.com/nasa/openmct/releases)
|
||||
to draft a new release.
|
||||
3. Choose the existing tag for the new version (created and pushed above.)
|
||||
2. Choose the existing tag for the new version (created and pushed above.)
|
||||
Enter the tag name as the release name as well; see existing releases
|
||||
for examples.
|
||||
4. Attach the release archive.
|
||||
5. Designate the release as a "pre-release" as appropriate (for instance,
|
||||
for examples. (e.g. `Open MCT v0.9.3-alpha`)
|
||||
3. Designate the release as a "pre-release" as appropriate (for instance,
|
||||
when the version number has been suffixed as unstable, or when
|
||||
the version number is below 1.0.0.)
|
||||
4. Restore snapshot status in `package.json`
|
||||
1. Remove any suffix from the version number, or increment the
|
||||
_patch_ version if there is no suffix.
|
||||
2. Append a `-SNAPSHOT` suffix.
|
||||
3. Commit changes to `package.json` on the `master` branch.
|
||||
4. Add release notes including any breaking changes, enhancements,
|
||||
bug fixes with solutions in brief.
|
||||
5. Publish the release.
|
||||
4. Publish the release to npm
|
||||
1. Login to npm
|
||||
2. Checkout the tag created in the previous step.
|
||||
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 publishj if this is a prerelease.
|
||||
6. Confirm the package has been published (e.g. `https://www.npmjs.com/package/openmct`)
|
||||
5. Update snapshot status in `package.json`
|
||||
1. Create a new branch off the `master` branch.
|
||||
2. Remove any suffix from the version number,
|
||||
or increment the _patch_ version if there is no 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
|
||||
GitHub.
|
||||
4. Verify that build still completes, that application passes
|
||||
5. Verify that build still completes, that application passes
|
||||
smoke-testing.
|
||||
5. Push the `master` branch.
|
||||
6. Create a PR to be merged into the `master` branch.
|
||||
|
||||
Projects dependent on Open MCT being co-developed by the Open MCT
|
||||
team should follow a similar process, except that they should
|
||||
|
15
e2e/.eslintrc.js
Normal file
15
e2e/.eslintrc.js
Normal file
@ -0,0 +1,15 @@
|
||||
/* eslint-disable no-undef */
|
||||
module.exports = {
|
||||
"extends": ["plugin:playwright/playwright-test"],
|
||||
"rules": {
|
||||
"playwright/max-nested-describe": ["error", { "max": 1 }]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["tests/visual/*.spec.js"],
|
||||
"rules": {
|
||||
"playwright/no-wait-for-timeout": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
5
e2e/.percy.yml
Normal file
5
e2e/.percy.yml
Normal file
@ -0,0 +1,5 @@
|
||||
version: 2
|
||||
snapshot:
|
||||
widths: [1024, 2000]
|
||||
min-height: 1440 # px
|
||||
|
318
e2e/README.md
Normal file
318
e2e/README.md
Normal file
@ -0,0 +1,318 @@
|
||||
# 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](#architecture)
|
||||
|
||||
## 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 an Example Telemetry Object (e.g.: 'Sine Wave Generator')
|
||||
3. Create a 'Plot' Object (e.g.: 'Stacked Plot')
|
||||
4. Expand the Tree on the left-hand nav and drag and drop the Example Telemetry Object into the Plot Object
|
||||
5. Create a 'Display Layout' object
|
||||
6. From the Tree, Drag the Plot object into the Display Layout
|
||||
|
||||
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
|
||||
2. Open another terminal window and start the Playwright codegen application `npx playwright codegen`
|
||||
3. Navigate the browser to `http://localhost:8080`
|
||||
4. Click the Create button and notice how your actions in the browser are being recorded in the Playwright Inspector
|
||||
5. 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 three 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.**
|
||||
|
||||
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` 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.
|
||||
|
||||
#### 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)
|
||||
|
||||
### (Advanced) Snapshot Testing
|
||||
|
||||
Snapshot testing is very similar to visual testing but allows us to be more precise in detecting change without relying on a 3rd party service. Unfortuantely, this precision requires advanced test setup and teardown and so we're using this pattern as a last resort.
|
||||
|
||||
To give an example, if a *single* visual test assertion for an Overlay plot is run through multiple DOM rendering engines at various viewports to see how the Plot looks. If that same test were run as a snapshot test, it could only be executed against a single browser, on a single platform (ubuntu docker container).
|
||||
|
||||
Read more about [Playwright Snapshots](https://playwright.dev/docs/test-snapshots)
|
||||
|
||||
Open MCT's implementation
|
||||
-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
|
||||
|
||||
```
|
||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:[GET THIS VERSION FROM OUR CIRCLECI CONFIG FILE]-focal /bin/bash
|
||||
npm install
|
||||
npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot
|
||||
```
|
||||
|
||||
(WIP) Updating Snapshots
|
||||
When the @snapshot tests fail, they will need to be evaluated to see if the failure is an acceptable change or
|
||||
|
||||
## Performance Testing
|
||||
|
||||
The open source performance tests function mostly as a contract for the locator logic, functionality, and assumptions will work in our downstream, closed source test suites.
|
||||
|
||||
They're found in the `/e2e/tests/performance` repo and are to be executed with the following npm script:
|
||||
|
||||
```npm run test:perf```
|
||||
|
||||
These tests are expected to become blocking and gating with assertions as we extend the capabilities of playwright.
|
||||
|
||||
## Test Architecture and CI
|
||||
|
||||
### Architecture (TODO)
|
||||
|
||||
|
||||
|
||||
### File Structure
|
||||
|
||||
Our file structure follows the type of type of testing being excercised at the e2e layer and files containing test suites which matcher application behavior or our `src` and `example` layout. This area is not well refined as we figure out what works best for closed source and downstream projects. This may change altogether if we move `e2e` to it's own npm package.
|
||||
|
||||
- `./helper` - contains helper functions or scripts which are leveraged directly within the testsuites. i.e. non-default plugin scripts injected into DOM
|
||||
- `./test-data` - contains test data which is leveraged or generated in the functional, performance, or visual test suites. i.e. 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
|
||||
- `./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 testframework functionality and assumptions will continue to work based on further refactoring or playwright version changes
|
||||
- `./tests/performance/` - performance tests
|
||||
- `./tests/visual/` - Visual tests
|
||||
- `./appActions.js` - Contains common fixtures which can be leveraged by testcase authors to quickly move through the application when writing new tests.
|
||||
- `./baseFixture.js` - Contains base fixtures which only extend default `@playwright/test` functionality. The goal is to remove these fixtures as native Playwright APIs improve.
|
||||
|
||||
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
|
||||
- `./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-visual.config.js` - Used to run the visual 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:
|
||||
- `@ipad` - Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no Create button).
|
||||
- `@gds` - Denotes a GDS Test Case used in the VIPER Mission.
|
||||
- `@addInit` - Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of app.js.
|
||||
- `@localStorage` - Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB).
|
||||
- `@snapshot` - Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of the playwright container.
|
||||
- `@unstable` - A new test or test which is known to be flaky.
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
The cheapest time to catch a bug is Pre-merge. Unfortuantely, this is the most expensive time to run all of the tests since each Merge event can consistent 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
|
||||
- Stable e2e tests against ubuntu and chrome
|
||||
- Performance tests against ubuntu and chrome
|
||||
- e2e tests are linted
|
||||
|
||||
#### 2. Per-Merge Testing
|
||||
Github Actions / Workflow
|
||||
- Full suite against all browsers/projects. Triggered with Github Label Event 'pr:e2e'
|
||||
- Visual Tests. Triggered with Github Label Event 'pr:visual'
|
||||
|
||||
#### 3. Scheduled / Batch Testing
|
||||
Nightly Testing in Circle CI
|
||||
- Full e2e suite against ubuntu and chrome
|
||||
- Performance tests against ubuntu and chrome
|
||||
|
||||
Github Actions / Workflow
|
||||
- Visual Test baseline generation.
|
||||
|
||||
#### 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 CircelCI 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.
|
||||
#### Test Promotion
|
||||
|
||||
In order to maintain fast and reliable feedback, tests go through a promotion process. All new test cases or test suites must be labeled with the `@unstable` annotation. The Open MCT dev team runs these unstable tests in our private repos to ensure they work downstream and are reliable.
|
||||
|
||||
To run the stable tests, use the ```npm run test:e2e:stable``` command. To run the new and flaky tests, use the ```npm run test:e2e:unstable``` command.
|
||||
|
||||
A testcase and testsuite are to be unmarked as @unstable when:
|
||||
1. They run as part of "full" run 5 times without failure.
|
||||
2. They've been by a Open MCT Developer 5 times in the closed source repo without failure.
|
||||
|
||||
### Cross-browser and Cross-operating system
|
||||
|
||||
- Where is it tested
|
||||
- What's supported
|
||||
- Mobile
|
||||
|
||||
## Test Design, Best Practices, and Tips & Tricks
|
||||
|
||||
### Test Design (TODO)
|
||||
|
||||
- How to make tests robust to function in other contexts (VISTA, VIPER, etc.)
|
||||
- Leverage the use of appActions.js like getOrCreateDomainObject
|
||||
- How to make tests faster and more resilient
|
||||
- When possible, navigate directly by URL
|
||||
- Leverage ```await page.goto('/', { waitUntil: 'networkidle' });```
|
||||
- Avoid repeated setup to test to test a single assertion. Write longer tests with multiple soft assertions.
|
||||
|
||||
### How to write a great test (TODO)
|
||||
|
||||
#### How to write a great visual test (TODO)
|
||||
|
||||
### 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.
|
||||
|
||||
### Tips & Tricks (TODO)
|
||||
|
||||
The following contains a list of tips and tricks which don't exactly fit into a FAQ or Best Practices doc.
|
||||
|
||||
- Working with multiple pages
|
||||
There are instances where multiple browser pages will need to be opened to verify multi-page or multi-tab application behavior.
|
||||
|
||||
### 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: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
|
||||
|
||||
Code coverage is collected during test execution using our custom [baseFixture](./baseFixtures.js). The raw coverage files are stored in a `.nyc_report` directory to be converted into a lcov file with the following [nyc](https://github.com/istanbuljs/nyc) command:
|
||||
|
||||
```npm run cov:e2e:report```
|
||||
|
||||
At this point, the nyc linecov report can be published to [codecov.io](https://about.codecov.io/) with the following command:
|
||||
|
||||
```npm run cov:e2e:stable:publish``` for the stable suite running in ubuntu.
|
||||
or
|
||||
```npm run cov:e2e:full:publish``` for the full suite running against all available platforms.
|
||||
|
||||
Codecov.io will combine each of the above commands with [Codecov.io Flags](https://docs.codecov.com/docs/flags). Effectively, this allows us to combine multiple reports which are run at various stages of our CI Pipeline or run as part of a parallel process.
|
||||
|
||||
This e2e coverage is combined with our unit test report to give a comprehensive (if flawed) view of line 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?
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
- Why is my test failing on CI and not locally?
|
||||
- How can I view the failing tests on CI?
|
||||
- Tests won't start because 'Error: http://localhost:8080/# is already used...'
|
||||
This error will appear when running the tests locally. Sometimes, the webserver is left in an orphaned state and needs to be cleaned up. To clear up the orphaned webserver, execute the following from your Terminal:
|
||||
```lsof -n -i4TCP:8080 | awk '{print$2}' | tail -1 | xargs kill -9```
|
83
e2e/appActions.js
Normal file
83
e2e/appActions.js
Normal file
@ -0,0 +1,83 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This common function creates a `domainObject` with 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
|
||||
* @param {string} type
|
||||
* @param {string | undefined} name
|
||||
*/
|
||||
async function createDomainObjectWithDefaults(page, type, name) {
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click the object specified by 'type'
|
||||
await page.click(`text=${type}`);
|
||||
|
||||
// Modify the name input field of the domain object to accept 'name'
|
||||
if (name) {
|
||||
const nameInput = page.locator('input[type="text"]').nth(2);
|
||||
await nameInput.fill("");
|
||||
await nameInput.fill(name);
|
||||
}
|
||||
|
||||
// Click OK button and wait for Navigate event
|
||||
await Promise.all([
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('text=OK')
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the given `domainObject`'s context menu from the object tree.
|
||||
* Expands the 'My Items' folder if it is not already expanded.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} myItemsFolderName the name of the "My Items" folder
|
||||
* @param {string} domainObjectName the display name of the `domainObject`
|
||||
*/
|
||||
async function openObjectTreeContextMenu(page, myItemsFolderName, domainObjectName) {
|
||||
const myItemsFolder = page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3);
|
||||
const className = await myItemsFolder.getAttribute('class');
|
||||
if (!className.includes('c-disclosure-triangle--expanded')) {
|
||||
await myItemsFolder.click();
|
||||
}
|
||||
|
||||
await page.locator(`a:has-text("${domainObjectName}")`).click({
|
||||
button: 'right'
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
module.exports = {
|
||||
createDomainObjectWithDefaults,
|
||||
openObjectTreeContextMenu
|
||||
};
|
174
e2e/baseFixtures.js
Normal file
174
e2e/baseFixtures.js
Normal file
@ -0,0 +1,174 @@
|
||||
/* eslint-disable no-undef */
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const base = require('@playwright/test');
|
||||
const { expect } = base;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { v4: uuid } = require('uuid');
|
||||
const sinon = require('sinon');
|
||||
|
||||
/**
|
||||
* 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)));
|
||||
}
|
||||
|
||||
/**
|
||||
* This is part of our codecoverage shim.
|
||||
* @see {@link https://github.com/mxschmitt/playwright-test-coverage Github Example Project}
|
||||
* @constant {string}
|
||||
*/
|
||||
const istanbulCLIOutput = path.join(process.cwd(), '.nyc_output');
|
||||
|
||||
exports.test = base.test.extend({
|
||||
/**
|
||||
* This allows the test to manipulate the browser clock. This is useful for Visual and Snapshot tests which need
|
||||
* the Time Indicator Clock to be in a specific state.
|
||||
* Usage:
|
||||
* ```
|
||||
* test.use({
|
||||
* clockOptions: {
|
||||
* now: 0,
|
||||
* shouldAdvanceTime: true
|
||||
* ```
|
||||
* If clockOptions are provided, will override the default clock with fake timers provided by SinonJS.
|
||||
*
|
||||
* Default: `undefined`
|
||||
*
|
||||
* @see {@link https://github.com/microsoft/playwright/issues/6347 Github RFE}
|
||||
* @see {@link https://github.com/sinonjs/fake-timers/#var-clock--faketimersinstallconfig SinonJS FakeTimers Config}
|
||||
*/
|
||||
clockOptions: [undefined, { option: true }],
|
||||
overrideClock: [async ({ context, clockOptions }, use) => {
|
||||
if (clockOptions !== undefined) {
|
||||
await context.addInitScript({
|
||||
path: path.join(__dirname, '../', './node_modules/sinon/pkg/sinon.js')
|
||||
});
|
||||
await context.addInitScript((options) => {
|
||||
window.__clock = sinon.useFakeTimers(options);
|
||||
}, clockOptions);
|
||||
}
|
||||
|
||||
await use(context);
|
||||
}, {
|
||||
auto: true,
|
||||
scope: 'test'
|
||||
}],
|
||||
/**
|
||||
* Extends the base context class to add codecoverage shim.
|
||||
* @see {@link https://github.com/mxschmitt/playwright-test-coverage Github Project}
|
||||
*/
|
||||
context: async ({ context }, use) => {
|
||||
await context.addInitScript(() =>
|
||||
window.addEventListener('beforeunload', () =>
|
||||
(window).collectIstanbulCoverage(JSON.stringify((window).__coverage__))
|
||||
)
|
||||
);
|
||||
await fs.promises.mkdir(istanbulCLIOutput, { recursive: true });
|
||||
await context.exposeFunction('collectIstanbulCoverage', (coverageJSON) => {
|
||||
if (coverageJSON) {
|
||||
fs.writeFileSync(path.join(istanbulCLIOutput, `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 => expect.soft(msg.type(), `Console error detected: ${_consoleMessageToString(msg)}`).not.toEqual('error')
|
||||
);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Extends the base browser class to enable CDP connection definition in playwright.config.js. Once
|
||||
* that RFE is implemented, this function can be removed.
|
||||
* @see {@link https://github.com/microsoft/playwright/issues/8379 Github RFE}
|
||||
*/
|
||||
browser: async ({ playwright, browser }, use, workerInfo) => {
|
||||
// Use browserless if configured
|
||||
if (workerInfo.project.name.match(/browserless/)) {
|
||||
const vBrowser = await playwright.chromium.connectOverCDP({
|
||||
endpointURL: 'ws://localhost:3003'
|
||||
});
|
||||
await use(vBrowser);
|
||||
} else {
|
||||
// Use Local Browser for testing.
|
||||
await use(browser);
|
||||
}
|
||||
}
|
||||
});
|
||||
exports.expect = expect;
|
||||
exports.waitForAnimations = waitForAnimations;
|
@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2018, United States Government
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
@ -20,17 +20,11 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(['objectUtils'], function (objectUtils) {
|
||||
function AdapterCapability(domainObject) {
|
||||
this.domainObject = domainObject;
|
||||
}
|
||||
// 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') });
|
||||
|
||||
AdapterCapability.prototype.invoke = function () {
|
||||
return objectUtils.toNewFormat(
|
||||
this.domainObject.getModel(),
|
||||
this.domainObject.getId()
|
||||
);
|
||||
};
|
||||
|
||||
return AdapterCapability;
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const openmct = window.openmct;
|
||||
openmct.install(openmct.plugins.RestrictedNotebook('CUSTOM_NAME'));
|
||||
});
|
27
e2e/helper/addNoneditableObject.js
Normal file
27
e2e/helper/addNoneditableObject.js
Normal file
@ -0,0 +1,27 @@
|
||||
(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: []
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}());
|
80
e2e/playwright-ci.config.js
Normal file
80
e2e/playwright-ci.config.js
Normal file
@ -0,0 +1,80 @@
|
||||
/* eslint-disable no-undef */
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { devices } = require('@playwright/test');
|
||||
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 maxFailures = 5, this should ensure that flake is managed without failing the full suite
|
||||
testDir: 'tests',
|
||||
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
|
||||
timeout: 60 * 1000,
|
||||
webServer: {
|
||||
command: 'cross-env NODE_ENV=test npm run start',
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: false
|
||||
},
|
||||
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'
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chrome',
|
||||
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' }],
|
||||
['github']
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = config;
|
107
e2e/playwright-local.config.js
Normal file
107
e2e/playwright-local.config.js
Normal file
@ -0,0 +1,107 @@
|
||||
/* eslint-disable no-undef */
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { devices } = require('@playwright/test');
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 0,
|
||||
testDir: 'tests',
|
||||
testIgnore: '**/*.perf.spec.js',
|
||||
timeout: 30 * 1000,
|
||||
webServer: {
|
||||
env: {
|
||||
NODE_ENV: 'test'
|
||||
},
|
||||
command: 'npm run start',
|
||||
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',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
viewport: {
|
||||
width: 2560,
|
||||
height: 1440
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'safari',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grep: /@ipad/, // only run ipad tests due to this bug https://github.com/microsoft/playwright/issues/8340
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'webkit'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'firefox'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'canary',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome-canary' //Note this is not available in ubuntu/CircleCI
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'chrome-beta',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome-beta'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'ipad',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grep: /@ipad/,
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'webkit',
|
||||
...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
|
||||
}
|
||||
}
|
||||
],
|
||||
reporter: [
|
||||
['list'],
|
||||
['html', {
|
||||
open: 'on-failure',
|
||||
outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
|
||||
}]
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = config;
|
43
e2e/playwright-performance.config.js
Normal file
43
e2e/playwright-performance.config.js
Normal file
@ -0,0 +1,43 @@
|
||||
/* eslint-disable no-undef */
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
const CI = process.env.CI === 'true';
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 1, //Only for debugging purposes because trace is enabled only on first retry
|
||||
testDir: 'tests/performance/',
|
||||
timeout: 60 * 1000,
|
||||
workers: 1, //Only run in serial with 1 worker
|
||||
webServer: {
|
||||
command: 'cross-env NODE_ENV=test npm run start',
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: !CI
|
||||
},
|
||||
use: {
|
||||
browserName: "chromium",
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: CI, //Only if running locally
|
||||
ignoreHTTPSErrors: true,
|
||||
screenshot: 'off',
|
||||
trace: 'on-first-retry',
|
||||
video: 'off'
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chrome',
|
||||
use: {
|
||||
browserName: 'chromium'
|
||||
}
|
||||
}
|
||||
],
|
||||
reporter: [
|
||||
['list'],
|
||||
['junit', { outputFile: 'test-results/results.xml' }],
|
||||
['json', { outputFile: 'test-results/results.json' }]
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = config;
|
32
e2e/playwright-visual.config.js
Normal file
32
e2e/playwright-visual.config.js
Normal file
@ -0,0 +1,32 @@
|
||||
/* eslint-disable no-undef */
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 0, // visual tests should never retry due to snapshot comparison errors
|
||||
testDir: 'tests/visual',
|
||||
timeout: 90 * 1000,
|
||||
workers: 1, // visual tests should never run in parallel due to test pollution
|
||||
webServer: {
|
||||
command: 'cross-env NODE_ENV=test npm run start',
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: !process.env.CI
|
||||
},
|
||||
use: {
|
||||
browserName: "chromium",
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: true, // this needs to remain headless to avoid visual changes due to GPU
|
||||
ignoreHTTPSErrors: true,
|
||||
screenshot: 'on',
|
||||
trace: 'off',
|
||||
video: 'off'
|
||||
},
|
||||
reporter: [
|
||||
['list'],
|
||||
['junit', { outputFile: 'test-results/results.xml' }]
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = config;
|
125
e2e/pluginFixtures.js
Normal file
125
e2e/pluginFixtures.js
Normal file
@ -0,0 +1,125 @@
|
||||
/* eslint-disable no-undef */
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* The file contains custom fixtures which extend the base functionality of the Playwright fixtures
|
||||
* and appActions. These fixtures should be generalized across all plugins.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('./baseFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('./appActions');
|
||||
|
||||
/**
|
||||
* @typedef {Object} ObjectCreateOptions
|
||||
* @property {string} type
|
||||
* @property {string} name
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
// Once object is created, get the uuid from the url
|
||||
const uuid = await page.evaluate(() => {
|
||||
return window.location.href.match(/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/)[0];
|
||||
});
|
||||
|
||||
createdObjects.set(objectName, uuid);
|
||||
|
||||
return uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 name of the "My Items" folder in the domain object tree.
|
||||
*
|
||||
* Default: `"My Items"`
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
const myItemsFolderName = "My Items";
|
||||
|
||||
exports.test = test.extend({
|
||||
myItemsFolderName: [myItemsFolderName, { option: true }],
|
||||
// eslint-disable-next-line no-shadow
|
||||
openmctConfig: async ({ myItemsFolderName }, use) => {
|
||||
await use({ myItemsFolderName });
|
||||
},
|
||||
objectCreateOptions: [objectCreateOptions, {option: true}],
|
||||
// eslint-disable-next-line no-shadow
|
||||
domainObject: [async ({ page, objectCreateOptions }, use) => {
|
||||
// FIXME: This is a false-positive caused by a bug in the eslint-plugin-playwright rule.
|
||||
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||
if (objectCreateOptions === null) {
|
||||
await use(page);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
const uuid = await getOrCreateDomainObject(page, objectCreateOptions);
|
||||
await use({ uuid });
|
||||
}, { auto: true }]
|
||||
});
|
||||
exports.expect = expect;
|
1
e2e/test-data/PerformanceDisplayLayout.json
Normal file
1
e2e/test-data/PerformanceDisplayLayout.json
Normal file
@ -0,0 +1 @@
|
||||
{"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
e2e/test-data/PerformanceNotebook.json
Normal file
1
e2e/test-data/PerformanceNotebook.json
Normal file
@ -0,0 +1 @@
|
||||
{"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"}
|
22
e2e/test-data/VisualTestData_storage.json
Normal file
22
e2e/test-data/VisualTestData_storage.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"cookies": [],
|
||||
"origins": [
|
||||
{
|
||||
"origin": "http://localhost:8080",
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "tcHistory",
|
||||
"value": "{\"utc\":[{\"start\":1658617611983,\"end\":1658619411983}]}"
|
||||
},
|
||||
{
|
||||
"name": "mct",
|
||||
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"7fa5749b-8969-494c-9d85-c272516d333c\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1658619412848,\"modified\":1658619412848},\"7fa5749b-8969-494c-9d85-c272516d333c\":{\"identifier\":{\"key\":\"7fa5749b-8969-494c-9d85-c272516d333c\",\"namespace\":\"\"},\"name\":\"Unnamed Overlay Plot\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\",\"namespace\":\"\"}}]},\"modified\":1658619413566,\"location\":\"mine\",\"persisted\":1658619413567},\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\":{\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"identifier\":{\"key\":\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\"},\"modified\":1658619413552,\"location\":\"7fa5749b-8969-494c-9d85-c272516d333c\",\"persisted\":1658619413552}}"
|
||||
},
|
||||
{
|
||||
"name": "mct-tree-expanded",
|
||||
"value": "[\"/browse/mine\"]"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
22
e2e/test-data/recycled_local_storage.json
Normal file
22
e2e/test-data/recycled_local_storage.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"cookies": [],
|
||||
"origins": [
|
||||
{
|
||||
"origin": "http://localhost:8080",
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "tcHistory",
|
||||
"value": "{\"utc\":[{\"start\":1658617494563,\"end\":1658619294563},{\"start\":1658617090044,\"end\":1658618890044},{\"start\":1658616460484,\"end\":1658618260484},{\"start\":1658608882159,\"end\":1658610682159},{\"start\":1654537164464,\"end\":1654538964464},{\"start\":1652301954635,\"end\":1652303754635}]}"
|
||||
},
|
||||
{
|
||||
"name": "mct",
|
||||
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1658619295366,\"modified\":1658619295366},\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"73f2d9ae-d1f3-4561-b7fc-ecd5df557249\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1652303755999,\"location\":\"mine\",\"persisted\":1652303756002},\"2d02a680-eb7e-4645-bba2-dd298f76efb8\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4291d80c-303c-4d8d-85e1-10f012b864fb\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1654538965702,\"location\":\"mine\",\"persisted\":1654538965702},\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"2b6bf89f-877b-42b8-acc1-a9a575efdbe1\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658610682787,\"location\":\"mine\",\"persisted\":1658610682787},\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"b9a9c413-4b94-401d-b0c7-5e404f182616\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658618261112,\"location\":\"mine\",\"persisted\":1658618261112},\"3e294eae-6124-409b-a870-554d1bdcdd6f\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"108043b1-9c88-4e1d-8deb-fbf2cdb528f9\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658618890910,\"location\":\"mine\",\"persisted\":1658618890910},\"ec24d05d-5df5-4c96-9241-b73636cd19a9\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4062bd9b-b788-43dd-ab0a-8fa10a78d4b3\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658619295363,\"location\":\"mine\",\"persisted\":1658619295363}}"
|
||||
},
|
||||
{
|
||||
"name": "mct-tree-expanded",
|
||||
"value": "[]"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
55
e2e/tests/framework/baseFixtures.e2e.spec.js
Normal file
55
e2e/tests/framework/baseFixtures.e2e.spec.js
Normal file
@ -0,0 +1,55 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to testing our use of the playwright framework as it
|
||||
relates to how we've extended it (i.e. ./e2e/baseFixtures.js) and assumptions made in our dev environment
|
||||
(app.js and ./e2e/webpack-dev-middleware.js)
|
||||
*/
|
||||
|
||||
const { test } = require('../../baseFixtures.js');
|
||||
|
||||
test.describe('baseFixtures tests', () => {
|
||||
test('Verify that tests fail if console.error is thrown @framework', async ({ page }) => {
|
||||
test.fail();
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
//Verify that ../fixtures.js detects console log errors
|
||||
await Promise.all([
|
||||
page.evaluate(() => console.error('This should result in a failure')),
|
||||
page.waitForEvent('console') // always wait for the event to happen while triggering it!
|
||||
]);
|
||||
|
||||
});
|
||||
test('Verify that tests pass if console.warn is thrown @framework', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
//Verify that ../fixtures.js detects console log errors
|
||||
await Promise.all([
|
||||
page.evaluate(() => console.warn('This should result in a pass')),
|
||||
page.waitForEvent('console') // always wait for the event to happen while triggering it!
|
||||
]);
|
||||
|
||||
});
|
||||
});
|
124
e2e/tests/framework/exampleTemplate.e2e.spec.js
Normal file
124
e2e/tests/framework/exampleTemplate.e2e.spec.js
Normal file
@ -0,0 +1,124 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
* This test suite template is to be used when creating new testsuites. It will be kept up to date with the latest improvements
|
||||
* made by the Open MCT team. It will also follow our best pratices as those evolve. Please use this structure as a _reference_ and clear
|
||||
* or update any references when creating a new test suite!
|
||||
*
|
||||
* To illustrate current best practices, we've included a mocked up test suite for Renaming a Timer domain object. In this example
|
||||
* this test suite should be cloned and renamed as /e2e/tests/plugins/timer/renameTimer.e2e.spec.js
|
||||
*
|
||||
* Demonstrated:
|
||||
* - Using appActions to leverage existing functions
|
||||
* - Structure
|
||||
* - @unstable annotation
|
||||
* - await, expect, test, describe syntax
|
||||
* - Writing a custom function for a test suite
|
||||
* - Test stub for unfinished test coverage (test.fixme)
|
||||
*
|
||||
* The structure should follow
|
||||
* 1. imports
|
||||
* 2. test.describe()
|
||||
* 3. -> test1
|
||||
* -> test2
|
||||
* -> test3(stub)
|
||||
* 4. Any custom functions
|
||||
*
|
||||
*/
|
||||
|
||||
//Structure: Some standard Imports. Please update the required pathing
|
||||
const { test, expect } = require('../../baseFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
|
||||
// Structure: Try to keep a single describe block per logical groups of tests. If your test runtime exceeds 5 minutes or 500 lines, it's likely that it will need to be split.
|
||||
// Annotations: Please use the @unstable tag so that our automation can pick it up as a part of our test promotion pipeline.
|
||||
test.describe('Renaming Timer Object @unstable', () => {
|
||||
//Create a testcase name which will be obvious when it fails in CI
|
||||
test('Can create a new Timer object and rename it from actions Menu', async ({ page }) => {
|
||||
//Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
//We provide some helper functions in appActions like createDomainObjectWithDefaults. This example will create a Timer object
|
||||
await createDomainObjectWithDefaults(page, 'Timer');
|
||||
//Assert the object to be created and check it's name in the title
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer');
|
||||
|
||||
const newObjectName = "Renamed Timer";
|
||||
//We've created an example of a shared function which pases the page and newObjectName values
|
||||
await renameObjectFrom3DotMenu(page, newObjectName);
|
||||
|
||||
//Assert that the name has changed in the browser bar to the value we assigned above
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName);
|
||||
|
||||
});
|
||||
test('An existing Timer object can be renamed twice', async ({ page }) => {
|
||||
//Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
//We provide some helper functions in appActions like createDomainObjectWithDefaults. This example will create a Timer object
|
||||
await createDomainObjectWithDefaults(page, 'Timer');
|
||||
//Expect the object to be created and check it's name in the title
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer');
|
||||
|
||||
const newObjectName = "Renamed Timer";
|
||||
const newObjectName2 = "Re-Renamed Timer";
|
||||
//We've created an example of a shared function which pases the page and newObjectName values
|
||||
await renameObjectFrom3DotMenu(page, newObjectName);
|
||||
|
||||
//Assert that the name has changed in the browser bar to the value we assigned above
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName);
|
||||
|
||||
await renameObjectFrom3DotMenu(page, newObjectName2);
|
||||
|
||||
//Assert that the name has changed in the browser bar to the second value
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName2);
|
||||
});
|
||||
|
||||
//If you run out of time to write new tests, please stub in the missing tests in place with a test.fixme and BDD-style test steps. Someone will carry the baton!
|
||||
test.fixme('Can Rename Timer Object from Tree', async ({ page }) => {
|
||||
//Create a new object
|
||||
//Copy this object
|
||||
//Delete first object
|
||||
//Expect copied object to persist
|
||||
});
|
||||
});
|
||||
|
||||
//Structure: custom functions should be declared last. We are leaning on JSDoc pretty heavily to describe functionality. It is not required, but heavily recommended.
|
||||
|
||||
/**
|
||||
* This is an example of a function which is shared between testcases in this test suite. When refactoring, we'll be looking
|
||||
* for common functionality which makes sense to generalize for the entire test framework.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} newNameForTimer New Name for object
|
||||
*/
|
||||
async function renameObjectFrom3DotMenu(page, newNameForTimer) {
|
||||
|
||||
// Click on 3 Dot Menu
|
||||
await page.locator('button[title="More options"]').click();
|
||||
// Click text=Edit Properties...
|
||||
await page.locator('text=Edit Properties...').click();
|
||||
|
||||
// Rename the object with newNameForTimer variable which is passed into this function
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(newNameForTimer);
|
||||
|
||||
// Click Ok button to Save
|
||||
await page.locator('text=OK').click();
|
||||
}
|
84
e2e/tests/framework/generateVisualTestData.e2e.spec.js
Normal file
84
e2e/tests/framework/generateVisualTestData.e2e.spec.js
Normal file
@ -0,0 +1,84 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to generating LocalStorage via Session Storage to be used
|
||||
in some visual test suites like controlledClock.visual.spec.js. This suite should run to completion
|
||||
and generate an artifact named ./e2e/test-data/VisualTestData_storage.json . This will run
|
||||
on every Commit to ensure that this object still loads into tests correctly and will retain the
|
||||
.e2e.spec.js suffix.
|
||||
|
||||
TODO: Provide additional validation of object properties as it grows.
|
||||
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures.js');
|
||||
|
||||
test('Generate Visual Test Data @localStorage', async ({ page, context, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// add overlay plot with defaults
|
||||
await page.locator('li:has-text("Overlay Plot")').click();
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear1
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
// save (exit edit mode)
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// click create button
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// add sine wave generator with defaults
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
|
||||
//Add a 5000 ms Delay
|
||||
await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000');
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear1
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
// focus the overlay plot
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Overlay Plot').first().click()
|
||||
]);
|
||||
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
|
||||
//Save localStorage for future test execution
|
||||
await context.storageState({ path: './e2e/test-data/VisualTestData_storage.json' });
|
||||
});
|
45
e2e/tests/framework/pluginFixtures.e2e.spec.js
Normal file
45
e2e/tests/framework/pluginFixtures.e2e.spec.js
Normal file
@ -0,0 +1,45 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to testing our use of our custom fixtures to verify
|
||||
that they are working as expected.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures.js');
|
||||
|
||||
test.describe('pluginFixtures tests', () => {
|
||||
test.use({ domainObjectName: 'Timer' });
|
||||
let timerUUID;
|
||||
|
||||
test('Creates a timer object @framework @unstable', ({ domainObject }) => {
|
||||
const { uuid } = domainObject;
|
||||
const uuidRegexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/;
|
||||
expect(uuid).toMatch(uuidRegexp);
|
||||
timerUUID = uuid;
|
||||
});
|
||||
|
||||
test('Provides same uuid for subsequent uses of the same object @framework', ({ domainObject }) => {
|
||||
const { uuid } = domainObject;
|
||||
expect(uuid).toEqual(timerUUID);
|
||||
});
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2018, United States Government
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
@ -20,20 +20,17 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'./InspectorRegion',
|
||||
'./PlotBrowseRegion',
|
||||
'./PlotEditRegion'
|
||||
], function (
|
||||
InspectorRegion,
|
||||
PlotBrowseRegion,
|
||||
PlotEditRegion
|
||||
) {
|
||||
/*
|
||||
* This test suite template is to be used when verifying Test Data files found in /e2e/test-data/
|
||||
*/
|
||||
|
||||
var plotInspector = new InspectorRegion();
|
||||
const { test } = require('../../baseFixtures');
|
||||
|
||||
plotInspector.addRegion(PlotBrowseRegion);
|
||||
plotInspector.addRegion(PlotEditRegion);
|
||||
|
||||
return plotInspector;
|
||||
test.describe('recycled_local_storage @localStorage', () => {
|
||||
//We may want to do some additional level of verification of this file. For now, we just verify that it exists and can be used in a test suite.
|
||||
test.use({ storageState: './e2e/test-data/recycled_local_storage.json' });
|
||||
test('Can use recycled_local_storage file', async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
});
|
||||
});
|
||||
|
63
e2e/tests/functional/branding.e2e.spec.js
Normal file
63
e2e/tests/functional/branding.e2e.spec.js
Normal file
@ -0,0 +1,63 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify branding related components.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../baseFixtures.js');
|
||||
|
||||
test.describe('Branding tests', () => {
|
||||
test('About Modal launches with basic branding properties', async ({ page }) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click About button
|
||||
await page.click('.l-shell__app-logo');
|
||||
|
||||
// Verify that the NASA Logo Appears
|
||||
await expect(page.locator('.c-about__image')).toBeVisible();
|
||||
|
||||
// Modify the Build information in 'about' Modal
|
||||
const versionInformationLocator = page.locator('ul.t-info.l-info.s-info');
|
||||
await expect(versionInformationLocator).toBeEnabled();
|
||||
await expect.soft(versionInformationLocator).toContainText(/Version: \d/);
|
||||
await expect.soft(versionInformationLocator).toContainText(/Build Date: ((?:Mon|Tue|Wed|Thu|Fri|Sat|Sun))/);
|
||||
await expect.soft(versionInformationLocator).toContainText(/Revision: \b[0-9a-f]{5,40}\b/);
|
||||
await expect.soft(versionInformationLocator).toContainText(/Branch: ./);
|
||||
});
|
||||
test('Verify Links in About Modal', async ({ page }) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click About button
|
||||
await page.click('.l-shell__app-logo');
|
||||
|
||||
// Verify that clicking on the third party licenses information opens up another tab on licenses url
|
||||
const [page2] = await Promise.all([
|
||||
page.waitForEvent('popup'),
|
||||
page.locator('text=click here for third party licensing information').click()
|
||||
]);
|
||||
await page2.waitForLoadState('networkidle'); //Avoids timing issues with juggler/firefox
|
||||
expect(page2.waitForURL('**/licenses**')).toBeTruthy();
|
||||
});
|
||||
});
|
56
e2e/tests/functional/example/eventGenerator.e2e.spec.js
Normal file
56
e2e/tests/functional/example/eventGenerator.e2e.spec.js
Normal file
@ -0,0 +1,56 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding the example event generator.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../baseFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../appActions');
|
||||
|
||||
test.describe('Example Event Generator CRUD Operations', () => {
|
||||
test('Can create a Test Event Generator and it results in the table View', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
//Create a name for the object
|
||||
const newObjectName = 'Test Event Generator';
|
||||
|
||||
await createDomainObjectWithDefaults(page, 'Event Message Generator', newObjectName);
|
||||
|
||||
//Assertions against newly created object which define standard behavior
|
||||
await expect(page.waitForURL(/.*&view=table/)).toBeTruthy();
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Example Event Generator Telemetry Event Verficiation', () => {
|
||||
|
||||
test.fixme('telemetry is coming in for test event', async ({ page }) => {
|
||||
// Go to object created in step one
|
||||
// Verify the telemetry table is filled with > 1 row
|
||||
});
|
||||
test.fixme('telemetry is sorted by time ascending', async ({ page }) => {
|
||||
// Go to object created in step one
|
||||
// Verify the telemetry table has a class with "is-sorting asc"
|
||||
});
|
||||
});
|
@ -0,0 +1,118 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../baseFixtures');
|
||||
|
||||
test.describe('Sine Wave Generator', () => {
|
||||
test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ page, browserName }) => {
|
||||
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click Sine Wave Generator
|
||||
await page.click('text=Sine Wave Generator');
|
||||
|
||||
// Verify that the each required field has required indicator
|
||||
// Title
|
||||
await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/req/);
|
||||
|
||||
// Verify that the Notes row does not have a required indicator
|
||||
await expect(page.locator('.c-form__section div:nth-child(3) .form-row .c-form-row__state-indicator')).not.toContain('.req');
|
||||
await page.locator('textarea[type="text"]').fill('Optional Note Text');
|
||||
|
||||
// Period
|
||||
await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/req/);
|
||||
|
||||
// Amplitude
|
||||
await expect(page.locator('div:nth-child(5) .c-form-row__state-indicator')).toHaveClass(/req/);
|
||||
|
||||
// Offset
|
||||
await expect(page.locator('div:nth-child(6) .c-form-row__state-indicator')).toHaveClass(/req/);
|
||||
|
||||
// Data Rate
|
||||
await expect(page.locator('div:nth-child(7) .c-form-row__state-indicator')).toHaveClass(/req/);
|
||||
|
||||
// Phase
|
||||
await expect(page.locator('div:nth-child(8) .c-form-row__state-indicator')).toHaveClass(/req/);
|
||||
|
||||
// Randomness
|
||||
await expect(page.locator('div:nth-child(9) .c-form-row__state-indicator')).toHaveClass(/req/);
|
||||
|
||||
// Verify that by removing value from required text field shows invalid indicator
|
||||
await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('');
|
||||
await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/);
|
||||
|
||||
// Verify that by adding value to empty required text field changes invalid to valid indicator
|
||||
await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('New Sine Wave Generator');
|
||||
await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/valid/);
|
||||
|
||||
// Verify that by removing value from required number field shows invalid indicator
|
||||
await page.locator('.field.control.l-input-sm input').first().fill('');
|
||||
await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/invalid/);
|
||||
|
||||
// Verify that by adding value to empty required number field changes invalid to valid indicator
|
||||
await page.locator('.field.control.l-input-sm input').first().fill('3');
|
||||
await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/valid/);
|
||||
|
||||
// Verify that can change value of number field by up/down arrows keys
|
||||
// Click .field.control.l-input-sm input >> nth=0
|
||||
await page.locator('.field.control.l-input-sm input').first().click();
|
||||
// Press ArrowUp 3 times to change value from 3 to 6
|
||||
await page.locator('.field.control.l-input-sm input').first().press('ArrowUp');
|
||||
await page.locator('.field.control.l-input-sm input').first().press('ArrowUp');
|
||||
await page.locator('.field.control.l-input-sm input').first().press('ArrowUp');
|
||||
|
||||
const value = await page.locator('.field.control.l-input-sm input').first().inputValue();
|
||||
await expect(value).toBe('6');
|
||||
|
||||
//Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.click('text=OK')
|
||||
]);
|
||||
|
||||
// Verify that the Sine Wave Generator is displayed and correct
|
||||
// Verify object properties
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('New Sine Wave Generator');
|
||||
|
||||
// Verify canvas rendered and can be interacted with
|
||||
await page.locator('canvas').nth(1).click({
|
||||
position: {
|
||||
x: 341,
|
||||
y: 28
|
||||
}
|
||||
});
|
||||
|
||||
// Verify that where we click on canvas shows the number we clicked on
|
||||
// Note that any number will do, we just care that a number exists
|
||||
await expect(page.locator('.value-to-display-nearestValue')).toContainText(/[+-]?([0-9]*[.])?[0-9]+/);
|
||||
|
||||
});
|
||||
});
|
100
e2e/tests/functional/forms.e2e.spec.js
Normal file
100
e2e/tests/functional/forms.e2e.spec.js
Normal file
@ -0,0 +1,100 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify form functionality in isolation
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../baseFixtures');
|
||||
const path = require('path');
|
||||
|
||||
const TEST_FOLDER = 'test folder';
|
||||
|
||||
test.describe('Form Validation Behavior', () => {
|
||||
test('Required Field indicators appear if title is empty and can be corrected', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
await page.click('button:has-text("Create")');
|
||||
await page.click(':nth-match(:text("Folder"), 2)');
|
||||
|
||||
// Fill in empty string into title and trigger validation with 'Tab'
|
||||
await page.click('text=Properties Title Notes >> input[type="text"]');
|
||||
await page.fill('text=Properties Title Notes >> input[type="text"]', '');
|
||||
await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab');
|
||||
|
||||
//Required Field Form Validation
|
||||
await expect(page.locator('text=OK')).toBeDisabled();
|
||||
await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/);
|
||||
|
||||
//Correct Form Validation for missing title and trigger validation with 'Tab'
|
||||
await page.click('text=Properties Title Notes >> input[type="text"]');
|
||||
await page.fill('text=Properties Title Notes >> input[type="text"]', TEST_FOLDER);
|
||||
await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab');
|
||||
|
||||
//Required Field Form Validation is corrected
|
||||
await expect(page.locator('text=OK')).toBeEnabled();
|
||||
await expect(page.locator('.c-form-row__state-indicator').first()).not.toHaveClass(/invalid/);
|
||||
|
||||
//Finish Creating Domain Object
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.click('text=OK')
|
||||
]);
|
||||
|
||||
//Verify that the Domain Object has been created with the corrected title property
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(TEST_FOLDER);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Persistence operations @addInit', () => {
|
||||
// add non persistable root item
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// eslint-disable-next-line no-undef
|
||||
await page.addInitScript({ path: path.join(__dirname, '../../helper', 'addNoneditableObject.js') });
|
||||
});
|
||||
|
||||
test('Persistability should be respected in the create form location field', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/4323'
|
||||
});
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
await page.click('text=Condition Set');
|
||||
|
||||
await page.locator('form[name="mctForm"] >> text=Persistence Testing').click();
|
||||
|
||||
const okButton = page.locator('button:has-text("OK")');
|
||||
await expect(okButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Form Correctness by Object Type', () => {
|
||||
test.fixme('Verify correct behavior of number object (SWG)', async ({page}) => {});
|
||||
test.fixme('Verify correct behavior of number object Timer', async ({page}) => {});
|
||||
test.fixme('Verify correct behavior of number object Plan View', async ({page}) => {});
|
||||
test.fixme('Verify correct behavior of number object Clock', async ({page}) => {});
|
||||
test.fixme('Verify correct behavior of number object Hyperlink', async ({page}) => {});
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2018, United States Government
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
@ -20,27 +20,31 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify persistability checks
|
||||
*/
|
||||
|
||||
define(
|
||||
["../src/PersistenceFailureController"],
|
||||
function (PersistenceFailureController) {
|
||||
const { test, expect } = require('../../baseFixtures.js');
|
||||
|
||||
describe("The persistence failure controller", function () {
|
||||
var controller;
|
||||
const path = require('path');
|
||||
|
||||
beforeEach(function () {
|
||||
controller = new PersistenceFailureController();
|
||||
});
|
||||
test.describe('Persistence operations @addInit', () => {
|
||||
// add non persistable root item
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// eslint-disable-next-line no-undef
|
||||
await page.addInitScript({ path: path.join(__dirname, '../../helper', 'addNoneditableObject.js') });
|
||||
});
|
||||
|
||||
it("converts timestamps to human-readable dates", function () {
|
||||
expect(controller.formatTimestamp(402514331000))
|
||||
.toEqual("1982-10-03 17:32:11Z");
|
||||
});
|
||||
test('Non-persistable objects should not show persistence related actions', async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
it("provides default user names", function () {
|
||||
expect(controller.formatUsername(undefined))
|
||||
.toEqual(jasmine.any(String));
|
||||
});
|
||||
await page.locator('text=Persistence Testing').first().click({
|
||||
button: 'right'
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const menuOptions = page.locator('.c-menu ul');
|
||||
|
||||
await expect.soft(menuOptions).toContainText(['Open In New Tab', 'View', 'Create Link']);
|
||||
await expect(menuOptions).not.toContainText(['Move', 'Duplicate', 'Remove', 'Add New Folder', 'Edit Properties...', 'Export as JSON', 'Import from JSON']);
|
||||
});
|
||||
});
|
148
e2e/tests/functional/moveObjects.e2e.spec.js
Normal file
148
e2e/tests/functional/moveObjects.e2e.spec.js
Normal file
@ -0,0 +1,148 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding moving objects.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
|
||||
test.describe('Move item tests', () => {
|
||||
test('Create a basic object and verify that it can be moved to another folder', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
// Go to Open MCT
|
||||
await page.goto('./');
|
||||
|
||||
// Create a new folder in the root my items folder
|
||||
let folder1 = "Folder1";
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li.icon-folder').click();
|
||||
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder1);
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// Create another folder with a new name at default location, which is currently inside Folder 1
|
||||
let folder2 = "Folder2";
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li.icon-folder').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder2);
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// Move Folder 2 from Folder 1 to My Items
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await page.locator('.c-tree__scrollable div div:nth-child(2) .c-tree__item .c-tree__item__view-control').click();
|
||||
|
||||
await page.locator(`a:has-text("${folder2}")`).click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.locator('li.icon-move').click();
|
||||
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
|
||||
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
// Expect that Folder 2 is in My Items, the root folder
|
||||
expect(page.locator(`text=${myItemsFolderName} >> nth=0:has(text=${folder2})`)).toBeTruthy();
|
||||
});
|
||||
test('Create a basic object and verify that it cannot be moved to telemetry object without Composition Provider', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
// Go to Open MCT
|
||||
await page.goto('./');
|
||||
|
||||
// Create Telemetry Table
|
||||
let telemetryTable = 'Test Telemetry Table';
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li:has-text("Telemetry Table")').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable);
|
||||
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
// Finish editing and save Telemetry Table
|
||||
await page.locator('.c-button--menu.c-button--major.icon-save').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Create New Folder Basic Domain Object
|
||||
let folder = 'Test Folder';
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li:has-text("Folder")').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder);
|
||||
|
||||
// See if it's possible to put the folder in the Telemetry object during creation (Soft Assert)
|
||||
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
|
||||
let okButton = await page.locator('button.c-button.c-button--major:has-text("OK")');
|
||||
let okButtonStateDisabled = await okButton.isDisabled();
|
||||
expect.soft(okButtonStateDisabled).toBeTruthy();
|
||||
|
||||
// Continue test regardless of assertion and create it in My Items
|
||||
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
// Open My Items
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
|
||||
// Select Folder Object and select Move from context menu
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator(`a:has-text("${folder}")`).click()
|
||||
]);
|
||||
await page.locator('.c-tree__item.is-navigated-object .c-tree__item__label .c-tree__item__type-icon').click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.locator('li.icon-move').click();
|
||||
|
||||
// See if it's possible to put the folder in the Telemetry object after creation
|
||||
await page.locator(`text=Location Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
|
||||
let okButton2 = await page.locator('button.c-button.c-button--major:has-text("OK")');
|
||||
let okButtonStateDisabled2 = await okButton2.isDisabled();
|
||||
expect(okButtonStateDisabled2).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test.fixme('Cannot move a previously created domain object to non-peristable object in Move Modal', async ({ page }) => {
|
||||
//Create a domain object
|
||||
//Save Domain object
|
||||
//Move Object and verify that cannot select non-persistable object
|
||||
//Move Object to My Items
|
||||
//Verify successful move
|
||||
});
|
66
e2e/tests/functional/plugins/clocks/clock.e2e.spec.js
Normal file
66
e2e/tests/functional/plugins/clocks/clock.e2e.spec.js
Normal file
@ -0,0 +1,66 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding Clock.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../baseFixtures');
|
||||
|
||||
test.describe('Clock Generator CRUD Operations', () => {
|
||||
|
||||
test('Timezone dropdown will collapse when clicked outside or on dropdown icon again', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/4878'
|
||||
});
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click Clock
|
||||
await page.click('text=Clock');
|
||||
|
||||
// Click .icon-arrow-down
|
||||
await page.locator('.icon-arrow-down').click();
|
||||
//verify if the autocomplete dropdown is visible
|
||||
await expect(page.locator(".c-input--autocomplete__options")).toBeVisible();
|
||||
// Click .icon-arrow-down
|
||||
await page.locator('.icon-arrow-down').click();
|
||||
|
||||
// Verify clicking on the autocomplete arrow collapses the dropdown
|
||||
await expect(page.locator(".c-input--autocomplete__options")).not.toBeVisible();
|
||||
|
||||
// Click timezone input to open dropdown
|
||||
await page.locator('.c-input--autocomplete__input').click();
|
||||
//verify if the autocomplete dropdown is visible
|
||||
await expect(page.locator(".c-input--autocomplete__options")).toBeVisible();
|
||||
|
||||
// Verify clicking outside the autocomplete dropdown collapses it
|
||||
await page.locator('text=Timezone').click();
|
||||
// Verify clicking on the autocomplete arrow collapses the dropdown
|
||||
await expect(page.locator(".c-input--autocomplete__options")).not.toBeVisible();
|
||||
|
||||
});
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2018, United States Government
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
@ -20,31 +20,22 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'./plugin'
|
||||
], function (plugin) {
|
||||
describe("The buildInfo plugin", function () {
|
||||
var mockmct;
|
||||
var testInfo;
|
||||
// FIXME: Remove this eslint exception once tests are implemented
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { test, expect } = require('../../../../baseFixtures');
|
||||
|
||||
beforeEach(function () {
|
||||
mockmct = jasmine.createSpyObj('openmct', ['legacyExtension']);
|
||||
testInfo = { foo: 123, bar: "baz" };
|
||||
plugin(testInfo)(mockmct);
|
||||
});
|
||||
|
||||
it("registers versions extensions", function () {
|
||||
Object.keys(testInfo).forEach(function (key) {
|
||||
expect(mockmct.legacyExtension).toHaveBeenCalledWith(
|
||||
"versions",
|
||||
{
|
||||
key: key,
|
||||
name: jasmine.any(String),
|
||||
value: testInfo[key],
|
||||
description: undefined
|
||||
}
|
||||
);
|
||||
});
|
||||
test.describe('Remote Clock', () => {
|
||||
// eslint-disable-next-line require-await
|
||||
test.fixme('blocks historical requests until first tick is received', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5221'
|
||||
});
|
||||
// addInitScript to with remote clock
|
||||
// Switch time conductor mode to 'remote clock'
|
||||
// Navigate to telemetry
|
||||
// Verify that the plot renders historical data within the correct bounds
|
||||
// Refresh the page
|
||||
// Verify again that the plot renders historical data within the correct bounds
|
||||
});
|
||||
});
|
@ -0,0 +1,180 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets. Note: this
|
||||
suite is sharing state between tests which is considered an anti-pattern. Implimenting in this way to
|
||||
demonstrate some playwright for test developers. This pattern should not be re-used in other CRUD suites.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures.js');
|
||||
|
||||
let conditionSetUrl;
|
||||
let getConditionSetIdentifierFromUrl;
|
||||
|
||||
test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
||||
test.beforeAll(async ({ browser}) => {
|
||||
//TODO: This needs to be refactored
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
await page.locator('li:has-text("Condition Set")').click();
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.click('text=OK')
|
||||
]);
|
||||
|
||||
//Save localStorage for future test execution
|
||||
await context.storageState({ path: './e2e/test-data/recycled_local_storage.json' });
|
||||
|
||||
//Set object identifier from url
|
||||
conditionSetUrl = page.url();
|
||||
console.log('conditionSetUrl ' + conditionSetUrl);
|
||||
|
||||
getConditionSetIdentifierFromUrl = conditionSetUrl.split('/').pop().split('?')[0];
|
||||
console.debug('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl);
|
||||
await page.close();
|
||||
});
|
||||
|
||||
//Load localStorage for subsequent tests
|
||||
test.use({ storageState: './e2e/test-data/recycled_local_storage.json' });
|
||||
|
||||
//Begin suite of tests again localStorage
|
||||
test('Condition set object properties persist in main view and inspector @localStorage', async ({ page }) => {
|
||||
//Navigate to baseURL with injected localStorage
|
||||
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
|
||||
|
||||
//Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
|
||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
||||
|
||||
//Assertions on loaded Condition Set in Inspector
|
||||
expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
|
||||
|
||||
//Reload Page
|
||||
await Promise.all([
|
||||
page.reload(),
|
||||
page.waitForLoadState('networkidle')
|
||||
]);
|
||||
|
||||
//Re-verify after reload
|
||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
||||
//Assertions on loaded Condition Set in Inspector
|
||||
expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
|
||||
|
||||
});
|
||||
test('condition set object can be modified on @localStorage', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
|
||||
|
||||
//Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
|
||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
||||
|
||||
//Update the Condition Set properties
|
||||
// Click Edit Button
|
||||
await page.locator('text=Conditions View Snapshot >> button').nth(3).click();
|
||||
|
||||
//Edit Condition Set Name from main view
|
||||
await page.locator('text=Unnamed Condition Set').first().fill('Renamed Condition Set');
|
||||
await page.locator('text=Renamed Condition Set').first().press('Enter');
|
||||
// Click Save Button
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
// Click Save and Finish Editing Option
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
//Verify Main section reflects updated Name Property
|
||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set');
|
||||
|
||||
// Verify Inspector properties
|
||||
// Verify Inspector has updated Name property
|
||||
expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
|
||||
// Verify Inspector Details has updated Name property
|
||||
expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
|
||||
|
||||
// Verify Tree reflects updated Name proprety
|
||||
// Expand Tree
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span >> nth=3`).click();
|
||||
// Verify Condition Set Object is renamed in Tree
|
||||
expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
||||
// Verify Search Tree reflects renamed Name property
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed');
|
||||
expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
||||
|
||||
//Reload Page
|
||||
await Promise.all([
|
||||
page.reload(),
|
||||
page.waitForLoadState('networkidle')
|
||||
]);
|
||||
|
||||
//Verify Main section reflects updated Name Property
|
||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set');
|
||||
|
||||
// Verify Inspector properties
|
||||
// Verify Inspector has updated Name property
|
||||
expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
|
||||
// Verify Inspector Details has updated Name property
|
||||
expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
|
||||
|
||||
// Verify Tree reflects updated Name proprety
|
||||
// Expand Tree
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span >> nth=3`).click();
|
||||
// Verify Condition Set Object is renamed in Tree
|
||||
expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
||||
// Verify Search Tree reflects renamed Name property
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed');
|
||||
expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
||||
});
|
||||
test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ page }) => {
|
||||
//Navigate to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
//Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
|
||||
await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set") >> nth=0')).toBeVisible();
|
||||
|
||||
const numberOfConditionSetsToStart = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count();
|
||||
|
||||
// Search for Unnamed Condition Set
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed Condition Set');
|
||||
// Click Search Result
|
||||
await page.locator('[aria-label="OpenMCT Search"] >> text=Unnamed Condition Set').first().click();
|
||||
// Click hamburger button
|
||||
await page.locator('[title="More options"]').click();
|
||||
|
||||
// Click text=Remove
|
||||
await page.locator('text=Remove').click();
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
//Expect Unnamed Condition Set to be removed in Main View
|
||||
const numberOfConditionSetsAtEnd = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count();
|
||||
|
||||
expect(numberOfConditionSetsAtEnd).toEqual(numberOfConditionSetsToStart - 1);
|
||||
|
||||
//Feature?
|
||||
//Domain Object is still available by direct URL after delete
|
||||
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
||||
|
||||
});
|
||||
});
|
813
e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js
Normal file
813
e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js
Normal file
@ -0,0 +1,813 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding imagery,
|
||||
but only assume that example imagery is present.
|
||||
*/
|
||||
/* globals process */
|
||||
|
||||
const { waitForAnimations } = require('../../../../baseFixtures');
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const backgroundImageSelector = '.c-imagery__main-image__background-image';
|
||||
const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
|
||||
const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
|
||||
|
||||
//The following block of tests verifies the basic functionality of example imagery and serves as a template for Imagery objects embedded in other objects.
|
||||
test.describe('Example Imagery Object', () => {
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=Example Imagery
|
||||
await page.click('text=Example Imagery');
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('text=OK'),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
// Close Banner
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
|
||||
//Wait until Save Banner is gone
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
});
|
||||
|
||||
test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
|
||||
const deltaYStep = 100; //equivalent to 1x zoom
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
|
||||
// zoom in
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
await page.mouse.wheel(0, deltaYStep * 2);
|
||||
// wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
|
||||
// zoom out
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
await page.mouse.wheel(0, -deltaYStep);
|
||||
// wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const imageMouseZoomedOut = await page.locator(backgroundImageSelector).boundingBox();
|
||||
|
||||
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
|
||||
expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
|
||||
expect(imageMouseZoomedOut.height).toBeLessThan(imageMouseZoomedIn.height);
|
||||
expect(imageMouseZoomedOut.width).toBeLessThan(imageMouseZoomedIn.width);
|
||||
});
|
||||
|
||||
test('Can adjust image brightness/contrast by dragging the sliders', async ({ page, browserName }) => {
|
||||
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
// Open the image filter menu
|
||||
await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click();
|
||||
|
||||
// Drag the brightness and contrast sliders around and assert filter values
|
||||
await dragBrightnessSliderAndAssertFilterValues(page);
|
||||
await dragContrastSliderAndAssertFilterValues(page);
|
||||
});
|
||||
|
||||
test('Can use alt+drag to move around image once zoomed in', async ({ page }) => {
|
||||
const deltaYStep = 100; //equivalent to 1x zoom
|
||||
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
|
||||
// zoom in
|
||||
await page.mouse.wheel(0, deltaYStep * 2);
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
|
||||
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
|
||||
// move to the right
|
||||
|
||||
// center the mouse pointer
|
||||
await page.mouse.move(imageCenterX, imageCenterY);
|
||||
|
||||
//Get Diagnostic info about process environment
|
||||
console.log('process.platform is ' + process.platform);
|
||||
const getUA = await page.evaluate(() => navigator.userAgent);
|
||||
console.log('navigator.userAgent ' + getUA);
|
||||
// Pan Imagery Hints
|
||||
const imageryHintsText = await page.locator('.c-imagery__hints').innerText();
|
||||
expect(expectedAltText).toEqual(imageryHintsText);
|
||||
|
||||
// pan right
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX - 200, imageCenterY, 10);
|
||||
await page.mouse.up();
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
||||
const afterRightPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);
|
||||
|
||||
// pan left
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX, imageCenterY, 10);
|
||||
await page.mouse.up();
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
||||
const afterLeftPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);
|
||||
|
||||
// pan up
|
||||
await page.mouse.move(imageCenterX, imageCenterY);
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX, imageCenterY + 200, 10);
|
||||
await page.mouse.up();
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
||||
const afterUpPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(afterUpPanBoundingBox.y).toBeGreaterThan(afterLeftPanBoundingBox.y);
|
||||
|
||||
// pan down
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX, imageCenterY - 200, 10);
|
||||
await page.mouse.up();
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
||||
const afterDownPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);
|
||||
|
||||
});
|
||||
|
||||
test('Can use + - buttons to zoom on the image @unstable', async ({ page }) => {
|
||||
// Get initial image dimensions
|
||||
const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
|
||||
// Zoom in twice via button
|
||||
await zoomIntoImageryByButton(page);
|
||||
await zoomIntoImageryByButton(page);
|
||||
|
||||
// Get and assert zoomed in image dimensions
|
||||
const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
|
||||
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
|
||||
|
||||
// Zoom out once via button
|
||||
await zoomOutOfImageryByButton(page);
|
||||
|
||||
// Get and assert zoomed out image dimensions
|
||||
const zoomedOutBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
|
||||
expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
|
||||
|
||||
// Zoom out again via button, assert against the initial image dimensions
|
||||
await zoomOutOfImageryByButton(page);
|
||||
const finalBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(finalBoundingBox).toEqual(initialBoundingBox);
|
||||
});
|
||||
|
||||
test('Can use the reset button to reset the image @unstable', async ({ page }, testInfo) => {
|
||||
test.slow(testInfo.project === 'chrome-beta', "This test is slow in chrome-beta");
|
||||
// Get initial image dimensions
|
||||
const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
|
||||
// Zoom in twice via button
|
||||
await zoomIntoImageryByButton(page);
|
||||
await zoomIntoImageryByButton(page);
|
||||
|
||||
// Get and assert zoomed in image dimensions
|
||||
const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
|
||||
expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
|
||||
|
||||
// Reset pan and zoom and assert against initial image dimensions
|
||||
await resetImageryPanAndZoom(page);
|
||||
const finalBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(finalBoundingBox).toEqual(initialBoundingBox);
|
||||
});
|
||||
|
||||
test('Using the zoom features does not pause telemetry', async ({ page }) => {
|
||||
const pausePlayButton = page.locator('.c-button.pause-play');
|
||||
|
||||
// open the time conductor drop down
|
||||
await page.locator('button:has-text("Fixed Timespan")').click();
|
||||
|
||||
// Click local clock
|
||||
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
|
||||
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
|
||||
|
||||
// Zoom in via button
|
||||
await zoomIntoImageryByButton(page);
|
||||
await expect(pausePlayButton).not.toHaveClass(/is-paused/);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// The following test case will cover these scenarios
|
||||
// ('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
// ('Can use alt+drag to move around image once zoomed in');
|
||||
// ('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
// ('If the imagery view is in pause mode, it should not be updated when new images come in');
|
||||
// ('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
test('Example Imagery in Display layout @unstable', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5265'
|
||||
});
|
||||
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=Example Imagery
|
||||
await page.click('text=Example Imagery');
|
||||
|
||||
// Clear and set Image load delay to minimum value
|
||||
await page.locator('input[type="number"]').fill('');
|
||||
await page.locator('input[type="number"]').fill('5000');
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('text=OK'),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
// Wait until Save Banner is gone
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
|
||||
// Click previous image button
|
||||
const previousImageButton = page.locator('.c-nav--prev');
|
||||
await previousImageButton.click();
|
||||
|
||||
// Verify previous image
|
||||
const selectedImage = page.locator('.selected');
|
||||
await expect(selectedImage).toBeVisible();
|
||||
|
||||
// Zoom in
|
||||
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const deltaYStep = 100; // equivalent to 1x zoom
|
||||
await page.mouse.wheel(0, deltaYStep * 2);
|
||||
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
|
||||
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
|
||||
|
||||
// Wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
|
||||
expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
|
||||
|
||||
// Center the mouse pointer
|
||||
await page.mouse.move(imageCenterX, imageCenterY);
|
||||
|
||||
// Pan Imagery Hints
|
||||
const imageryHintsText = await page.locator('.c-imagery__hints').innerText();
|
||||
expect(expectedAltText).toEqual(imageryHintsText);
|
||||
|
||||
// Click next image button
|
||||
const nextImageButton = page.locator('.c-nav--next');
|
||||
await nextImageButton.click();
|
||||
|
||||
// Click time conductor mode button
|
||||
await page.locator('.c-mode-button').click();
|
||||
|
||||
// Select local clock mode
|
||||
await page.locator('[data-testid=conductor-modeOption-realtime]').click();
|
||||
|
||||
// Zoom in on next image
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
await page.mouse.wheel(0, deltaYStep * 2);
|
||||
|
||||
// Wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const imageNextMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(imageNextMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
|
||||
expect(imageNextMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
|
||||
|
||||
// Click previous image button
|
||||
await previousImageButton.click();
|
||||
|
||||
// Verify previous image
|
||||
await expect(selectedImage).toBeVisible();
|
||||
|
||||
const imageCount = await page.locator('.c-imagery__thumb').count();
|
||||
await expect.poll(async () => {
|
||||
const newImageCount = await page.locator('.c-imagery__thumb').count();
|
||||
|
||||
return newImageCount;
|
||||
}, {
|
||||
message: "verify that old images are discarded",
|
||||
timeout: 6 * 1000
|
||||
}).toBe(imageCount);
|
||||
|
||||
// Verify selected image is still displayed
|
||||
await expect(selectedImage).toBeVisible();
|
||||
|
||||
// Unpause imagery
|
||||
await page.locator('.pause-play').click();
|
||||
|
||||
//Get background-image url from background-image css prop
|
||||
await assertBackgroundImageUrlFromBackgroundCss(page);
|
||||
|
||||
// Open the image filter menu
|
||||
await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click();
|
||||
|
||||
// Drag the brightness and contrast sliders around and assert filter values
|
||||
await dragBrightnessSliderAndAssertFilterValues(page);
|
||||
await dragContrastSliderAndAssertFilterValues(page);
|
||||
});
|
||||
|
||||
test.describe('Example imagery thumbnails resize in display layouts', () => {
|
||||
test('Resizing the layout changes thumbnail visibility and size', async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
const thumbsWrapperLocator = page.locator('.c-imagery__thumbs-wrapper');
|
||||
// Click button:has-text("Create")
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// Click li:has-text("Display Layout")
|
||||
await page.locator('li:has-text("Display Layout")').click();
|
||||
const displayLayoutTitleField = page.locator('text=Properties Title Notes Horizontal grid (px) Vertical grid (px) Horizontal size ( >> input[type="text"]');
|
||||
await displayLayoutTitleField.click();
|
||||
|
||||
await displayLayoutTitleField.fill('Thumbnail Display Layout');
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click()
|
||||
]);
|
||||
|
||||
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
|
||||
// Click text=Save and Finish Editing
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Click button:has-text("Create")
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// Click li:has-text("Example Imagery")
|
||||
await page.locator('li:has-text("Example Imagery")').click();
|
||||
|
||||
const imageryTitleField = page.locator('text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]');
|
||||
// Click text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]
|
||||
await imageryTitleField.click();
|
||||
|
||||
// Fill text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]
|
||||
await imageryTitleField.fill('Thumbnail Example Imagery');
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click()
|
||||
]);
|
||||
|
||||
// Click text=Thumbnail Example Imagery Imagery Layout Snapshot >> button >> nth=0
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Thumbnail Example Imagery Imagery Layout Snapshot >> button').first().click()
|
||||
]);
|
||||
|
||||
// Edit mode
|
||||
await page.locator('text=Thumbnail Display Layout Snapshot >> button').nth(3).click();
|
||||
|
||||
// Click on example imagery to expose toolbar
|
||||
await page.locator('text=Thumbnail Example Imagery Snapshot Large View').click();
|
||||
|
||||
// expect thumbnails not be visible when first added
|
||||
expect.soft(thumbsWrapperLocator.isHidden()).toBeTruthy();
|
||||
|
||||
// Resize the example imagery vertically to change the thumbnail visibility
|
||||
/*
|
||||
The following arbitrary values are added to observe the separate visual
|
||||
conditions of the thumbnails (hidden, small thumbnails, regular thumbnails).
|
||||
Specifically, height is set to 50px for small thumbs and 100px for regular
|
||||
*/
|
||||
// Click #mct-input-id-103
|
||||
await page.locator('#mct-input-id-103').click();
|
||||
|
||||
// Fill #mct-input-id-103
|
||||
await page.locator('#mct-input-id-103').fill('50');
|
||||
|
||||
expect(thumbsWrapperLocator.isVisible()).toBeTruthy();
|
||||
await expect(thumbsWrapperLocator).toHaveClass(/is-small-thumbs/);
|
||||
|
||||
// Resize the example imagery vertically to change the thumbnail visibility
|
||||
// Click #mct-input-id-103
|
||||
await page.locator('#mct-input-id-103').click();
|
||||
|
||||
// Fill #mct-input-id-103
|
||||
await page.locator('#mct-input-id-103').fill('100');
|
||||
|
||||
expect(thumbsWrapperLocator.isVisible()).toBeTruthy();
|
||||
await expect(thumbsWrapperLocator).not.toHaveClass(/is-small-thumbs/);
|
||||
});
|
||||
});
|
||||
|
||||
// test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
// test.fixme('Can use alt+drag to move around image once zoomed in');
|
||||
// test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
// test.fixme('If the imagery view is in pause mode, images still come in');
|
||||
// test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
test.describe('Example Imagery in Flexible layout', () => {
|
||||
test('Example Imagery in Flexible layout @unstable', async ({ page, browserName, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5326'
|
||||
});
|
||||
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=Example Imagery
|
||||
await page.click('text=Example Imagery');
|
||||
|
||||
// Clear and set Image load delay (milliseconds)
|
||||
await page.click('input[type="number"]', {clickCount: 3});
|
||||
await page.type('input[type="number"]', "20");
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('text=OK'),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
// Wait until Save Banner is gone
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
|
||||
// Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=Flexible Layout
|
||||
await page.click('text=Flexible Layout');
|
||||
|
||||
// Assert Flexible layout
|
||||
await expect(page.locator('.js-form-title')).toHaveText('Create a New Flexible Layout');
|
||||
|
||||
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
|
||||
|
||||
// Click My Items
|
||||
await Promise.all([
|
||||
page.locator('text=OK').click(),
|
||||
page.waitForNavigation({waitUntil: 'networkidle'})
|
||||
]);
|
||||
|
||||
// Click My Items
|
||||
await page.locator('.c-disclosure-triangle').click();
|
||||
|
||||
// Right click example imagery
|
||||
await page.click(('text=Unnamed Example Imagery'), { button: 'right' });
|
||||
|
||||
// Click move
|
||||
await page.locator('.icon-move').click();
|
||||
|
||||
// Click triangle to open sub menu
|
||||
await page.locator('.c-form__section .c-disclosure-triangle').click();
|
||||
|
||||
// Click Flexable Layout
|
||||
await page.click('.c-overlay__outer >> text=Unnamed Flexible Layout');
|
||||
|
||||
// Click text=OK
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
// Save template
|
||||
await saveTemplate(page);
|
||||
|
||||
// Zoom in
|
||||
await mouseZoomIn(page);
|
||||
|
||||
// Center the mouse pointer
|
||||
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
|
||||
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
|
||||
await page.mouse.move(imageCenterX, imageCenterY);
|
||||
|
||||
// Pan zoom
|
||||
await panZoomAndAssertImageProperties(page);
|
||||
|
||||
// Click previous image button
|
||||
const previousImageButton = page.locator('.c-nav--prev');
|
||||
await previousImageButton.click();
|
||||
|
||||
// Verify previous image
|
||||
const selectedImage = page.locator('.selected');
|
||||
await expect(selectedImage).toBeVisible();
|
||||
|
||||
// Click time conductor mode button
|
||||
await page.locator('.c-mode-button').click();
|
||||
|
||||
// Select local clock mode
|
||||
await page.locator('[data-testid=conductor-modeOption-realtime]').click();
|
||||
|
||||
// Zoom in on next image
|
||||
await mouseZoomIn(page);
|
||||
|
||||
// Click previous image button
|
||||
await previousImageButton.click();
|
||||
|
||||
// Verify previous image
|
||||
await expect(selectedImage).toBeVisible();
|
||||
|
||||
const imageCount = await page.locator('.c-imagery__thumb').count();
|
||||
await expect.poll(async () => {
|
||||
const newImageCount = await page.locator('.c-imagery__thumb').count();
|
||||
|
||||
return newImageCount;
|
||||
}, {
|
||||
message: "verify that old images are discarded",
|
||||
timeout: 6 * 1000
|
||||
}).toBe(imageCount);
|
||||
|
||||
// Verify selected image is still displayed
|
||||
await expect(selectedImage).toBeVisible();
|
||||
|
||||
// Unpause imagery
|
||||
await page.locator('.pause-play').click();
|
||||
|
||||
//Get background-image url from background-image css prop
|
||||
await assertBackgroundImageUrlFromBackgroundCss(page);
|
||||
|
||||
// Open the image filter menu
|
||||
await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click();
|
||||
|
||||
// Drag the brightness and contrast sliders around and assert filter values
|
||||
await dragBrightnessSliderAndAssertFilterValues(page);
|
||||
await dragContrastSliderAndAssertFilterValues(page);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Example Imagery in Tabs view', () => {
|
||||
test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
test.fixme('Can use alt+drag to move around image once zoomed in');
|
||||
test.fixme('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||
test.fixme('Can zoom into a previous image from thumbstrip in real-time or fixed-time');
|
||||
test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in');
|
||||
test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function saveTemplate(page) {
|
||||
await page.locator('.c-button--menu.c-button--major.icon-save').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag the brightness slider to max, min, and midpoint and assert the filter values
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function dragBrightnessSliderAndAssertFilterValues(page) {
|
||||
const brightnessSlider = 'div.c-image-controls__slider-wrapper.icon-brightness > input';
|
||||
const brightnessBoundingBox = await page.locator(brightnessSlider).boundingBox();
|
||||
const brightnessMidX = brightnessBoundingBox.x + brightnessBoundingBox.width / 2;
|
||||
const brightnessMidY = brightnessBoundingBox.y + brightnessBoundingBox.height / 2;
|
||||
|
||||
await page.locator(brightnessSlider).hover({trial: true});
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(brightnessBoundingBox.x + brightnessBoundingBox.width, brightnessMidY);
|
||||
await assertBackgroundImageBrightness(page, '500');
|
||||
await page.mouse.move(brightnessBoundingBox.x, brightnessMidY);
|
||||
await assertBackgroundImageBrightness(page, '0');
|
||||
await page.mouse.move(brightnessMidX, brightnessMidY);
|
||||
await assertBackgroundImageBrightness(page, '250');
|
||||
await page.mouse.up();
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag the contrast slider to max, min, and midpoint and assert the filter values
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function dragContrastSliderAndAssertFilterValues(page) {
|
||||
const contrastSlider = 'div.c-image-controls__slider-wrapper.icon-contrast > input';
|
||||
const contrastBoundingBox = await page.locator(contrastSlider).boundingBox();
|
||||
const contrastMidX = contrastBoundingBox.x + contrastBoundingBox.width / 2;
|
||||
const contrastMidY = contrastBoundingBox.y + contrastBoundingBox.height / 2;
|
||||
|
||||
await page.locator(contrastSlider).hover({trial: true});
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(contrastBoundingBox.x + contrastBoundingBox.width, contrastMidY);
|
||||
await assertBackgroundImageContrast(page, '500');
|
||||
await page.mouse.move(contrastBoundingBox.x, contrastMidY);
|
||||
await assertBackgroundImageContrast(page, '0');
|
||||
await page.mouse.move(contrastMidX, contrastMidY);
|
||||
await assertBackgroundImageContrast(page, '250');
|
||||
await page.mouse.up();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the filter:brightness value of the current background-image and
|
||||
* asserts against an expected value
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {String} expected The expected brightness value
|
||||
*/
|
||||
async function assertBackgroundImageBrightness(page, expected) {
|
||||
const backgroundImage = page.locator('.c-imagery__main-image__background-image');
|
||||
|
||||
// Get the brightness filter value (i.e: filter: brightness(500%) => "500")
|
||||
const actual = await backgroundImage.evaluate((el) => {
|
||||
return el.style.filter.match(/brightness\((\d{1,3})%\)/)[1];
|
||||
});
|
||||
expect(actual).toBe(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function assertBackgroundImageUrlFromBackgroundCss(page) {
|
||||
const backgroundImage = page.locator('.c-imagery__main-image__background-image');
|
||||
let backgroundImageUrl = await backgroundImage.evaluate((el) => {
|
||||
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
|
||||
});
|
||||
let backgroundImageUrl1 = backgroundImageUrl.slice(1, -1); //forgive me, padre
|
||||
console.log('backgroundImageUrl1 ' + backgroundImageUrl1);
|
||||
|
||||
let backgroundImageUrl2;
|
||||
await expect.poll(async () => {
|
||||
// Verify next image has updated
|
||||
let backgroundImageUrlNext = await backgroundImage.evaluate((el) => {
|
||||
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
|
||||
});
|
||||
backgroundImageUrl2 = backgroundImageUrlNext.slice(1, -1); //forgive me, padre
|
||||
|
||||
return backgroundImageUrl2;
|
||||
}, {
|
||||
message: "verify next image has updated",
|
||||
timeout: 6 * 1000
|
||||
}).not.toBe(backgroundImageUrl1);
|
||||
console.log('backgroundImageUrl2 ' + backgroundImageUrl2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function panZoomAndAssertImageProperties(page) {
|
||||
const imageryHintsText = await page.locator('.c-imagery__hints').innerText();
|
||||
expect(expectedAltText).toEqual(imageryHintsText);
|
||||
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
|
||||
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
|
||||
|
||||
// Pan right
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX - 200, imageCenterY, 10);
|
||||
await page.mouse.up();
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
||||
const afterRightPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);
|
||||
|
||||
// Pan left
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX, imageCenterY, 10);
|
||||
await page.mouse.up();
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
||||
const afterLeftPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);
|
||||
|
||||
// Pan up
|
||||
await page.mouse.move(imageCenterX, imageCenterY);
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX, imageCenterY + 200, 10);
|
||||
await page.mouse.up();
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
||||
const afterUpPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(afterUpPanBoundingBox.y).toBeGreaterThanOrEqual(afterLeftPanBoundingBox.y);
|
||||
|
||||
// Pan down
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX, imageCenterY - 200, 10);
|
||||
await page.mouse.up();
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
||||
const afterDownPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(afterDownPanBoundingBox.y).toBeLessThanOrEqual(afterUpPanBoundingBox.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function mouseZoomIn(page) {
|
||||
// Zoom in
|
||||
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const deltaYStep = 100; // equivalent to 1x zoom
|
||||
await page.mouse.wheel(0, deltaYStep * 2);
|
||||
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
|
||||
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
|
||||
|
||||
// center the mouse pointer
|
||||
await page.mouse.move(imageCenterX, imageCenterY);
|
||||
|
||||
// Wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
|
||||
expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the filter:contrast value of the current background-image and
|
||||
* asserts against an expected value
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {String} expected The expected contrast value
|
||||
*/
|
||||
async function assertBackgroundImageContrast(page, expected) {
|
||||
const backgroundImage = page.locator('.c-imagery__main-image__background-image');
|
||||
|
||||
// Get the contrast filter value (i.e: filter: contrast(500%) => "500")
|
||||
const actual = await backgroundImage.evaluate((el) => {
|
||||
return el.style.filter.match(/contrast\((\d{1,3})%\)/)[1];
|
||||
});
|
||||
expect(actual).toBe(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the '+' button to zoom in. Hovers first if the toolbar is not visible
|
||||
* and waits for the zoom animation to finish afterwards.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function zoomIntoImageryByButton(page) {
|
||||
// FIXME: There should only be one set of imagery buttons, but there are two?
|
||||
const zoomInBtn = page.locator("[role='toolbar'][aria-label='Image controls'] .t-btn-zoom-in").nth(0);
|
||||
const backgroundImage = page.locator(backgroundImageSelector);
|
||||
if (!(await zoomInBtn.isVisible())) {
|
||||
await backgroundImage.hover({trial: true});
|
||||
}
|
||||
|
||||
await zoomInBtn.click();
|
||||
await waitForAnimations(backgroundImage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the '-' button to zoom out. Hovers first if the toolbar is not visible
|
||||
* and waits for the zoom animation to finish afterwards.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function zoomOutOfImageryByButton(page) {
|
||||
// FIXME: There should only be one set of imagery buttons, but there are two?
|
||||
const zoomOutBtn = page.locator("[role='toolbar'][aria-label='Image controls'] .t-btn-zoom-out").nth(0);
|
||||
const backgroundImage = page.locator(backgroundImageSelector);
|
||||
if (!(await zoomOutBtn.isVisible())) {
|
||||
await backgroundImage.hover({trial: true});
|
||||
}
|
||||
|
||||
await zoomOutBtn.click();
|
||||
await waitForAnimations(backgroundImage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the reset button to reset image pan and zoom. Hovers first if the toolbar is not visible
|
||||
* and waits for the zoom animation to finish afterwards.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function resetImageryPanAndZoom(page) {
|
||||
// FIXME: There should only be one set of imagery buttons, but there are two?
|
||||
const panZoomResetBtn = page.locator("[role='toolbar'][aria-label='Image controls'] .t-btn-zoom-reset").nth(0);
|
||||
const backgroundImage = page.locator(backgroundImageSelector);
|
||||
if (!(await panZoomResetBtn.isVisible())) {
|
||||
await backgroundImage.hover({trial: true});
|
||||
}
|
||||
|
||||
await panZoomResetBtn.click();
|
||||
await waitForAnimations(backgroundImage);
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding exportAsJSON.
|
||||
*/
|
||||
|
||||
// FIXME: Remove this eslint exception once tests are implemented
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { test, expect } = require('../../../../baseFixtures');
|
||||
|
||||
test.describe('ExportAsJSON', () => {
|
||||
test.fixme('Create a basic object and verify that it can be exported as JSON from Tree', async ({ page }) => {
|
||||
//Create domain object
|
||||
//Save Domain Object
|
||||
//Verify that the newly created domain object can be exported as JSON from the Tree
|
||||
});
|
||||
test.fixme('Create a basic object and verify that it can be exported as JSON from 3 dot menu', async ({ page }) => {
|
||||
//Create domain object
|
||||
//Save Domain Object
|
||||
//Verify that the newly created domain object can be exported as JSON from the 3 dot menu
|
||||
});
|
||||
test.fixme('Verify that a nested Object can be exported as JSON', async ({ page }) => {
|
||||
// Create 2 objects with hierarchy
|
||||
// Export as JSON
|
||||
// Verify Hiearchy
|
||||
});
|
||||
test.fixme('Verify that the ExportAsJSON dropdown does not appear for the item X', async ({ page }) => {
|
||||
// Other than non-persistible objects
|
||||
});
|
||||
});
|
@ -0,0 +1,48 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding importAsJSON.
|
||||
*/
|
||||
|
||||
// FIXME: Remove this eslint exception once tests are implemented
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { test, expect } = require('../../../../baseFixtures');
|
||||
|
||||
test.describe('ExportAsJSON', () => {
|
||||
test.fixme('Verify that domain object can be importAsJSON from Tree', async ({ page }) => {
|
||||
//Verify that an testdata JSON file can be imported from Tree
|
||||
//Verify correctness of imported domain object
|
||||
});
|
||||
test.fixme('Verify that domain object can be importAsJSON from 3 dot menu on folder', async ({ page }) => {
|
||||
//Verify that an testdata JSON file can be imported from 3 dot menu on folder domain object
|
||||
//Verify correctness of imported domain object
|
||||
});
|
||||
test.fixme('Verify that a nested Objects can be importAsJSON', async ({ page }) => {
|
||||
// Testdata with hierarchy
|
||||
// ImportAsJSON on Tree
|
||||
// Verify Hierarchy
|
||||
});
|
||||
test.fixme('Verify that the ImportAsJSON dropdown does not appear for the item X', async ({ page }) => {
|
||||
// Other than non-persistible objects
|
||||
});
|
||||
});
|
200
e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js
Normal file
200
e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js
Normal file
@ -0,0 +1,200 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks.
|
||||
*/
|
||||
|
||||
// FIXME: Remove this eslint exception once tests are implemented
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { test, expect } = require('../../../../baseFixtures');
|
||||
|
||||
test.describe('Notebook CRUD Operations', () => {
|
||||
test.fixme('Can create a Notebook Object', async ({ page }) => {
|
||||
//Create domain object
|
||||
//Newly created notebook should have one Section and one page, 'Unnamed Section'/'Unnamed Page'
|
||||
});
|
||||
test.fixme('Can update a Notebook Object', async ({ page }) => {});
|
||||
test.fixme('Can view a perviously created Notebook Object', async ({ page }) => {});
|
||||
test.fixme('Can Delete a Notebook Object', async ({ page }) => {
|
||||
// Other than non-persistible objects
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Default Notebook', () => {
|
||||
// General Default Notebook statements
|
||||
// ## Useful commands:
|
||||
// 1. - To check default notebook:
|
||||
// `JSON.parse(localStorage.getItem('notebook-storage'));`
|
||||
// 1. - Clear default notebook:
|
||||
// `localStorage.setItem('notebook-storage', null);`
|
||||
test.fixme('A newly created Notebook is automatically set as the default notebook if no other notebooks exist', async ({ page }) => {
|
||||
//Create new notebook
|
||||
//Verify Default Notebook Characteristics
|
||||
});
|
||||
test.fixme('A newly created Notebook is automatically set as the default notebook if at least one other notebook exists', async ({ page }) => {
|
||||
//Create new notebook A
|
||||
//Create second notebook B
|
||||
//Verify Non-Default Notebook A Characteristics
|
||||
//Verify Default Notebook B Characteristics
|
||||
});
|
||||
test.fixme('If a default notebook is deleted, the second most recent notebook becomes the default', async ({ page }) => {
|
||||
//Create new notebook A
|
||||
//Create second notebook B
|
||||
//Delete Notebook B
|
||||
//Verify Default Notebook A Characteristics
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Notebook section tests', () => {
|
||||
//The following test cases are associated with Notebook Sections
|
||||
test.fixme('New sections are automatically named Unnamed Section with Unnamed Page', async ({ page }) => {
|
||||
//Create new notebook A
|
||||
//Add section
|
||||
//Verify new section and new page details
|
||||
});
|
||||
test.fixme('Section selection operations and associated behavior', async ({ page }) => {
|
||||
//Create new notebook A
|
||||
//Add Sections until 6 total with no default section/page
|
||||
//Select 3rd section
|
||||
//Delete 4th section
|
||||
//3rd section is still selected
|
||||
//Delete 3rd section
|
||||
//1st section is selected
|
||||
//Set 3rd section as default
|
||||
//Delete 2nd section
|
||||
//3rd section is still default
|
||||
//Delete 3rd section
|
||||
//1st is selected and there is no default notebook
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Notebook page tests', () => {
|
||||
//The following test cases are associated with Notebook Pages
|
||||
test.fixme('Page selection operations and associated behavior', async ({ page }) => {
|
||||
//Create new notebook A
|
||||
//Delete existing Page
|
||||
//New 'Unnamed Page' automatically created
|
||||
//Create 6 total Pages without a default page
|
||||
//Select 3rd
|
||||
//Delete 3rd
|
||||
//First is now selected
|
||||
//Set 3rd as default
|
||||
//Select 2nd page
|
||||
//Delete 2nd page
|
||||
//3rd (default) is now selected
|
||||
//Set 3rd as default page
|
||||
//Select 3rd (default) page
|
||||
//Delete 3rd page
|
||||
//First is now selected and there is no default notebook
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Notebook search tests', () => {
|
||||
test.fixme('Can search for a single result', async ({ page }) => {});
|
||||
test.fixme('Can search for many results', async ({ page }) => {});
|
||||
test.fixme('Can search for new and recently modified entries', async ({ page }) => {});
|
||||
test.fixme('Can search for section text', async ({ page }) => {});
|
||||
test.fixme('Can search for page text', async ({ page }) => {});
|
||||
test.fixme('Can search for entry text', async ({ page }) => {});
|
||||
});
|
||||
|
||||
test.describe('Notebook entry tests', () => {
|
||||
test.fixme('When a new entry is created, it should be focused', async ({ page }) => {});
|
||||
test.fixme('When a telemetry object is dropped into a notebook, a new entry is created and it should be focused', async ({ page }) => {
|
||||
// Drag and drop any telmetry object on 'drop object'
|
||||
// new entry gets created with telemtry object
|
||||
});
|
||||
test.fixme('When a telemetry object is dropped into a notebooks existing entry, it should be focused', async ({ page }) => {
|
||||
// Drag and drop any telemetry object onto existing entry
|
||||
// Entry updated with object and snapshot
|
||||
});
|
||||
test.fixme('new entries persist through navigation events without save', async ({ page }) => {});
|
||||
test.fixme('previous and new entries can be deleted', async ({ page }) => {});
|
||||
});
|
||||
|
||||
test.describe('Snapshot Menu tests', () => {
|
||||
test.fixme('When no default notebook is selected, Snapshot Menu dropdown should only have a single option', async ({ page }) => {
|
||||
// There should be no default notebook
|
||||
// Clear default notebook if exists using `localStorage.setItem('notebook-storage', null);`
|
||||
// refresh page
|
||||
// Click on 'Notebook Snaphot Menu'
|
||||
// 'save to Notebook Snapshots' should be only option there
|
||||
});
|
||||
test.fixme('When default notebook is updated selected, Snapshot Menu dropdown should list it as the newest option', async ({ page }) => {
|
||||
// Create 2a notebooks
|
||||
// Set Notebook A as Default
|
||||
// Open Snapshot Menu and note that Notebook A is listed
|
||||
// Close Snapshot Menu
|
||||
// Set Default Notebook to Notebook B
|
||||
// Open Snapshot Notebook and note that Notebook B is listed
|
||||
// Select Default Notebook Option and verify that Snapshot is added to Notebook B
|
||||
});
|
||||
test.fixme('Can add Snapshots via Snapshot Menu and details are correct', async ({ page }) => {
|
||||
//Note this should be a visual test, too
|
||||
// Create Telemetry object
|
||||
// Create A notebook with many pages and sections.
|
||||
// Set page and section defaults to be between first and last of many. i.e. 3 of 5
|
||||
// Navigate to Telemetry object
|
||||
// Select Default Notebook Option and verify that Snapshot is added to Notebook A
|
||||
// Verify Snapshot Details appear correctly
|
||||
});
|
||||
test.fixme('Snapshots adjust time conductor', async ({ page }) => {
|
||||
// Create Telemetry object
|
||||
// Set Telemetry object's timeconductor to Fixed time with Start and Endtimes are recorded
|
||||
// Embed Telemetry object into notebook
|
||||
// Set Time Conductor to Local clock
|
||||
// Click into embedded telemetry object and verify object appears with same fixed time from record
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Snapshot Container tests', () => {
|
||||
test.fixme('5 Snapshots can be added to a container', async ({ page }) => {});
|
||||
test.fixme('5 Snapshots can be added to a container and Deleted with Delete All action', async ({ page }) => {});
|
||||
test.fixme('A snapshot can be Deleted from Container', async ({ page }) => {});
|
||||
test.fixme('A snapshot can be Previewed from Container', async ({ page }) => {});
|
||||
test.fixme('A snapshot Container can be open and closed', async ({ page }) => {});
|
||||
test.fixme('Can add object to Snapshot container and pull into notebook and create a new entry', async ({ page }) => {
|
||||
//Create Notebook
|
||||
//Create Telemetry Object
|
||||
//From Telemetry Object, use 'save to Notebook Snapshots'
|
||||
//Snapshots indicator should blink, click on it to view snapshots
|
||||
//Navigate to Notebook
|
||||
//Drag and Drop onto droppable area for new entry
|
||||
//New Entry created with given snapshot added
|
||||
//Snapshot removed from container?
|
||||
});
|
||||
test.fixme('Can add object to Snapshot container and pull into notebook and existing entry', async ({ page }) => {
|
||||
//Create Notebook
|
||||
//Create Telemetry Object
|
||||
//From Telemetry Object, use 'save to Notebook Snapshots'
|
||||
//Snapshots indicator should blink, click on it to view snapshots
|
||||
//Navigate to Notebook
|
||||
//Drag and Drop into exiting entry
|
||||
//Existing Entry updated with given snapshot
|
||||
//Snapshot removed from container?
|
||||
});
|
||||
test.fixme('Verify Embedded options for PNG, JPG, and Annotate work correctly', async ({ page }) => {
|
||||
//Add snapshot to container
|
||||
//Verify PNG, JPG, and Annotate buttons work correctly
|
||||
});
|
||||
});
|
@ -0,0 +1,235 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { openObjectTreeContextMenu } = require('../../../../appActions');
|
||||
const path = require('path');
|
||||
|
||||
const TEST_TEXT = 'Testing text for entries.';
|
||||
const TEST_TEXT_NAME = 'Test Page';
|
||||
const CUSTOM_NAME = 'CUSTOM_NAME';
|
||||
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
|
||||
|
||||
test.describe('Restricted Notebook', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await startAndAddRestrictedNotebookObject(page);
|
||||
});
|
||||
|
||||
test('Can be renamed @addInit', async ({ page }) => {
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(`Unnamed ${CUSTOM_NAME}`);
|
||||
});
|
||||
|
||||
test('Can be deleted if there are no locked pages @addInit', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
await openObjectTreeContextMenu(page, myItemsFolderName, `Unnamed ${CUSTOM_NAME}`);
|
||||
|
||||
const menuOptions = page.locator('.c-menu ul');
|
||||
await expect.soft(menuOptions).toContainText('Remove');
|
||||
|
||||
const restrictedNotebookTreeObject = page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`);
|
||||
|
||||
// notebook tree object exists
|
||||
expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1);
|
||||
|
||||
// Click Remove Text
|
||||
await page.locator('text=Remove').click();
|
||||
|
||||
// Click 'OK' on confirmation window and wait for save banner to appear
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
// has been deleted
|
||||
expect(await restrictedNotebookTreeObject.count()).toEqual(0);
|
||||
});
|
||||
|
||||
test('Can be locked if at least one page has one entry @addInit', async ({ page }) => {
|
||||
|
||||
await enterTextEntry(page);
|
||||
|
||||
const commitButton = page.locator('button:has-text("Commit Entries")');
|
||||
expect(await commitButton.count()).toEqual(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test.describe('Restricted Notebook with at least one entry and with the page locked @addInit', () => {
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await startAndAddRestrictedNotebookObject(page);
|
||||
await enterTextEntry(page);
|
||||
await lockPage(page);
|
||||
|
||||
// open sidebar
|
||||
await page.locator('button.c-notebook__toggle-nav-button').click();
|
||||
});
|
||||
|
||||
test('Locked page should now be in a locked state @addInit @unstable', async ({ page, openmctConfig }, testInfo) => {
|
||||
test.fixme(testInfo.project === 'chrome-beta', "Test is unreliable on chrome-beta");
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
// main lock message on page
|
||||
const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed');
|
||||
expect.soft(await lockMessage.count()).toEqual(1);
|
||||
|
||||
// lock icon on page in sidebar
|
||||
const pageLockIcon = page.locator('ul.c-notebook__pages li div.icon-lock');
|
||||
expect.soft(await pageLockIcon.count()).toEqual(1);
|
||||
|
||||
// no way to remove a restricted notebook with a locked page
|
||||
await openObjectTreeContextMenu(page, myItemsFolderName, `Unnamed ${CUSTOM_NAME}`);
|
||||
const menuOptions = page.locator('.c-menu ul');
|
||||
|
||||
await expect(menuOptions).not.toContainText('Remove');
|
||||
});
|
||||
|
||||
test('Can still: add page, rename, add entry, delete unlocked pages @addInit', async ({ page }) => {
|
||||
// Click text=Page Add >> button
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Page Add >> button').click()
|
||||
]);
|
||||
// Click text=Unnamed Page >> nth=1
|
||||
await page.locator('text=Unnamed Page').nth(1).click();
|
||||
// Press a with modifiers
|
||||
await page.locator('text=Unnamed Page').nth(1).fill(TEST_TEXT_NAME);
|
||||
|
||||
// expect to be able to rename unlocked pages
|
||||
const newPageElement = page.locator(`text=${TEST_TEXT_NAME}`);
|
||||
const newPageCount = await newPageElement.count();
|
||||
await newPageElement.press('Enter'); // exit contenteditable state
|
||||
expect.soft(newPageCount).toEqual(1);
|
||||
|
||||
// enter test text
|
||||
await enterTextEntry(page);
|
||||
|
||||
// expect new page to be lockable
|
||||
const commitButton = page.locator('BUTTON:HAS-TEXT("COMMIT ENTRIES")');
|
||||
expect.soft(await commitButton.count()).toEqual(1);
|
||||
|
||||
// Click text=Unnamed PageTest Page >> button
|
||||
await page.locator('text=Unnamed PageTest Page >> button').click();
|
||||
// Click text=Delete Page
|
||||
await page.locator('text=Delete Page').click();
|
||||
// Click text=Ok
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Ok').click()
|
||||
]);
|
||||
|
||||
// deleted page, should no longer exist
|
||||
const deletedPageElement = page.locator(`text=${TEST_TEXT_NAME}`);
|
||||
expect(await deletedPageElement.count()).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Restricted Notebook with a page locked and with an embed @addInit', () => {
|
||||
|
||||
test.beforeEach(async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
await startAndAddRestrictedNotebookObject(page);
|
||||
await dragAndDropEmbed(page, myItemsFolderName);
|
||||
});
|
||||
|
||||
test('Allows embeds to be deleted if page unlocked @addInit', async ({ page }) => {
|
||||
// Click .c-ne__embed__name .c-popup-menu-button
|
||||
await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu
|
||||
|
||||
const embedMenu = page.locator('body >> .c-menu');
|
||||
await expect(embedMenu).toContainText('Remove This Embed');
|
||||
});
|
||||
|
||||
test('Disallows embeds to be deleted if page locked @addInit', async ({ page }) => {
|
||||
await lockPage(page);
|
||||
// Click .c-ne__embed__name .c-popup-menu-button
|
||||
await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu
|
||||
|
||||
const embedMenu = page.locator('body >> .c-menu');
|
||||
await expect(embedMenu).not.toContainText('Remove This Embed');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function startAndAddRestrictedNotebookObject(page) {
|
||||
// eslint-disable-next-line no-undef
|
||||
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitRestrictedNotebook.js') });
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.click('button:has-text("Create")');
|
||||
await page.click(`text=${CUSTOM_NAME}`); // secondarily tests renamability also
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('text=OK')
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function enterTextEntry(page) {
|
||||
// Click .c-notebook__drag-area
|
||||
await page.locator(NOTEBOOK_DROP_AREA).click();
|
||||
|
||||
// enter text
|
||||
await page.locator('div.c-ne__text').click();
|
||||
await page.locator('div.c-ne__text').fill(TEST_TEXT);
|
||||
await page.locator('div.c-ne__text').press('Enter');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function dragAndDropEmbed(page, myItemsFolderName) {
|
||||
// Click button:has-text("Create")
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
// Click li:has-text("Sine Wave Generator")
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
// Click form[name="mctForm"] >> text=My Items
|
||||
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
|
||||
// Click text=OK
|
||||
await page.locator('text=OK').click();
|
||||
// Click text=Open MCT My Items >> span >> nth=3
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
// Click text=Unnamed CUSTOM_NAME
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed CUSTOM_NAME').click()
|
||||
]);
|
||||
|
||||
await page.dragAndDrop('text=UNNAMED SINE WAVE GENERATOR', NOTEBOOK_DROP_AREA);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function lockPage(page) {
|
||||
const commitButton = page.locator('button:has-text("Commit Entries")');
|
||||
await commitButton.click();
|
||||
|
||||
//Wait until Lock Banner is visible
|
||||
await page.locator('text=Lock Page').click();
|
||||
}
|
214
e2e/tests/functional/plugins/notebook/tags.e2e.spec.js
Normal file
214
e2e/tests/functional/plugins/notebook/tags.e2e.spec.js
Normal file
@ -0,0 +1,214 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify form functionality.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
|
||||
/**
|
||||
* 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, myItemsFolderName, iterations = 1) {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click button:has-text("Create")
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('[title="Create and save timestamped notes with embedded object snapshots."]').click();
|
||||
// Click button:has-text("OK")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator(`[name="mctForm"] >> text=${myItemsFolderName}`).click(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
|
||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||
// Click text=To start a new entry, click here or drag and drop any object
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = ${iteration}`;
|
||||
await page.locator(entryLocator).click();
|
||||
await page.locator(entryLocator).fill(`Entry ${iteration}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, myItemsFolderName, iterations = 1) {
|
||||
await createNotebookAndEntry(page, myItemsFolderName, iterations);
|
||||
|
||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||
// Click text=To start a new entry, click here or drag and drop any object
|
||||
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
|
||||
|
||||
// Click [placeholder="Type to select tag"]
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
// Click text=Driving
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
|
||||
|
||||
// Click button:has-text("Add Tag")
|
||||
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
|
||||
// Click [placeholder="Type to select tag"]
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
// Click text=Science
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('Tagging in Notebooks', () => {
|
||||
test('Can load tags', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
await createNotebookAndEntry(page, myItemsFolderName);
|
||||
// Click text=To start a new entry, click here or drag and drop any object
|
||||
await page.locator('button:has-text("Add Tag")').click();
|
||||
|
||||
// Click [placeholder="Type to select tag"]
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
|
||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling");
|
||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Driving");
|
||||
});
|
||||
test('Can add tags', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
await createNotebookEntryAndTags(page, myItemsFolderName);
|
||||
|
||||
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving");
|
||||
|
||||
// Click button:has-text("Add Tag")
|
||||
await page.locator('button:has-text("Add Tag")').click();
|
||||
// Click [placeholder="Type to select tag"]
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
|
||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Driving");
|
||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling");
|
||||
});
|
||||
test('Can search for tags', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
await createNotebookEntryAndTags(page, myItemsFolderName);
|
||||
// Click [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving");
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving");
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('Can delete tags', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
await createNotebookEntryAndTags(page, myItemsFolderName);
|
||||
await page.locator('[aria-label="Notebook Entries"]').click();
|
||||
// Delete Driving
|
||||
await page.locator('text=Science Driving Add Tag >> button').nth(1).click();
|
||||
|
||||
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving");
|
||||
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
|
||||
});
|
||||
test('Tags persist across reload', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Create a clock object we can navigate to
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click Clock
|
||||
await page.click('text=Clock');
|
||||
// Click button:has-text("OK")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator(`[name="mctForm"] >> text=${myItemsFolderName}`).click(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
|
||||
await page.click('.c-disclosure-triangle');
|
||||
|
||||
const ITERATIONS = 4;
|
||||
await createNotebookEntryAndTags(page, myItemsFolderName, ITERATIONS);
|
||||
|
||||
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
|
||||
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
|
||||
await expect(page.locator(entryLocator)).toContainText("Science");
|
||||
await expect(page.locator(entryLocator)).toContainText("Driving");
|
||||
}
|
||||
|
||||
// Click Unnamed Clock
|
||||
await page.click('text="Unnamed Clock"');
|
||||
|
||||
// Click Unnamed Notebook
|
||||
await page.click('text="Unnamed Notebook"');
|
||||
|
||||
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
|
||||
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
|
||||
await expect(page.locator(entryLocator)).toContainText("Science");
|
||||
await expect(page.locator(entryLocator)).toContainText("Driving");
|
||||
}
|
||||
|
||||
//Reload Page
|
||||
await Promise.all([
|
||||
page.reload(),
|
||||
page.waitForLoadState('networkidle')
|
||||
]);
|
||||
|
||||
// Click Unnamed Notebook
|
||||
await page.click('text="Unnamed Notebook"');
|
||||
|
||||
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
|
||||
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
|
||||
await expect(page.locator(entryLocator)).toContainText("Science");
|
||||
await expect(page.locator(entryLocator)).toContainText("Driving");
|
||||
}
|
||||
|
||||
});
|
||||
});
|
186
e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js
Normal file
186
e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js
Normal file
@ -0,0 +1,186 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Testsuite for plot autoscale.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
test.use({
|
||||
viewport: {
|
||||
width: 1280,
|
||||
height: 720
|
||||
}
|
||||
});
|
||||
|
||||
test.describe('ExportAsJSON', () => {
|
||||
test('User can set autoscale with a valid range @snapshot', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
//This is necessary due to the size of the test suite.
|
||||
test.slow();
|
||||
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
await setTimeRange(page);
|
||||
|
||||
await createSinewaveOverlayPlot(page, myItemsFolderName);
|
||||
|
||||
await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']);
|
||||
|
||||
await turnOffAutoscale(page);
|
||||
|
||||
// Make sure that after turning off autoscale, the user selected range values start at the same values the plot had prior.
|
||||
await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']);
|
||||
|
||||
const canvas = page.locator('canvas').nth(1);
|
||||
|
||||
await canvas.hover({trial: true});
|
||||
|
||||
expect(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-prepan');
|
||||
|
||||
//Alt Drag Start
|
||||
await page.keyboard.down('Alt');
|
||||
|
||||
await canvas.dragTo(canvas, {
|
||||
sourcePosition: {
|
||||
x: 200,
|
||||
y: 200
|
||||
},
|
||||
targetPosition: {
|
||||
x: 400,
|
||||
y: 400
|
||||
}
|
||||
});
|
||||
|
||||
//Alt Drag End
|
||||
await page.keyboard.up('Alt');
|
||||
|
||||
// Ensure the drag worked.
|
||||
await testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']);
|
||||
|
||||
await canvas.hover({trial: true});
|
||||
|
||||
expect(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-panned');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} start
|
||||
* @param {string} end
|
||||
*/
|
||||
async function setTimeRange(page, start = '2022-03-29 22:00:00.000Z', end = '2022-03-29 22:00:30.000Z') {
|
||||
// Set a specific time range for consistency, otherwise it will change
|
||||
// on every test to a range based on the current time.
|
||||
|
||||
const timeInputs = page.locator('input.c-input--datetime');
|
||||
await timeInputs.first().click();
|
||||
await timeInputs.first().fill(start);
|
||||
|
||||
await timeInputs.nth(1).click();
|
||||
await timeInputs.nth(1).fill(end);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} myItemsFolderName
|
||||
*/
|
||||
async function createSinewaveOverlayPlot(page, myItemsFolderName) {
|
||||
// click create button
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// add overlay plot with defaults
|
||||
await page.locator('li:has-text("Overlay Plot")').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear1
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// save (exit edit mode)
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// click create button
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// add sine wave generator with defaults
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear1
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// focus the overlay plot
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Overlay Plot').first().click()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function turnOffAutoscale(page) {
|
||||
// enter edit mode
|
||||
await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
|
||||
|
||||
// uncheck autoscale
|
||||
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"] >> nth=1').uncheck();
|
||||
|
||||
// save
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
await Promise.all([
|
||||
page.locator('text=Save and Finish Editing').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function testYTicks(page, values) {
|
||||
const yTicks = page.locator('.gl-plot-y-tick-label');
|
||||
await page.locator('canvas >> nth=1').hover();
|
||||
let promises = [yTicks.count().then(c => expect(c).toBe(values.length))];
|
||||
|
||||
for (let i = 0, l = values.length; i < l; i += 1) {
|
||||
promises.push(expect(yTicks.nth(i)).toHaveText(values[i])); // eslint-disable-line
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
301
e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js
Normal file
301
e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js
Normal file
@ -0,0 +1,301 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Tests to verify log plot functionality. Note this test suite if very much under active development and should not
|
||||
necessarily be used for reference when writing new tests in this area.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
test.describe('Log plot tests', () => {
|
||||
test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
//Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
|
||||
test.slow();
|
||||
|
||||
await makeOverlayPlot(page, myItemsFolderName);
|
||||
await testRegularTicks(page);
|
||||
await enableEditMode(page);
|
||||
await enableLogMode(page);
|
||||
await testLogTicks(page);
|
||||
await disableLogMode(page);
|
||||
await testRegularTicks(page);
|
||||
await enableLogMode(page);
|
||||
await testLogTicks(page);
|
||||
await saveOverlayPlot(page);
|
||||
await testLogTicks(page);
|
||||
});
|
||||
|
||||
// Leaving test as 'TODO' for now.
|
||||
// NOTE: Not eligible for community contributions.
|
||||
test.fixme('Verify that log mode option is reflected in import/export JSON', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
await makeOverlayPlot(page, myItemsFolderName);
|
||||
await enableEditMode(page);
|
||||
await enableLogMode(page);
|
||||
await saveOverlayPlot(page);
|
||||
|
||||
// TODO ...export, delete the overlay, then import it...
|
||||
|
||||
//await testLogTicks(page);
|
||||
|
||||
// TODO, the plot is slightly at different position that in the other test, so this fails.
|
||||
// ...We can fix it by copying all steps from the first test...
|
||||
// await testLogPlotPixels(page);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Makes an overlay plot with a sine wave generator and clicks on the overlay plot in the sidebar so it is the active thing displayed.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} myItemsFolderName
|
||||
*/
|
||||
async function makeOverlayPlot(page, myItemsFolderName) {
|
||||
// fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Set a specific time range for consistency, otherwise it will change
|
||||
// on every test to a range based on the current time.
|
||||
|
||||
const timeInputs = page.locator('input.c-input--datetime');
|
||||
await timeInputs.first().click();
|
||||
await timeInputs.first().fill('2022-03-29 22:00:00.000Z');
|
||||
|
||||
await timeInputs.nth(1).click();
|
||||
await timeInputs.nth(1).fill('2022-03-29 22:00:30.000Z');
|
||||
|
||||
// create overlay plot
|
||||
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li:has-text("Overlay Plot")').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// save the overlay plot
|
||||
|
||||
await saveOverlayPlot(page);
|
||||
|
||||
// create a sinewave generator
|
||||
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
|
||||
// set amplitude to 6, offset 4, period 2
|
||||
|
||||
await page.locator('div:nth-child(5) .c-form-row__controls .form-control .field input').click();
|
||||
await page.locator('div:nth-child(5) .c-form-row__controls .form-control .field input').fill('6');
|
||||
|
||||
await page.locator('div:nth-child(6) .c-form-row__controls .form-control .field input').click();
|
||||
await page.locator('div:nth-child(6) .c-form-row__controls .form-control .field input').fill('4');
|
||||
|
||||
await page.locator('div:nth-child(7) .c-form-row__controls .form-control .field input').click();
|
||||
await page.locator('div:nth-child(7) .c-form-row__controls .form-control .field input').fill('2');
|
||||
|
||||
// Click OK to make generator
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// click on overlay plot
|
||||
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Overlay Plot').first().click()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function testRegularTicks(page) {
|
||||
const yTicks = await page.locator('.gl-plot-y-tick-label');
|
||||
expect(await yTicks.count()).toBe(7);
|
||||
await expect(yTicks.nth(0)).toHaveText('-2');
|
||||
await expect(yTicks.nth(1)).toHaveText('0');
|
||||
await expect(yTicks.nth(2)).toHaveText('2');
|
||||
await expect(yTicks.nth(3)).toHaveText('4');
|
||||
await expect(yTicks.nth(4)).toHaveText('6');
|
||||
await expect(yTicks.nth(5)).toHaveText('8');
|
||||
await expect(yTicks.nth(6)).toHaveText('10');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function testLogTicks(page) {
|
||||
const yTicks = await page.locator('.gl-plot-y-tick-label');
|
||||
expect(await yTicks.count()).toBe(28);
|
||||
await expect(yTicks.nth(0)).toHaveText('-2.98');
|
||||
await expect(yTicks.nth(1)).toHaveText('-2.50');
|
||||
await expect(yTicks.nth(2)).toHaveText('-2.00');
|
||||
await expect(yTicks.nth(3)).toHaveText('-1.51');
|
||||
await expect(yTicks.nth(4)).toHaveText('-1.20');
|
||||
await expect(yTicks.nth(5)).toHaveText('-1.00');
|
||||
await expect(yTicks.nth(6)).toHaveText('-0.80');
|
||||
await expect(yTicks.nth(7)).toHaveText('-0.58');
|
||||
await expect(yTicks.nth(8)).toHaveText('-0.40');
|
||||
await expect(yTicks.nth(9)).toHaveText('-0.20');
|
||||
await expect(yTicks.nth(10)).toHaveText('-0.00');
|
||||
await expect(yTicks.nth(11)).toHaveText('0.20');
|
||||
await expect(yTicks.nth(12)).toHaveText('0.40');
|
||||
await expect(yTicks.nth(13)).toHaveText('0.58');
|
||||
await expect(yTicks.nth(14)).toHaveText('0.80');
|
||||
await expect(yTicks.nth(15)).toHaveText('1.00');
|
||||
await expect(yTicks.nth(16)).toHaveText('1.20');
|
||||
await expect(yTicks.nth(17)).toHaveText('1.51');
|
||||
await expect(yTicks.nth(18)).toHaveText('2.00');
|
||||
await expect(yTicks.nth(19)).toHaveText('2.50');
|
||||
await expect(yTicks.nth(20)).toHaveText('2.98');
|
||||
await expect(yTicks.nth(21)).toHaveText('3.50');
|
||||
await expect(yTicks.nth(22)).toHaveText('4.00');
|
||||
await expect(yTicks.nth(23)).toHaveText('4.50');
|
||||
await expect(yTicks.nth(24)).toHaveText('5.31');
|
||||
await expect(yTicks.nth(25)).toHaveText('7.00');
|
||||
await expect(yTicks.nth(26)).toHaveText('8.00');
|
||||
await expect(yTicks.nth(27)).toHaveText('9.00');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function enableEditMode(page) {
|
||||
// turn on edit mode
|
||||
await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
|
||||
await expect(await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1)).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function enableLogMode(page) {
|
||||
// turn on log mode
|
||||
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function disableLogMode(page) {
|
||||
// turn off log mode
|
||||
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().uncheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function saveOverlayPlot(page) {
|
||||
// save overlay plot
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
|
||||
await Promise.all([
|
||||
page.locator('text=Save and Finish Editing').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
// FIXME: Remove this eslint exception once implemented
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
async function testLogPlotPixels(page) {
|
||||
const pixelsMatch = await page.evaluate(async () => {
|
||||
// TODO get canvas pixels at a few locations to make sure they're the correct color, to test that the plot comes out as expected.
|
||||
|
||||
await new Promise((r) => setTimeout(r, 5 * 1000));
|
||||
|
||||
// These are some pixels that should be blue points in the log plot.
|
||||
// If the plot changes shape to an unexpected shape, this will
|
||||
// likely fail, which is what we want.
|
||||
//
|
||||
// I found these pixels by pausing playwright in debug mode at this
|
||||
// point, and using similar code as below to output the pixel data, then
|
||||
// I logged those pixels here.
|
||||
const expectedBluePixels = [
|
||||
// TODO these pixel sets only work with the first test, but not the second test.
|
||||
|
||||
// [60, 35],
|
||||
// [121, 125],
|
||||
// [156, 377],
|
||||
// [264, 73],
|
||||
// [372, 186],
|
||||
// [576, 73],
|
||||
// [659, 439],
|
||||
// [675, 423]
|
||||
|
||||
[60, 35],
|
||||
[120, 125],
|
||||
[156, 375],
|
||||
[264, 73],
|
||||
[372, 185],
|
||||
[575, 72],
|
||||
[659, 437],
|
||||
[675, 421]
|
||||
];
|
||||
|
||||
// The first canvas in the DOM is the one that has the plot point
|
||||
// icons (canvas 2d), which is the one we are testing. The second
|
||||
// one in the DOM is the WebGL canvas with the line. (Why aren't
|
||||
// they both WebGL?)
|
||||
const canvas = document.querySelector('canvas');
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
for (const pixel of expectedBluePixels) {
|
||||
// XXX Possible optimization: call getImageData only once with
|
||||
// area including all pixels to be tested.
|
||||
const data = ctx.getImageData(pixel[0], pixel[1], 1, 1).data;
|
||||
|
||||
// #43b0ffff <-- openmct cyanish-blue with 100% opacity
|
||||
// if (data[0] !== 0x43 || data[1] !== 0xb0 || data[2] !== 0xff || data[3] !== 0xff) {
|
||||
if (data[0] === 0 && data[1] === 0 && data[2] === 0 && data[3] === 0) {
|
||||
// If any pixel is empty, it means we didn't hit a plot point.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
expect(pixelsMatch).toBe(true);
|
||||
}
|
156
e2e/tests/functional/plugins/plot/missingPlotObj.e2e.spec.js
Normal file
156
e2e/tests/functional/plugins/plot/missingPlotObj.e2e.spec.js
Normal file
@ -0,0 +1,156 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Tests to verify log plot functionality when objects are missing
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
|
||||
test.describe('Handle missing object for plots', () => {
|
||||
test('Displays empty div for missing stacked plot item @unstable', async ({ page, browserName, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
test.fixme(browserName === 'firefox', 'Firefox failing due to console events being missed');
|
||||
const errorLogs = [];
|
||||
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === 'warning' && message.text().includes('Missing domain object')) {
|
||||
errorLogs.push(message.text());
|
||||
}
|
||||
});
|
||||
|
||||
//Make stacked plot
|
||||
await makeStackedPlot(page, myItemsFolderName);
|
||||
|
||||
//Gets local storage and deletes the last sine wave generator in the stacked plot
|
||||
const localStorage = await page.evaluate(() => window.localStorage);
|
||||
const parsedData = JSON.parse(localStorage.mct);
|
||||
const keys = Object.keys(parsedData);
|
||||
const lastKey = keys[keys.length - 1];
|
||||
|
||||
delete parsedData[lastKey];
|
||||
|
||||
//Sets local storage with missing object
|
||||
await page.evaluate(
|
||||
`window.localStorage.setItem('mct', '${JSON.stringify(parsedData)}')`
|
||||
);
|
||||
|
||||
//Reloads page and clicks on stacked plot
|
||||
await Promise.all([
|
||||
page.reload(),
|
||||
page.waitForLoadState('networkidle')
|
||||
]);
|
||||
|
||||
//Verify Main section is there on load
|
||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Stacked Plot');
|
||||
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Stacked Plot').first().click()
|
||||
]);
|
||||
|
||||
//Check that there is only one stacked item plot with a plot, the missing one will be empty
|
||||
await expect(page.locator(".c-plot--stacked-container:has(.gl-plot)")).toHaveCount(1);
|
||||
//Verify that console.warn is thrown
|
||||
expect(errorLogs).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This is used the create a stacked plot object
|
||||
* @private
|
||||
*/
|
||||
async function makeStackedPlot(page, myItemsFolderName) {
|
||||
// fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// create stacked plot
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li:has-text("Stacked Plot")').click();
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
// save the stacked plot
|
||||
await saveStackedPlot(page);
|
||||
|
||||
// create a sinewave generator
|
||||
await createSineWaveGenerator(page);
|
||||
|
||||
// click on stacked plot
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Stacked Plot').first().click()
|
||||
]);
|
||||
|
||||
// create a second sinewave generator
|
||||
await createSineWaveGenerator(page);
|
||||
|
||||
// click on stacked plot
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Stacked Plot').first().click()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to save a stacked plot object
|
||||
* @private
|
||||
*/
|
||||
async function saveStackedPlot(page) {
|
||||
// save stacked plot
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
|
||||
await Promise.all([
|
||||
page.locator('text=Save and Finish Editing').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to create a sine wave generator object
|
||||
* @private
|
||||
*/
|
||||
async function createSineWaveGenerator(page) {
|
||||
//Create sine wave generator
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
|
||||
test.describe('Telemetry Table', () => {
|
||||
test('unpauses and filters data when paused by button and user changes bounds', async ({ page, openmctConfig }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5113'
|
||||
});
|
||||
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
const bannerMessage = '.c-message-banner__message';
|
||||
const createButton = 'button:has-text("Create")';
|
||||
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click create button
|
||||
await page.locator(createButton).click();
|
||||
await page.locator('li:has-text("Telemetry Table")').click();
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
// Wait for Save Banner to appear
|
||||
page.waitForSelector(bannerMessage)
|
||||
]);
|
||||
|
||||
// Save (exit edit mode)
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(3).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Click create button
|
||||
await page.locator(createButton).click();
|
||||
|
||||
// add Sine Wave Generator with defaults
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
// Wait for Save Banner to appear
|
||||
page.waitForSelector(bannerMessage)
|
||||
]);
|
||||
|
||||
// focus the Telemetry Table
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Telemetry Table').first().click()
|
||||
]);
|
||||
|
||||
// Click pause button
|
||||
const pauseButton = page.locator('button.c-button.icon-pause');
|
||||
await pauseButton.click();
|
||||
|
||||
const tableWrapper = page.locator('div.c-table-wrapper');
|
||||
await expect(tableWrapper).toHaveClass(/is-paused/);
|
||||
|
||||
// Subtract 5 minutes from the current end bound datetime and set it
|
||||
const endTimeInput = page.locator('input[type="text"].c-input--datetime').nth(1);
|
||||
await endTimeInput.click();
|
||||
|
||||
let endDate = await endTimeInput.inputValue();
|
||||
endDate = new Date(endDate);
|
||||
|
||||
endDate.setUTCMinutes(endDate.getUTCMinutes() - 5);
|
||||
endDate = endDate.toISOString().replace(/T/, ' ');
|
||||
|
||||
await endTimeInput.fill('');
|
||||
await endTimeInput.fill(endDate);
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await expect(tableWrapper).not.toHaveClass(/is-paused/);
|
||||
|
||||
// Get the most recent telemetry date
|
||||
const latestTelemetryDate = await page.locator('table.c-telemetry-table__body > tbody > tr').last().locator('td').nth(1).getAttribute('title');
|
||||
|
||||
// Verify that it is <= our new end bound
|
||||
const latestMilliseconds = Date.parse(latestTelemetryDate);
|
||||
const endBoundMilliseconds = Date.parse(endDate);
|
||||
expect(latestMilliseconds).toBeLessThanOrEqual(endBoundMilliseconds);
|
||||
});
|
||||
});
|
@ -0,0 +1,234 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../../../baseFixtures');
|
||||
|
||||
test.describe('Time conductor operations', () => {
|
||||
test('validate start time does not exceeds end time', async ({ page }) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
let startDate = 'xxxx-01-01 01:00:00.000Z';
|
||||
startDate = year + startDate.substring(4);
|
||||
|
||||
let endDate = 'xxxx-01-01 02:00:00.000Z';
|
||||
endDate = year + endDate.substring(4);
|
||||
|
||||
const startTimeLocator = page.locator('input[type="text"]').first();
|
||||
const endTimeLocator = page.locator('input[type="text"]').nth(1);
|
||||
|
||||
// Click start time
|
||||
await startTimeLocator.click();
|
||||
|
||||
// Click end time
|
||||
await endTimeLocator.click();
|
||||
|
||||
await endTimeLocator.fill(endDate.toString());
|
||||
await startTimeLocator.fill(startDate.toString());
|
||||
|
||||
// invalid start date
|
||||
startDate = (year + 1) + startDate.substring(4);
|
||||
await startTimeLocator.fill(startDate.toString());
|
||||
await endTimeLocator.click();
|
||||
|
||||
const startDateValidityStatus = await startTimeLocator.evaluate((element) => element.checkValidity());
|
||||
expect(startDateValidityStatus).not.toBeTruthy();
|
||||
|
||||
// fix to valid start date
|
||||
startDate = (year - 1) + startDate.substring(4);
|
||||
await startTimeLocator.fill(startDate.toString());
|
||||
|
||||
// invalid end date
|
||||
endDate = (year - 2) + endDate.substring(4);
|
||||
await endTimeLocator.fill(endDate.toString());
|
||||
await startTimeLocator.click();
|
||||
|
||||
const endDateValidityStatus = await endTimeLocator.evaluate((element) => element.checkValidity());
|
||||
expect(endDateValidityStatus).not.toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
// Testing instructions:
|
||||
// Try to change the realtime offsets when in realtime (local clock) mode.
|
||||
test.describe('Time conductor input fields real-time mode', () => {
|
||||
test('validate input fields in real-time mode', async ({ page }) => {
|
||||
const startOffset = {
|
||||
secs: '23'
|
||||
};
|
||||
|
||||
const endOffset = {
|
||||
secs: '31'
|
||||
};
|
||||
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Switch to real-time mode
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// Set start time offset
|
||||
await setStartOffset(page, startOffset);
|
||||
|
||||
// Verify time was updated on time offset button
|
||||
await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23');
|
||||
|
||||
// Set end time offset
|
||||
await setEndOffset(page, endOffset);
|
||||
|
||||
// Verify time was updated on preceding time offset button
|
||||
await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:31');
|
||||
});
|
||||
|
||||
/**
|
||||
* Verify that offsets and url params are preserved when switching
|
||||
* between fixed timespan and real-time mode.
|
||||
*/
|
||||
test('preserve offsets and url params when switching between fixed and real-time mode', async ({ page }) => {
|
||||
const startOffset = {
|
||||
mins: '30',
|
||||
secs: '23'
|
||||
};
|
||||
|
||||
const endOffset = {
|
||||
secs: '01'
|
||||
};
|
||||
|
||||
// Convert offsets to milliseconds
|
||||
const startDelta = (30 * 60 * 1000) + (23 * 1000);
|
||||
const endDelta = (1 * 1000);
|
||||
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Switch to real-time mode
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// Set start time offset
|
||||
await setStartOffset(page, startOffset);
|
||||
|
||||
// Set end time offset
|
||||
await setEndOffset(page, endOffset);
|
||||
|
||||
// Switch to fixed timespan mode
|
||||
await setFixedTimeMode(page);
|
||||
|
||||
// Switch back to real-time mode
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// Verify updated start time offset persists after mode switch
|
||||
await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23');
|
||||
|
||||
// Verify updated end time offset persists after mode switch
|
||||
await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01');
|
||||
|
||||
// Verify url parameters persist after mode switch
|
||||
await page.waitForNavigation();
|
||||
expect(page.url()).toContain(`startDelta=${startDelta}`);
|
||||
expect(page.url()).toContain(`endDelta=${endDelta}`);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {Object} OffsetValues
|
||||
* @property {string | undefined} hours
|
||||
* @property {string | undefined} mins
|
||||
* @property {string | undefined} secs
|
||||
*/
|
||||
|
||||
/**
|
||||
* Set the values (hours, mins, secs) for the start time offset when in realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {OffsetValues} offset
|
||||
*/
|
||||
async function setStartOffset(page, offset) {
|
||||
const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
|
||||
await setTimeConductorOffset(page, offset, startOffsetButton);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the values (hours, mins, secs) for the end time offset when in realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {OffsetValues} offset
|
||||
*/
|
||||
async function setEndOffset(page, offset) {
|
||||
const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
|
||||
await setTimeConductorOffset(page, offset, endOffsetButton);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {OffsetValues} offset
|
||||
* @param {import('@playwright/test').Locator} offsetButton
|
||||
*/
|
||||
async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) {
|
||||
await offsetButton.click();
|
||||
|
||||
if (hours) {
|
||||
await page.fill('.pr-time-controls__hrs', hours);
|
||||
}
|
||||
|
||||
if (mins) {
|
||||
await page.fill('.pr-time-controls__mins', mins);
|
||||
}
|
||||
|
||||
if (secs) {
|
||||
await page.fill('.pr-time-controls__secs', secs);
|
||||
}
|
||||
|
||||
// Click the check button
|
||||
await page.locator('.icon-check').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time conductor mode to either fixed timespan or realtime mode.
|
||||
* @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.locator('.c-mode-button').click();
|
||||
|
||||
// Switch time conductor mode
|
||||
if (isFixedTimespan) {
|
||||
await page.locator('data-testid=conductor-modeOption-fixed').click();
|
||||
} else {
|
||||
await page.locator('data-testid=conductor-modeOption-realtime').click();
|
||||
}
|
||||
}
|
155
e2e/tests/functional/plugins/timer/timer.e2e.spec.js
Normal file
155
e2e/tests/functional/plugins/timer/timer.e2e.spec.js
Normal file
@ -0,0 +1,155 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { openObjectTreeContextMenu } = require('../../../../appActions');
|
||||
|
||||
const options = {
|
||||
type: 'Timer'
|
||||
};
|
||||
|
||||
test.describe('Timer', () => {
|
||||
test.use({ objectCreateOptions: options });
|
||||
test('Can perform actions on the Timer', async ({ page, openmctConfig }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/4313'
|
||||
});
|
||||
|
||||
const { myItemsFolderName } = await openmctConfig;
|
||||
|
||||
await test.step("From the tree context menu", async () => {
|
||||
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Start');
|
||||
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Pause');
|
||||
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Restart at 0');
|
||||
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Stop');
|
||||
});
|
||||
|
||||
await test.step("From the 3dot menu", async () => {
|
||||
await triggerTimer3dotMenuAction(page, 'Start');
|
||||
await triggerTimer3dotMenuAction(page, 'Pause');
|
||||
await triggerTimer3dotMenuAction(page, 'Restart at 0');
|
||||
await triggerTimer3dotMenuAction(page, 'Stop');
|
||||
});
|
||||
|
||||
await test.step("From the object view", async () => {
|
||||
await triggerTimerViewAction(page, 'Start');
|
||||
await triggerTimerViewAction(page, 'Pause');
|
||||
await triggerTimerViewAction(page, 'Restart at 0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Actions that can be performed on a timer from context menus.
|
||||
* @typedef {'Start' | 'Stop' | 'Pause' | 'Restart at 0'} TimerAction
|
||||
*/
|
||||
|
||||
/**
|
||||
* Actions that can be performed on a timer from the object view.
|
||||
* @typedef {'Start' | 'Pause' | 'Restart at 0'} TimerViewAction
|
||||
*/
|
||||
|
||||
/**
|
||||
* Trigger a timer action from the tree context menu
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {TimerAction} action
|
||||
*/
|
||||
async function triggerTimerContextMenuAction(page, myItemsFolderName, action) {
|
||||
const menuAction = `.c-menu ul li >> text="${action}"`;
|
||||
await openObjectTreeContextMenu(page, myItemsFolderName, "Unnamed Timer");
|
||||
await page.locator(menuAction).click();
|
||||
assertTimerStateAfterAction(page, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a timer action from the 3dot menu
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {TimerAction} action
|
||||
*/
|
||||
async function triggerTimer3dotMenuAction(page, action) {
|
||||
const menuAction = `.c-menu ul li >> text="${action}"`;
|
||||
const threeDotMenuButton = 'button[title="More options"]';
|
||||
let isActionAvailable = false;
|
||||
let iterations = 0;
|
||||
// Dismiss/open the 3dot menu until the action is available
|
||||
// or a maximum number of iterations is reached
|
||||
while (!isActionAvailable && iterations <= 20) {
|
||||
await page.click('.c-object-view');
|
||||
await page.click(threeDotMenuButton);
|
||||
isActionAvailable = await page.locator(menuAction).isVisible();
|
||||
iterations++;
|
||||
}
|
||||
|
||||
await page.locator(menuAction).click();
|
||||
assertTimerStateAfterAction(page, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a timer action from the object view
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {TimerViewAction} action
|
||||
*/
|
||||
async function triggerTimerViewAction(page, action) {
|
||||
await page.locator('.c-timer').hover({trial: true});
|
||||
const buttonTitle = buttonTitleFromAction(action);
|
||||
await page.click(`button[title="${buttonTitle}"]`);
|
||||
assertTimerStateAfterAction(page, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes in a TimerViewAction and returns the button title
|
||||
* @param {TimerViewAction} action
|
||||
*/
|
||||
function buttonTitleFromAction(action) {
|
||||
switch (action) {
|
||||
case 'Start':
|
||||
return 'Start';
|
||||
case 'Pause':
|
||||
return 'Pause';
|
||||
case 'Restart at 0':
|
||||
return 'Reset';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the timer state after a timer action has been performed.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {TimerAction} action
|
||||
*/
|
||||
async function assertTimerStateAfterAction(page, action) {
|
||||
let timerStateClass;
|
||||
switch (action) {
|
||||
case 'Start':
|
||||
case 'Restart at 0':
|
||||
timerStateClass = "is-started";
|
||||
break;
|
||||
case 'Stop':
|
||||
timerStateClass = 'is-stopped';
|
||||
break;
|
||||
case 'Pause':
|
||||
timerStateClass = 'is-paused';
|
||||
break;
|
||||
}
|
||||
|
||||
await expect.soft(page.locator('.c-timer')).toHaveClass(new RegExp(timerStateClass));
|
||||
}
|
263
e2e/tests/functional/search.e2e.spec.js
Normal file
263
e2e/tests/functional/search.e2e.spec.js
Normal file
@ -0,0 +1,263 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/**
|
||||
* This test suite is dedicated to tests which verify search functionalities.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
|
||||
test.describe('Grand Search', () => {
|
||||
test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
await createObjectsForSearch(page, myItemsFolderName);
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cl');
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText(`Clock A ${myItemsFolderName} Red Folder Blue Folder`);
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=1')).toContainText(`Clock B ${myItemsFolderName} Red Folder Blue Folder`);
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=2')).toContainText(`Clock C ${myItemsFolderName} Red Folder Blue Folder`);
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=3')).toContainText(`Clock D ${myItemsFolderName} Red Folder Blue Folder`);
|
||||
// Click text=Elements >> nth=0
|
||||
await page.locator('text=Elements').first().click();
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toBeVisible();
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
|
||||
await page.locator('[aria-label="Clock A clock result"] >> text=Clock A').click();
|
||||
await expect(page.locator('.js-preview-window')).toBeVisible();
|
||||
|
||||
// Click [aria-label="Close"]
|
||||
await page.locator('[aria-label="Close"]').click();
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeVisible();
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText(`Clock A ${myItemsFolderName} Red Folder Blue Folder`);
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] a >> nth=0
|
||||
await page.locator('[aria-label="OpenMCT Search"] a').first().click();
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toBeVisible();
|
||||
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('foo');
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toBeVisible();
|
||||
|
||||
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
// Click text=Save and Finish Editing
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
|
||||
// Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl');
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Clock A').click()
|
||||
]);
|
||||
await expect(page.locator('.is-object-type-clock')).toBeVisible();
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Disp');
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText('Unnamed Display Layout');
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toContainText('Folder');
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Clock C');
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText(`Clock C ${myItemsFolderName} Red Folder Blue Folder`);
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cloc');
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText(`Clock A ${myItemsFolderName} Red Folder Blue Folder`);
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=1')).toContainText(`Clock B ${myItemsFolderName} Red Folder Blue Folder`);
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=2')).toContainText(`Clock C ${myItemsFolderName} Red Folder Blue Folder`);
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=3')).toContainText(`Clock D ${myItemsFolderName} Red Folder Blue Folder`);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Search Tests @unstable", () => {
|
||||
const searchResultSelector = '.c-gsearch-result__title';
|
||||
|
||||
test('Validate empty search result', async ({ page }) => {
|
||||
// Go to baseURL
|
||||
await page.goto("./", { waitUntil: "networkidle" });
|
||||
|
||||
// Invalid search for objects
|
||||
await page.type("input[type=search]", 'not found');
|
||||
|
||||
// Wait for search to complete
|
||||
await waitForSearchCompletion(page);
|
||||
|
||||
// Get the search results
|
||||
const searchResults = await page.locator(searchResultSelector);
|
||||
|
||||
// Verify that no results are found
|
||||
expect(await searchResults.count()).toBe(0);
|
||||
});
|
||||
|
||||
test('Validate single object in search result', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto("./", { waitUntil: "networkidle" });
|
||||
|
||||
// Create a folder object
|
||||
const folderName = 'testFolder';
|
||||
await createFolderObject(page, folderName);
|
||||
|
||||
// Full search for object
|
||||
await page.type("input[type=search]", folderName);
|
||||
|
||||
// Wait for search to complete
|
||||
await waitForSearchCompletion(page);
|
||||
|
||||
// Get the search results
|
||||
const searchResults = await page.locator(searchResultSelector);
|
||||
|
||||
// Verify that one result is found
|
||||
expect(await searchResults.count()).toBe(1);
|
||||
await expect(searchResults).toHaveText(folderName);
|
||||
});
|
||||
|
||||
test("Validate multiple objects in search results return partial matches", async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/4667'
|
||||
});
|
||||
|
||||
// Go to baseURL
|
||||
await page.goto("/", { waitUntil: "networkidle" });
|
||||
|
||||
// Create folder objects
|
||||
const folderName = "e928a26e-e924-4ea0";
|
||||
const folderName2 = "e928a26e-e924-4001";
|
||||
|
||||
await createFolderObject(page, folderName);
|
||||
await createFolderObject(page, folderName2);
|
||||
|
||||
// Partial search for objects
|
||||
await page.type("input[type=search]", 'e928a26e');
|
||||
|
||||
// Wait for search to finish
|
||||
await waitForSearchCompletion(page);
|
||||
|
||||
// Get the search results
|
||||
const searchResults = await page.locator(searchResultSelector);
|
||||
|
||||
// Verify that the search result/s correctly match the search query
|
||||
expect(await searchResults.count()).toBe(2);
|
||||
await expect(await searchResults.first()).toHaveText(folderName);
|
||||
await expect(await searchResults.last()).toHaveText(folderName2);
|
||||
});
|
||||
});
|
||||
|
||||
async function createFolderObject(page, folderName) {
|
||||
// Open Create menu
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// Select Folder object
|
||||
await page.locator('text=Folder').nth(1).click();
|
||||
|
||||
// Click folder title to enter edit mode
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
|
||||
|
||||
// Enter folder name
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folderName);
|
||||
|
||||
// Create folder object
|
||||
await page.locator('text=OK').click();
|
||||
}
|
||||
|
||||
async function waitForSearchCompletion(page) {
|
||||
// Wait loading spinner to disappear
|
||||
await page.waitForSelector('.c-tree-and-search__loading', { state: 'detached' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates some domain objects for searching
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function createObjectsForSearch(page, myItemsFolderName) {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li:has-text("Folder") >> nth=1').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill('Red Folder'),
|
||||
await page.locator(`text=Save In Open MCT ${myItemsFolderName} >> span`).nth(3).click(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li:has-text("Folder") >> nth=2').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill('Blue Folder'),
|
||||
await page.locator('form[name="mctForm"] >> text=Red Folder').click(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li[title="A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts."]').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock A'),
|
||||
await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li[title="A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts."]').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock B'),
|
||||
await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li[title="A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts."]').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock C'),
|
||||
await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li[title="A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts."]').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock D'),
|
||||
await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator(`a:has-text("${myItemsFolderName}") >> nth=0`).click()
|
||||
]);
|
||||
// Click button:has-text("Create")
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
// Click li:has-text("Notebook")
|
||||
await page.locator('li:has-text("Display Layout")').click();
|
||||
// Click button:has-text("OK")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
}
|
59
e2e/tests/functional/smoke.e2e.spec.js
Normal file
59
e2e/tests/functional/smoke.e2e.spec.js
Normal file
@ -0,0 +1,59 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which can quickly verify that any openmct installation is
|
||||
operable and that any type of testing can proceed.
|
||||
|
||||
Ideally, smoke tests should make zero assumptions about how and where they are run. This makes them
|
||||
more resilient to change and therefor a better indicator of failure. Smoke tests will also run quickly
|
||||
as they cover a very "thin surface" of functionality.
|
||||
|
||||
When deciding between authoring new smoke tests or functional tests, ask yourself "would I feel
|
||||
comfortable running this test during a live mission?" Avoid creating or deleting Domain Objects.
|
||||
Make no assumptions about the order that elements appear in the DOM.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
|
||||
test('Verify that the create button appears and that the Folder Domain Object is available for selection', async ({ page }) => {
|
||||
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Verify that Create Folder appears in the dropdown
|
||||
await expect(page.locator(':nth-match(:text("Folder"), 2)')).toBeEnabled();
|
||||
});
|
||||
|
||||
test('Verify that My Items Tree appears @ipad', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
//Test.slow annotation is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
|
||||
test.slow();
|
||||
//Go to baseURL
|
||||
await page.goto('./');
|
||||
|
||||
//My Items to be visible
|
||||
await expect(page.locator(`a:has-text("${myItemsFolderName}")`)).toBeEnabled();
|
||||
});
|
177
e2e/tests/performance/imagery.perf.spec.js
Normal file
177
e2e/tests/performance/imagery.perf.spec.js
Normal file
@ -0,0 +1,177 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to performance tests to ensure that testability of performance
|
||||
is not broken upstream on Open MCT. Any assumptions made downstream will be tested here
|
||||
|
||||
TODO:
|
||||
- Update resolution of performance config
|
||||
- Add Performance Observer on init to push all performance marks
|
||||
- Move client CDP connection to before or to a fixture
|
||||
-
|
||||
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
|
||||
|
||||
test.describe('Performance tests', () => {
|
||||
test.beforeEach(async ({ page, browser }, testInfo) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click a:has-text("My Items")
|
||||
await page.locator('a:has-text("My Items")').click({
|
||||
button: 'right'
|
||||
});
|
||||
|
||||
// Click text=Import from JSON
|
||||
await page.locator('text=Import from JSON').click();
|
||||
|
||||
// Upload Performance Display Layout.json
|
||||
await page.setInputFiles('#fileElem', filePath);
|
||||
|
||||
// Click text=OK
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible();
|
||||
|
||||
//Create a Chrome Performance Timeline trace to store as a test artifact
|
||||
console.log("\n==== Devtools: startTracing ====\n");
|
||||
await browser.startTracing(page, {
|
||||
path: `${testInfo.outputPath()}-trace.json`,
|
||||
screenshots: true
|
||||
});
|
||||
});
|
||||
test.afterEach(async ({ page, browser}) => {
|
||||
console.log("\n==== Devtools: stopTracing ====\n");
|
||||
await browser.stopTracing();
|
||||
|
||||
/* Measurement Section
|
||||
/ The following section includes a block of performance measurements.
|
||||
*/
|
||||
//Get time difference between viewlarge actionability and evaluate time
|
||||
await page.evaluate(() => (window.performance.measure("machine-time-difference", "viewlarge.start", "viewLarge.start.test")));
|
||||
|
||||
//Get StartTime
|
||||
const startTime = await page.evaluate(() => window.performance.timing.navigationStart);
|
||||
console.log('window.performance.timing.navigationStart', startTime);
|
||||
|
||||
//Get All Performance Marks
|
||||
const getAllMarksJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("mark")));
|
||||
const getAllMarks = JSON.parse(getAllMarksJson);
|
||||
console.log('window.performance.getEntriesByType("mark")', getAllMarks);
|
||||
|
||||
//Get All Performance Measures
|
||||
const getAllMeasuresJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("measure")));
|
||||
const getAllMeasures = JSON.parse(getAllMeasuresJson);
|
||||
console.log('window.performance.getEntriesByType("measure")', getAllMeasures);
|
||||
|
||||
});
|
||||
/* The following test will navigate to a previously created Performance Display Layout and measure the
|
||||
/ following metrics:
|
||||
/ - ElementResourceTiming
|
||||
/ - Interaction Timing
|
||||
*/
|
||||
test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => {
|
||||
const client = await page.context().newCDPSession(page);
|
||||
// Tell the DevTools session to record performance metrics
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/Performance/#method-getMetrics
|
||||
await client.send('Performance.enable');
|
||||
// Go to baseURL
|
||||
await page.goto('./');
|
||||
|
||||
// Search Available after Launch
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
await page.evaluate(() => window.performance.mark("search-available"));
|
||||
// Fill Search input
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Display Layout');
|
||||
await page.evaluate(() => window.performance.mark("search-entered"));
|
||||
//Search Result Appears and is clicked
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('a:has-text("Performance Display Layout")').first().click(),
|
||||
page.evaluate(() => window.performance.mark("click-search-result"))
|
||||
]);
|
||||
|
||||
//Time to Example Imagery Frame loads within Display Layout
|
||||
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
|
||||
//Time to Example Imagery object loads
|
||||
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
|
||||
|
||||
//Get background-image url from background-image css prop
|
||||
const backgroundImage = await page.locator('.c-imagery__main-image__background-image');
|
||||
let backgroundImageUrl = await backgroundImage.evaluate((el) => {
|
||||
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
|
||||
});
|
||||
backgroundImageUrl = backgroundImageUrl.slice(1, -1); //forgive me, padre
|
||||
console.log('backgroundImageurl ' + backgroundImageUrl);
|
||||
|
||||
//Get ResourceTiming of background-image jpg
|
||||
const resourceTimingJson = await page.evaluate((bgImageUrl) =>
|
||||
JSON.stringify(window.performance.getEntriesByName(bgImageUrl).pop()),
|
||||
backgroundImageUrl
|
||||
);
|
||||
console.log('resourceTimingJson ' + resourceTimingJson);
|
||||
|
||||
//Open Large view
|
||||
await page.locator('button:has-text("Large View")').click(); //This action includes the performance.mark named 'viewLarge.start'
|
||||
await page.evaluate(() => window.performance.mark("viewLarge.start.test")); //This is a mark only to compare evaluate timing
|
||||
|
||||
//Time to Imagery Rendered in Large Frame
|
||||
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
|
||||
await page.evaluate(() => window.performance.mark("background-image-frame"));
|
||||
|
||||
//Time to Example Imagery object loads
|
||||
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
|
||||
await page.evaluate(() => window.performance.mark("background-image-visible"));
|
||||
|
||||
// Get Current number of images in thumbstrip
|
||||
await page.waitForSelector('.c-imagery__thumb');
|
||||
const thumbCount = await page.locator('.c-imagery__thumb').count();
|
||||
console.log('number of thumbs rendered ' + thumbCount);
|
||||
await page.locator('.c-imagery__thumb').last().click();
|
||||
|
||||
//Get ResourceTiming of all jpg resources
|
||||
const resourceTimingJson2 = await page.evaluate(() =>
|
||||
JSON.stringify(window.performance.getEntriesByType('resource'))
|
||||
);
|
||||
const resourceTiming = JSON.parse(resourceTimingJson2);
|
||||
const jpgResourceTiming = resourceTiming.find((element) =>
|
||||
element.name.includes('.jpg')
|
||||
);
|
||||
console.log('jpgResourceTiming ' + JSON.stringify(jpgResourceTiming));
|
||||
|
||||
// Click Close Icon
|
||||
await page.locator('[aria-label="Close"]').click();
|
||||
await page.evaluate(() => window.performance.mark("view-large-close-button"));
|
||||
|
||||
//await client.send('HeapProfiler.enable');
|
||||
await client.send('HeapProfiler.collectGarbage');
|
||||
|
||||
let performanceMetrics = await client.send('Performance.getMetrics');
|
||||
console.log(performanceMetrics.metrics);
|
||||
|
||||
});
|
||||
});
|
119
e2e/tests/performance/memleak-imagery.perf.spec.js
Normal file
119
e2e/tests/performance/memleak-imagery.perf.spec.js
Normal file
@ -0,0 +1,119 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is an initial example for memory leak testing using performance. This configuration and execution must
|
||||
be kept separate from the traditional performance measurements to avoid any "observer" effects associated with tracing
|
||||
or profiling playwright and/or the browser.
|
||||
|
||||
Based on a pattern identified in https://github.com/trentmwillis/devtools-protocol-demos/blob/master/testing-demos/memory-leak-by-heap.js
|
||||
and https://github.com/paulirish/automated-chrome-profiling/issues/3
|
||||
|
||||
Best path forward: https://github.com/cowchimp/headless-devtools/blob/master/src/Memory/example.js
|
||||
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
|
||||
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.describe.skip('Memory Performance tests', () => {
|
||||
test.beforeEach(async ({ page, browser }, testInfo) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click a:has-text("My Items")
|
||||
await page.locator('a:has-text("My Items")').click({
|
||||
button: 'right'
|
||||
});
|
||||
|
||||
// Click text=Import from JSON
|
||||
await page.locator('text=Import from JSON').click();
|
||||
|
||||
// Upload Performance Display Layout.json
|
||||
await page.setInputFiles('#fileElem', filePath);
|
||||
|
||||
// Click text=OK
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => {
|
||||
|
||||
await page.goto('./', {waitUntil: 'networkidle'});
|
||||
|
||||
// To to Search Available after Launch
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
// Fill Search input
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Display Layout');
|
||||
//Search Result Appears and is clicked
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('a:has-text("Performance Display Layout")').first().click()
|
||||
]);
|
||||
|
||||
//Time to Example Imagery Frame loads within Display Layout
|
||||
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
|
||||
//Time to Example Imagery object loads
|
||||
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
|
||||
|
||||
const client = await page.context().newCDPSession(page);
|
||||
await client.send('HeapProfiler.enable');
|
||||
await client.send('HeapProfiler.startSampling');
|
||||
// await client.send('HeapProfiler.collectGarbage');
|
||||
await client.send('Performance.enable');
|
||||
|
||||
let performanceMetricsBefore = await client.send('Performance.getMetrics');
|
||||
console.log(performanceMetricsBefore.metrics);
|
||||
|
||||
//await client.send('Performance.disable');
|
||||
|
||||
//Open Large view
|
||||
await page.locator('button:has-text("Large View")').click();
|
||||
await client.send('HeapProfiler.takeHeapSnapshot');
|
||||
|
||||
//Time to Imagery Rendered in Large Frame
|
||||
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
|
||||
|
||||
//Time to Example Imagery object loads
|
||||
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
|
||||
|
||||
// Click Close Icon
|
||||
await page.locator('.c-click-icon').click();
|
||||
|
||||
//Time to Example Imagery Frame loads within Display Layout
|
||||
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
|
||||
//Time to Example Imagery object loads
|
||||
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
|
||||
|
||||
await client.send('HeapProfiler.collectGarbage');
|
||||
//await client.send('Performance.enable');
|
||||
|
||||
let performanceMetricsAfter = await client.send('Performance.getMetrics');
|
||||
console.log(performanceMetricsAfter.metrics);
|
||||
|
||||
//await client.send('Performance.disable');
|
||||
|
||||
});
|
||||
});
|
158
e2e/tests/performance/notebook.perf.spec.js
Normal file
158
e2e/tests/performance/notebook.perf.spec.js
Normal file
@ -0,0 +1,158 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to performance tests to ensure that testability of performance
|
||||
is not broken upstream on Open MCT. Any assumptions made downstream will be tested here.
|
||||
|
||||
TODO:
|
||||
- Update resolution of performance config
|
||||
- Add Performance Observer on init to push all performance marks
|
||||
- Move client CDP connection to before or to a fixture
|
||||
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
const notebookFilePath = 'e2e/test-data/PerformanceNotebook.json';
|
||||
|
||||
test.describe('Performance tests', () => {
|
||||
test.beforeEach(async ({ page, browser }, testInfo) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click a:has-text("My Items")
|
||||
await page.locator('a:has-text("My Items")').click({
|
||||
button: 'right'
|
||||
});
|
||||
|
||||
// Click text=Import from JSON
|
||||
await page.locator('text=Import from JSON').click();
|
||||
|
||||
// Upload Performance Display Layout.json
|
||||
await page.setInputFiles('#fileElem', notebookFilePath);
|
||||
|
||||
// TODO Fix this
|
||||
await page.locator('text=OK >> nth=1').click();
|
||||
|
||||
await expect(page.locator('a:has-text("Performance Notebook")')).toBeVisible();
|
||||
|
||||
//Create a Chrome Performance Timeline trace to store as a test artifact
|
||||
console.log("\n==== Devtools: startTracing ====\n");
|
||||
await browser.startTracing(page, {
|
||||
path: `${testInfo.outputPath()}-trace.json`,
|
||||
screenshots: true
|
||||
});
|
||||
});
|
||||
test.afterEach(async ({ page, browser}) => {
|
||||
console.log("\n==== Devtools: stopTracing ====\n");
|
||||
await browser.stopTracing();
|
||||
|
||||
/* Measurement Section
|
||||
/ The following section includes a block of performance measurements.
|
||||
*/
|
||||
const startTime = await page.evaluate(() => window.performance.timing.navigationStart);
|
||||
console.log('window.performance.timing.navigationStart', startTime);
|
||||
|
||||
//Get All Performance Marks
|
||||
const getAllMarksJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("mark")));
|
||||
const getAllMarks = JSON.parse(getAllMarksJson);
|
||||
console.log('window.performance.getEntriesByType("mark")', getAllMarks);
|
||||
|
||||
//Get All Performance Measures
|
||||
const getAllMeasuresJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("measure")));
|
||||
const getAllMeasures = JSON.parse(getAllMeasuresJson);
|
||||
console.log('window.performance.getEntriesByType("measure")', getAllMeasures);
|
||||
|
||||
});
|
||||
/* The following test will navigate to a previously created Performance Display Layout and measure the
|
||||
/ following metrics:
|
||||
/ - ElementResourceTiming
|
||||
/ - Interaction Timing
|
||||
*/
|
||||
test('Notebook Search, Add Entry, Update Entry are performant', async ({ page, browser }) => {
|
||||
const client = await page.context().newCDPSession(page);
|
||||
// Tell the DevTools session to record performance metrics
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/Performance/#method-getMetrics
|
||||
await client.send('Performance.enable');
|
||||
// Go to baseURL
|
||||
await page.goto('./');
|
||||
|
||||
// To to Search Available after Launch
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
await page.evaluate(() => window.performance.mark("search-available"));
|
||||
// Fill Search input
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Notebook');
|
||||
await page.evaluate(() => window.performance.mark("search-entered"));
|
||||
//Search Result Appears and is clicked
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('a:has-text("Performance Notebook")').first().click(),
|
||||
page.evaluate(() => window.performance.mark("click-search-result"))
|
||||
]);
|
||||
|
||||
await page.waitForSelector('.c-tree__item c-tree-and-search__loading loading', {state: 'hidden'});
|
||||
await page.evaluate(() => window.performance.mark("search-spinner-gone"));
|
||||
|
||||
await page.waitForSelector('.l-browse-bar__object-name', { state: 'visible'});
|
||||
await page.evaluate(() => window.performance.mark("object-title-appears"));
|
||||
|
||||
await page.waitForSelector('.c-notebook__entry >> nth=0', { state: 'visible'});
|
||||
await page.evaluate(() => window.performance.mark("notebook-entry-appears"));
|
||||
|
||||
// Click Add new Notebook Entry
|
||||
await page.locator('.c-notebook__drag-area').click();
|
||||
await page.evaluate(() => window.performance.mark("new-notebook-entry-created"));
|
||||
|
||||
// Enter Notebook Entry text
|
||||
await page.locator('div.c-ne__text').last().fill('New Entry');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.evaluate(() => window.performance.mark("new-notebook-entry-filled"));
|
||||
|
||||
//Individual Notebook Entry Search
|
||||
await page.evaluate(() => window.performance.mark("notebook-search-start"));
|
||||
await page.locator('.c-notebook__search >> input').fill('Existing Entry');
|
||||
await page.evaluate(() => window.performance.mark("notebook-search-filled"));
|
||||
await page.waitForSelector('text=Search Results (3)', { state: 'visible'});
|
||||
await page.evaluate(() => window.performance.mark("notebook-search-processed"));
|
||||
await page.waitForSelector('.c-notebook__entry >> nth=2', { state: 'visible'});
|
||||
await page.evaluate(() => window.performance.mark("notebook-search-processed"));
|
||||
|
||||
//Clear Search
|
||||
await page.locator('.c-search.c-notebook__search .c-search__clear-input').click();
|
||||
await page.evaluate(() => window.performance.mark("notebook-search-processed"));
|
||||
|
||||
// Hover on Last
|
||||
await page.evaluate(() => window.performance.mark("new-notebook-entry-delete"));
|
||||
await page.locator('div.c-ne__time-and-content').last().hover();
|
||||
await page.locator('button[title="Delete this entry"]').last().click();
|
||||
await page.locator('button:has-text("Ok")').click();
|
||||
await page.waitForSelector('.c-notebook__entry >> nth=3', { state: 'detached'});
|
||||
await page.evaluate(() => window.performance.mark("new-notebook-entry-deleted"));
|
||||
|
||||
//await client.send('HeapProfiler.enable');
|
||||
await client.send('HeapProfiler.collectGarbage');
|
||||
|
||||
let performanceMetrics = await client.send('Performance.getMetrics');
|
||||
console.log(performanceMetrics.metrics);
|
||||
});
|
||||
});
|
70
e2e/tests/visual/addInit.visual.spec.js
Normal file
70
e2e/tests/visual/addInit.visual.spec.js
Normal file
@ -0,0 +1,70 @@
|
||||
/* eslint-disable no-undef */
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Collection of Visual Tests set to run with modified init scripts to inject plugins not otherwise available in the default contexts.
|
||||
|
||||
These should only use functional expect statements to verify assumptions about the state
|
||||
in a test and not for functional verification of correctness. Visual tests are not supposed
|
||||
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
|
||||
|
||||
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { test, expect } = require('../../baseFixtures.js');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
const path = require('path');
|
||||
|
||||
const VISUAL_GRACE_PERIOD = 5 * 1000; //Lets the application "simmer" before the snapshot is taken
|
||||
|
||||
const CUSTOM_NAME = 'CUSTOM_NAME';
|
||||
|
||||
test.describe('Visual - addInit', () => {
|
||||
test.use({
|
||||
clockOptions: {
|
||||
shouldAdvanceTime: true
|
||||
}
|
||||
});
|
||||
|
||||
test('Restricted Notebook is visually correct @addInit', async ({ page }) => {
|
||||
// eslint-disable-next-line no-undef
|
||||
await page.addInitScript({ path: path.join(__dirname, '../../helper', './addInitRestrictedNotebook.js') });
|
||||
//Go to baseURL
|
||||
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
// Click text=CUSTOM_NAME
|
||||
await page.click(`text=${CUSTOM_NAME}`);
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('text=OK')
|
||||
]);
|
||||
|
||||
// Take a snapshot of the newly created CUSTOM_NAME notebook
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'Restricted Notebook with CUSTOM_NAME');
|
||||
|
||||
});
|
||||
});
|
62
e2e/tests/visual/controlledClock.visual.spec.js
Normal file
62
e2e/tests/visual/controlledClock.visual.spec.js
Normal file
@ -0,0 +1,62 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Collection of Visual Tests set to run in a default context. The tests within this suite
|
||||
are only meant to run against openmct's app.js started by `npm run start` within the
|
||||
`./e2e/playwright-visual.config.js` file.
|
||||
|
||||
These should only use functional expect statements to verify assumptions about the state
|
||||
in a test and not for functional verification of correctness. Visual tests are not supposed
|
||||
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
|
||||
|
||||
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../baseFixtures.js');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
|
||||
test.describe('Visual - Controlled Clock', () => {
|
||||
test.use({
|
||||
storageState: './e2e/test-data/VisualTestData_storage.json',
|
||||
clockOptions: {
|
||||
now: 0, //Set browser clock to UNIX Epoch
|
||||
shouldAdvanceTime: false, //Don't advance the clock
|
||||
toFake: ["setTimeout", "nextTick"]
|
||||
}
|
||||
});
|
||||
|
||||
test('Overlay Plot Loading Indicator @localstorage', async ({ page }) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
|
||||
|
||||
await page.locator('a:has-text("Unnamed Overlay Plot Overlay Plot")').click();
|
||||
//Ensure that we're on the Unnamed Overlay Plot object
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
|
||||
|
||||
//Wait for canvas to be rendered and stop animating
|
||||
await page.locator('canvas >> nth=1').hover({trial: true});
|
||||
|
||||
//Take snapshot of Sine Wave Generator within Overlay Plot
|
||||
await percySnapshot(page, 'SineWaveInOverlayPlot');
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user