Selaa lähdekoodia

feat(tests): Grid, FF and iframeAPI tests. (#15372)

* fix(tests): Fix include/excludes of tests based on participant count.

* feat(tests): Define context globally.

The context is being created on every new suite in before hook.

* feat(tests): Drop extra char in room name.

* feat(tests): Adds an option to load iframeAPI.

* feat(tests): Adds some ts types.

* fix(tests): Fix iframeAPI helper with events received too early.

* fix(tests): Fix iframeAPI helper detecting own role changed.

* feat(tests): Adds run script to start tests with local dev server.

* feat(tests): Adds participants iframeAPI tests.

* feat(tests): Updates wdio dependencies.

* feat: Adds grid config.

* feat: Simplify iframeAPI.

Drop URL params.

* feat: Adds tenant to iframeAPI.

* feat: Adds firefox target.

Certain tests are disable as not supported on FF. Missing upload file function for iframeAPI helper. Missing option to set audio file as a mic source.

* fix: Fix using tenant from baseUrl.

* feat: Adds audio only tests.

* feat: Adds option to generate tokens for the moderator.

* feat: Adds option to test and webhooks.

* fix: Improve error stack trace on error.

* fix: Address comments.

* fix: Fix test exclusion for FF.

* squash: Revert the strophe change and add a comment.
factor2
Дамян Минков 6 kuukautta sitten
vanhempi
commit
b9017176a8
No account linked to committer's email address

+ 271
- 65
package-lock.json Näytä tiedosto

@@ -135,6 +135,7 @@
135 135
         "@types/dom-screen-wake-lock": "1.0.1",
136 136
         "@types/jasmine": "5.1.4",
137 137
         "@types/js-md5": "0.4.3",
138
+        "@types/jsonwebtoken": "9.0.7",
138 139
         "@types/lodash-es": "4.17.12",
139 140
         "@types/moment-duration-format": "2.2.6",
140 141
         "@types/offscreencanvas": "2019.7.2",
@@ -155,11 +156,11 @@
155 156
         "@typescript-eslint/eslint-plugin": "5.59.5",
156 157
         "@typescript-eslint/parser": "5.59.5",
157 158
         "@wdio/allure-reporter": "9.2.14",
158
-        "@wdio/cli": "9.2.14",
159
-        "@wdio/globals": "9.2.14",
160
-        "@wdio/jasmine-framework": "9.2.14",
159
+        "@wdio/cli": "9.4.1",
160
+        "@wdio/globals": "9.4.1",
161
+        "@wdio/jasmine-framework": "9.4.1",
161 162
         "@wdio/junit-reporter": "9.2.14",
162
-        "@wdio/local-runner": "9.2.15",
163
+        "@wdio/local-runner": "9.4.1",
163 164
         "babel-loader": "9.1.0",
164 165
         "babel-plugin-optional-require": "0.3.1",
165 166
         "circular-dependency-plugin": "5.2.0",
@@ -172,6 +173,7 @@
172 173
         "eslint-plugin-react-native": "4.0.0",
173 174
         "eslint-plugin-typescript-sort-keys": "2.3.0",
174 175
         "jetifier": "1.6.4",
176
+        "jsonwebtoken": "9.0.2",
175 177
         "metro-react-native-babel-preset": "0.77.0",
176 178
         "patch-package": "6.4.7",
177 179
         "process": "0.11.10",
@@ -181,7 +183,7 @@
181 183
         "ts-loader": "9.4.2",
182 184
         "typescript": "5.0.4",
183 185
         "unorm": "1.6.0",
184
-        "webdriverio": "9.2.14",
186
+        "webdriverio": "9.4.1",
185 187
         "webpack": "5.95.0",
186 188
         "webpack-bundle-analyzer": "4.4.2",
187 189
         "webpack-cli": "5.1.4",
@@ -7122,6 +7124,15 @@
7122 7124
       "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
7123 7125
       "dev": true
7124 7126
     },
7127
+    "node_modules/@types/jsonwebtoken": {
7128
+      "version": "9.0.7",
7129
+      "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz",
7130
+      "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==",
7131
+      "dev": true,
7132
+      "dependencies": {
7133
+        "@types/node": "*"
7134
+      }
7135
+    },
7125 7136
     "node_modules/@types/lodash": {
7126 7137
       "version": "4.14.182",
7127 7138
       "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz",
@@ -7898,15 +7909,15 @@
7898 7909
       }
7899 7910
     },
7900 7911
     "node_modules/@wdio/cli": {
7901
-      "version": "9.2.14",
7902
-      "resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-9.2.14.tgz",
7903
-      "integrity": "sha512-nYNAuF5HPW8b+B21t83N6NGEUpszZZygTDMy+xemBQFrkp7qpDsaeaxv60rFf6AR1mapcnK4ghHJUqod0qL7cg==",
7912
+      "version": "9.4.1",
7913
+      "resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-9.4.1.tgz",
7914
+      "integrity": "sha512-GDyAer63WDsr2ckXmrpUyAcIZFd3pCRIpi85rL1ZjnWthRy/UtwY0EHPMDuSeUEJ28iYwW3esKgq2ZKlsdbMeA==",
7904 7915
       "dev": true,
7905 7916
       "dependencies": {
7906 7917
         "@types/node": "^20.1.1",
7907 7918
         "@vitest/snapshot": "^2.1.1",
7908 7919
         "@wdio/config": "9.2.8",
7909
-        "@wdio/globals": "9.2.14",
7920
+        "@wdio/globals": "9.4.1",
7910 7921
         "@wdio/logger": "9.1.3",
7911 7922
         "@wdio/protocols": "9.2.2",
7912 7923
         "@wdio/types": "9.2.2",
@@ -7926,7 +7937,7 @@
7926 7937
         "read-pkg-up": "^10.0.0",
7927 7938
         "recursive-readdir": "^2.2.3",
7928 7939
         "tsx": "^4.7.2",
7929
-        "webdriverio": "9.2.14",
7940
+        "webdriverio": "9.4.1",
7930 7941
         "yargs": "^17.7.2"
7931 7942
       },
7932 7943
       "bin": {
@@ -8190,26 +8201,26 @@
8190 8201
       }
8191 8202
     },
8192 8203
     "node_modules/@wdio/globals": {
8193
-      "version": "9.2.14",
8194
-      "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-9.2.14.tgz",
8195
-      "integrity": "sha512-Hgi85bp5vpckK+k5iJ6zz8wUUL6IhCzywIG6uXzSgH/zkUOp5Til/NYyJmzSv6hRsfGaFia9WSaoqol93bfEIA==",
8204
+      "version": "9.4.1",
8205
+      "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-9.4.1.tgz",
8206
+      "integrity": "sha512-CTVAVJ7iFyT54XF9iRmNvsDB+WSHoztJPG9XPL/mHzQ2LYfSyUR8E/j+3iHbTx3v/qRNucgPcGwhxiuY2RcaDg==",
8196 8207
       "dev": true,
8197 8208
       "engines": {
8198 8209
         "node": ">=18.20.0"
8199 8210
       },
8200 8211
       "optionalDependencies": {
8201 8212
         "expect-webdriverio": "^5.0.1",
8202
-        "webdriverio": "9.2.14"
8213
+        "webdriverio": "9.4.1"
8203 8214
       }
8204 8215
     },
8205 8216
     "node_modules/@wdio/jasmine-framework": {
8206
-      "version": "9.2.14",
8207
-      "resolved": "https://registry.npmjs.org/@wdio/jasmine-framework/-/jasmine-framework-9.2.14.tgz",
8208
-      "integrity": "sha512-jU+xF08Kq2EsX3RnbUzFjbSMg0gdVYJl2p5fc41SRZls2o2EC8Pvo1B0qaWLTpXYYHAD1Q/efKWhT0AOdudQwA==",
8217
+      "version": "9.4.1",
8218
+      "resolved": "https://registry.npmjs.org/@wdio/jasmine-framework/-/jasmine-framework-9.4.1.tgz",
8219
+      "integrity": "sha512-N2X8MfsfX8JemD4DQwvSksXmBzYZxCKwuB7LaBiw+hH2YuFu+0L2ekR0Xvwtr8SpiENVEHAoiNFEj4i9dMTqlA==",
8209 8220
       "dev": true,
8210 8221
       "dependencies": {
8211 8222
         "@types/node": "^20.1.0",
8212
-        "@wdio/globals": "9.2.14",
8223
+        "@wdio/globals": "9.4.1",
8213 8224
         "@wdio/logger": "9.1.3",
8214 8225
         "@wdio/types": "9.2.2",
8215 8226
         "@wdio/utils": "9.2.8",
@@ -8236,15 +8247,15 @@
8236 8247
       }
8237 8248
     },
8238 8249
     "node_modules/@wdio/local-runner": {
8239
-      "version": "9.2.15",
8240
-      "resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-9.2.15.tgz",
8241
-      "integrity": "sha512-+qh/fkA362r+Wnu58BsC/O8xJ0TKPxdzdyHNsz5qgBdmromQIniWCC4xoKg7rBLv4aA5nkyJjXhC7lXcuWYkLQ==",
8250
+      "version": "9.4.1",
8251
+      "resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-9.4.1.tgz",
8252
+      "integrity": "sha512-MM5VM0V7zvajICr6eNROjkppRhGNpdV4nU5hrgSap92nou8G+zBgLxJ45P5BzLw67KQTOEa1E32b/zCBEkO+0g==",
8242 8253
       "dev": true,
8243 8254
       "dependencies": {
8244 8255
         "@types/node": "^20.1.0",
8245 8256
         "@wdio/logger": "9.1.3",
8246 8257
         "@wdio/repl": "9.0.8",
8247
-        "@wdio/runner": "9.2.15",
8258
+        "@wdio/runner": "9.4.1",
8248 8259
         "@wdio/types": "9.2.2",
8249 8260
         "async-exit-hook": "^2.0.1",
8250 8261
         "split2": "^4.1.0",
@@ -8343,21 +8354,21 @@
8343 8354
       }
8344 8355
     },
8345 8356
     "node_modules/@wdio/runner": {
8346
-      "version": "9.2.15",
8347
-      "resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-9.2.15.tgz",
8348
-      "integrity": "sha512-kVQ+YqVijkD2rJPgbJ7yEzzjOUoJ/HKqvBa3Y/kTJPLcSKQCn47GxqzXlOO6wPWLGWkf22Xn7CiTxFN1t+Zz0w==",
8357
+      "version": "9.4.1",
8358
+      "resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-9.4.1.tgz",
8359
+      "integrity": "sha512-WUpYamLafv+GUofGqvA2HO1kSVnwLyPO5ujq3TS/YaJLnUDOj4bjHiRo5+KDbKw1xhqjW0GjbfDupVU5LeL9iw==",
8349 8360
       "dev": true,
8350 8361
       "dependencies": {
8351 8362
         "@types/node": "^20.11.28",
8352 8363
         "@wdio/config": "9.2.8",
8353
-        "@wdio/globals": "9.2.14",
8364
+        "@wdio/globals": "9.4.1",
8354 8365
         "@wdio/logger": "9.1.3",
8355 8366
         "@wdio/types": "9.2.2",
8356 8367
         "@wdio/utils": "9.2.8",
8357 8368
         "deepmerge-ts": "^7.0.3",
8358 8369
         "expect-webdriverio": "^5.0.1",
8359
-        "webdriver": "9.2.8",
8360
-        "webdriverio": "9.2.14"
8370
+        "webdriver": "9.4.1",
8371
+        "webdriverio": "9.4.1"
8361 8372
       },
8362 8373
       "engines": {
8363 8374
         "node": ">=18.20.0"
@@ -9899,6 +9910,12 @@
9899 9910
         "node": ">=8.0.0"
9900 9911
       }
9901 9912
     },
9913
+    "node_modules/buffer-equal-constant-time": {
9914
+      "version": "1.0.1",
9915
+      "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
9916
+      "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
9917
+      "dev": true
9918
+    },
9902 9919
     "node_modules/buffer-from": {
9903 9920
       "version": "1.1.2",
9904 9921
       "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -11395,6 +11412,15 @@
11395 11412
       "resolved": "https://registry.npmjs.org/ebml-block/-/ebml-block-1.1.2.tgz",
11396 11413
       "integrity": "sha512-HgNlIsRFP6D9VKU5atCeHRJY7XkJP8bOe8yEhd8NB7B3b4++VWTyauz6g650iiPmLfPLGlVpoJmGSgMfXDYusg=="
11397 11414
     },
11415
+    "node_modules/ecdsa-sig-formatter": {
11416
+      "version": "1.0.11",
11417
+      "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
11418
+      "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
11419
+      "dev": true,
11420
+      "dependencies": {
11421
+        "safe-buffer": "^5.0.1"
11422
+      }
11423
+    },
11398 11424
     "node_modules/edge-paths": {
11399 11425
       "version": "3.0.5",
11400 11426
       "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-3.0.5.tgz",
@@ -15961,6 +15987,28 @@
15961 15987
         "graceful-fs": "^4.1.6"
15962 15988
       }
15963 15989
     },
15990
+    "node_modules/jsonwebtoken": {
15991
+      "version": "9.0.2",
15992
+      "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
15993
+      "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
15994
+      "dev": true,
15995
+      "dependencies": {
15996
+        "jws": "^3.2.2",
15997
+        "lodash.includes": "^4.3.0",
15998
+        "lodash.isboolean": "^3.0.3",
15999
+        "lodash.isinteger": "^4.0.4",
16000
+        "lodash.isnumber": "^3.0.3",
16001
+        "lodash.isplainobject": "^4.0.6",
16002
+        "lodash.isstring": "^4.0.1",
16003
+        "lodash.once": "^4.0.0",
16004
+        "ms": "^2.1.1",
16005
+        "semver": "^7.5.4"
16006
+      },
16007
+      "engines": {
16008
+        "node": ">=12",
16009
+        "npm": ">=6"
16010
+      }
16011
+    },
15964 16012
     "node_modules/jsx-ast-utils": {
15965 16013
       "version": "3.2.1",
15966 16014
       "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz",
@@ -16030,6 +16078,27 @@
16030 16078
         "node": ">=16"
16031 16079
       }
