diff --git a/package-lock.json b/package-lock.json index 642110276e55..f6c57856c381 100644 --- a/package-lock.json +++ b/package-lock.json @@ -135,6 +135,7 @@ "@types/dom-screen-wake-lock": "1.0.1", "@types/jasmine": "5.1.4", "@types/js-md5": "0.4.3", + "@types/jsonwebtoken": "9.0.7", "@types/lodash-es": "4.17.12", "@types/moment-duration-format": "2.2.6", "@types/offscreencanvas": "2019.7.2", @@ -155,11 +156,11 @@ "@typescript-eslint/eslint-plugin": "5.59.5", "@typescript-eslint/parser": "5.59.5", "@wdio/allure-reporter": "9.2.14", - "@wdio/cli": "9.2.14", - "@wdio/globals": "9.2.14", - "@wdio/jasmine-framework": "9.2.14", + "@wdio/cli": "9.4.1", + "@wdio/globals": "9.4.1", + "@wdio/jasmine-framework": "9.4.1", "@wdio/junit-reporter": "9.2.14", - "@wdio/local-runner": "9.2.15", + "@wdio/local-runner": "9.4.1", "babel-loader": "9.1.0", "babel-plugin-optional-require": "0.3.1", "circular-dependency-plugin": "5.2.0", @@ -172,6 +173,7 @@ "eslint-plugin-react-native": "4.0.0", "eslint-plugin-typescript-sort-keys": "2.3.0", "jetifier": "1.6.4", + "jsonwebtoken": "9.0.2", "metro-react-native-babel-preset": "0.77.0", "patch-package": "6.4.7", "process": "0.11.10", @@ -181,7 +183,7 @@ "ts-loader": "9.4.2", "typescript": "5.0.4", "unorm": "1.6.0", - "webdriverio": "9.2.14", + "webdriverio": "9.4.1", "webpack": "5.95.0", "webpack-bundle-analyzer": "4.4.2", "webpack-cli": "5.1.4", @@ -7122,6 +7124,15 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/lodash": { "version": "4.14.182", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz", @@ -7898,15 +7909,15 @@ } }, "node_modules/@wdio/cli": { - "version": "9.2.14", - "resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-9.2.14.tgz", - "integrity": "sha512-nYNAuF5HPW8b+B21t83N6NGEUpszZZygTDMy+xemBQFrkp7qpDsaeaxv60rFf6AR1mapcnK4ghHJUqod0qL7cg==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-9.4.1.tgz", + "integrity": "sha512-GDyAer63WDsr2ckXmrpUyAcIZFd3pCRIpi85rL1ZjnWthRy/UtwY0EHPMDuSeUEJ28iYwW3esKgq2ZKlsdbMeA==", "dev": true, "dependencies": { "@types/node": "^20.1.1", "@vitest/snapshot": "^2.1.1", "@wdio/config": "9.2.8", - "@wdio/globals": "9.2.14", + "@wdio/globals": "9.4.1", "@wdio/logger": "9.1.3", "@wdio/protocols": "9.2.2", "@wdio/types": "9.2.2", @@ -7926,7 +7937,7 @@ "read-pkg-up": "^10.0.0", "recursive-readdir": "^2.2.3", "tsx": "^4.7.2", - "webdriverio": "9.2.14", + "webdriverio": "9.4.1", "yargs": "^17.7.2" }, "bin": { @@ -8190,26 +8201,26 @@ } }, "node_modules/@wdio/globals": { - "version": "9.2.14", - "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-9.2.14.tgz", - "integrity": "sha512-Hgi85bp5vpckK+k5iJ6zz8wUUL6IhCzywIG6uXzSgH/zkUOp5Til/NYyJmzSv6hRsfGaFia9WSaoqol93bfEIA==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-9.4.1.tgz", + "integrity": "sha512-CTVAVJ7iFyT54XF9iRmNvsDB+WSHoztJPG9XPL/mHzQ2LYfSyUR8E/j+3iHbTx3v/qRNucgPcGwhxiuY2RcaDg==", "dev": true, "engines": { "node": ">=18.20.0" }, "optionalDependencies": { "expect-webdriverio": "^5.0.1", - "webdriverio": "9.2.14" + "webdriverio": "9.4.1" } }, "node_modules/@wdio/jasmine-framework": { - "version": "9.2.14", - "resolved": "https://registry.npmjs.org/@wdio/jasmine-framework/-/jasmine-framework-9.2.14.tgz", - "integrity": "sha512-jU+xF08Kq2EsX3RnbUzFjbSMg0gdVYJl2p5fc41SRZls2o2EC8Pvo1B0qaWLTpXYYHAD1Q/efKWhT0AOdudQwA==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@wdio/jasmine-framework/-/jasmine-framework-9.4.1.tgz", + "integrity": "sha512-N2X8MfsfX8JemD4DQwvSksXmBzYZxCKwuB7LaBiw+hH2YuFu+0L2ekR0Xvwtr8SpiENVEHAoiNFEj4i9dMTqlA==", "dev": true, "dependencies": { "@types/node": "^20.1.0", - "@wdio/globals": "9.2.14", + "@wdio/globals": "9.4.1", "@wdio/logger": "9.1.3", "@wdio/types": "9.2.2", "@wdio/utils": "9.2.8", @@ -8236,15 +8247,15 @@ } }, "node_modules/@wdio/local-runner": { - "version": "9.2.15", - "resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-9.2.15.tgz", - "integrity": "sha512-+qh/fkA362r+Wnu58BsC/O8xJ0TKPxdzdyHNsz5qgBdmromQIniWCC4xoKg7rBLv4aA5nkyJjXhC7lXcuWYkLQ==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-9.4.1.tgz", + "integrity": "sha512-MM5VM0V7zvajICr6eNROjkppRhGNpdV4nU5hrgSap92nou8G+zBgLxJ45P5BzLw67KQTOEa1E32b/zCBEkO+0g==", "dev": true, "dependencies": { "@types/node": "^20.1.0", "@wdio/logger": "9.1.3", "@wdio/repl": "9.0.8", - "@wdio/runner": "9.2.15", + "@wdio/runner": "9.4.1", "@wdio/types": "9.2.2", "async-exit-hook": "^2.0.1", "split2": "^4.1.0", @@ -8343,21 +8354,21 @@ } }, "node_modules/@wdio/runner": { - "version": "9.2.15", - "resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-9.2.15.tgz", - "integrity": "sha512-kVQ+YqVijkD2rJPgbJ7yEzzjOUoJ/HKqvBa3Y/kTJPLcSKQCn47GxqzXlOO6wPWLGWkf22Xn7CiTxFN1t+Zz0w==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-9.4.1.tgz", + "integrity": "sha512-WUpYamLafv+GUofGqvA2HO1kSVnwLyPO5ujq3TS/YaJLnUDOj4bjHiRo5+KDbKw1xhqjW0GjbfDupVU5LeL9iw==", "dev": true, "dependencies": { "@types/node": "^20.11.28", "@wdio/config": "9.2.8", - "@wdio/globals": "9.2.14", + "@wdio/globals": "9.4.1", "@wdio/logger": "9.1.3", "@wdio/types": "9.2.2", "@wdio/utils": "9.2.8", "deepmerge-ts": "^7.0.3", "expect-webdriverio": "^5.0.1", - "webdriver": "9.2.8", - "webdriverio": "9.2.14" + "webdriver": "9.4.1", + "webdriverio": "9.4.1" }, "engines": { "node": ">=18.20.0" @@ -9899,6 +9910,12 @@ "node": ">=8.0.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -11395,6 +11412,15 @@ "resolved": "https://registry.npmjs.org/ebml-block/-/ebml-block-1.1.2.tgz", "integrity": "sha512-HgNlIsRFP6D9VKU5atCeHRJY7XkJP8bOe8yEhd8NB7B3b4++VWTyauz6g650iiPmLfPLGlVpoJmGSgMfXDYusg==" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/edge-paths": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-3.0.5.tgz", @@ -15961,6 +15987,28 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dev": true, + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jsx-ast-utils": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz", @@ -16030,6 +16078,27 @@ "node": ">=16" } }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dev": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dev": true, + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/jwt-decode": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz", @@ -16555,12 +16624,42 @@ "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", "dev": true }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true + }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "dev": true }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, "node_modules/lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", @@ -16572,6 +16671,12 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true + }, "node_modules/lodash.pickby": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz", @@ -23313,9 +23418,9 @@ } }, "node_modules/webdriver": { - "version": "9.2.8", - "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.2.8.tgz", - "integrity": "sha512-40NtUC1zME9tPHNfZv6ETSE3+aE75qZuKjbVAA0gj02AkO1Nl3yJmf5RLdaLLfIQ2WlrbRP1g8KXlkiiVCmakg==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.4.1.tgz", + "integrity": "sha512-vFDdxMj/9W1+6jhpHSiRYfO8dix23HjAUtLx7aOv9ejEsntC0EzCIAftJ59YsF3Ppu184+FkdDVhnivpkZPTFw==", "dev": true, "dependencies": { "@types/node": "^20.1.0", @@ -23326,6 +23431,7 @@ "@wdio/types": "9.2.2", "@wdio/utils": "9.2.8", "deepmerge-ts": "^7.0.3", + "undici": "^6.20.1", "ws": "^8.8.0" }, "engines": { @@ -23354,9 +23460,9 @@ } }, "node_modules/webdriverio": { - "version": "9.2.14", - "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.2.14.tgz", - "integrity": "sha512-85yEbwN3MwdrGzKZoGkLUf1J5cpfnc7knL4u/Y6XWd0gGwYjv60I5ZPsgSnXzNXAkq2kmtkammf1AM3ihqFM3A==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.4.1.tgz", + "integrity": "sha512-XIPtRnxSES4CoxH2BfUY/6QzNgEgJEUjMYu7t7SJR8bVfbLRVXAA1ie9kM0MtdLs4oZ2Pr8rR8fqytsA1CjEWw==", "dev": true, "dependencies": { "@types/node": "^20.11.30", @@ -23385,7 +23491,7 @@ "rgb2hex": "0.2.5", "serialize-error": "^11.0.3", "urlpattern-polyfill": "^10.0.0", - "webdriver": "9.2.8" + "webdriver": "9.4.1" }, "engines": { "node": ">=18.20.0" @@ -29046,6 +29152,15 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "@types/jsonwebtoken": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/lodash": { "version": "4.14.182", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz", @@ -29651,15 +29766,15 @@ } }, "@wdio/cli": { - "version": "9.2.14", - "resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-9.2.14.tgz", - "integrity": "sha512-nYNAuF5HPW8b+B21t83N6NGEUpszZZygTDMy+xemBQFrkp7qpDsaeaxv60rFf6AR1mapcnK4ghHJUqod0qL7cg==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-9.4.1.tgz", + "integrity": "sha512-GDyAer63WDsr2ckXmrpUyAcIZFd3pCRIpi85rL1ZjnWthRy/UtwY0EHPMDuSeUEJ28iYwW3esKgq2ZKlsdbMeA==", "dev": true, "requires": { "@types/node": "^20.1.1", "@vitest/snapshot": "^2.1.1", "@wdio/config": "9.2.8", - "@wdio/globals": "9.2.14", + "@wdio/globals": "9.4.1", "@wdio/logger": "9.1.3", "@wdio/protocols": "9.2.2", "@wdio/types": "9.2.2", @@ -29679,7 +29794,7 @@ "read-pkg-up": "^10.0.0", "recursive-readdir": "^2.2.3", "tsx": "^4.7.2", - "webdriverio": "9.2.14", + "webdriverio": "9.4.1", "yargs": "^17.7.2" }, "dependencies": { @@ -29844,23 +29959,23 @@ } }, "@wdio/globals": { - "version": "9.2.14", - "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-9.2.14.tgz", - "integrity": "sha512-Hgi85bp5vpckK+k5iJ6zz8wUUL6IhCzywIG6uXzSgH/zkUOp5Til/NYyJmzSv6hRsfGaFia9WSaoqol93bfEIA==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-9.4.1.tgz", + "integrity": "sha512-CTVAVJ7iFyT54XF9iRmNvsDB+WSHoztJPG9XPL/mHzQ2LYfSyUR8E/j+3iHbTx3v/qRNucgPcGwhxiuY2RcaDg==", "dev": true, "requires": { "expect-webdriverio": "^5.0.1", - "webdriverio": "9.2.14" + "webdriverio": "9.4.1" } }, "@wdio/jasmine-framework": { - "version": "9.2.14", - "resolved": "https://registry.npmjs.org/@wdio/jasmine-framework/-/jasmine-framework-9.2.14.tgz", - "integrity": "sha512-jU+xF08Kq2EsX3RnbUzFjbSMg0gdVYJl2p5fc41SRZls2o2EC8Pvo1B0qaWLTpXYYHAD1Q/efKWhT0AOdudQwA==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@wdio/jasmine-framework/-/jasmine-framework-9.4.1.tgz", + "integrity": "sha512-N2X8MfsfX8JemD4DQwvSksXmBzYZxCKwuB7LaBiw+hH2YuFu+0L2ekR0Xvwtr8SpiENVEHAoiNFEj4i9dMTqlA==", "dev": true, "requires": { "@types/node": "^20.1.0", - "@wdio/globals": "9.2.14", + "@wdio/globals": "9.4.1", "@wdio/logger": "9.1.3", "@wdio/types": "9.2.2", "@wdio/utils": "9.2.8", @@ -29881,15 +29996,15 @@ } }, "@wdio/local-runner": { - "version": "9.2.15", - "resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-9.2.15.tgz", - "integrity": "sha512-+qh/fkA362r+Wnu58BsC/O8xJ0TKPxdzdyHNsz5qgBdmromQIniWCC4xoKg7rBLv4aA5nkyJjXhC7lXcuWYkLQ==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-9.4.1.tgz", + "integrity": "sha512-MM5VM0V7zvajICr6eNROjkppRhGNpdV4nU5hrgSap92nou8G+zBgLxJ45P5BzLw67KQTOEa1E32b/zCBEkO+0g==", "dev": true, "requires": { "@types/node": "^20.1.0", "@wdio/logger": "9.1.3", "@wdio/repl": "9.0.8", - "@wdio/runner": "9.2.15", + "@wdio/runner": "9.4.1", "@wdio/types": "9.2.2", "async-exit-hook": "^2.0.1", "split2": "^4.1.0", @@ -29960,21 +30075,21 @@ } }, "@wdio/runner": { - "version": "9.2.15", - "resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-9.2.15.tgz", - "integrity": "sha512-kVQ+YqVijkD2rJPgbJ7yEzzjOUoJ/HKqvBa3Y/kTJPLcSKQCn47GxqzXlOO6wPWLGWkf22Xn7CiTxFN1t+Zz0w==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-9.4.1.tgz", + "integrity": "sha512-WUpYamLafv+GUofGqvA2HO1kSVnwLyPO5ujq3TS/YaJLnUDOj4bjHiRo5+KDbKw1xhqjW0GjbfDupVU5LeL9iw==", "dev": true, "requires": { "@types/node": "^20.11.28", "@wdio/config": "9.2.8", - "@wdio/globals": "9.2.14", + "@wdio/globals": "9.4.1", "@wdio/logger": "9.1.3", "@wdio/types": "9.2.2", "@wdio/utils": "9.2.8", "deepmerge-ts": "^7.0.3", "expect-webdriverio": "^5.0.1", - "webdriver": "9.2.8", - "webdriverio": "9.2.14" + "webdriver": "9.4.1", + "webdriverio": "9.4.1" } }, "@wdio/types": { @@ -31140,6 +31255,12 @@ "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", "dev": true }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true + }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -32221,6 +32342,15 @@ "resolved": "https://registry.npmjs.org/ebml-block/-/ebml-block-1.1.2.tgz", "integrity": "sha512-HgNlIsRFP6D9VKU5atCeHRJY7XkJP8bOe8yEhd8NB7B3b4++VWTyauz6g650iiPmLfPLGlVpoJmGSgMfXDYusg==" }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, "edge-paths": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-3.0.5.tgz", @@ -35503,6 +35633,24 @@ "graceful-fs": "^4.1.6" } }, + "jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dev": true, + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + } + }, "jsx-ast-utils": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz", @@ -35568,6 +35716,27 @@ "xmlbuilder": "^15.1.1" } }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dev": true, + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dev": true, + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "jwt-decode": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz", @@ -35986,12 +36155,42 @@ "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", "dev": true }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true + }, "lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "dev": true }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, "lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", @@ -36003,6 +36202,12 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true + }, "lodash.pickby": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz", @@ -40733,9 +40938,9 @@ "dev": true }, "webdriver": { - "version": "9.2.8", - "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.2.8.tgz", - "integrity": "sha512-40NtUC1zME9tPHNfZv6ETSE3+aE75qZuKjbVAA0gj02AkO1Nl3yJmf5RLdaLLfIQ2WlrbRP1g8KXlkiiVCmakg==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.4.1.tgz", + "integrity": "sha512-vFDdxMj/9W1+6jhpHSiRYfO8dix23HjAUtLx7aOv9ejEsntC0EzCIAftJ59YsF3Ppu184+FkdDVhnivpkZPTFw==", "dev": true, "requires": { "@types/node": "^20.1.0", @@ -40746,6 +40951,7 @@ "@wdio/types": "9.2.2", "@wdio/utils": "9.2.8", "deepmerge-ts": "^7.0.3", + "undici": "^6.20.1", "ws": "^8.8.0" }, "dependencies": { @@ -40758,9 +40964,9 @@ } }, "webdriverio": { - "version": "9.2.14", - "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.2.14.tgz", - "integrity": "sha512-85yEbwN3MwdrGzKZoGkLUf1J5cpfnc7knL4u/Y6XWd0gGwYjv60I5ZPsgSnXzNXAkq2kmtkammf1AM3ihqFM3A==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.4.1.tgz", + "integrity": "sha512-XIPtRnxSES4CoxH2BfUY/6QzNgEgJEUjMYu7t7SJR8bVfbLRVXAA1ie9kM0MtdLs4oZ2Pr8rR8fqytsA1CjEWw==", "dev": true, "requires": { "@types/node": "^20.11.30", @@ -40789,7 +40995,7 @@ "rgb2hex": "0.2.5", "serialize-error": "^11.0.3", "urlpattern-polyfill": "^10.0.0", - "webdriver": "9.2.8" + "webdriver": "9.4.1" }, "dependencies": { "brace-expansion": { diff --git a/package.json b/package.json index 5850367ae055..a80f4a28e9c9 100644 --- a/package.json +++ b/package.json @@ -141,6 +141,7 @@ "@types/dom-screen-wake-lock": "1.0.1", "@types/jasmine": "5.1.4", "@types/js-md5": "0.4.3", + "@types/jsonwebtoken": "9.0.7", "@types/lodash-es": "4.17.12", "@types/moment-duration-format": "2.2.6", "@types/offscreencanvas": "2019.7.2", @@ -161,11 +162,11 @@ "@typescript-eslint/eslint-plugin": "5.59.5", "@typescript-eslint/parser": "5.59.5", "@wdio/allure-reporter": "9.2.14", - "@wdio/cli": "9.2.14", - "@wdio/globals": "9.2.14", - "@wdio/jasmine-framework": "9.2.14", + "@wdio/cli": "9.4.1", + "@wdio/globals": "9.4.1", + "@wdio/jasmine-framework": "9.4.1", "@wdio/junit-reporter": "9.2.14", - "@wdio/local-runner": "9.2.15", + "@wdio/local-runner": "9.4.1", "babel-loader": "9.1.0", "babel-plugin-optional-require": "0.3.1", "circular-dependency-plugin": "5.2.0", @@ -178,6 +179,7 @@ "eslint-plugin-react-native": "4.0.0", "eslint-plugin-typescript-sort-keys": "2.3.0", "jetifier": "1.6.4", + "jsonwebtoken": "9.0.2", "metro-react-native-babel-preset": "0.77.0", "patch-package": "6.4.7", "process": "0.11.10", @@ -187,7 +189,7 @@ "ts-loader": "9.4.2", "typescript": "5.0.4", "unorm": "1.6.0", - "webdriverio": "9.2.14", + "webdriverio": "9.4.1", "webpack": "5.95.0", "webpack-bundle-analyzer": "4.4.2", "webpack-cli": "5.1.4", @@ -215,7 +217,10 @@ "tsc-test:web": "tsc --project tsconfig.web.json --listFilesOnly | grep -v node_modules | grep native", "tsc-test:native": "tsc --project tsconfig.native.json --listFilesOnly | grep -v node_modules | grep web", "start": "make dev", - "test": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.conf.ts" + "test": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.conf.ts", + "test-ff": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.firefox.conf.ts", + "test-dev": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.dev.conf.ts", + "test-grid": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.conf.ts" }, "resolutions": { "@types/react": "17.0.14", diff --git a/tests/env.example b/tests/env.example index 2411fd702b7b..d148838be4e0 100644 --- a/tests/env.example +++ b/tests/env.example @@ -13,3 +13,19 @@ # The path to the browser video capture file #VIDEO_CAPTURE_FILE=tests/resources/FourPeople_1280x720_30.y4m + +# The path to the helper iframe page that will be used for the iframeAPI tests +#IFRAME_PAGE_BASE= + +# The grid host url (https://mygrid.com/wd/hub) +#GRID_HOST_URL= + +# The path to the private key used for generating JWT token (.pk) +#JWT_PRIVATE_KEY_PATH= +# The kid to use in the token +#JWT_KID= + +# The address of the webhooks proxy used to test the webhooks feature (e.g. wss://your.service/?tenant=sometenant) +#WEBHOOKS_PROXY_URL= +# A shared secret to authenticate the webhook proxy connection +#WEBHOOKS_PROXY_SHARED_SECRET= diff --git a/tests/globals.d.ts b/tests/globals.d.ts new file mode 100644 index 000000000000..41648b9ebde5 --- /dev/null +++ b/tests/globals.d.ts @@ -0,0 +1,5 @@ +import { IContext } from './helpers/types'; + +declare global { + const context: IContext; +} diff --git a/tests/helpers/Participant.ts b/tests/helpers/Participant.ts index a4bd29d90c04..d527578d8faf 100644 --- a/tests/helpers/Participant.ts +++ b/tests/helpers/Participant.ts @@ -5,10 +5,13 @@ import { multiremotebrowser } from '@wdio/globals'; import { IConfig } from '../../react/features/base/config/configType'; import { urlObjectToString } from '../../react/features/base/util/uri'; import Filmstrip from '../pageobjects/Filmstrip'; +import IframeAPI from '../pageobjects/IframeAPI'; +import ParticipantsPane from '../pageobjects/ParticipantsPane'; import Toolbar from '../pageobjects/Toolbar'; +import VideoQualityDialog from '../pageobjects/VideoQualityDialog'; import { LOG_PREFIX, logInfo } from './browserLogger'; -import { IContext } from './participants'; +import { IContext } from './types'; /** * Participant. @@ -19,9 +22,9 @@ export class Participant { * * @private */ - private context: { roomName: string; }; private _name: string; private _endpointId: string; + private _jwt?: string; /** * The default config to use when joining. @@ -59,9 +62,11 @@ export class Participant { * Creates a participant with given name. * * @param {string} name - The name of the participant. + * @param {string }jwt - The jwt if any. */ - constructor(name: string) { + constructor(name: string, jwt?: string) { this._name = name; + this._jwt = jwt; } /** @@ -69,7 +74,7 @@ export class Participant { * * @returns {Promise} The endpoint ID. */ - async getEndpointId() { + async getEndpointId(): Promise { if (!this._endpointId) { this._endpointId = await this.driver.execute(() => { // eslint-disable-line arrow-body-style return APP.conference.getMyUserId(); @@ -99,7 +104,7 @@ export class Participant { * @param {string} message - The message to log. * @returns {void} */ - log(message: string) { + log(message: string): void { logInfo(this.driver, message); } @@ -110,10 +115,8 @@ export class Participant { * @param {boolean} skipInMeetingChecks - Whether to skip in meeting checks. * @returns {Promise} */ - async joinConference(context: IContext, skipInMeetingChecks = false) { - this.context = context; - - const url = urlObjectToString({ + async joinConference(context: IContext, skipInMeetingChecks = false): Promise { + const config = { room: context.roomName, configOverwrite: this.config, interfaceConfigOverwrite: { @@ -122,14 +125,47 @@ export class Participant { userInfo: { displayName: this._name } - }) || ''; + }; + + if (context.iframeAPI) { + config.room = 'iframeAPITest.html'; + } + + let url = urlObjectToString(config) || ''; + + if (context.iframeAPI) { + const baseUrl = new URL(this.driver.options.baseUrl || ''); + + // @ts-ignore + url = `${this.driver.iframePageBase}${url}&domain="${baseUrl.host}"&room="${context.roomName}"`; + + if (baseUrl.pathname.length > 1) { + // remove leading slash + url = `${url}&tenant="${baseUrl.pathname.substring(1)}"`; + } + } + if (this._jwt) { + url = `${url}&jwt="${this._jwt}"`; + } await this.driver.setTimeout({ 'pageLoad': 30000 }); - await this.driver.url(url); + // workaround for https://github.com/webdriverio/webdriverio/issues/13956 + if (url.startsWith('file://')) { + // eslint-disable-next-line @typescript-eslint/no-empty-function + await this.driver.url(url).catch(() => {}); + } else { + await this.driver.url(url.substring(1)); // drop the leading '/' so we can use the tenant if any + } await this.waitForPageToLoad(); + if (context.iframeAPI) { + const mainFrame = this.driver.$('iframe'); + + await this.driver.switchFrame(mainFrame); + } + await this.waitToJoinMUC(); await this.postLoadProcess(skipInMeetingChecks); @@ -142,7 +178,7 @@ export class Participant { * @returns {Promise} * @private */ - private async postLoadProcess(skipInMeetingChecks: boolean) { + private async postLoadProcess(skipInMeetingChecks: boolean): Promise { const driver = this.driver; const parallel = []; @@ -189,7 +225,7 @@ export class Participant { * * @returns {Promise} */ - async waitForPageToLoad() { + async waitForPageToLoad(): Promise { return this.driver.waitUntil( () => this.driver.execute(() => document.readyState === 'complete'), { @@ -199,14 +235,21 @@ export class Participant { ); } + /** + * Checks if the participant is in the meeting. + */ + isInMuc() { + return this.driver.execute(() => APP.conference.isJoined()); + } + /** * Waits to join the muc. * * @returns {Promise} */ - async waitToJoinMUC() { + async waitToJoinMUC(): Promise { return this.driver.waitUntil( - () => this.driver.execute(() => APP.conference.isJoined()), + () => this.isInMuc(), { timeout: 10_000, // 10 seconds timeoutMsg: 'Timeout waiting to join muc.' @@ -219,7 +262,7 @@ export class Participant { * * @returns {Promise} */ - async waitForIceConnected() { + async waitForIceConnected(): Promise { const driver = this.driver; return driver.waitUntil(async () => @@ -234,7 +277,7 @@ export class Participant { * * @returns {Promise} */ - async waitForSendReceiveData() { + async waitForSendReceiveData(): Promise { const driver = this.driver; return driver.waitUntil(async () => @@ -259,7 +302,7 @@ export class Participant { * @param {number} number - The number of remote streams o wait for. * @returns {Promise} */ - waitForRemoteStreams(number: number) { + waitForRemoteStreams(number: number): Promise { const driver = this.driver; return driver.waitUntil(async () => @@ -274,7 +317,7 @@ export class Participant { * * @returns {Toolbar} */ - getToolbar() { + getToolbar(): Toolbar { return new Toolbar(this); } @@ -283,7 +326,61 @@ export class Participant { * * @returns {Filmstrip} */ - getFilmstrip() { + getFilmstrip(): Filmstrip { return new Filmstrip(this); } + + /** + * Returns the participants pane. + * + * @returns {ParticipantsPane} + */ + getParticipantsPane(): ParticipantsPane { + return new ParticipantsPane(this); + } + + /** + * Returns the videoQuality Dialog. + * + * @returns {VideoQualityDialog} + */ + getVideoQualityDialog(): VideoQualityDialog { + return new VideoQualityDialog(this); + } + + /** + * Switches to the iframe API context + */ + async switchToAPI() { + await this.driver.switchFrame(null); + } + + /** + * Switches to the meeting page context. + */ + async switchInPage() { + const mainFrame = this.driver.$('iframe'); + + await this.driver.switchFrame(mainFrame); + } + + /** + * Returns the iframe API for this participant. + */ + getIframeAPI() { + return new IframeAPI(this); + } + + /** + * Returns the local display name. + */ + async getLocalDisplayName() { + const localVideoContainer = this.driver.$('span[id="localVideoContainer"]'); + + await localVideoContainer.moveTo(); + + const localDisplayName = localVideoContainer.$('span[id="localDisplayName"]'); + + return await localDisplayName.getText(); + } } diff --git a/tests/helpers/WebhookProxy.ts b/tests/helpers/WebhookProxy.ts new file mode 100644 index 000000000000..210d33adf42b --- /dev/null +++ b/tests/helpers/WebhookProxy.ts @@ -0,0 +1,129 @@ +import WebSocket from 'ws'; + +/** + * Uses the webhook proxy service to proxy events to the testing clients. + */ +export default class WebhookProxy { + private url; + private secret; + private ws: WebSocket | undefined; + private cache = new Map(); + private listeners = new Map(); + private consumers = new Map(); + + /** + * Initializes the webhook proxy. + * @param url + * @param secret + */ + constructor(url: string, secret: string) { + this.url = url; + this.secret = secret; + } + + /** + * Connects. + */ + connect() { + this.ws = new WebSocket(this.url, { + headers: { + Authorization: this.secret + } + }); + + this.ws.on('error', console.error); + + this.ws.on('open', function open() { + console.log('WebhookProxy connected'); + }); + + this.ws.on('message', (data: any) => { + const msg = JSON.parse(data.toString()); + + if (msg.eventType) { + if (this.consumers.has(msg.eventType)) { + this.consumers.get(msg.eventType)(msg); + this.consumers.delete(msg.eventType); + } else { + this.cache.set(msg.eventType, msg); + } + + if (this.listeners.has(msg.eventType)) { + this.listeners.get(msg.eventType)(msg); + } + } + }); + } + + /** + * Adds event consumer. Consumers receive the event single time and we remove them from the list of consumers. + * @param eventType + * @param callback + */ + addConsumer(eventType: string, callback: (deventata: any) => void) { + if (this.cache.has(eventType)) { + callback(this.cache.get(eventType)); + this.cache.delete(eventType); + + return; + } + + this.consumers.set(eventType, callback); + } + + /** + * Clear any stored event. + */ + clearCache() { + this.cache.clear(); + } + + /** + * Waits for the event to be received. + * @param eventType + * @param timeout + */ + async waitForEvent(eventType: string, timeout = 4000): Promise { + // we create the error here so we have a meaningful stack trace + const error = new Error(`Timeout waiting for event:${eventType}`); + + return new Promise((resolve, reject) => { + const waiter = setTimeout(() => reject(error), timeout); + + this.addConsumer(eventType, event => { + clearTimeout(waiter); + + resolve(event); + }); + + }); + } + + /** + * Adds a listener for the event type. + * @param eventType + * @param callback + */ + addListener(eventType: string, callback: (data: any) => void) { + this.listeners.set(eventType, callback); + } + + /** + * Adds a listener for the event type. + * @param eventType + */ + removeListener(eventType: string) { + this.listeners.delete(eventType); + } + + /** + * Disconnects the webhook proxy. + */ + disconnect() { + if (this.ws) { + this.ws.close(); + console.log('WebhookProxy disconnected'); + this.ws = undefined; + } + } +} diff --git a/tests/helpers/participants.ts b/tests/helpers/participants.ts index 2766f8255a1b..1b5d78151b21 100644 --- a/tests/helpers/participants.ts +++ b/tests/helpers/participants.ts @@ -1,20 +1,30 @@ -import { Participant } from './Participant'; +import fs from 'fs'; +import jwt from 'jsonwebtoken'; +import process from 'node:process'; +import { v4 as uuidv4 } from 'uuid'; -export type IContext = { - p1: Participant; - p2: Participant; - p3: Participant; - p4: Participant; - roomName: string; -}; +import { Participant } from './Participant'; +import WebhookProxy from './WebhookProxy'; +import { IContext } from './types'; /** * Generate a random room name. + * Everytime we generate a name and iframeAPI is enabled and there is a configured + * webhooks proxy we connect to it with the new room name. * * @returns {string} - The random room name. */ function generateRandomRoomName(): string { - return `jitsimeettorture-${crypto.randomUUID()}}`; + const roomName = `jitsimeettorture-${crypto.randomUUID()}`; + + if (context.iframeAPI && !context.webhooksProxy + && process.env.WEBHOOKS_PROXY_URL && process.env.WEBHOOKS_PROXY_SHARED_SECRET) { + context.webhooksProxy = new WebhookProxy(`${process.env.WEBHOOKS_PROXY_URL}&room=${roomName}`, + process.env.WEBHOOKS_PROXY_SHARED_SECRET); + context.webhooksProxy.connect(); + } + + return roomName; } /** @@ -24,7 +34,9 @@ function generateRandomRoomName(): string { * @returns {Promise} */ export async function ensureOneParticipant(context: IContext): Promise { - context.roomName = generateRandomRoomName(); + if (!context.roomName) { + context.roomName = generateRandomRoomName(); + } context.p1 = new Participant('participant1'); @@ -38,7 +50,9 @@ export async function ensureOneParticipant(context: IContext): Promise { * @returns {Promise} */ export async function ensureThreeParticipants(context: IContext): Promise { - context.roomName = generateRandomRoomName(); + if (!context.roomName) { + context.roomName = generateRandomRoomName(); + } const p1 = new Participant('participant1'); const p2 = new Participant('participant2'); @@ -62,6 +76,77 @@ export async function ensureThreeParticipants(context: IContext): Promise ]); } +/** + * Ensure that there are two participants. + * + * @param {Object} context - The context. + * @returns {Promise} + */ +export async function ensureTwoParticipants(context: IContext): Promise { + if (!context.roomName) { + context.roomName = generateRandomRoomName(); + } + + const p1DisplayName = 'participant1'; + let token; + + // if it is jaas create the first one to be moderator and second not moderator + if (context.jwtPrivateKeyPath) { + token = getModeratorToken(p1DisplayName); + } + + // make sure the first participant is moderator, if supported by deployment + await _joinParticipant(p1DisplayName, context.p1, p => { + context.p1 = p; + }, true, token); + + await Promise.all([ + _joinParticipant('participant2', context.p2, p => { + context.p2 = p; + }), + context.p1.waitForRemoteStreams(1), + context.p2.waitForRemoteStreams(1) + ]); +} + +/** + * Creates a participant instance or prepares one for re-joining. + * @param name - The name of the participant. + * @param p - The participant instance to prepare or undefined if new one is needed. + * @param setter - The setter to use for setting the new participant instance into the context if needed. + * @param {boolean} skipInMeetingChecks - Whether to skip in meeting checks. + * @param {string?} jwtToken - The token to use if any. + */ +async function _joinParticipant( // eslint-disable-line max-params + name: string, + p: Participant, + setter: (p: Participant) => void, + skipInMeetingChecks = false, + jwtToken?: string) { + if (p) { + await p.switchInPage(); + + if (await p.isInMuc()) { + return; + } + + // when loading url make sure we are on the top page context or strange errors may occur + await p.switchToAPI(); + + // Change the page so we can reload same url if we need to, base.html is supposed to be empty or close to empty + await p.driver.url('/base.html'); + + // we want the participant instance re-recreated so we clear any kept state, like endpoint ID + } + + const newParticipant = new Participant(name, jwtToken); + + // set the new participant instance, pass it to setter + setter(newParticipant); + + return newParticipant.joinConference(context, skipInMeetingChecks); +} + /** * Toggles the mute state of a specific Meet conference participant and verifies that a specific other Meet * conference participants sees a specific mute state for the former. @@ -78,3 +163,64 @@ export async function toggleMuteAndCheck(testee: Participant, observer: Particip await observer.getFilmstrip().assertAudioMuteIconIsDisplayed(testee); await testee.getFilmstrip().assertAudioMuteIconIsDisplayed(testee); } + +/** + * Get a JWT token for a moderator. + */ +function getModeratorToken(displayName: string) { + const keyid = process.env.JWT_KID; + const headers = { + algorithm: 'RS256', + noTimestamp: true, + expiresIn: '24h', + keyid + }; + + if (!keyid) { + console.error('JWT_KID is not set'); + + return; + } + + const key = fs.readFileSync(context.jwtPrivateKeyPath); + + const payload = { + 'aud': 'jitsi', + 'iss': 'chat', + 'sub': keyid.substring(0, keyid.indexOf('/')), + 'context': { + 'user': { + 'name': displayName, + 'id': uuidv4(), + 'avatar': 'https://avatars0.githubusercontent.com/u/3671647', + 'email': 'john.doe@jitsi.org' + } + }, + 'room': '*' + }; + + // @ts-ignore + payload.context.user.moderator = true; + + // @ts-ignore + return jwt.sign(payload, key, headers); +} + +/** + * Parse a JID string. + * @param str the string to parse. + */ +export function parseJid(str: string): { + domain: string; + node: string; + resource: string | undefined; +} { + const parts = str.split('@'); + const domainParts = parts[1].split('/'); + + return { + node: parts[0], + domain: domainParts[0], + resource: domainParts.length > 0 ? domainParts[1] : undefined + }; +} diff --git a/tests/helpers/types.ts b/tests/helpers/types.ts new file mode 100644 index 000000000000..e75c3c9e916e --- /dev/null +++ b/tests/helpers/types.ts @@ -0,0 +1,15 @@ +import type { Participant } from './Participant'; +import WebhookProxy from './WebhookProxy'; + +export type IContext = { + conferenceJid: string; + iframeAPI: boolean; + jwtKid: string; + jwtPrivateKeyPath: string; + p1: Participant; + p2: Participant; + p3: Participant; + p4: Participant; + roomName: string; + webhooksProxy: WebhookProxy; +}; diff --git a/tests/pageobjects/BaseDialog.ts b/tests/pageobjects/BaseDialog.ts new file mode 100644 index 000000000000..bab64b961875 --- /dev/null +++ b/tests/pageobjects/BaseDialog.ts @@ -0,0 +1,26 @@ +import { Participant } from '../helpers/Participant'; + +const CLOSE_BUTTON = 'modal-header-close-button'; + +/** + * Base class for all dialogs. + */ +export default class BaseDialog { + participant: Participant; + + /** + * Initializes for a participant. + * + * @param {Participant} participant - The participant. + */ + constructor(participant: Participant) { + this.participant = participant; + } + + /** + * Clicks on the X (close) button. + */ + async clickCloseButton(): Promise { + await this.participant.driver.$(`#${CLOSE_BUTTON}`).click(); + } +} diff --git a/tests/pageobjects/Filmstrip.ts b/tests/pageobjects/Filmstrip.ts index 0d480b6b9eef..e9cc14919b1b 100644 --- a/tests/pageobjects/Filmstrip.ts +++ b/tests/pageobjects/Filmstrip.ts @@ -20,12 +20,12 @@ export default class Filmstrip { * mute icon for the conference participant identified by * {@code testee}. * - * @param {Participant} testee - The {@code WebParticipant} for whom we're checking the status of audio muted icon. + * @param {Participant} testee - The {@code Participant} for whom we're checking the status of audio muted icon. * @param {boolean} reverse - If {@code true}, the method will assert the absence of the "mute" icon; * otherwise, it will assert its presence. * @returns {Promise} */ - async assertAudioMuteIconIsDisplayed(testee: Participant, reverse = false) { + async assertAudioMuteIconIsDisplayed(testee: Participant, reverse = false): Promise { let id; if (testee === this.participant) { @@ -40,7 +40,50 @@ export default class Filmstrip { await this.participant.driver.$(mutedIconXPath).waitForDisplayed({ reverse, timeout: 2000, - timeoutMsg: `Audio mute icon is not displayed for ${testee.name}` + timeoutMsg: `Audio mute icon is ${reverse ? '' : 'not'} displayed for ${testee.name}` }); } + + /** + * Asserts that {@code participant} shows or doesn't show the video mute icon for the conference participant + * identified by {@code testee}. + * + * @param {Participant} testee - The {@code Participant} for whom we're checking the status of audio muted icon. + * @param {boolean} reverse - If {@code true}, the method will assert the absence of the "mute" icon; + * otherwise, it will assert its presence. + * @returns {Promise} + */ + async assertVideoMuteIconIsDisplayed(testee: Participant, reverse = false): Promise { + const isOpen = await this.participant.getParticipantsPane().isOpen(); + + if (!isOpen) { + await this.participant.getParticipantsPane().open(); + } + + const id = `participant-item-${await testee.getEndpointId()}`; + const mutedIconXPath + = `//div[@id='${id}']//div[contains(@class, 'indicators')]//*[local-name()='svg' and @id='videoMuted']`; + + await this.participant.driver.$(mutedIconXPath).waitForDisplayed({ + reverse, + timeout: 2000, + timeoutMsg: `Video mute icon is ${reverse ? '' : 'not'} displayed for ${testee.name}` + }); + + if (!isOpen) { + await this.participant.getParticipantsPane().close(); + } + } + + /** + * Returns the remote display name for an endpoint. + * @param endpointId The endpoint id. + */ + async getRemoteDisplayName(endpointId: string) { + const remoteDisplayName = this.participant.driver.$(`span[id="participant_${endpointId}_name"]`); + + await remoteDisplayName.moveTo(); + + return await remoteDisplayName.getText(); + } } diff --git a/tests/pageobjects/IframeAPI.ts b/tests/pageobjects/IframeAPI.ts new file mode 100644 index 000000000000..12f25f21fdb8 --- /dev/null +++ b/tests/pageobjects/IframeAPI.ts @@ -0,0 +1,90 @@ +import { Participant } from '../helpers/Participant'; +import { LOG_PREFIX } from '../helpers/browserLogger'; + +/** + * The Iframe API and helpers from iframeAPITest.html + */ +export default class IframeAPI { + private participant: Participant; + + /** + * Initializes for a participant. + * @param participant + */ + constructor(participant: Participant) { + this.participant = participant; + } + + /** + * Returns the json object from the iframeAPI helper. + * @param event + */ + async getEventResult(event: string): Promise { + return this.participant.driver.execute( + eventName => { + const result = window.jitsiAPI.test[eventName]; + + if (!result) { + return false; + } + + return result; + }, event); + } + + /** + * Adds an event listener to the iframeAPI. + * @param eventName The event name. + */ + async addEventListener(eventName: string) { + return this.participant.driver.executeAsync((event, prefix, done) => { + console.log(`${new Date().toISOString()} ${prefix} Adding listener for event: ${event}`); + window.jitsiAPI.addListener(event, evt => { + console.log(`${new Date().toISOString()} ${prefix} Received ${event} event: ${JSON.stringify(evt)}`); + window.jitsiAPI.test[event] = evt; + }); + done(); + }, eventName, LOG_PREFIX); + } + + /** + * Returns an array of available rooms and details of it. + */ + async getRoomsInfo() { + return this.participant.driver.execute(() => window.jitsiAPI.getRoomsInfo()); + } + + /** + * Returns the number of participants in the conference. + */ + async getNumberOfParticipants() { + return this.participant.driver.execute(() => window.jitsiAPI.getNumberOfParticipants()); + } + + /** + * Executes command using iframeAPI. + * @param command The command. + * @param args The arguments. + */ + async executeCommand(command: string, ...args: any[]) { + return this.participant.driver.execute( + (commandName, commandArgs) => + window.jitsiAPI.executeCommand(commandName, ...commandArgs) + , command, args); + } + + /** + * Returns the current state of the participant's pane. + */ + async isParticipantsPaneOpen() { + return this.participant.driver.execute(() => window.jitsiAPI.isParticipantsPaneOpen()); + } + + /** + * Removes the embedded Jitsi Meet conference. + */ + async dispose() { + return this.participant.driver.execute(() => window.jitsiAPI.dispose()); + } + +} diff --git a/tests/pageobjects/ParticipantsPane.ts b/tests/pageobjects/ParticipantsPane.ts new file mode 100644 index 000000000000..a792eac9f636 --- /dev/null +++ b/tests/pageobjects/ParticipantsPane.ts @@ -0,0 +1,47 @@ +import { Participant } from '../helpers/Participant'; + +/** + * Classname of the closed/hidden participants pane + */ +const PARTICIPANTS_PANE = 'participants_pane'; + +/** + * Represents the participants pane from the UI. + */ +export default class ParticipantsPane { + private participant: Participant; + + /** + * Initializes for a participant. + * + * @param {Participant} participant - The participant. + */ + constructor(participant: Participant) { + this.participant = participant; + } + + /** + * Checks if the pane is open. + */ + async isOpen() { + return this.participant.driver.$(`.${PARTICIPANTS_PANE}`).isExisting(); + } + + /** + * Clicks the "participants" toolbar button to open the participants pane. + */ + async open() { + await this.participant.getToolbar().clickParticipantsPaneButton(); + + await this.participant.driver.$(`.${PARTICIPANTS_PANE}`).waitForDisplayed(); + } + + /** + * Clicks the "participants" toolbar button to close the participants pane. + */ + async close() { + await this.participant.getToolbar().clickCloseParticipantsPaneButton(); + + await this.participant.driver.$(`.${PARTICIPANTS_PANE}`).waitForDisplayed({ reverse: true }); + } +} diff --git a/tests/pageobjects/Toolbar.ts b/tests/pageobjects/Toolbar.ts index bd718cdce452..6ae797deb12b 100644 --- a/tests/pageobjects/Toolbar.ts +++ b/tests/pageobjects/Toolbar.ts @@ -3,6 +3,13 @@ import { Participant } from '../helpers/Participant'; const AUDIO_MUTE = 'Mute microphone'; const AUDIO_UNMUTE = 'Unmute microphone'; +const CLOSE_PARTICIPANTS_PANE = 'Close participants pane'; +const OVERFLOW_MENU = 'More actions menu'; +const OVERFLOW = 'More actions'; +const PARTICIPANTS = 'Open participants pane'; +const VIDEO_QUALITY = 'Manage video quality'; +const VIDEO_MUTE = 'Stop camera'; +const VIDEO_UNMUTE = 'Start camera'; /** * The toolbar elements. @@ -49,8 +56,8 @@ export default class Toolbar { * * @returns {Promise} */ - async clickAudioMuteButton() { - await this.participant.log('Clicking on: Audio Mute Button'); + async clickAudioMuteButton(): Promise { + this.participant.log('Clicking on: Audio Mute Button'); await this.audioMuteBtn.click(); } @@ -59,8 +66,141 @@ export default class Toolbar { * * @returns {Promise} */ - async clickAudioUnmuteButton() { - await this.participant.log('Clicking on: Audio Unmute Button'); + async clickAudioUnmuteButton(): Promise { + this.participant.log('Clicking on: Audio Unmute Button'); await this.audioUnMuteBtn.click(); } + + /** + * The video mute button. + */ + get videoMuteBtn() { + return this.getButton(VIDEO_MUTE); + } + + /** + * The video unmute button. + */ + get videoUnMuteBtn() { + return this.getButton(VIDEO_UNMUTE); + } + + /** + * Clicks video mute button. + * + * @returns {Promise} + */ + async clickVideoMuteButton(): Promise { + this.participant.log('Clicking on: Video Mute Button'); + await this.videoMuteBtn.click(); + } + + /** + * Clicks video unmute button. + * + * @returns {Promise} + */ + async clickVideoUnmuteButton(): Promise { + this.participant.log('Clicking on: Video Unmute Button'); + await this.videoUnMuteBtn.click(); + } + + /** + * Clicks Participants pane button. + * + * @returns {Promise} + */ + async clickCloseParticipantsPaneButton(): Promise { + this.participant.log('Clicking on: Close Participants pane Button'); + await this.getButton(CLOSE_PARTICIPANTS_PANE).click(); + } + + + /** + * Clicks Participants pane button. + * + * @returns {Promise} + */ + async clickParticipantsPaneButton(): Promise { + this.participant.log('Clicking on: Participants pane Button'); + await this.getButton(PARTICIPANTS).click(); + } + + /** + * Clicks on the video quality toolbar button which opens the + * dialog for adjusting max-received video quality. + */ + async clickVideoQualityButton(): Promise { + return this.clickButtonInOverflowMenu(VIDEO_QUALITY); + } + + /** + * Ensure the overflow menu is open and clicks on a specified button. + * @param accessibilityLabel The accessibility label of the button to be clicked. + * @private + */ + private async clickButtonInOverflowMenu(accessibilityLabel: string) { + await this.openOverflowMenu(); + + await this.getButton(accessibilityLabel).click(); + + await this.closeOverflowMenu(); + } + + /** + * Checks if the overflow menu is open and visible. + * @private + */ + private async isOverflowMenuOpen() { + return await this.participant.driver.$$(`[aria-label^="${OVERFLOW_MENU}"]`).length > 0; + } + + /** + * Clicks on the overflow toolbar button which opens or closes the overflow menu. + * @private + */ + private async clickOverflowButton(): Promise { + await this.getButton(OVERFLOW).click(); + } + + /** + * Ensure the overflow menu is displayed. + * @private + */ + private async openOverflowMenu() { + if (await this.isOverflowMenuOpen()) { + return; + } + + await this.clickOverflowButton(); + + await this.waitForOverFlowMenu(true); + } + + /** + * Ensures the overflow menu is not displayed. + * @private + */ + private async closeOverflowMenu() { + if (!await this.isOverflowMenuOpen()) { + return; + } + + await this.clickOverflowButton(); + + await this.waitForOverFlowMenu(false); + } + + /** + * Waits for the overflow menu to be visible or hidden. + * @param visible + * @private + */ + private async waitForOverFlowMenu(visible: boolean) { + await this.participant.driver.$(`[aria-label^="${OVERFLOW_MENU}"]`).waitForDisplayed({ + reverse: !visible, + timeout: 3000, + timeoutMsg: `Overflow menu is not ${visible ? 'visible' : 'hidden'}` + }); + } } diff --git a/tests/pageobjects/VideoQualityDialog.ts b/tests/pageobjects/VideoQualityDialog.ts new file mode 100644 index 000000000000..abc685f49ecd --- /dev/null +++ b/tests/pageobjects/VideoQualityDialog.ts @@ -0,0 +1,42 @@ +import { Key } from 'webdriverio'; + +import BaseDialog from './BaseDialog'; + +const VIDEO_QUALITY_SLIDER_CLASS = 'custom-slider'; + +/** + * The video quality dialog. + */ +export default class VideoQualityDialog extends BaseDialog { + /** + * Opens the video quality dialog and sets the video quality to the minimum or maximum definition. + * @param audioOnly - Whether to set the video quality to audio only (minimum). + * @private + */ + async setVideoQuality(audioOnly: boolean) { + await this.participant.getToolbar().clickVideoQualityButton(); + + const videoQualitySlider = this.participant.driver.$(`.${VIDEO_QUALITY_SLIDER_CLASS}`); + + const audioOnlySliderValue = parseInt(await videoQualitySlider.getAttribute('min'), 10); + + const maxDefinitionSliderValue = parseInt(await videoQualitySlider.getAttribute('max'), 10); + const activeValue = parseInt(await videoQualitySlider.getAttribute('value'), 10); + + const targetValue = audioOnly ? audioOnlySliderValue : maxDefinitionSliderValue; + const distanceToTargetValue = targetValue - activeValue; + const keyDirection = distanceToTargetValue > 0 ? Key.ArrowRight : Key.ArrowLeft; + + // we need to click the element to activate it so it will receive the keys + await videoQualitySlider.click(); + + // Move the slider to the target value. + for (let i = 0; i < Math.abs(distanceToTargetValue); i++) { + + await this.participant.driver.keys(keyDirection); + } + + // Close the video quality dialog. + await this.clickCloseButton(); + } +} diff --git a/tests/resources/iframeAPITest.html b/tests/resources/iframeAPITest.html new file mode 100644 index 000000000000..dc3f64948747 --- /dev/null +++ b/tests/resources/iframeAPITest.html @@ -0,0 +1,121 @@ + + + + + iframe API test + + + + + diff --git a/tests/specs/2way/audioOnly.spec.ts b/tests/specs/2way/audioOnly.spec.ts new file mode 100644 index 000000000000..d0e778b7b903 --- /dev/null +++ b/tests/specs/2way/audioOnly.spec.ts @@ -0,0 +1,86 @@ +import { ensureTwoParticipants } from '../../helpers/participants'; + +describe('Audio only - ', () => { + it('joining the meeting', async () => { + await ensureTwoParticipants(context); + }); + + /** + * Enables audio only mode for p1 and verifies that the other participant sees participant1 as video muted. + */ + it('set and check', async () => { + await setAudioOnlyAndCheck(true); + }); + + /** + * Verifies that participant1 sees avatars for itself and other participants. + */ + it('avatars check', async () => { + await context.p1.driver.$('//div[@id="dominantSpeaker"]').waitForDisplayed(); + + // Makes sure that the avatar is displayed in the local thumbnail and that the video is not displayed. + await context.p1.driver.$('//span[@id="localVideoContainer"]//div[contains(@class,"userAvatar")]') + .waitForDisplayed(); + await context.p1.driver.$('//span[@id="localVideoWrapper"]//video').waitForDisplayed({ reverse: true }); + }); + + /** + * Disables audio only mode and verifies that both participants see p1 as not video muted. + */ + it('disable and check', async () => { + await setAudioOnlyAndCheck(false); + }); + + /** + * Toggles the audio only state of a p1 participant and verifies participant sees the audio only label and that + * p2 participant sees a video mute state for the former. + * @param enable + */ + async function setAudioOnlyAndCheck(enable: boolean) { + await context.p1.getVideoQualityDialog().setVideoQuality(enable); + + await verifyVideoMute(enable); + + await context.p1.driver.$('//div[@id="videoResolutionLabel"][contains(@class, "audio-only")]') + .waitForDisplayed({ reverse: !enable }); + } + + /** + * Verifies that p1 and p2 see p1 as video muted or not. + * @param muted + */ + async function verifyVideoMute(muted: boolean) { + // Verify the observer sees the testee in the desired muted state. + await context.p2.getFilmstrip().assertVideoMuteIconIsDisplayed(context.p1, !muted); + + // Verify the testee sees itself in the desired muted state. + await context.p1.getFilmstrip().assertVideoMuteIconIsDisplayed(context.p1, !muted); + } + + /** + * Mutes video on participant1, toggles audio-only twice and then verifies if both participants see participant1 + * as video muted. + */ + it('mute video, set twice and check muted', async () => { + // Mute video on participant1. + await context.p1.getToolbar().clickVideoMuteButton(); + + await verifyVideoMute(true); + + // Enable audio-only mode. + await setAudioOnlyAndCheck(true); + + // Disable audio-only mode. + await context.p1.getVideoQualityDialog().setVideoQuality(false); + + // p1 should stay muted since it was muted before audio-only was enabled. + await verifyVideoMute(true); + }); + + it('unmute video and check not muted', async () => { + // Unmute video on participant1. + await context.p1.getToolbar().clickVideoUnmuteButton(); + + await verifyVideoMute(false); + }); +}); diff --git a/tests/specs/2way/iFrameParticipantsPresence.spec.ts b/tests/specs/2way/iFrameParticipantsPresence.spec.ts new file mode 100644 index 000000000000..0f647e737d2e --- /dev/null +++ b/tests/specs/2way/iFrameParticipantsPresence.spec.ts @@ -0,0 +1,424 @@ +import { isEqual } from 'lodash-es'; + +import type { Participant } from '../../helpers/Participant'; +import { ensureTwoParticipants, parseJid } from '../../helpers/participants'; + +/** + * Tests PARTICIPANT_LEFT webhook. + */ +async function checkParticipantLeftHook(p: Participant, reason: string) { + const { webhooksProxy } = context; + + if (webhooksProxy) { + // PARTICIPANT_LEFT webhook + // @ts-ignore + const event: { + data: { + conference: string; + disconnectReason: string; + isBreakout: boolean; + participantId: string; + }; + eventType: string; + } = await context.webhooksProxy.waitForEvent('PARTICIPANT_LEFT'); + + expect('PARTICIPANT_LEFT').toBe(event.eventType); + expect(event.data.conference).toBe(context.conferenceJid); + expect(event.data.disconnectReason).toBe(reason); + expect(event.data.isBreakout).toBe(false); + expect(event.data.participantId).toBe(await p.getEndpointId()); + } +} + +describe('Participants presence - ', () => { + it('joining the meeting', async () => { + context.iframeAPI = true; + + // ensure 2 participants one moderator and one guest, we will load both with iframeAPI + await ensureTwoParticipants(context); + + const { p1, p2, webhooksProxy } = context; + + // let's populate endpoint ids + await Promise.all([ + p1.getEndpointId(), + p2.getEndpointId() + ]); + + await p1.switchToAPI(); + await p2.switchToAPI(); + + expect(await p1.getIframeAPI().getEventResult('isModerator')) + .withContext('Is p1 moderator') + .toBeTrue(); + expect(await p2.getIframeAPI().getEventResult('isModerator')) + .withContext('Is p2 non-moderator') + .toBeFalse(); + + expect(await p1.getIframeAPI().getEventResult('videoConferenceJoined')).toBeDefined(); + expect(await p2.getIframeAPI().getEventResult('videoConferenceJoined')).toBeDefined(); + + if (webhooksProxy) { + // USAGE webhook + // @ts-ignore + const event: { + data: [ + { participantId: string; } + ]; + eventType: string; + } = await context.webhooksProxy.waitForEvent('USAGE'); + + expect('USAGE').toBe(event.eventType); + + const p1EpId = await p1.getEndpointId(); + const p2EpId = await p2.getEndpointId(); + + expect(event.data.filter(d => d.participantId === p1EpId + || d.participantId === p2EpId).length).toBe(2); + } + + // we will use it later + // TODO figure out why adding those just before grantModerator and we miss the events + await p1.getIframeAPI().addEventListener('participantRoleChanged'); + await p2.getIframeAPI().addEventListener('participantRoleChanged'); + }); + + it('participants info', + async () => { + const { p1, roomName, webhooksProxy } = context; + const roomsInfo = (await p1.getIframeAPI().getRoomsInfo()).rooms[0]; + + expect(roomsInfo).toBeDefined(); + expect(roomsInfo.isMainRoom).toBeTrue(); + + expect(roomsInfo.id).toBeDefined(); + const { node: roomNode } = parseJid(roomsInfo.id); + + expect(roomNode).toBe(roomName); + + const { node, resource } = parseJid(roomsInfo.jid); + + context.conferenceJid = roomsInfo.jid.substring(0, roomsInfo.jid.indexOf('/')); + + const p1EpId = await p1.getEndpointId(); + + expect(node).toBe(roomName); + expect(resource).toBe(p1EpId); + + expect(roomsInfo.participants.length).toBe(2); + expect(await p1.getIframeAPI().getNumberOfParticipants()).toBe(2); + + if (webhooksProxy) { + // ROOM_CREATED webhook + // @ts-ignore + const event: { + data: { + conference: string; + isBreakout: boolean; + }; + eventType: string; + } = await context.webhooksProxy.waitForEvent('ROOM_CREATED'); + + expect('ROOM_CREATED').toBe(event.eventType); + expect(event.data.conference).toBe(context.conferenceJid); + expect(event.data.isBreakout).toBe(false); + } + } + ); + + it('participants pane', async () => { + const { p1 } = context; + + await p1.switchToAPI(); + + expect(await p1.getIframeAPI().isParticipantsPaneOpen()).toBe(false); + + await p1.getIframeAPI().addEventListener('participantsPaneToggled'); + await p1.getIframeAPI().executeCommand('toggleParticipantsPane', true); + + expect(await p1.getIframeAPI().isParticipantsPaneOpen()).toBe(true); + expect((await p1.getIframeAPI().getEventResult('participantsPaneToggled'))?.open).toBe(true); + + await p1.getIframeAPI().executeCommand('toggleParticipantsPane', false); + expect(await p1.getIframeAPI().isParticipantsPaneOpen()).toBe(false); + expect((await p1.getIframeAPI().getEventResult('participantsPaneToggled'))?.open).toBe(false); + }); + + it('grant moderator', async () => { + const { p1, p2, webhooksProxy } = context; + const p2EpId = await p2.getEndpointId(); + + await p1.getIframeAPI().executeCommand('grantModerator', p2EpId); + + await p2.driver.waitUntil(async () => await p2.getIframeAPI().getEventResult('isModerator'), { + timeout: 3000, + timeoutMsg: 'Moderator role not granted' + }); + + const event1 = await p1.getIframeAPI().getEventResult('participantRoleChanged'); + + expect(event1?.id).toBe(p2EpId); + expect(event1?.role).toBe('moderator'); + + const event2 = await p2.getIframeAPI().getEventResult('participantRoleChanged'); + + expect(event2?.id).toBe(p2EpId); + expect(event2?.role).toBe('moderator'); + + if (webhooksProxy) { + // ROLE_CHANGED webhook + // @ts-ignore + const event: { + data: { + grantedBy: { + participantId: string; + }; + grantedTo: { + participantId: string; + }; + role: string; + }; + eventType: string; + } = await context.webhooksProxy.waitForEvent('ROLE_CHANGED'); + + expect('ROLE_CHANGED').toBe(event.eventType); + expect(event.data.role).toBe('moderator'); + expect(event.data.grantedBy.participantId).toBe(await p1.getEndpointId()); + expect(event.data.grantedTo.participantId).toBe(await p2.getEndpointId()); + } + }); + + it('kick participant', async () => { + const { p1, p2 } = context; + const p1EpId = await p1.getEndpointId(); + const p2EpId = await p2.getEndpointId(); + + await p1.switchInPage(); + await p2.switchInPage(); + + const p1DisplayName = await p1.getLocalDisplayName(); + const p2DisplayName = await p2.getLocalDisplayName(); + + await p1.switchToAPI(); + await p2.switchToAPI(); + + await p1.getIframeAPI().addEventListener('participantKickedOut'); + await p2.getIframeAPI().addEventListener('participantKickedOut'); + await p2.getIframeAPI().addEventListener('videoConferenceLeft'); + + await p1.getIframeAPI().executeCommand('kickParticipant', p2EpId); + + const eventP1 = await p1.driver.waitUntil(async () => + await p1.getIframeAPI().getEventResult('participantKickedOut'), { + timeout: 2000, + timeoutMsg: 'participantKickedOut event not received on participant1 side' + }); + const eventP2 = await p2.driver.waitUntil(async () => + await p2.getIframeAPI().getEventResult('participantKickedOut'), { + timeout: 2000, + timeoutMsg: 'participantKickedOut event not received on participant2 side' + }); + + await checkParticipantLeftHook(p2, 'kicked'); + + expect(eventP1).toBeDefined(); + expect(eventP2).toBeDefined(); + + expect(isEqual(eventP1, { + kicked: { + id: p2EpId, + local: false, + name: p2DisplayName + }, + kicker: { + id: p1EpId, + local: true, + name: p1DisplayName + } + })).toBeTrue(); + + expect(isEqual(eventP2, { + kicked: { + id: 'local', + local: true, + name: p2DisplayName + }, + kicker: { + id: p1EpId, + name: p1DisplayName + } + })).toBeTrue(); + + const eventConferenceLeftP2 = await p2.driver.waitUntil(async () => + await p2.getIframeAPI().getEventResult('videoConferenceLeft'), { + timeout: 2000, + timeoutMsg: 'videoConferenceLeft not received' + }); + + expect(eventConferenceLeftP2).toBeDefined(); + expect(eventConferenceLeftP2.roomName).toBe(context.roomName); + }); + + it('join after kick', async () => { + const { p1, webhooksProxy } = context; + + await p1.getIframeAPI().addEventListener('participantJoined'); + await p1.getIframeAPI().addEventListener('participantMenuButtonClick'); + + webhooksProxy?.clearCache(); + + // join again + await ensureTwoParticipants(context); + + if (webhooksProxy) { + // PARTICIPANT_JOINED webhook + // @ts-ignore + const event: { + data: { + conference: string; + isBreakout: boolean; + moderator: boolean; + name: string; + participantId: string; + }; + eventType: string; + } = await context.webhooksProxy.waitForEvent('PARTICIPANT_JOINED'); + + expect('PARTICIPANT_JOINED').toBe(event.eventType); + expect(event.data.conference).toBe(context.conferenceJid); + expect(event.data.isBreakout).toBe(false); + expect(event.data.moderator).toBe(false); + expect(event.data.name).toBe(await context.p2.getLocalDisplayName()); + expect(event.data.participantId).toBe(await context.p2.getEndpointId()); + } + + await p1.switchToAPI(); + + const event = await p1.driver.waitUntil(async () => + await p1.getIframeAPI().getEventResult('participantJoined'), { + timeout: 2000, + timeoutMsg: 'participantJoined not received' + }); + + const { p2 } = context; + const p2DisplayName = await p2.getLocalDisplayName(); + + expect(event).toBeDefined(); + expect(event.id).toBe(await p2.getEndpointId()); + expect(event.displayName).toBe(p2DisplayName); + expect(event.formattedDisplayName).toBe(p2DisplayName); + + }); + + it('overwrite names', async () => { + const { p1, p2 } = context; + + const p1EpId = await p1.getEndpointId(); + const p2EpId = await p2.getEndpointId(); + + const newP1Name = 'p1'; + const newP2Name = 'p2'; + const newNames: ({ id: string; name: string; })[] = [ { + id: p2EpId, + name: newP2Name + }, { + id: p1EpId, + name: newP1Name + } ]; + + await p1.getIframeAPI().executeCommand('overwriteNames', newNames); + + await p1.switchInPage(); + + expect(await p1.getLocalDisplayName()).toBe(newP1Name); + + expect(await p1.getFilmstrip().getRemoteDisplayName(p2EpId)).toBe(newP2Name); + + }); + + it('hangup', async () => { + const { p1, p2 } = context; + + await p1.switchToAPI(); + await p2.switchToAPI(); + + await p2.getIframeAPI().addEventListener('videoConferenceLeft'); + await p2.getIframeAPI().addEventListener('readyToClose'); + + await p2.getIframeAPI().executeCommand('hangup'); + + const eventConferenceLeftP2 = await p2.driver.waitUntil(async () => + await p2.getIframeAPI().getEventResult('videoConferenceLeft'), { + timeout: 2000, + timeoutMsg: 'videoConferenceLeft not received' + }); + + expect(eventConferenceLeftP2).toBeDefined(); + expect(eventConferenceLeftP2.roomName).toBe(context.roomName); + + await checkParticipantLeftHook(p2, 'left'); + + const eventReadyToCloseP2 = await p2.driver.waitUntil(async () => + await p2.getIframeAPI().getEventResult('readyToClose'), { + timeout: 2000, + timeoutMsg: 'readyToClose not received' + }); + + expect(eventReadyToCloseP2).toBeDefined(); + }); + + it('dispose conference', async () => { + const { p1, webhooksProxy } = context; + + await p1.switchToAPI(); + + await p1.getIframeAPI().addEventListener('videoConferenceLeft'); + await p1.getIframeAPI().addEventListener('readyToClose'); + + await p1.getIframeAPI().executeCommand('hangup'); + + const eventConferenceLeft = await p1.driver.waitUntil(async () => + await p1.getIframeAPI().getEventResult('videoConferenceLeft'), { + timeout: 2000, + timeoutMsg: 'videoConferenceLeft not received' + }); + + expect(eventConferenceLeft).toBeDefined(); + expect(eventConferenceLeft.roomName).toBe(context.roomName); + + await checkParticipantLeftHook(p1, 'left'); + if (webhooksProxy) { + // ROOM_DESTROYED webhook + // @ts-ignore + const event: { + data: { + conference: string; + isBreakout: boolean; + }; + eventType: string; + } = await context.webhooksProxy.waitForEvent('ROOM_DESTROYED'); + + expect('ROOM_DESTROYED').toBe(event.eventType); + expect(event.data.conference).toBe(context.conferenceJid); + expect(event.data.isBreakout).toBe(false); + } + + const eventReadyToClose = await p1.driver.waitUntil(async () => + await p1.getIframeAPI().getEventResult('readyToClose'), { + timeout: 2000, + timeoutMsg: 'readyToClose not received' + }); + + expect(eventReadyToClose).toBeDefined(); + + // dispose + await p1.getIframeAPI().dispose(); + + // check there is no iframe on the page + await p1.driver.$('iframe').waitForExist({ + reverse: true, + timeout: 2000, + timeoutMsg: 'iframe is still on the page' + }); + }); +}); diff --git a/tests/specs/3way/activeSpeaker.spec.ts b/tests/specs/3way/activeSpeaker.spec.ts index aa1bdebead34..0e637307ab45 100644 --- a/tests/specs/3way/activeSpeaker.spec.ts +++ b/tests/specs/3way/activeSpeaker.spec.ts @@ -1,10 +1,8 @@ /* global APP */ import type { Participant } from '../../helpers/Participant'; -import { IContext, ensureThreeParticipants, toggleMuteAndCheck } from '../../helpers/participants'; +import { ensureThreeParticipants, toggleMuteAndCheck } from '../../helpers/participants'; describe('ActiveSpeaker ', () => { - const context = {} as IContext; - it('testActiveSpeaker', async () => { await ensureThreeParticipants(context); @@ -64,7 +62,7 @@ async function testActiveSpeaker( await otherParticipant1Driver.waitUntil( () => otherParticipant1Driver.execute((id: string) => APP.UI.getLargeVideoID() === id, speakerEndpoint), { - timeout: 30_1000, // 30 seconds + timeout: 30_000, // 30 seconds timeoutMsg: 'Active speaker not displayed on large video.' }); diff --git a/tests/tsconfig.json b/tests/tsconfig.json index 8fa446e53037..2749ca81da3d 100644 --- a/tests/tsconfig.json +++ b/tests/tsconfig.json @@ -1,5 +1,9 @@ { - "include": ["**/*.ts", "../globals.d.ts"], + "include": [ + "**/*.ts", + "../globals.d.ts", + "./globals.d.ts" + ], "extends": "../tsconfig.web", "compilerOptions": { "types": [ diff --git a/tests/wdio.conf.ts b/tests/wdio.conf.ts index 3d2fa1b97273..22967dd4a1a7 100644 --- a/tests/wdio.conf.ts +++ b/tests/wdio.conf.ts @@ -1,9 +1,11 @@ import AllureReporter from '@wdio/allure-reporter'; import { multiremotebrowser } from '@wdio/globals'; import { Buffer } from 'buffer'; +import path from 'node:path'; import process from 'node:process'; import { getLogs, initLogger, logInfo } from './helpers/browserLogger'; +import { IContext } from './helpers/types'; // eslint-disable-next-line @typescript-eslint/no-var-requires const allure = require('allure-commandline'); @@ -24,7 +26,7 @@ const chromeArgs = [ '--no-sandbox', '--disable-dev-shm-usage', '--disable-setuid-sandbox', - '--use-file-for-fake-audio-capture=tests/resources/fakeAudioStream.wav' + `--use-file-for-fake-audio-capture=${process.env.REMOTE_RESOURCE_PATH || 'tests/resources'}/fakeAudioStream.wav` ]; if (process.env.RESOLVER_RULES) { @@ -105,6 +107,7 @@ export const config: WebdriverIO.MultiremoteConfig = { prefs: chromePreferences }, 'wdio:exclude': [ + 'specs/alone/**', 'specs/2way/**' ] } @@ -117,6 +120,8 @@ export const config: WebdriverIO.MultiremoteConfig = { prefs: chromePreferences }, 'wdio:exclude': [ + 'specs/alone/**', + 'specs/2way/**', 'specs/3way/**' ] } @@ -157,10 +162,37 @@ export const config: WebdriverIO.MultiremoteConfig = { * * @returns {Promise} */ - before() { - multiremotebrowser.instances.forEach((instance: string) => { - initLogger(multiremotebrowser.getInstance(instance), instance, TEST_RESULTS_DIR); - }); + async before() { + await Promise.all(multiremotebrowser.instances.map(async (instance: string) => { + const bInstance = multiremotebrowser.getInstance(instance); + + initLogger(bInstance, instance, TEST_RESULTS_DIR); + + if (bInstance.isFirefox) { + return; + } + + // if (process.env.GRID_HOST_URL) { + // TODO: make sure we use uploadFile only with chrome (it does not work with FF), + // we need to test it with the grid and FF, does it work there + const rpath = await bInstance.uploadFile('tests/resources/iframeAPITest.html'); + + // @ts-ignore + bInstance.iframePageBase = `file://${path.dirname(rpath)}`; + })); + + const globalAny: any = global; + + globalAny.context = {} as IContext; + + globalAny.context.jwtPrivateKeyPath = process.env.JWT_PRIVATE_KEY_PATH; + globalAny.context.jwtKid = process.env.JWT_KID; + }, + + after() { + if (context.webhooksProxy) { + context.webhooksProxy.disconnect(); + } }, /** @@ -270,4 +302,4 @@ export const config: WebdriverIO.MultiremoteConfig = { }); }); } -}; +} as WebdriverIO.MultiremoteConfig; diff --git a/tests/wdio.dev.conf.ts b/tests/wdio.dev.conf.ts new file mode 100644 index 000000000000..f78ced041993 --- /dev/null +++ b/tests/wdio.dev.conf.ts @@ -0,0 +1,11 @@ +// wdio.dev.conf.ts +// extends te main configuration file for the development environment (make dev) +// it will connect to the webpack-dev-server running locally on port 8080 +import { deepmerge } from 'deepmerge-ts'; + +// @ts-ignore +import { config as defaultConfig } from './wdio.conf.ts'; + +export const config = deepmerge(defaultConfig, { + baseUrl: 'https://127.0.0.1:8080/torture' +}, { clone: false }); diff --git a/tests/wdio.firefox.conf.ts b/tests/wdio.firefox.conf.ts new file mode 100644 index 000000000000..fd3c221bcdf5 --- /dev/null +++ b/tests/wdio.firefox.conf.ts @@ -0,0 +1,39 @@ +// wdio.firefox.conf.ts +// extends te main configuration file changing first participant to be Firefox +import { merge } from 'lodash-es'; +import process from 'node:process'; + +// @ts-ignore +import { config as defaultConfig } from './wdio.conf.ts'; + +const ffArgs = []; + +const ffPreferences = { + 'intl.accept_languages': 'en-US', + 'media.navigator.permission.disabled': true, + 'media.navigator.streams.fake': true, + 'media.autoplay.default': 0 +}; + +if (process.env.HEADLESS === 'true') { + ffArgs.push('--headless'); +} + +export const config = merge(defaultConfig, { + exclude: [ + 'specs/2way/iFrameParticipantsPresence.spec.ts', // FF does not support uploading files (uploadFile) + 'specs/3way/activeSpeaker.spec.ts' // FF does not support setting a file as mic input + ], + capabilities: { + participant1: { + capabilities: { + browserName: 'firefox', + 'moz:firefoxOptions': { + args: ffArgs, + prefs: ffPreferences + }, + acceptInsecureCerts: process.env.ALLOW_INSECURE_CERTS === 'true' + } + } + } +}, { clone: false }); diff --git a/tests/wdio.grid.conf.ts b/tests/wdio.grid.conf.ts new file mode 100644 index 000000000000..09166c0ffc45 --- /dev/null +++ b/tests/wdio.grid.conf.ts @@ -0,0 +1,18 @@ +// wdio.grid.conf.ts +// extends the main configuration file to add the selenium grid address +import { deepmerge } from 'deepmerge-ts'; +import { URL } from 'url'; + +// @ts-ignore +import { config as defaultConfig } from './wdio.conf.ts'; + +const gridUrl = new URL(process.env.GRID_HOST_URL as string); +const protocol = gridUrl.protocol.replace(':', ''); + +export const config = deepmerge(defaultConfig, { + protocol, + hostname: gridUrl.hostname, + port: gridUrl.port ? parseInt(gridUrl.port, 10) // Convert port to number + : protocol === 'http' ? 80 : 443, + path: gridUrl.pathname +}, { clone: false }); diff --git a/webpack.config.js b/webpack.config.js index 6459a3dbd6e4..a5d19f5308cd 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -253,7 +253,10 @@ function getDevServerConfig() { ], server: process.env.CODESPACES ? 'http' : 'https', static: { - directory: process.cwd() + directory: process.cwd(), + watch: { + ignored: file => file.endsWith('.log') + } } }; }