16032 16080
     },
16081
+    "node_modules/jwa": {
16082
+      "version": "1.4.1",
16083
+      "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
16084
+      "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
16085
+      "dev": true,
16086
+      "dependencies": {
16087
+        "buffer-equal-constant-time": "1.0.1",
16088
+        "ecdsa-sig-formatter": "1.0.11",
16089
+        "safe-buffer": "^5.0.1"
16090
+      }
16091
+    },
16092
+    "node_modules/jws": {
16093
+      "version": "3.2.2",
16094
+      "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
16095
+      "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
16096
+      "dev": true,
16097
+      "dependencies": {
16098
+        "jwa": "^1.4.1",
16099
+        "safe-buffer": "^5.0.1"
16100
+      }
16101
+    },
16033 16102
     "node_modules/jwt-decode": {
16034 16103
       "version": "2.2.0",
16035 16104
       "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz",
@@ -16555,12 +16624,42 @@
16555 16624
       "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==",
16556 16625
       "dev": true
16557 16626
     },
16627
+    "node_modules/lodash.includes": {
16628
+      "version": "4.3.0",
16629
+      "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
16630
+      "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
16631
+      "dev": true
16632
+    },
16633
+    "node_modules/lodash.isboolean": {
16634
+      "version": "3.0.3",
16635
+      "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
16636
+      "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
16637
+      "dev": true
16638
+    },
16558 16639
     "node_modules/lodash.isequal": {
16559 16640
       "version": "4.5.0",
16560 16641
       "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
16561 16642
       "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
16562 16643
       "dev": true
16563 16644
     },
16645
+    "node_modules/lodash.isinteger": {
16646
+      "version": "4.0.4",
16647
+      "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
16648
+      "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
16649
+      "dev": true
16650
+    },
16651
+    "node_modules/lodash.isnumber": {
16652
+      "version": "3.0.3",
16653
+      "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
16654
+      "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
16655
+      "dev": true
16656
+    },
16657
+    "node_modules/lodash.isplainobject": {
16658
+      "version": "4.0.6",
16659
+      "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
16660
+      "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
16661
+      "dev": true
16662
+    },
16564 16663
     "node_modules/lodash.isstring": {
16565 16664
       "version": "4.0.1",
16566 16665
       "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
@@ -16572,6 +16671,12 @@
16572 16671
       "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
16573 16672
       "dev": true
16574 16673
     },
16674
+    "node_modules/lodash.once": {
16675
+      "version": "4.1.1",
16676
+      "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
16677
+      "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
16678
+      "dev": true
16679
+    },
16575 16680
     "node_modules/lodash.pickby": {
16576 16681
       "version": "4.6.0",
16577 16682
       "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz",
@@ -23313,9 +23418,9 @@
23313 23418
       }
23314 23419
     },
23315 23420
     "node_modules/webdriver": {
23316
-      "version": "9.2.8",
23317
-      "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.2.8.tgz",
23318
-      "integrity": "sha512-40NtUC1zME9tPHNfZv6ETSE3+aE75qZuKjbVAA0gj02AkO1Nl3yJmf5RLdaLLfIQ2WlrbRP1g8KXlkiiVCmakg==",
23421
+      "version": "9.4.1",
23422
+      "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.4.1.tgz",
23423
+      "integrity": "sha512-vFDdxMj/9W1+6jhpHSiRYfO8dix23HjAUtLx7aOv9ejEsntC0EzCIAftJ59YsF3Ppu184+FkdDVhnivpkZPTFw==",
23319 23424
       "dev": true,
23320 23425
       "dependencies": {
23321 23426
         "@types/node": "^20.1.0",
@@ -23326,6 +23431,7 @@
23326 23431
         "@wdio/types": "9.2.2",
23327 23432
         "@wdio/utils": "9.2.8",
23328 23433
         "deepmerge-ts": "^7.0.3",
23434
+        "undici": "^6.20.1",
23329 23435
         "ws": "^8.8.0"
23330 23436
       },
23331 23437
       "engines": {
@@ -23354,9 +23460,9 @@
23354 23460
       }
23355 23461
     },
23356 23462
     "node_modules/webdriverio": {
23357
-      "version": "9.2.14",
23358
-      "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.2.14.tgz",
23359
-      "integrity": "sha512-85yEbwN3MwdrGzKZoGkLUf1J5cpfnc7knL4u/Y6XWd0gGwYjv60I5ZPsgSnXzNXAkq2kmtkammf1AM3ihqFM3A==",
23463
+      "version": "9.4.1",
23464
+      "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.4.1.tgz",
23465
+      "integrity": "sha512-XIPtRnxSES4CoxH2BfUY/6QzNgEgJEUjMYu7t7SJR8bVfbLRVXAA1ie9kM0MtdLs4oZ2Pr8rR8fqytsA1CjEWw==",
23360 23466
       "dev": true,
23361 23467
       "dependencies": {
23362 23468
         "@types/node": "^20.11.30",
@@ -23385,7 +23491,7 @@
23385 23491
         "rgb2hex": "0.2.5",
23386 23492
         "serialize-error": "^11.0.3",
23387 23493
         "urlpattern-polyfill": "^10.0.0",
23388
-        "webdriver": "9.2.8"
23494
+        "webdriver": "9.4.1"
23389 23495
       },
23390 23496
       "engines": {
23391 23497
         "node": ">=18.20.0"
@@ -29046,6 +29152,15 @@
29046 29152
       "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
29047 29153
       "dev": true
29048 29154
     },
29155
+    "@types/jsonwebtoken": {
29156
+      "version": "9.0.7",
29157
+      "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz",
29158
+      "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==",
29159
+      "dev": true,
29160
+      "requires": {
29161
+        "@types/node": "*"
29162
+      }
29163
+    },
29049 29164
     "@types/lodash": {
29050 29165
       "version": "4.14.182",
29051 29166
       "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz",
@@ -29651,15 +29766,15 @@
29651 29766
       }
29652 29767
     },
29653 29768
     "@wdio/cli": {
29654
-      "version": "9.2.14",
29655
-      "resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-9.2.14.tgz",
29656
-      "integrity": "sha512-nYNAuF5HPW8b+B21t83N6NGEUpszZZygTDMy+xemBQFrkp7qpDsaeaxv60rFf6AR1mapcnK4ghHJUqod0qL7cg==",
29769
+      "version": "9.4.1",
29770
+      "resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-9.4.1.tgz",
29771
+      "integrity": "sha512-GDyAer63WDsr2ckXmrpUyAcIZFd3pCRIpi85rL1ZjnWthRy/UtwY0EHPMDuSeUEJ28iYwW3esKgq2ZKlsdbMeA==",
29657 29772
       "dev": true,
29658 29773
       "requires": {
29659 29774
         "@types/node": "^20.1.1",
29660 29775
         "@vitest/snapshot": "^2.1.1",
29661 29776
         "@wdio/config": "9.2.8",
29662
-        "@wdio/globals": "9.2.14",
29777
+        "@wdio/globals": "9.4.1",
29663 29778
         "@wdio/logger": "9.1.3",
29664 29779
         "@wdio/protocols": "9.2.2",
29665 29780
         "@wdio/types": "9.2.2",
@@ -29679,7 +29794,7 @@
29679 29794
         "read-pkg-up": "^10.0.0",
29680 29795
         "recursive-readdir": "^2.2.3",
29681 29796
         "tsx": "^4.7.2",
29682
-        "webdriverio": "9.2.14",
29797
+        "webdriverio": "9.4.1",
29683 29798
         "yargs": "^17.7.2"
29684 29799
       },
29685 29800
       "dependencies": {
@@ -29844,23 +29959,23 @@
29844 29959
       }
29845 29960
     },
29846 29961
     "@wdio/globals": {
29847
-      "version": "9.2.14",
29848
-      "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-9.2.14.tgz",
29849
-      "integrity": "sha512-Hgi85bp5vpckK+k5iJ6zz8wUUL6IhCzywIG6uXzSgH/zkUOp5Til/NYyJmzSv6hRsfGaFia9WSaoqol93bfEIA==",
29962
+      "version": "9.4.1",
29963
+      "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-9.4.1.tgz",
29964
+      "integrity": "sha512-CTVAVJ7iFyT54XF9iRmNvsDB+WSHoztJPG9XPL/mHzQ2LYfSyUR8E/j+3iHbTx3v/qRNucgPcGwhxiuY2RcaDg==",
29850 29965
       "dev": true,
29851 29966
       "requires": {
29852 29967
         "expect-webdriverio": "^5.0.1",
29853
-        "webdriverio": "9.2.14"
29968
+        "webdriverio": "9.4.1"
29854 29969
       }
29855 29970
     },
29856 29971
     "@wdio/jasmine-framework": {
29857
-      "version": "9.2.14",
29858
-      "resolved": "https://registry.npmjs.org/@wdio/jasmine-framework/-/jasmine-framework-9.2.14.tgz",
29859
-      "integrity": "sha512-jU+xF08Kq2EsX3RnbUzFjbSMg0gdVYJl2p5fc41SRZls2o2EC8Pvo1B0qaWLTpXYYHAD1Q/efKWhT0AOdudQwA==",
29972
+      "version": "9.4.1",
29973
+      "resolved": "https://registry.npmjs.org/@wdio/jasmine-framework/-/jasmine-framework-9.4.1.tgz",
29974
+      "integrity": "sha512-N2X8MfsfX8JemD4DQwvSksXmBzYZxCKwuB7LaBiw+hH2YuFu+0L2ekR0Xvwtr8SpiENVEHAoiNFEj4i9dMTqlA==",
29860 29975
       "dev": true,
29861 29976
       "requires": {
29862 29977
         "@types/node": "^20.1.0",
29863
-        "@wdio/globals": "9.2.14",
29978
+        "@wdio/globals": "9.4.1",
29864 29979
         "@wdio/logger": "9.1.3",
29865 29980
         "@wdio/types": "9.2.2",
29866 29981
         "@wdio/utils": "9.2.8",
@@ -29881,15 +29996,15 @@
29881 29996
       }
29882 29997
     },
29883 29998
     "@wdio/local-runner": {
29884
-      "version": "9.2.15",
29885
-      "resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-9.2.15.tgz",
29886
-      "integrity": "sha512-+qh/fkA362r+Wnu58BsC/O8xJ0TKPxdzdyHNsz5qgBdmromQIniWCC4xoKg7rBLv4aA5nkyJjXhC7lXcuWYkLQ==",
29999
+      "version": "9.4.1",
30000
+      "resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-9.4.1.tgz",
30001
+      "integrity": "sha512-MM5VM0V7zvajICr6eNROjkppRhGNpdV4nU5hrgSap92nou8G+zBgLxJ45P5BzLw67KQTOEa1E32b/zCBEkO+0g==",
29887 30002
       "dev": true,
29888 30003
       "requires": {
29889 30004
         "@types/node": "^20.1.0",
29890 30005
         "@wdio/logger": "9.1.3",
29891 30006
         "@wdio/repl": "9.0.8",
29892
-        "@wdio/runner": "9.2.15",
30007
+        "@wdio/runner": "9.4.1",
29893 30008
         "@wdio/types": "9.2.2",
29894 30009
         "async-exit-hook": "^2.0.1",
29895 30010
         "split2": "^4.1.0",
@@ -29960,21 +30075,21 @@
29960 30075
       }
29961 30076
     },
29962 30077
     "@wdio/runner": {
29963
-      "version": "9.2.15",
29964
-      "resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-9.2.15.tgz",
29965
-      "integrity": "sha512-kVQ+YqVijkD2rJPgbJ7yEzzjOUoJ/HKqvBa3Y/kTJPLcSKQCn47GxqzXlOO6wPWLGWkf22Xn7CiTxFN1t+Zz0w==",
30078
+      "version": "9.4.1",
30079
+      "resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-9.4.1.tgz",
30080
+      "integrity": "sha512-WUpYamLafv+GUofGqvA2HO1kSVnwLyPO5ujq3TS/YaJLnUDOj4bjHiRo5+KDbKw1xhqjW0GjbfDupVU5LeL9iw==",
29966 30081
       "dev": true,
29967 30082
       "requires": {
29968 30083
         "@types/node": "^20.11.28",
29969 30084
         "@wdio/config": "9.2.8",
29970
-        "@wdio/globals": "9.2.14",
30085
+        "@wdio/globals": "9.4.1",
29971 30086
         "@wdio/logger": "9.1.3",
29972 30087
         "@wdio/types": "9.2.2",
29973 30088
         "@wdio/utils": "9.2.8",
29974 30089
         "deepmerge-ts": "^7.0.3",
29975 30090
         "expect-webdriverio": "^5.0.1",
29976
-        "webdriver": "9.2.8",
29977
-        "webdriverio": "9.2.14"
30091
+        "webdriver": "9.4.1",
30092
+        "webdriverio": "9.4.1"
29978 30093
       }
29979 30094
     },
29980 30095
     "@wdio/types": {
@@ -31140,6 +31255,12 @@
31140 31255
       "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
31141 31256
       "dev": true
31142 31257
     },
31258
+    "buffer-equal-constant-time": {
31259
+      "version": "1.0.1",
31260
+      "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
31261
+      "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
31262
+      "dev": true
31263
+    },
31143 31264
     "buffer-from": {
31144 31265
       "version": "1.1.2",
31145 31266
       "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -32221,6 +32342,15 @@
32221 32342
       "resolved": "https://registry.npmjs.org/ebml-block/-/ebml-block-1.1.2.tgz",
32222 32343
       "integrity": "sha512-HgNlIsRFP6D9VKU5atCeHRJY7XkJP8bOe8yEhd8NB7B3b4++VWTyauz6g650iiPmLfPLGlVpoJmGSgMfXDYusg=="
32223 32344
     },
32345
+    "ecdsa-sig-formatter": {
32346
+      "version": "1.0.11",
32347
+      "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
32348
+      "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
32349
+      "dev": true,
32350
+      "requires": {
32351
+        "safe-buffer": "^5.0.1"
32352
+      }
32353
+    },
32224 32354
     "edge-paths": {
32225 32355
       "version": "3.0.5",
32226 32356
       "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-3.0.5.tgz",
@@ -35503,6 +35633,24 @@
35503 35633
         "graceful-fs": "^4.1.6"
35504 35634
       }
35505 35635
     },
35636
+    "jsonwebtoken": {
35637
+      "version": "9.0.2",
35638
+      "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
35639
+      "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
35640
+      "dev": true,
35641
+      "requires": {
35642
+        "jws": "^3.2.2",
35643
+        "lodash.includes": "^4.3.0",
35644
+        "lodash.isboolean": "^3.0.3",
35645
+        "lodash.isinteger": "^4.0.4",
35646
+        "lodash.isnumber": "^3.0.3",
35647
+        "lodash.isplainobject": "^4.0.6",
35648
+        "lodash.isstring": "^4.0.1",
35649
+        "lodash.once": "^4.0.0",
35650
+        "ms": "^2.1.1",
35651
+        "semver": "^7.5.4"
35652
+      }
35653
+    },
35506 35654
     "jsx-ast-utils": {
35507 35655
       "version": "3.2.1",
35508 35656
       "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz",
@@ -35568,6 +35716,27 @@
35568 35716
         "xmlbuilder": "^15.1.1"
35569 35717
       }
35570 35718
     },
35719
+    "jwa": {
35720
+      "version": "1.4.1",
35721
+      "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
35722
+      "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
35723
+      "dev": true,
35724
+      "requires": {
35725
+        "buffer-equal-constant-time": "1.0.1",
35726
+        "ecdsa-sig-formatter": "1.0.11",
35727
+        "safe-buffer": "^5.0.1"
35728
+      }
35729
+    },
35730
+    "jws": {
35731
+      "version": "3.2.2",
35732
+      "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
35733
+      "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
35734
+      "dev": true,
35735
+      "requires": {
35736
+        "jwa": "^1.4.1",
35737
+        "safe-buffer": "^5.0.1"
35738
+      }
35739
+    },
35571 35740
     "jwt-decode": {
35572 35741
       "version": "2.2.0",
35573 35742
       "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz",
@@ -35986,12 +36155,42 @@
35986 36155
       "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==",
35987 36156
       "dev": true
35988 36157
     },
36158
+    "lodash.includes": {
36159
+      "version": "4.3.0",
36160
+      "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
36161
+      "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
36162
+      "dev": true
36163
+    },
36164
+    "lodash.isboolean": {
36165
+      "version": "3.0.3",
36166
+      "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
36167
+      "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
36168
+      "dev": true
36169
+    },
35989 36170
     "lodash.isequal": {
35990 36171
       "version": "4.5.0",
35991 36172
       "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
35992 36173
       "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
35993 36174
       "dev": true
35994 36175
     },
36176
+    "lodash.isinteger": {
36177
+      "version": "4.0.4",
36178
+      "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
36179
+      "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
36180
+      "dev": true
36181
+    },
36182
+    "lodash.isnumber": {
36183
+      "version": "3.0.3",
36184
+      "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
36185
+      "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
36186
+      "dev": true
36187
+    },
36188
+    "lodash.isplainobject": {
36189
+      "version": "4.0.6",
36190
+      "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
36191
+      "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
36192
+      "dev": true
36193
+    },
35995 36194
     "lodash.isstring": {
35996 36195
       "version": "4.0.1",
35997 36196
       "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
@@ -36003,6 +36202,12 @@
36003 36202
       "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
36004 36203
       "dev": true
36005 36204
     },
36205
+    "lodash.once": {
36206
+      "version": "4.1.1",
36207
+      "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
36208
+      "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
36209
+      "dev": true
36210
+    },
36006 36211
     "lodash.pickby": {
36007 36212
       "version": "4.6.0",
36008 36213
       "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz",
@@ -40733,9 +40938,9 @@
40733 40938
       "dev": true
40734 40939
     },
40735 40940
     "webdriver": {
40736
-      "version": "9.2.8",
40737
-      "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.2.8.tgz",
40738
-      "integrity": "sha512-40NtUC1zME9tPHNfZv6ETSE3+aE75qZuKjbVAA0gj02AkO1Nl3yJmf5RLdaLLfIQ2WlrbRP1g8KXlkiiVCmakg==",
40941
+      "version": "9.4.1",
40942
+      "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.4.1.tgz",
40943
+      "integrity": "sha512-vFDdxMj/9W1+6jhpHSiRYfO8dix23HjAUtLx7aOv9ejEsntC0EzCIAftJ59YsF3Ppu184+FkdDVhnivpkZPTFw==",
40739 40944
       "dev": true,
40740 40945
       "requires": {
40741 40946
         "@types/node": "^20.1.0",
@@ -40746,6 +40951,7 @@
40746 40951
         "@wdio/types": "9.2.2",
40747 40952
         "@wdio/utils": "9.2.8",
40748 40953
         "deepmerge-ts": "^7.0.3",
40954
+        "undici": "^6.20.1",
40749 40955
         "ws": "^8.8.0"
40750 40956
       },
40751 40957
       "dependencies": {
@@ -40758,9 +40964,9 @@
40758 40964
       }
40759 40965
     },
40760 40966
     "webdriverio": {
40761
-      "version": "9.2.14",
40762
-      "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.2.14.tgz",
40763
-      "integrity": "sha512-85yEbwN3MwdrGzKZoGkLUf1J5cpfnc7knL4u/Y6XWd0gGwYjv60I5ZPsgSnXzNXAkq2kmtkammf1AM3ihqFM3A==",
40967
+      "version": "9.4.1",
40968
+      "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.4.1.tgz",
40969
+      "integrity": "sha512-XIPtRnxSES4CoxH2BfUY/6QzNgEgJEUjMYu7t7SJR8bVfbLRVXAA1ie9kM0MtdLs4oZ2Pr8rR8fqytsA1CjEWw==",
40764 40970
       "dev": true,
40765 40971
       "requires": {
40766 40972
         "@types/node": "^20.11.30",
@@ -40789,7 +40995,7 @@
40789 40995
         "rgb2hex": "0.2.5",
40790 40996
         "serialize-error": "^11.0.3",
40791 40997
         "urlpattern-polyfill": "^10.0.0",
40792
-        "webdriver": "9.2.8"
40998
+        "webdriver": "9.4.1"
40793 40999
       },
40794 41000
       "dependencies": {
40795 41001
         "brace-expansion": {

+ 11
- 6
package.json Näytä tiedosto

@@ -141,6 +141,7 @@
141 141
     "@types/dom-screen-wake-lock": "1.0.1",
142 142
     "@types/jasmine": "5.1.4",
143 143
     "@types/js-md5": "0.4.3",
144
+    "@types/jsonwebtoken": "9.0.7",
144 145
     "@types/lodash-es": "4.17.12",
145 146
     "@types/moment-duration-format": "2.2.6",
146 147
     "@types/offscreencanvas": "2019.7.2",
@@ -161,11 +162,11 @@
161 162
     "@typescript-eslint/eslint-plugin": "5.59.5",
162 163
     "@typescript-eslint/parser": "5.59.5",
163 164
     "@wdio/allure-reporter": "9.2.14",
164
-    "@wdio/cli": "9.2.14",
165
-    "@wdio/globals": "9.2.14",
166
-    "@wdio/jasmine-framework": "9.2.14",
165
+    "@wdio/cli": "9.4.1",
166
+    "@wdio/globals": "9.4.1",
167
+    "@wdio/jasmine-framework": "9.4.1",
167 168
     "@wdio/junit-reporter": "9.2.14",
168
-    "@wdio/local-runner": "9.2.15",
169
+    "@wdio/local-runner": "9.4.1",
169 170
     "babel-loader": "9.1.0",
170 171
     "babel-plugin-optional-require": "0.3.1",
171 172
     "circular-dependency-plugin": "5.2.0",
@@ -178,6 +179,7 @@
178 179
     "eslint-plugin-react-native": "4.0.0",
179 180
     "eslint-plugin-typescript-sort-keys": "2.3.0",
180 181
     "jetifier": "1.6.4",
182
+    "jsonwebtoken": "9.0.2",
181 183
     "metro-react-native-babel-preset": "0.77.0",
182 184
     "patch-package": "6.4.7",
183 185
     "process": "0.11.10",
@@ -187,7 +189,7 @@
187 189
     "ts-loader": "9.4.2",
188 190
     "typescript": "5.0.4",
189 191
     "unorm": "1.6.0",
190
-    "webdriverio": "9.2.14",
192
+    "webdriverio": "9.4.1",
191 193
     "webpack": "5.95.0",
192 194
     "webpack-bundle-analyzer": "4.4.2",
193 195
     "webpack-cli": "5.1.4",
@@ -215,7 +217,10 @@
215 217
     "tsc-test:web": "tsc --project tsconfig.web.json --listFilesOnly | grep -v node_modules | grep native",
216 218
     "tsc-test:native": "tsc --project tsconfig.native.json --listFilesOnly | grep -v node_modules | grep web",
217 219
     "start": "make dev",
218
-    "test": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.conf.ts"
220
+    "test": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.conf.ts",
221
+    "test-ff": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.firefox.conf.ts",
222
+    "test-dev": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.dev.conf.ts",
223
+    "test-grid": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.conf.ts"
219 224
   },
220 225
   "resolutions": {
221 226
     "@types/react": "17.0.14",

+ 16
- 0
tests/env.example Näytä tiedosto

@@ -13,3 +13,19 @@
13 13
 
14 14
 # The path to the browser video capture file
15 15
 #VIDEO_CAPTURE_FILE=tests/resources/FourPeople_1280x720_30.y4m
16
+
17
+# The path to the helper iframe page that will be used for the iframeAPI tests
18
+#IFRAME_PAGE_BASE=
19
+
20
+# The grid host url (https://mygrid.com/wd/hub)
21
+#GRID_HOST_URL=
22
+
23
+# The path to the private key used for generating JWT token (.pk)
24
+#JWT_PRIVATE_KEY_PATH=
25
+# The kid to use in the token
26
+#JWT_KID=
27
+
28
+# The address of the webhooks proxy used to test the webhooks feature (e.g. wss://your.service/?tenant=sometenant)
29
+#WEBHOOKS_PROXY_URL=
30
+# A shared secret to authenticate the webhook proxy connection
31
+#WEBHOOKS_PROXY_SHARED_SECRET=

+ 5
- 0
tests/globals.d.ts Näytä tiedosto

@@ -0,0 +1,5 @@
1
+import { IContext } from './helpers/types';
2
+
3
+declare global {
4
+    const context: IContext;
5
+}

+ 117
- 20
tests/helpers/Participant.ts Näytä tiedosto

@@ -5,10 +5,13 @@ import { multiremotebrowser } from '@wdio/globals';
5 5
 import { IConfig } from '../../react/features/base/config/configType';
6 6
 import { urlObjectToString } from '../../react/features/base/util/uri';
7 7
 import Filmstrip from '../pageobjects/Filmstrip';
8
+import IframeAPI from '../pageobjects/IframeAPI';
9
+import ParticipantsPane from '../pageobjects/ParticipantsPane';
8 10
 import Toolbar from '../pageobjects/Toolbar';
11
+import VideoQualityDialog from '../pageobjects/VideoQualityDialog';
9 12
 
10 13
 import { LOG_PREFIX, logInfo } from './browserLogger';
11
-import { IContext } from './participants';
14
+import { IContext } from './types';
12 15
 
13 16
 /**
14 17
  * Participant.
@@ -19,9 +22,9 @@ export class Participant {
19 22
      *
20 23
      * @private
21 24
      */
22
-    private context: { roomName: string; };
23 25
     private _name: string;
24 26
     private _endpointId: string;
27
+    private _jwt?: string;
25 28
 
26 29
     /**
27 30
      * The default config to use when joining.
@@ -59,9 +62,11 @@ export class Participant {
59 62
      * Creates a participant with given name.
60 63
      *
61 64
      * @param {string} name - The name of the participant.
65
+     * @param {string }jwt - The jwt if any.
62 66
      */
63
-    constructor(name: string) {
67
+    constructor(name: string, jwt?: string) {
64 68
         this._name = name;
69
+        this._jwt = jwt;
65 70
     }
66 71
 
67 72
     /**
@@ -69,7 +74,7 @@ export class Participant {
69 74
      *
70 75
      * @returns {Promise<string>} The endpoint ID.
71 76
      */
72
-    async getEndpointId() {
77
+    async getEndpointId(): Promise<string> {
73 78
         if (!this._endpointId) {
74 79
             this._endpointId = await this.driver.execute(() => { // eslint-disable-line arrow-body-style
75 80
                 return APP.conference.getMyUserId();
@@ -99,7 +104,7 @@ export class Participant {
99 104
      * @param {string} message - The message to log.
100 105
      * @returns {void}
101 106
      */
102
-    log(message: string) {
107
+    log(message: string): void {
103 108
         logInfo(this.driver, message);
104 109
     }
105 110
 
@@ -110,10 +115,8 @@ export class Participant {
110 115
      * @param {boolean} skipInMeetingChecks - Whether to skip in meeting checks.
111 116
      * @returns {Promise<void>}
112 117
      */
113
-    async joinConference(context: IContext, skipInMeetingChecks = false) {
114
-        this.context = context;
115
-
116
-        const url = urlObjectToString({
118
+    async joinConference(context: IContext, skipInMeetingChecks = false): Promise<void> {
119
+        const config = {
117 120
             room: context.roomName,
118 121
             configOverwrite: this.config,
119 122
             interfaceConfigOverwrite: {
@@ -122,14 +125,47 @@ export class Participant {
122 125
             userInfo: {
123 126
                 displayName: this._name
124 127
             }
125
-        }) || '';
128
+        };
129
+
130
+        if (context.iframeAPI) {
131
+            config.room = 'iframeAPITest.html';
132
+        }
133
+
134
+        let url = urlObjectToString(config) || '';
135
+
136
+        if (context.iframeAPI) {
137
+            const baseUrl = new URL(this.driver.options.baseUrl || '');
138
+
139
+            // @ts-ignore
140
+            url = `${this.driver.iframePageBase}${url}&domain="${baseUrl.host}"&room="${context.roomName}"`;
141
+
142
+            if (baseUrl.pathname.length > 1) {
143
+                // remove leading slash
144
+                url = `${url}&tenant="${baseUrl.pathname.substring(1)}"`;
145
+            }
146
+        }
147
+        if (this._jwt) {
148
+            url = `${url}&jwt="${this._jwt}"`;
149
+        }
126 150
 
127 151
         await this.driver.setTimeout({ 'pageLoad': 30000 });
128 152
 
129
-        await this.driver.url(url);
153
+        // workaround for https://github.com/webdriverio/webdriverio/issues/13956
154
+        if (url.startsWith('file://')) {
155
+            // eslint-disable-next-line @typescript-eslint/no-empty-function
156
+            await this.driver.url(url).catch(() => {});
157
+        } else {
158
+            await this.driver.url(url.substring(1)); // drop the leading '/' so we can use the tenant if any
159
+        }
130 160
 
131 161
         await this.waitForPageToLoad();
132 162
 
163
+        if (context.iframeAPI) {
164
+            const mainFrame = this.driver.$('iframe');
165
+
166
+            await this.driver.switchFrame(mainFrame);
167
+        }
168
+
133 169
         await this.waitToJoinMUC();
134 170
 
135 171
         await this.postLoadProcess(skipInMeetingChecks);
@@ -142,7 +178,7 @@ export class Participant {
142 178
      * @returns {Promise<void>}
143 179
      * @private
144 180
      */
145
-    private async postLoadProcess(skipInMeetingChecks: boolean) {
181
+    private async postLoadProcess(skipInMeetingChecks: boolean): Promise<void> {
146 182
         const driver = this.driver;
147 183
 
148 184
         const parallel = [];
@@ -189,7 +225,7 @@ export class Participant {
189 225
      *
190 226
      * @returns {Promise<void>}
191 227
      */
192
-    async waitForPageToLoad() {
228
+    async waitForPageToLoad(): Promise<void> {
193 229
         return this.driver.waitUntil(
194 230
             () => this.driver.execute(() => document.readyState === 'complete'),
195 231
             {
@@ -199,14 +235,21 @@ export class Participant {
199 235
         );
200 236
     }
201 237
 
238
+    /**
239
+     * Checks if the participant is in the meeting.
240
+     */
241
+    isInMuc() {
242
+        return this.driver.execute(() => APP.conference.isJoined());
243
+    }
244
+
202 245
     /**
203 246
      * Waits to join the muc.
204 247
      *
205 248
      * @returns {Promise<void>}
206 249
      */
207
-    async waitToJoinMUC() {
250
+    async waitToJoinMUC(): Promise<void> {
208 251
         return this.driver.waitUntil(
209
-            () => this.driver.execute(() => APP.conference.isJoined()),
252
+            () => this.isInMuc(),
210 253
             {
211 254
                 timeout: 10_000, // 10 seconds
212 255
                 timeoutMsg: 'Timeout waiting to join muc.'
@@ -219,7 +262,7 @@ export class Participant {
219 262
      *
220 263
      * @returns {Promise<void>}
221 264
      */
222
-    async waitForIceConnected() {
265
+    async waitForIceConnected(): Promise<void> {
223 266
         const driver = this.driver;
224 267
 
225 268
         return driver.waitUntil(async () =>
@@ -234,7 +277,7 @@ export class Participant {
234 277
      *
235 278
      * @returns {Promise<void>}
236 279
      */
237
-    async waitForSendReceiveData() {
280
+    async waitForSendReceiveData(): Promise<void> {
238 281
         const driver = this.driver;
239 282
 
240 283
         return driver.waitUntil(async () =>
@@ -259,7 +302,7 @@ export class Participant {
259 302
      * @param {number} number - The number of remote streams o wait for.
260 303
      * @returns {Promise<void>}
261 304
      */
262
-    waitForRemoteStreams(number: number) {
305
+    waitForRemoteStreams(number: number): Promise<void> {
263 306
         const driver = this.driver;
264 307
 
265 308
         return driver.waitUntil(async () =>
@@ -274,7 +317,7 @@ export class Participant {
274 317
      *
275 318
      * @returns {Toolbar}
276 319
      */
277
-    getToolbar() {
320
+    getToolbar(): Toolbar {
278 321
         return new Toolbar(this);
279 322
     }
280 323
 
@@ -283,7 +326,61 @@ export class Participant {
283 326
      *
284 327
      * @returns {Filmstrip}
285 328
      */
286
-    getFilmstrip() {
329
+    getFilmstrip(): Filmstrip {
287 330
         return new Filmstrip(this);
288 331
     }
332
+
333
+    /**
334
+     * Returns the participants pane.
335
+     *
336
+     * @returns {ParticipantsPane}
337
+     */
338
+    getParticipantsPane(): ParticipantsPane {
339
+        return new ParticipantsPane(this);
340
+    }
341
+
342
+    /**
343
+     * Returns the videoQuality Dialog.
344
+     *
345
+     * @returns {VideoQualityDialog}
346
+     */
347
+    getVideoQualityDialog(): VideoQualityDialog {
348
+        return new VideoQualityDialog(this);
349
+    }
350
+
351
+    /**
352
+     * Switches to the iframe API context
353
+     */
354
+    async switchToAPI() {
355
+        await this.driver.switchFrame(null);
356
+    }
357
+
358
+    /**
359
+     * Switches to the meeting page context.
360
+     */
361
+    async switchInPage() {
362
+        const mainFrame = this.driver.$('iframe');
363
+
364
+        await this.driver.switchFrame(mainFrame);
365
+    }
366
+
367
+    /**
368
+     * Returns the iframe API for this participant.
369
+     */
370
+    getIframeAPI() {
371
+        return new IframeAPI(this);
372
+    }
373
+
374
+    /**
375
+     * Returns the local display name.
376
+     */
377
+    async getLocalDisplayName() {
378
+        const localVideoContainer = this.driver.$('span[id="localVideoContainer"]');
379
+
380
+        await localVideoContainer.moveTo();
381
+
382
+        const localDisplayName = localVideoContainer.$('span[id="localDisplayName"]');
383
+
384
+        return await localDisplayName.getText();
385
+    }
289 386
 }

+ 129
- 0
tests/helpers/WebhookProxy.ts Näytä tiedosto

@@ -0,0 +1,129 @@
1
+import WebSocket from 'ws';
2
+
3
+/**
4
+ * Uses the webhook proxy service to proxy events to the testing clients.
5
+ */
6
+export default class WebhookProxy {
7
+    private url;
8
+    private secret;
9
+    private ws: WebSocket | undefined;
10
+    private cache = new Map();
11
+    private listeners = new Map();
12
+    private consumers = new Map();
13
+
14
+    /**
15
+     * Initializes the webhook proxy.
16
+     * @param url
17
+     * @param secret
18
+     */
19
+    constructor(url: string, secret: string) {
20
+        this.url = url;
21
+        this.secret = secret;
22
+    }
23
+
24
+    /**
25
+     * Connects.
26
+     */
27
+    connect() {
28
+        this.ws = new WebSocket(this.url, {
29
+            headers: {
30
+                Authorization: this.secret
31
+            }
32
+        });
33
+
34
+        this.ws.on('error', console.error);
35
+
36
+        this.ws.on('open', function open() {
37
+            console.log('WebhookProxy connected');
38
+        });
39
+
40
+        this.ws.on('message', (data: any) => {
41
+            const msg = JSON.parse(data.toString());
42
+
43
+            if (msg.eventType) {
44
+                if (this.consumers.has(msg.eventType)) {
45
+                    this.consumers.get(msg.eventType)(msg);
46
+                    this.consumers.delete(msg.eventType);
47
+                } else {
48
+                    this.cache.set(msg.eventType, msg);
49
+                }
50
+
51
+                if (this.listeners.has(msg.eventType)) {
52
+                    this.listeners.get(msg.eventType)(msg);
53
+                }
54
+            }
55
+        });
56
+    }
57
+
58
+    /**
59
+     * Adds event consumer. Consumers receive the event single time and we remove them from the list of consumers.
60
+     * @param eventType
61
+     * @param callback
62
+     */
63
+    addConsumer(eventType: string, callback: (deventata: any) => void) {
64
+        if (this.cache.has(eventType)) {
65
+            callback(this.cache.get(eventType));
66
+            this.cache.delete(eventType);
67
+
68
+            return;
69
+        }
70
+
71
+        this.consumers.set(eventType, callback);
72
+    }
73
+
74
+    /**
75
+     * Clear any stored event.
76
+     */
77
+    clearCache() {
78
+        this.cache.clear();
79
+    }
80
+
81
+    /**
82
+     * Waits for the event to be received.
83
+     * @param eventType
84
+     * @param timeout
85
+     */
86
+    async waitForEvent(eventType: string, timeout = 4000): Promise<any> {
87
+        // we create the error here so we have a meaningful stack trace
88
+        const error = new Error(`Timeout waiting for event:${eventType}`);
89
+
90
+        return new Promise((resolve, reject) => {
91
+            const waiter = setTimeout(() => reject(error), timeout);
92
+
93
+            this.addConsumer(eventType, event => {
94
+                clearTimeout(waiter);
95
+
96
+                resolve(event);
97
+            });
98
+
99
+        });
100
+    }
101
+
102
+    /**
103
+     * Adds a listener for the event type.
104
+     * @param eventType
105
+     * @param callback
106
+     */
107
+    addListener(eventType: string, callback: (data: any) => void) {
108
+        this.listeners.set(eventType, callback);
109
+    }
110
+
111
+    /**
112
+     * Adds a listener for the event type.
113
+     * @param eventType
114
+     */
115
+    removeListener(eventType: string) {
116
+        this.listeners.delete(eventType);
117
+    }
118
+
119
+    /**
120
+     * Disconnects the webhook proxy.
121
+     */
122
+    disconnect() {
123
+        if (this.ws) {
124
+            this.ws.close();
125
+            console.log('WebhookProxy disconnected');
126
+            this.ws = undefined;
127
+        }
128
+    }
129
+}

+ 157
- 11
tests/helpers/participants.ts Näytä tiedosto

@@ -1,20 +1,30 @@
1
-import { Participant } from './Participant';
1
+import fs from 'fs';
2
+import jwt from 'jsonwebtoken';
3
+import process from 'node:process';
4
+import { v4 as uuidv4 } from 'uuid';
2 5
 
3
-export type IContext = {
4
-    p1: Participant;
5
-    p2: Participant;
6
-    p3: Participant;
7
-    p4: Participant;
8
-    roomName: string;
9
-};
6
+import { Participant } from './Participant';
7
+import WebhookProxy from './WebhookProxy';
8
+import { IContext } from './types';
10 9
 
11 10
 /**
12 11
  * Generate a random room name.
12
+ * Everytime we generate a name and iframeAPI is enabled and there is a configured
13
+ * webhooks proxy we connect to it with the new room name.
13 14
  *
14 15
  * @returns {string} - The random room name.
15 16
  */
16 17
 function generateRandomRoomName(): string {
17
-    return `jitsimeettorture-${crypto.randomUUID()}}`;
18
+    const roomName = `jitsimeettorture-${crypto.randomUUID()}`;
19
+
20
+    if (context.iframeAPI && !context.webhooksProxy
21
+        && process.env.WEBHOOKS_PROXY_URL && process.env.WEBHOOKS_PROXY_SHARED_SECRET) {
22
+        context.webhooksProxy = new WebhookProxy(`${process.env.WEBHOOKS_PROXY_URL}&room=${roomName}`,
23
+            process.env.WEBHOOKS_PROXY_SHARED_SECRET);
24
+        context.webhooksProxy.connect();
25
+    }
26
+
27
+    return roomName;
18 28
 }
19 29
 
20 30
 /**
@@ -24,7 +34,9 @@ function generateRandomRoomName(): string {
24 34
  * @returns {Promise<void>}
25 35
  */
26 36
 export async function ensureOneParticipant(context: IContext): Promise<void> {
27
-    context.roomName = generateRandomRoomName();
37
+    if (!context.roomName) {
38
+        context.roomName = generateRandomRoomName();
39
+    }
28 40
 
29 41
     context.p1 = new Participant('participant1');
30 42
 
@@ -38,7 +50,9 @@ export async function ensureOneParticipant(context: IContext): Promise<void> {
38 50
  * @returns {Promise<void>}
39 51
  */
40 52
 export async function ensureThreeParticipants(context: IContext): Promise<void> {
41
-    context.roomName = generateRandomRoomName();
53
+    if (!context.roomName) {
54
+        context.roomName = generateRandomRoomName();
55
+    }
42 56
 
43 57
     const p1 = new Participant('participant1');
44 58
     const p2 = new Participant('participant2');
@@ -62,6 +76,77 @@ export async function ensureThreeParticipants(context: IContext): Promise<void>
62 76
     ]);
63 77
 }
64 78
 
79
+/**
80
+ * Ensure that there are two participants.
81
+ *
82
+ * @param {Object} context - The context.
83
+ * @returns {Promise<void>}
84
+ */
85
+export async function ensureTwoParticipants(context: IContext): Promise<void> {
86
+    if (!context.roomName) {
87
+        context.roomName = generateRandomRoomName();
88
+    }
89
+
90
+    const p1DisplayName = 'participant1';
91
+    let token;
92
+
93
+    // if it is jaas create the first one to be moderator and second not moderator
94
+    if (context.jwtPrivateKeyPath) {
95
+        token = getModeratorToken(p1DisplayName);
96
+    }
97
+
98
+    // make sure the first participant is moderator, if supported by deployment
99
+    await _joinParticipant(p1DisplayName, context.p1, p => {
100
+        context.p1 = p;
101
+    }, true, token);
102
+
103
+    await Promise.all([
104
+        _joinParticipant('participant2', context.p2, p => {
105
+            context.p2 = p;
106
+        }),
107
+        context.p1.waitForRemoteStreams(1),
108
+        context.p2.waitForRemoteStreams(1)
109
+    ]);
110
+}
111
+
112
+/**
113
+ * Creates a participant instance or prepares one for re-joining.
114
+ * @param name - The name of the participant.
115
+ * @param p - The participant instance to prepare or undefined if new one is needed.
116
+ * @param setter - The setter to use for setting the new participant instance into the context if needed.
117
+ * @param {boolean} skipInMeetingChecks - Whether to skip in meeting checks.
118
+ * @param {string?} jwtToken - The token to use if any.
119
+ */
120
+async function _joinParticipant( // eslint-disable-line max-params
121
+        name: string,
122
+        p: Participant,
123
+        setter: (p: Participant) => void,
124
+        skipInMeetingChecks = false,
125
+        jwtToken?: string) {
126
+    if (p) {
127
+        await p.switchInPage();
128
+
129
+        if (await p.isInMuc()) {
130
+            return;
131
+        }
132
+
133
+        // when loading url make sure we are on the top page context or strange errors may occur
134
+        await p.switchToAPI();
135
+
136
+        // Change the page so we can reload same url if we need to, base.html is supposed to be empty or close to empty
137
+        await p.driver.url('/base.html');
138
+
139
+        // we want the participant instance re-recreated so we clear any kept state, like endpoint ID
140
+    }
141
+
142
+    const newParticipant = new Participant(name, jwtToken);
143
+
144
+    // set the new participant instance, pass it to setter
145
+    setter(newParticipant);
146
+
147
+    return newParticipant.joinConference(context, skipInMeetingChecks);
148
+}
149
+
65 150
 /**
66 151
  * Toggles the mute state of a specific Meet conference participant and verifies that a specific other Meet
67 152
  * conference participants sees a specific mute state for the former.
@@ -78,3 +163,64 @@ export async function toggleMuteAndCheck(testee: Participant, observer: Particip
78 163
     await observer.getFilmstrip().assertAudioMuteIconIsDisplayed(testee);
79 164
     await testee.getFilmstrip().assertAudioMuteIconIsDisplayed(testee);
80 165
 }
166
+
167
+/**
168
+ * Get a JWT token for a moderator.
169
+ */
170
+function getModeratorToken(displayName: string) {
171
+    const keyid = process.env.JWT_KID;
172
+    const headers = {
173
+        algorithm: 'RS256',
174
+        noTimestamp: true,
175
+        expiresIn: '24h',
176
+        keyid
177
+    };
178
+
179
+    if (!keyid) {
180
+        console.error('JWT_KID is not set');
181
+
182
+        return;
183
+    }
184
+
185
+    const key = fs.readFileSync(context.jwtPrivateKeyPath);
186
+
187
+    const payload = {
188
+        'aud': 'jitsi',
189
+        'iss': 'chat',
190
+        'sub': keyid.substring(0, keyid.indexOf('/')),
191
+        'context': {
192
+            'user': {
193
+                'name': displayName,
194
+                'id': uuidv4(),
195
+                'avatar': 'https://avatars0.githubusercontent.com/u/3671647',
196
+                'email': 'john.doe@jitsi.org'
197
+            }
198
+        },
199
+        'room': '*'
200
+    };
201
+
202
+    // @ts-ignore
203
+    payload.context.user.moderator = true;
204
+
205
+    // @ts-ignore
206
+    return jwt.sign(payload, key, headers);
207
+}
208
+
209
+/**
210
+ * Parse a JID string.
211
+ * @param str the string to parse.
212
+ */
213
+export function parseJid(str: string): {
214
+    domain: string;
215
+    node: string;
216
+    resource: string | undefined;
217
+} {
218
+    const parts = str.split('@');
219
+    const domainParts = parts[1].split('/');
220
+
221
+    return {
222
+        node: parts[0],
223
+        domain: domainParts[0],
224
+        resource: domainParts.length > 0 ? domainParts[1] : undefined
225
+    };
226
+}

+ 15
- 0
tests/helpers/types.ts Näytä tiedosto

@@ -0,0 +1,15 @@
1
+import type { Participant } from './Participant';
2
+import WebhookProxy from './WebhookProxy';
3
+
4
+export type IContext = {
5
+    conferenceJid: string;
6
+    iframeAPI: boolean;
7
+    jwtKid: string;
8
+    jwtPrivateKeyPath: string;
9
+    p1: Participant;
10
+    p2: Participant;
11
+    p3: Participant;
12
+    p4: Participant;
13
+    roomName: string;
14
+    webhooksProxy: WebhookProxy;
15
+};

+ 26
- 0
tests/pageobjects/BaseDialog.ts Näytä tiedosto

@@ -0,0 +1,26 @@
1
+import { Participant } from '../helpers/Participant';
2
+
3
+const CLOSE_BUTTON = 'modal-header-close-button';
4
+
5
+/**
6
+ * Base class for all dialogs.
7
+ */
8
+export default class BaseDialog {
9
+    participant: Participant;
10
+
11
+    /**
12
+     * Initializes for a participant.
13
+     *
14
+     * @param {Participant} participant - The participant.
15
+     */
16
+    constructor(participant: Participant) {
17
+        this.participant = participant;
18
+    }
19
+
20
+    /**
21
+     *  Clicks on the X (close) button.
22
+     */
23
+    async clickCloseButton(): Promise<void> {
24
+        await this.participant.driver.$(`#${CLOSE_BUTTON}`).click();
25
+    }
26
+}

+ 46
- 3
tests/pageobjects/Filmstrip.ts Näytä tiedosto

@@ -20,12 +20,12 @@ export default class Filmstrip {
20 20
      * mute icon for the conference participant identified by
21 21
      * {@code testee}.
22 22
      *
23
-     * @param {Participant} testee - The {@code WebParticipant} for whom we're checking the status of audio muted icon.
23
+     * @param {Participant} testee - The {@code Participant} for whom we're checking the status of audio muted icon.
24 24
      * @param {boolean} reverse - If {@code true}, the method will assert the absence of the "mute" icon;
25 25
      * otherwise, it will assert its presence.
26 26
      * @returns {Promise<void>}
27 27
      */
28
-    async assertAudioMuteIconIsDisplayed(testee: Participant, reverse = false) {
28
+    async assertAudioMuteIconIsDisplayed(testee: Participant, reverse = false): Promise<void> {
29 29
         let id;
30 30
 
31 31
         if (testee === this.participant) {
@@ -40,7 +40,50 @@ export default class Filmstrip {
40 40
         await this.participant.driver.$(mutedIconXPath).waitForDisplayed({
41 41
             reverse,
42 42
             timeout: 2000,
43
-            timeoutMsg: `Audio mute icon is not displayed for ${testee.name}`
43
+            timeoutMsg: `Audio mute icon is ${reverse ? '' : 'not'} displayed for ${testee.name}`
44 44
         });
45 45
     }
46
+
47
+    /**
48
+     * Asserts that {@code participant} shows or doesn't show the video mute icon for the conference participant
49
+     * identified by {@code testee}.
50
+     *
51
+     * @param {Participant} testee - The {@code Participant} for whom we're checking the status of audio muted icon.
52
+     * @param {boolean} reverse - If {@code true}, the method will assert the absence of the "mute" icon;
53
+     * otherwise, it will assert its presence.
54
+     * @returns {Promise<void>}
55
+     */
56
+    async assertVideoMuteIconIsDisplayed(testee: Participant, reverse = false): Promise<void> {
57
+        const isOpen = await this.participant.getParticipantsPane().isOpen();
58
+
59
+        if (!isOpen) {
60
+            await this.participant.getParticipantsPane().open();
61
+        }
62
+
63
+        const id = `participant-item-${await testee.getEndpointId()}`;
64
+        const mutedIconXPath
65
+            = `//div[@id='${id}']//div[contains(@class, 'indicators')]//*[local-name()='svg' and @id='videoMuted']`;
66
+
67
+        await this.participant.driver.$(mutedIconXPath).waitForDisplayed({
68
+            reverse,
69
+            timeout: 2000,
70
+            timeoutMsg: `Video mute icon is ${reverse ? '' : 'not'} displayed for ${testee.name}`
71
+        });
72
+
73
+        if (!isOpen) {
74
+            await this.participant.getParticipantsPane().close();
75
+        }
76
+    }
77
+
78
+    /**
79
+     * Returns the remote display name for an endpoint.
80
+     * @param endpointId The endpoint id.
81
+     */
82
+    async getRemoteDisplayName(endpointId: string) {
83
+        const remoteDisplayName = this.participant.driver.$(`span[id="participant_${endpointId}_name"]`);
84
+
85
+        await remoteDisplayName.moveTo();
86
+
87
+        return await remoteDisplayName.getText();
88
+    }
46 89
 }

+ 90
- 0
tests/pageobjects/IframeAPI.ts Näytä tiedosto

@@ -0,0 +1,90 @@
1
+import { Participant } from '../helpers/Participant';
2
+import { LOG_PREFIX } from '../helpers/browserLogger';
3
+
4
+/**
5
+ * The Iframe API and helpers from iframeAPITest.html
6
+ */
7
+export default class IframeAPI {
8
+    private participant: Participant;
9
+
10
+    /**
11
+     * Initializes for a participant.
12
+     * @param participant
13
+     */
14
+    constructor(participant: Participant) {
15
+        this.participant = participant;
16
+    }
17
+
18
+    /**
19
+     * Returns the json object from the iframeAPI helper.
20
+     * @param event
21
+     */
22
+    async getEventResult(event: string): Promise<any> {
23
+        return this.participant.driver.execute(
24
+            eventName => {
25
+                const result = window.jitsiAPI.test[eventName];
26
+
27
+                if (!result) {
28
+                    return false;
29
+                }
30
+
31
+                return result;
32
+            }, event);
33
+    }
34
+
35
+    /**
36
+     * Adds an event listener to the iframeAPI.
37
+     * @param eventName The event name.
38
+     */
39
+    async addEventListener(eventName: string) {
40
+        return this.participant.driver.executeAsync((event, prefix, done) => {
41
+            console.log(`${new Date().toISOString()} ${prefix} Adding listener for event: ${event}`);
42
+            window.jitsiAPI.addListener(event, evt => {
43
+                console.log(`${new Date().toISOString()} ${prefix} Received ${event} event: ${JSON.stringify(evt)}`);
44
+                window.jitsiAPI.test[event] = evt;
45
+            });
46
+            done();
47
+        }, eventName, LOG_PREFIX);
48
+    }
49
+
50
+    /**
51
+     * Returns an array of available rooms and details of it.
52
+     */
53
+    async getRoomsInfo() {
54
+        return this.participant.driver.execute(() => window.jitsiAPI.getRoomsInfo());
55
+    }
56
+
57
+    /**
58
+     * Returns the number of participants in the conference.
59
+     */
60
+    async getNumberOfParticipants() {
61
+        return this.participant.driver.execute(() => window.jitsiAPI.getNumberOfParticipants());
62
+    }
63
+
64
+    /**
65
+     * Executes command using iframeAPI.
66
+     * @param command The command.
67
+     * @param args The arguments.
68
+     */
69
+    async executeCommand(command: string, ...args: any[]) {
70
+        return this.participant.driver.execute(
71
+            (commandName, commandArgs) =>
72
+                window.jitsiAPI.executeCommand(commandName, ...commandArgs)
73
+            , command, args);
74
+    }
75
+
76
+    /**
77
+     * Returns the current state of the participant's pane.
78
+     */
79
+    async isParticipantsPaneOpen() {
80
+        return this.participant.driver.execute(() => window.jitsiAPI.isParticipantsPaneOpen());
81
+    }
82
+
83
+    /**
84
+     * Removes the embedded Jitsi Meet conference.
85
+     */
86
+    async dispose() {
87
+        return this.participant.driver.execute(() => window.jitsiAPI.dispose());
88
+    }
89
+
90
+}

+ 47
- 0
tests/pageobjects/ParticipantsPane.ts Näytä tiedosto

@@ -0,0 +1,47 @@
1
+import { Participant } from '../helpers/Participant';
2
+
3
+/**
4
+ * Classname of the closed/hidden participants pane
5
+ */
6
+const PARTICIPANTS_PANE = 'participants_pane';
7
+
8
+/**
9
+ * Represents the participants pane from the UI.
10
+ */
11
+export default class ParticipantsPane {
12
+    private participant: Participant;
13
+
14
+    /**
15
+     * Initializes for a participant.
16
+     *
17
+     * @param {Participant} participant - The participant.
18
+     */
19
+    constructor(participant: Participant) {
20
+        this.participant = participant;
21
+    }
22
+
23
+    /**
24
+     * Checks if the pane is open.
25
+     */
26
+    async isOpen() {
27
+        return this.participant.driver.$(`.${PARTICIPANTS_PANE}`).isExisting();
28
+    }
29
+
30
+    /**
31
+     * Clicks the "participants" toolbar button to open the participants pane.
32
+     */
33
+    async open() {
34
+        await this.participant.getToolbar().clickParticipantsPaneButton();
35
+
36
+        await this.participant.driver.$(`.${PARTICIPANTS_PANE}`).waitForDisplayed();
37
+    }
38
+
39
+    /**
40
+     * Clicks the "participants" toolbar button to close the participants pane.
41
+     */
42
+    async close() {
43
+        await this.participant.getToolbar().clickCloseParticipantsPaneButton();
44
+
45
+        await this.participant.driver.$(`.${PARTICIPANTS_PANE}`).waitForDisplayed({ reverse: true });
46
+    }
47
+}

+ 144
- 4
tests/pageobjects/Toolbar.ts Näytä tiedosto

@@ -3,6 +3,13 @@ import { Participant } from '../helpers/Participant';
3 3
 
4 4
 const AUDIO_MUTE = 'Mute microphone';
5 5
 const AUDIO_UNMUTE = 'Unmute microphone';
6
+const CLOSE_PARTICIPANTS_PANE = 'Close participants pane';
7
+const OVERFLOW_MENU = 'More actions menu';
8
+const OVERFLOW = 'More actions';
9
+const PARTICIPANTS = 'Open participants pane';
10
+const VIDEO_QUALITY = 'Manage video quality';
11
+const VIDEO_MUTE = 'Stop camera';
12
+const VIDEO_UNMUTE = 'Start camera';
6 13
 
7 14
 /**
8 15
  * The toolbar elements.
@@ -49,8 +56,8 @@ export default class Toolbar {
49 56
      *
50 57
      * @returns {Promise<void>}
51 58
      */
52
-    async clickAudioMuteButton() {
53
-        await this.participant.log('Clicking on: Audio Mute Button');
59
+    async clickAudioMuteButton(): Promise<void> {
60
+        this.participant.log('Clicking on: Audio Mute Button');
54 61
         await this.audioMuteBtn.click();
55 62
     }
56 63
 
@@ -59,8 +66,141 @@ export default class Toolbar {
59 66
      *
60 67
      * @returns {Promise<void>}
61 68
      */
62
-    async clickAudioUnmuteButton() {
63
-        await this.participant.log('Clicking on: Audio Unmute Button');
69
+    async clickAudioUnmuteButton(): Promise<void> {
70
+        this.participant.log('Clicking on: Audio Unmute Button');
64 71
         await this.audioUnMuteBtn.click();
65 72
     }
73
+
74
+    /**
75
+     * The video mute button.
76
+     */
77
+    get videoMuteBtn() {
78
+        return this.getButton(VIDEO_MUTE);
79
+    }
80
+
81
+    /**
82
+     * The video unmute button.
83
+     */
84
+    get videoUnMuteBtn() {
85
+        return this.getButton(VIDEO_UNMUTE);
86
+    }
87
+
88
+    /**
89
+     * Clicks video mute button.
90
+     *
91
+     * @returns {Promise<void>}
92
+     */
93
+    async clickVideoMuteButton(): Promise<void> {
94
+        this.participant.log('Clicking on: Video Mute Button');
95
+        await this.videoMuteBtn.click();
96
+    }
97
+
98
+    /**
99
+     * Clicks video unmute button.
100
+     *
101
+     * @returns {Promise<void>}
102
+     */
103
+    async clickVideoUnmuteButton(): Promise<void> {
104
+        this.participant.log('Clicking on: Video Unmute Button');
105
+        await this.videoUnMuteBtn.click();
106
+    }
107
+
108
+    /**
109
+     * Clicks Participants pane button.
110
+     *
111
+     * @returns {Promise<void>}
112
+     */
113
+    async clickCloseParticipantsPaneButton(): Promise<void> {
114
+        this.participant.log('Clicking on: Close Participants pane Button');
115
+        await this.getButton(CLOSE_PARTICIPANTS_PANE).click();
116
+    }
117
+
118
+
119
+    /**
120
+     * Clicks Participants pane button.
121
+     *
122
+     * @returns {Promise<void>}
123
+     */
124
+    async clickParticipantsPaneButton(): Promise<void> {
125
+        this.participant.log('Clicking on: Participants pane Button');
126
+        await this.getButton(PARTICIPANTS).click();
127
+    }
128
+
129
+    /**
130
+     * Clicks on the video quality toolbar button which opens the
131
+     * dialog for adjusting max-received video quality.
132
+     */
133
+    async clickVideoQualityButton(): Promise<void> {
134
+        return this.clickButtonInOverflowMenu(VIDEO_QUALITY);
135
+    }
136
+
137
+    /**
138
+     * Ensure the overflow menu is open and clicks on a specified button.
139
+     * @param accessibilityLabel The accessibility label of the button to be clicked.
140
+     * @private
141
+     */
142
+    private async clickButtonInOverflowMenu(accessibilityLabel: string) {
143
+        await this.openOverflowMenu();
144
+
145
+        await this.getButton(accessibilityLabel).click();
146
+
147
+        await this.closeOverflowMenu();
148
+    }
149
+
150
+    /**
151
+     * Checks if the overflow menu is open and visible.
152
+     * @private
153
+     */
154
+    private async isOverflowMenuOpen() {
155
+        return await this.participant.driver.$$(`[aria-label^="${OVERFLOW_MENU}"]`).length > 0;
156
+    }
157
+
158
+    /**
159
+     * Clicks on the overflow toolbar button which opens or closes the overflow menu.
160
+     * @private
161
+     */
162
+    private async clickOverflowButton(): Promise<void> {
163
+        await this.getButton(OVERFLOW).click();
164
+    }
165
+
166
+    /**
167
+     * Ensure the overflow menu is displayed.
168
+     * @private
169
+     */
170
+    private async openOverflowMenu() {
171
+        if (await this.isOverflowMenuOpen()) {
172
+            return;
173
+        }
174
+
175
+        await this.clickOverflowButton();
176
+
177
+        await this.waitForOverFlowMenu(true);
178
+    }
179
+
180
+    /**
181
+     * Ensures the overflow menu is not displayed.
182
+     * @private
183
+     */
184
+    private async closeOverflowMenu() {
185
+        if (!await this.isOverflowMenuOpen()) {
186
+            return;
187
+        }
188
+
189
+        await this.clickOverflowButton();
190
+
191
+        await this.waitForOverFlowMenu(false);
192
+    }
193
+
194
+    /**
195
+     * Waits for the overflow menu to be visible or hidden.
196
+     * @param visible
197
+     * @private
198
+     */
199
+    private async waitForOverFlowMenu(visible: boolean) {
200
+        await this.participant.driver.$(`[aria-label^="${OVERFLOW_MENU}"]`).waitForDisplayed({
201
+            reverse: !visible,
202
+            timeout: 3000,
203
+            timeoutMsg: `Overflow menu is not ${visible ? 'visible' : 'hidden'}`
204
+        });
205
+    }
66 206
 }

+ 42
- 0
tests/pageobjects/VideoQualityDialog.ts Näytä tiedosto

@@ -0,0 +1,42 @@
1
+import { Key } from 'webdriverio';
2
+
3
+import BaseDialog from './BaseDialog';
4
+
5
+const VIDEO_QUALITY_SLIDER_CLASS = 'custom-slider';
6
+
7
+/**
8
+ * The video quality dialog.
9
+ */
10
+export default class VideoQualityDialog extends BaseDialog {
11
+    /**
12
+     * Opens the video quality dialog and sets the video quality to the minimum or maximum definition.
13
+     * @param audioOnly - Whether to set the video quality to audio only (minimum).
14
+     * @private
15
+     */
16
+    async setVideoQuality(audioOnly: boolean) {
17
+        await this.participant.getToolbar().clickVideoQualityButton();
18
+
19
+        const videoQualitySlider = this.participant.driver.$(`.${VIDEO_QUALITY_SLIDER_CLASS}`);
20
+
21
+        const audioOnlySliderValue = parseInt(await videoQualitySlider.getAttribute('min'), 10);
22
+
23
+        const maxDefinitionSliderValue = parseInt(await videoQualitySlider.getAttribute('max'), 10);
24
+        const activeValue = parseInt(await videoQualitySlider.getAttribute('value'), 10);
25
+
26
+        const targetValue = audioOnly ? audioOnlySliderValue : maxDefinitionSliderValue;
27
+        const distanceToTargetValue = targetValue - activeValue;
28
+        const keyDirection = distanceToTargetValue > 0 ? Key.ArrowRight : Key.ArrowLeft;
29
+
30
+        // we need to click the element to activate it so it will receive the keys
31
+        await videoQualitySlider.click();
32
+
33
+        // Move the slider to the target value.
34
+        for (let i = 0; i < Math.abs(distanceToTargetValue); i++) {
35
+
36
+            await this.participant.driver.keys(keyDirection);
37
+        }
38
+
39
+        // Close the video quality dialog.
40
+        await this.clickCloseButton();
41
+    }
42
+}

+ 121
- 0
tests/resources/iframeAPITest.html Näytä tiedosto

@@ -0,0 +1,121 @@
1
+<html lang="en">
2
+<head>
3
+    <meta charset="utf-8">
4
+    <meta http-equiv="content-type" content="text/html;charset=utf-8">
5
+    <title>iframe API test</title>
6
+</head>
7
+<body>
8
+<script>
9
+    /**
10
+     * Ported from https://github.com/jitsi/jitsi-meet-torture/blob/master/src/test/resources/files/iframeAPITest.html
11
+     */
12
+    const blacklist = [ '__proto__', 'constructor', 'prototype' ];
13
+    const paramStr = document.location.hash;
14
+    const params = {};
15
+    const paramParts = paramStr?.substring(1).split('&') || [];
16
+
17
+    paramParts.forEach(part => {
18
+        const param = part.split('=');
19
+        const key = param[0];
20
+
21
+        if (!key || key.split('.').some(k => blacklist.includes(k))) {
22
+            return;
23
+        }
24
+
25
+        let value;
26
+
27
+        try {
28
+            value = param[1];
29
+
30
+
31
+            const decoded = decodeURIComponent(value).replace(/\\&/, '&')
32
+                .replace(/[\u2018\u2019]/g, '\'')
33
+                .replace(/[\u201C\u201D]/g, '"');
34
+
35
+            value = decoded === 'undefined' || decoded === '' ? undefined : JSON.parse(decoded);
36
+
37
+        } catch (e) {
38
+            console.error(`Failed to parse URL parameter value: ${String(value)}`, e);
39
+
40
+            return;
41
+        }
42
+        params[key] = value;
43
+    });
44
+    const json = {
45
+        config: {},
46
+        interfaceConfig: {}
47
+    };
48
+
49
+    for (const param of Object.keys(params)) {
50
+        let base = json;
51
+        const names = param.split('.');
52
+        const last = names.pop() ?? '';
53
+
54
+        for (const name of names) {
55
+            base = base[name] = base[name] || {};
56
+        }
57
+
58
+        base[last] = params[param];
59
+    }
60
+
61
+    const { config, domain, interfaceConfig, jwt, password, room:roomName, userInfo: uInfoObj } = json;
62
+    let tenant = json.tenant || '';
63
+
64
+    let userInfo;
65
+    if (uInfoObj) {
66
+        if (uInfoObj.length > 0) {
67
+            userInfo = JSON.parse(uInfoObj);
68
+        } else if (Object.keys(uInfoObj).length) {
69
+            userInfo = uInfoObj;
70
+        }
71
+    }
72
+
73
+    if (tenant.length > 0) {
74
+        tenant = tenant + '/';
75
+    }
76
+
77
+    const options = {
78
+        jwt,
79
+        roomName: `${tenant}${roomName}`,
80
+        configOverwrite: config,
81
+        interfaceConfigOverwrite: interfaceConfig,
82
+        userInfo,
83
+        onload: function () {
84
+            // we use this to save data from api to be accessible to tests
85
+            window.jitsiAPI.test = {};
86
+
87
+            window.jitsiAPI.addEventListener('participantRoleChanged', function(event) {
88
+                if (event.role === "moderator" && event.id === window.jitsiAPI.test.myEndpointId) {
89
+                    window.jitsiAPI.test.isModerator = true;
90
+                }
91
+            });
92
+            window.jitsiAPI.addEventListener('audioAvailabilityChanged', function(event) {
93
+                window.jitsiAPI.test.audioAvailabilityChanged = event;
94
+            });
95
+            window.jitsiAPI.addEventListener('videoAvailabilityChanged', function(event) {
96
+                window.jitsiAPI.test.videoAvailabilityChanged = event;
97
+            });
98
+            window.jitsiAPI.addEventListener('videoConferenceJoined', function(event) {
99
+                window.jitsiAPI.test.videoConferenceJoined = event;
100
+                window.jitsiAPI.test.myEndpointId = event.id;
101
+            });
102
+            if (password && password.length > 0) {
103
+                // join a protected channel with the password supplied
104
+                window.jitsiAPI.on('passwordRequired', function ()
105
+                {
106
+                    window.jitsiAPI.executeCommand('password', password);
107
+                });
108
+            }
109
+        }
110
+    };
111
+
112
+    const externalAPIScript = document.createElement('script');
113
+    externalAPIScript.src = `https://${domain}/${tenant}external_api.js`;
114
+    externalAPIScript.type = "text/javascript";
115
+    externalAPIScript.onload = function(){
116
+        window.jitsiAPI = new JitsiMeetExternalAPI(domain, options);
117
+    }
118
+    document.getElementsByTagName('head')[0].appendChild(externalAPIScript);
119
+</script>
120
+</body>
121
+</html>

+ 86
- 0
tests/specs/2way/audioOnly.spec.ts Näytä tiedosto

@@ -0,0 +1,86 @@
1
+import { ensureTwoParticipants } from '../../helpers/participants';
2
+
3
+describe('Audio only - ', () => {
4
+    it('joining the meeting', async () => {
5
+        await ensureTwoParticipants(context);
6
+    });
7
+
8
+    /**
9
+     * Enables audio only mode for p1 and verifies that the other participant sees participant1 as video muted.
10
+     */
11
+    it('set and check', async () => {
12
+        await setAudioOnlyAndCheck(true);
13
+    });
14
+
15
+    /**
16
+     * Verifies that participant1 sees avatars for itself and other participants.
17
+     */
18
+    it('avatars check', async () => {
19
+        await context.p1.driver.$('//div[@id="dominantSpeaker"]').waitForDisplayed();
20
+
21
+        // Makes sure that the avatar is displayed in the local thumbnail and that the video is not displayed.
22
+        await context.p1.driver.$('//span[@id="localVideoContainer"]//div[contains(@class,"userAvatar")]')
23
+            .waitForDisplayed();
24
+        await context.p1.driver.$('//span[@id="localVideoWrapper"]//video').waitForDisplayed({ reverse: true });
25
+    });
26
+
27
+    /**
28
+     * Disables audio only mode and verifies that both participants see p1 as not video muted.
29
+     */
30
+    it('disable and check', async () => {
31
+        await setAudioOnlyAndCheck(false);
32
+    });
33
+
34
+    /**
35
+     * Toggles the audio only state of a p1 participant and verifies participant sees the audio only label and that
36
+     * p2 participant sees a video mute state for the former.
37
+     * @param enable
38
+     */
39
+    async function setAudioOnlyAndCheck(enable: boolean) {
40
+        await context.p1.getVideoQualityDialog().setVideoQuality(enable);
41
+
42
+        await verifyVideoMute(enable);
43
+
44
+        await context.p1.driver.$('//div[@id="videoResolutionLabel"][contains(@class, "audio-only")]')
45
+            .waitForDisplayed({ reverse: !enable });
46
+    }
47
+
48
+    /**
49
+     * Verifies that p1 and p2 see p1 as video muted or not.
50
+     * @param muted
51
+     */
52
+    async function verifyVideoMute(muted: boolean) {
53
+        // Verify the observer sees the testee in the desired muted state.
54
+        await context.p2.getFilmstrip().assertVideoMuteIconIsDisplayed(context.p1, !muted);
55
+
56
+        // Verify the testee sees itself in the desired muted state.
57
+        await context.p1.getFilmstrip().assertVideoMuteIconIsDisplayed(context.p1, !muted);
58
+    }
59
+
60
+    /**
61
+     * Mutes video on participant1, toggles audio-only twice and then verifies if both participants see participant1
62
+     * as video muted.
63
+     */
64
+    it('mute video, set twice and check muted', async () => {
65
+        // Mute video on participant1.
66
+        await context.p1.getToolbar().clickVideoMuteButton();
67
+
68
+        await verifyVideoMute(true);
69
+
70
+        // Enable audio-only mode.
71
+        await setAudioOnlyAndCheck(true);
72
+
73
+        // Disable audio-only mode.
74
+        await context.p1.getVideoQualityDialog().setVideoQuality(false);
75
+
76
+        // p1 should stay muted since it was muted before audio-only was enabled.
77
+        await verifyVideoMute(true);
78
+    });
79
+
80
+    it('unmute video and check not muted', async () => {
81
+        // Unmute video on participant1.
82
+        await context.p1.getToolbar().clickVideoUnmuteButton();
83
+
84
+        await verifyVideoMute(false);
85
+    });
86
+});

+ 424
- 0
tests/specs/2way/iFrameParticipantsPresence.spec.ts Näytä tiedosto

@@ -0,0 +1,424 @@
1
+import { isEqual } from 'lodash-es';
2
+
3
+import type { Participant } from '../../helpers/Participant';
4
+import { ensureTwoParticipants, parseJid } from '../../helpers/participants';
5
+
6
+/**
7
+ * Tests PARTICIPANT_LEFT webhook.
8
+ */
9
+async function checkParticipantLeftHook(p: Participant, reason: string) {
10
+    const { webhooksProxy } = context;
11
+
12
+    if (webhooksProxy) {
13
+        // PARTICIPANT_LEFT webhook
14
+        // @ts-ignore
15
+        const event: {
16
+            data: {
17
+                conference: string;
18
+                disconnectReason: string;
19
+                isBreakout: boolean;
20
+                participantId: string;
21
+            };
22
+            eventType: string;
23
+        } = await context.webhooksProxy.waitForEvent('PARTICIPANT_LEFT');
24
+
25
+        expect('PARTICIPANT_LEFT').toBe(event.eventType);
26
+        expect(event.data.conference).toBe(context.conferenceJid);
27
+        expect(event.data.disconnectReason).toBe(reason);
28
+        expect(event.data.isBreakout).toBe(false);
29
+        expect(event.data.participantId).toBe(await p.getEndpointId());
30
+    }
31
+}
32
+
33
+describe('Participants presence - ', () => {
34
+    it('joining the meeting', async () => {
35
+        context.iframeAPI = true;
36
+
37
+        // ensure 2 participants one moderator and one guest, we will load both with iframeAPI
38
+        await ensureTwoParticipants(context);
39
+
40
+        const { p1, p2, webhooksProxy } = context;
41
+
42
+        // let's populate endpoint ids
43
+        await Promise.all([
44
+            p1.getEndpointId(),
45
+            p2.getEndpointId()
46
+        ]);
47
+
48
+        await p1.switchToAPI();
49
+        await p2.switchToAPI();
50
+
51
+        expect(await p1.getIframeAPI().getEventResult('isModerator'))
52
+            .withContext('Is p1 moderator')
53
+            .toBeTrue();
54
+        expect(await p2.getIframeAPI().getEventResult('isModerator'))
55
+            .withContext('Is p2 non-moderator')
56
+            .toBeFalse();
57
+
58
+        expect(await p1.getIframeAPI().getEventResult('videoConferenceJoined')).toBeDefined();
59
+        expect(await p2.getIframeAPI().getEventResult('videoConferenceJoined')).toBeDefined();
60
+
61
+        if (webhooksProxy) {
62
+            // USAGE webhook
63
+            // @ts-ignore
64
+            const event: {
65
+                data: [
66
+                    { participantId: string; }
67
+                ];
68
+                eventType: string;
69
+            } = await context.webhooksProxy.waitForEvent('USAGE');
70
+
71
+            expect('USAGE').toBe(event.eventType);
72
+
73
+            const p1EpId = await p1.getEndpointId();
74
+            const p2EpId = await p2.getEndpointId();
75
+
76
+            expect(event.data.filter(d => d.participantId === p1EpId
77
+                || d.participantId === p2EpId).length).toBe(2);
78
+        }
79
+
80
+        // we will use it later
81
+        // TODO figure out why adding those just before grantModerator and we miss the events
82
+        await p1.getIframeAPI().addEventListener('participantRoleChanged');
83
+        await p2.getIframeAPI().addEventListener('participantRoleChanged');
84
+    });
85
+
86
+    it('participants info',
87
+        async () => {
88
+            const { p1, roomName, webhooksProxy } = context;
89
+            const roomsInfo = (await p1.getIframeAPI().getRoomsInfo()).rooms[0];
90
+
91
+            expect(roomsInfo).toBeDefined();
92
+            expect(roomsInfo.isMainRoom).toBeTrue();
93
+
94
+            expect(roomsInfo.id).toBeDefined();
95
+            const { node: roomNode } = parseJid(roomsInfo.id);
96
+
97
+            expect(roomNode).toBe(roomName);
98
+
99
+            const { node, resource } = parseJid(roomsInfo.jid);
100
+
101
+            context.conferenceJid = roomsInfo.jid.substring(0, roomsInfo.jid.indexOf('/'));
102
+
103
+            const p1EpId = await p1.getEndpointId();
104
+
105
+            expect(node).toBe(roomName);
106
+            expect(resource).toBe(p1EpId);
107
+
108
+            expect(roomsInfo.participants.length).toBe(2);
109
+            expect(await p1.getIframeAPI().getNumberOfParticipants()).toBe(2);
110
+
111
+            if (webhooksProxy) {
112
+                // ROOM_CREATED webhook
113
+                // @ts-ignore
114
+                const event: {
115
+                    data: {
116
+                        conference: string;
117
+                        isBreakout: boolean;
118
+                    };
119
+                    eventType: string;
120
+                } = await context.webhooksProxy.waitForEvent('ROOM_CREATED');
121
+
122
+                expect('ROOM_CREATED').toBe(event.eventType);
123
+                expect(event.data.conference).toBe(context.conferenceJid);
124
+                expect(event.data.isBreakout).toBe(false);
125
+            }
126
+        }
127
+    );
128
+
129
+    it('participants pane', async () => {
130
+        const { p1 } = context;
131
+
132
+        await p1.switchToAPI();
133
+
134
+        expect(await p1.getIframeAPI().isParticipantsPaneOpen()).toBe(false);
135
+
136
+        await p1.getIframeAPI().addEventListener('participantsPaneToggled');
137
+        await p1.getIframeAPI().executeCommand('toggleParticipantsPane', true);
138
+
139
+        expect(await p1.getIframeAPI().isParticipantsPaneOpen()).toBe(true);
140
+        expect((await p1.getIframeAPI().getEventResult('participantsPaneToggled'))?.open).toBe(true);
141
+
142
+        await p1.getIframeAPI().executeCommand('toggleParticipantsPane', false);
143
+        expect(await p1.getIframeAPI().isParticipantsPaneOpen()).toBe(false);
144
+        expect((await p1.getIframeAPI().getEventResult('participantsPaneToggled'))?.open).toBe(false);
145
+    });
146
+
147
+    it('grant moderator', async () => {
148
+        const { p1, p2, webhooksProxy } = context;
149
+        const p2EpId = await p2.getEndpointId();
150
+
151
+        await p1.getIframeAPI().executeCommand('grantModerator', p2EpId);
152
+
153
+        await p2.driver.waitUntil(async () => await p2.getIframeAPI().getEventResult('isModerator'), {
154
+            timeout: 3000,
155
+            timeoutMsg: 'Moderator role not granted'
156
+        });
157
+
158
+        const event1 = await p1.getIframeAPI().getEventResult('participantRoleChanged');
159
+
160
+        expect(event1?.id).toBe(p2EpId);
161
+        expect(event1?.role).toBe('moderator');
162
+
163
+        const event2 = await p2.getIframeAPI().getEventResult('participantRoleChanged');
164
+
165
+        expect(event2?.id).toBe(p2EpId);
166
+        expect(event2?.role).toBe('moderator');
167
+
168
+        if (webhooksProxy) {
169
+            // ROLE_CHANGED webhook
170
+            // @ts-ignore
171
+            const event: {
172
+                data: {
173
+                    grantedBy: {
174
+                        participantId: string;
175
+                    };
176
+                    grantedTo: {
177
+                        participantId: string;
178
+                    };
179
+                    role: string;
180
+                };
181
+                eventType: string;
182
+            } = await context.webhooksProxy.waitForEvent('ROLE_CHANGED');
183
+
184
+            expect('ROLE_CHANGED').toBe(event.eventType);
185
+            expect(event.data.role).toBe('moderator');
186
+            expect(event.data.grantedBy.participantId).toBe(await p1.getEndpointId());
187
+            expect(event.data.grantedTo.participantId).toBe(await p2.getEndpointId());
188
+        }
189
+    });
190
+
191
+    it('kick participant', async () => {
192
+        const { p1, p2 } = context;
193
+        const p1EpId = await p1.getEndpointId();
194
+        const p2EpId = await p2.getEndpointId();
195
+
196
+        await p1.switchInPage();
197
+        await p2.switchInPage();
198
+
199
+        const p1DisplayName = await p1.getLocalDisplayName();
200
+        const p2DisplayName = await p2.getLocalDisplayName();
201
+
202
+        await p1.switchToAPI();
203
+        await p2.switchToAPI();
204
+
205
+        await p1.getIframeAPI().addEventListener('participantKickedOut');
206
+        await p2.getIframeAPI().addEventListener('participantKickedOut');
207
+        await p2.getIframeAPI().addEventListener('videoConferenceLeft');
208
+
209
+        await p1.getIframeAPI().executeCommand('kickParticipant', p2EpId);
210
+
211
+        const eventP1 = await p1.driver.waitUntil(async () =>
212
+            await p1.getIframeAPI().getEventResult('participantKickedOut'), {
213
+            timeout: 2000,
214
+            timeoutMsg: 'participantKickedOut event not received on participant1 side'
215
+        });
216
+        const eventP2 = await p2.driver.waitUntil(async () =>
217
+            await p2.getIframeAPI().getEventResult('participantKickedOut'), {
218
+            timeout: 2000,
219
+            timeoutMsg: 'participantKickedOut event not received on participant2 side'
220
+        });
221
+
222
+        await checkParticipantLeftHook(p2, 'kicked');
223
+
224
+        expect(eventP1).toBeDefined();
225
+        expect(eventP2).toBeDefined();
226
+
227
+        expect(isEqual(eventP1, {
228
+            kicked: {
229
+                id: p2EpId,
230
+                local: false,
231
+                name: p2DisplayName
232
+            },
233
+            kicker: {
234
+                id: p1EpId,
235
+                local: true,
236
+                name: p1DisplayName
237
+            }
238
+        })).toBeTrue();
239
+
240
+        expect(isEqual(eventP2, {
241
+            kicked: {
242
+                id: 'local',
243
+                local: true,
244
+                name: p2DisplayName
245
+            },
246
+            kicker: {
247
+                id: p1EpId,
248
+                name: p1DisplayName
249
+            }
250
+        })).toBeTrue();
251
+
252
+        const eventConferenceLeftP2 = await p2.driver.waitUntil(async () =>
253
+            await p2.getIframeAPI().getEventResult('videoConferenceLeft'), {
254
+            timeout: 2000,
255
+            timeoutMsg: 'videoConferenceLeft not received'
256
+        });
257
+
258
+        expect(eventConferenceLeftP2).toBeDefined();
259
+        expect(eventConferenceLeftP2.roomName).toBe(context.roomName);
260
+    });
261
+
262
+    it('join after kick', async () => {
263
+        const { p1, webhooksProxy } = context;
264
+
265
+        await p1.getIframeAPI().addEventListener('participantJoined');
266
+        await p1.getIframeAPI().addEventListener('participantMenuButtonClick');
267
+
268
+        webhooksProxy?.clearCache();
269
+
270
+        // join again
271
+        await ensureTwoParticipants(context);
272
+
273
+        if (webhooksProxy) {
274
+            // PARTICIPANT_JOINED webhook
275
+            // @ts-ignore
276
+            const event: {
277
+                data: {
278
+                    conference: string;
279
+                    isBreakout: boolean;
280
+                    moderator: boolean;
281
+                    name: string;
282
+                    participantId: string;
283
+                };
284
+                eventType: string;
285
+            } = await context.webhooksProxy.waitForEvent('PARTICIPANT_JOINED');
286
+
287
+            expect('PARTICIPANT_JOINED').toBe(event.eventType);
288
+            expect(event.data.conference).toBe(context.conferenceJid);
289
+            expect(event.data.isBreakout).toBe(false);
290
+            expect(event.data.moderator).toBe(false);
291
+            expect(event.data.name).toBe(await context.p2.getLocalDisplayName());
292
+            expect(event.data.participantId).toBe(await context.p2.getEndpointId());
293
+        }
294
+
295
+        await p1.switchToAPI();
296
+
297
+        const event = await p1.driver.waitUntil(async () =>
298
+            await p1.getIframeAPI().getEventResult('participantJoined'), {
299
+            timeout: 2000,
300
+            timeoutMsg: 'participantJoined not received'
301
+        });
302
+
303
+        const { p2 } = context;
304
+        const p2DisplayName = await p2.getLocalDisplayName();
305
+
306
+        expect(event).toBeDefined();
307
+        expect(event.id).toBe(await p2.getEndpointId());
308
+        expect(event.displayName).toBe(p2DisplayName);
309
+        expect(event.formattedDisplayName).toBe(p2DisplayName);
310
+
311
+    });
312
+
313
+    it('overwrite names', async () => {
314
+        const { p1, p2 } = context;
315
+
316
+        const p1EpId = await p1.getEndpointId();
317
+        const p2EpId = await p2.getEndpointId();
318
+
319
+        const newP1Name = 'p1';
320
+        const newP2Name = 'p2';
321
+        const newNames: ({ id: string; name: string; })[] = [ {
322
+            id: p2EpId,
323
+            name: newP2Name
324
+        }, {
325
+            id: p1EpId,
326
+            name: newP1Name
327
+        } ];
328
+
329
+        await p1.getIframeAPI().executeCommand('overwriteNames', newNames);
330
+
331
+        await p1.switchInPage();
332
+
333
+        expect(await p1.getLocalDisplayName()).toBe(newP1Name);
334
+
335
+        expect(await p1.getFilmstrip().getRemoteDisplayName(p2EpId)).toBe(newP2Name);
336
+
337
+    });
338
+
339
+    it('hangup', async () => {
340
+        const { p1, p2 } = context;
341
+
342
+        await p1.switchToAPI();
343
+        await p2.switchToAPI();
344
+
345
+        await p2.getIframeAPI().addEventListener('videoConferenceLeft');
346
+        await p2.getIframeAPI().addEventListener('readyToClose');
347
+
348
+        await p2.getIframeAPI().executeCommand('hangup');
349
+
350
+        const eventConferenceLeftP2 = await p2.driver.waitUntil(async () =>
351
+            await p2.getIframeAPI().getEventResult('videoConferenceLeft'), {
352
+            timeout: 2000,
353
+            timeoutMsg: 'videoConferenceLeft not received'
354
+        });
355
+
356
+        expect(eventConferenceLeftP2).toBeDefined();
357
+        expect(eventConferenceLeftP2.roomName).toBe(context.roomName);
358
+
359
+        await checkParticipantLeftHook(p2, 'left');
360
+
361
+        const eventReadyToCloseP2 = await p2.driver.waitUntil(async () =>
362
+            await p2.getIframeAPI().getEventResult('readyToClose'), {
363
+            timeout: 2000,
364
+            timeoutMsg: 'readyToClose not received'
365
+        });
366
+
367
+        expect(eventReadyToCloseP2).toBeDefined();
368
+    });
369
+
370
+    it('dispose conference', async () => {
371
+        const { p1, webhooksProxy } = context;
372
+
373
+        await p1.switchToAPI();
374
+
375
+        await p1.getIframeAPI().addEventListener('videoConferenceLeft');
376
+        await p1.getIframeAPI().addEventListener('readyToClose');
377
+
378
+        await p1.getIframeAPI().executeCommand('hangup');
379
+
380
+        const eventConferenceLeft = await p1.driver.waitUntil(async () =>
381
+            await p1.getIframeAPI().getEventResult('videoConferenceLeft'), {
382
+            timeout: 2000,
383
+            timeoutMsg: 'videoConferenceLeft not received'
384
+        });
385
+
386
+        expect(eventConferenceLeft).toBeDefined();
387
+        expect(eventConferenceLeft.roomName).toBe(context.roomName);
388
+
389
+        await checkParticipantLeftHook(p1, 'left');
390
+        if (webhooksProxy) {
391
+            // ROOM_DESTROYED webhook
392
+            // @ts-ignore
393
+            const event: {
394
+                data: {
395
+                    conference: string;
396
+                    isBreakout: boolean;
397
+                };
398
+                eventType: string;
399
+            } = await context.webhooksProxy.waitForEvent('ROOM_DESTROYED');
400
+
401
+            expect('ROOM_DESTROYED').toBe(event.eventType);
402
+            expect(event.data.conference).toBe(context.conferenceJid);
403
+            expect(event.data.isBreakout).toBe(false);
404
+        }
405
+
406
+        const eventReadyToClose = await p1.driver.waitUntil(async () =>
407
+            await p1.getIframeAPI().getEventResult('readyToClose'), {
408
+            timeout: 2000,
409
+            timeoutMsg: 'readyToClose not received'
410
+        });
411
+
412
+        expect(eventReadyToClose).toBeDefined();
413
+
414
+        // dispose
415
+        await p1.getIframeAPI().dispose();
416
+
417
+        // check there is no iframe on the page
418
+        await p1.driver.$('iframe').waitForExist({
419
+            reverse: true,
420
+            timeout: 2000,
421
+            timeoutMsg: 'iframe is still on the page'
422
+        });
423
+    });
424
+});

+ 2
- 4
tests/specs/3way/activeSpeaker.spec.ts Näytä tiedosto

@@ -1,10 +1,8 @@
1 1
 /* global APP */
2 2
 import type { Participant } from '../../helpers/Participant';
3
-import { IContext, ensureThreeParticipants, toggleMuteAndCheck } from '../../helpers/participants';
3
+import { ensureThreeParticipants, toggleMuteAndCheck } from '../../helpers/participants';
4 4
 
5 5
 describe('ActiveSpeaker ', () => {
6
-    const context = {} as IContext;
7
-
8 6
     it('testActiveSpeaker', async () => {
9 7
         await ensureThreeParticipants(context);
10 8
 
@@ -64,7 +62,7 @@ async function testActiveSpeaker(
64 62
     await otherParticipant1Driver.waitUntil(
65 63
         () => otherParticipant1Driver.execute((id: string) => APP.UI.getLargeVideoID() === id, speakerEndpoint),
66 64
         {
67
-            timeout: 30_1000, // 30 seconds
65
+            timeout: 30_000, // 30 seconds
68 66
             timeoutMsg: 'Active speaker not displayed on large video.'
69 67
         });
70 68
 

+ 5
- 1
tests/tsconfig.json Näytä tiedosto

@@ -1,5 +1,9 @@
1 1
 {
2
-    "include": ["**/*.ts", "../globals.d.ts"],
2
+    "include": [
3
+        "**/*.ts",
4
+        "../globals.d.ts",
5
+        "./globals.d.ts"
6
+    ],
3 7
     "extends": "../tsconfig.web",
4 8
     "compilerOptions": {
5 9
         "types": [

+ 38
- 6
tests/wdio.conf.ts Näytä tiedosto

@@ -1,9 +1,11 @@
1 1
 import AllureReporter from '@wdio/allure-reporter';
2 2
 import { multiremotebrowser } from '@wdio/globals';
3 3
 import { Buffer } from 'buffer';
4
+import path from 'node:path';
4 5
 import process from 'node:process';
5 6
 
6 7
 import { getLogs, initLogger, logInfo } from './helpers/browserLogger';
8
+import { IContext } from './helpers/types';
7 9
 
8 10
 // eslint-disable-next-line @typescript-eslint/no-var-requires
9 11
 const allure = require('allure-commandline');
@@ -24,7 +26,7 @@ const chromeArgs = [
24 26
     '--no-sandbox',
25 27
     '--disable-dev-shm-usage',
26 28
     '--disable-setuid-sandbox',
27
-    '--use-file-for-fake-audio-capture=tests/resources/fakeAudioStream.wav'
29
+    `--use-file-for-fake-audio-capture=${process.env.REMOTE_RESOURCE_PATH || 'tests/resources'}/fakeAudioStream.wav`
28 30
 ];
29 31
 
30 32
 if (process.env.RESOLVER_RULES) {
@@ -105,6 +107,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
105 107
                     prefs: chromePreferences
106 108
                 },
107 109
                 'wdio:exclude': [
110
+                    'specs/alone/**',
108 111
                     'specs/2way/**'
109 112
                 ]
110 113
             }
@@ -117,6 +120,8 @@ export const config: WebdriverIO.MultiremoteConfig = {
117 120
                     prefs: chromePreferences
118 121
                 },
119 122
                 'wdio:exclude': [
123
+                    'specs/alone/**',
124
+                    'specs/2way/**',
120 125
                     'specs/3way/**'
121 126
                 ]
122 127
             }
@@ -157,10 +162,37 @@ export const config: WebdriverIO.MultiremoteConfig = {
157 162
      *
158 163
      * @returns {Promise<void>}
159 164
      */
160
-    before() {
161
-        multiremotebrowser.instances.forEach((instance: string) => {
162
-            initLogger(multiremotebrowser.getInstance(instance), instance, TEST_RESULTS_DIR);
163
-        });
165
+    async before() {
166
+        await Promise.all(multiremotebrowser.instances.map(async (instance: string) => {
167
+            const bInstance = multiremotebrowser.getInstance(instance);
168
+
169
+            initLogger(bInstance, instance, TEST_RESULTS_DIR);
170
+
171
+            if (bInstance.isFirefox) {
172
+                return;
173
+            }
174
+
175
+            // if (process.env.GRID_HOST_URL) {
176
+            // TODO: make sure we use uploadFile only with chrome (it does not work with FF),
177
+            // we need to test it with the grid and FF, does it work there
178
+            const rpath = await bInstance.uploadFile('tests/resources/iframeAPITest.html');
179
+
180
+            // @ts-ignore
181
+            bInstance.iframePageBase = `file://${path.dirname(rpath)}`;
182
+        }));
183
+
184
+        const globalAny: any = global;
185
+
186
+        globalAny.context = {} as IContext;
187
+
188
+        globalAny.context.jwtPrivateKeyPath = process.env.JWT_PRIVATE_KEY_PATH;
189
+        globalAny.context.jwtKid = process.env.JWT_KID;
190
+    },
191
+
192
+    after() {
193
+        if (context.webhooksProxy) {
194
+            context.webhooksProxy.disconnect();
195
+        }
164 196
     },
165 197
 
166 198
     /**
@@ -270,4 +302,4 @@ export const config: WebdriverIO.MultiremoteConfig = {
270 302
             });
271 303
         });
272 304
     }
273
-};
305
+} as WebdriverIO.MultiremoteConfig;

+ 11
- 0
tests/wdio.dev.conf.ts Näytä tiedosto

@@ -0,0 +1,11 @@
1
+// wdio.dev.conf.ts
2
+// extends te main configuration file for the development environment (make dev)
3
+// it will connect to the webpack-dev-server running locally on port 8080
4
+import { deepmerge } from 'deepmerge-ts';
5
+
6
+// @ts-ignore
7
+import { config as defaultConfig } from './wdio.conf.ts';
8
+
9
+export const config = deepmerge(defaultConfig, {
10
+    baseUrl: 'https://127.0.0.1:8080/torture'
11
+}, { clone: false });

+ 39
- 0
tests/wdio.firefox.conf.ts Näytä tiedosto

@@ -0,0 +1,39 @@
1
+// wdio.firefox.conf.ts
2
+// extends te main configuration file changing first participant to be Firefox
3
+import { merge } from 'lodash-es';
4
+import process from 'node:process';
5
+
6
+// @ts-ignore
7
+import { config as defaultConfig } from './wdio.conf.ts';
8
+
9
+const ffArgs = [];
10
+
11
+const ffPreferences = {
12
+    'intl.accept_languages': 'en-US',
13
+    'media.navigator.permission.disabled': true,
14
+    'media.navigator.streams.fake': true,
15
+    'media.autoplay.default': 0
16
+};
17
+
18
+if (process.env.HEADLESS === 'true') {
19
+    ffArgs.push('--headless');
20
+}
21
+
22
+export const config = merge(defaultConfig, {
23
+    exclude: [
24
+        'specs/2way/iFrameParticipantsPresence.spec.ts', // FF does not support uploading files (uploadFile)
25
+        'specs/3way/activeSpeaker.spec.ts' // FF does not support setting a file as mic input
26
+    ],
27
+    capabilities: {
28
+        participant1: {
29
+            capabilities: {
30
+                browserName: 'firefox',
31
+                'moz:firefoxOptions': {
32
+                    args: ffArgs,
33
+                    prefs: ffPreferences
34
+                },
35
+                acceptInsecureCerts: process.env.ALLOW_INSECURE_CERTS === 'true'
36
+            }
37
+        }
38
+    }
39
+}, { clone: false });

+ 18
- 0
tests/wdio.grid.conf.ts Näytä tiedosto

@@ -0,0 +1,18 @@
1
+// wdio.grid.conf.ts
2
+// extends the main configuration file to add the selenium grid address
3
+import { deepmerge } from 'deepmerge-ts';
4
+import { URL } from 'url';
5
+
6
+// @ts-ignore
7
+import { config as defaultConfig } from './wdio.conf.ts';
8
+
9
+const gridUrl = new URL(process.env.GRID_HOST_URL as string);
10
+const protocol = gridUrl.protocol.replace(':', '');
11
+
12
+export const config = deepmerge(defaultConfig, {
13
+    protocol,
14
+    hostname: gridUrl.hostname,
15
+    port: gridUrl.port ? parseInt(gridUrl.port, 10) // Convert port to number
16
+        : protocol === 'http' ? 80 : 443,
17
+    path: gridUrl.pathname
18
+}, { clone: false });

+ 4
- 1
webpack.config.js Näytä tiedosto

@@ -253,7 +253,10 @@ function getDevServerConfig() {
253 253
         ],
254 254
         server: process.env.CODESPACES ? 'http' : 'https',
255 255
         static: {
256
-            directory: process.cwd()
256
+            directory: process.cwd(),
257
+            watch: {
258
+                ignored: file => file.endsWith('.log')
259
+            }
257 260
         }
258 261
     };
259 262
 }

Loading…
Peruuta
Tallenna