diff --git a/package-lock.json b/package-lock.json index f6c57856c3..d830c8407c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -140,6 +140,7 @@ "@types/moment-duration-format": "2.2.6", "@types/offscreencanvas": "2019.7.2", "@types/pixelmatch": "5.2.5", + "@types/pretty": "2.0.3", "@types/punycode": "2.1.0", "@types/react": "17.0.14", "@types/react-dom": "17.0.14", @@ -176,6 +177,7 @@ "jsonwebtoken": "9.0.2", "metro-react-native-babel-preset": "0.77.0", "patch-package": "6.4.7", + "pretty": "2.0.0", "process": "0.11.10", "sass": "1.26.8", "style-loader": "3.3.1", @@ -4661,6 +4663,12 @@ "node": ">= 8" } }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -7219,6 +7227,12 @@ "@types/node": "*" } }, + "node_modules/@types/pretty": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/pretty/-/pretty-2.0.3.tgz", + "integrity": "sha512-xR96pShNlrxLd3gZqzCnbaAmbYhiRYjW51CDFjektZemqpBZBAAkMwxm4gBraJP/xSgKcsQhLXdlXOwDNWo4VQ==", + "dev": true + }, "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", @@ -8659,6 +8673,15 @@ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", "deprecated": "Use your platform's native atob() and btoa() methods instead" }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -10624,6 +10647,42 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "node_modules/condense-newlines": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/condense-newlines/-/condense-newlines-0.2.1.tgz", + "integrity": "sha512-P7X+QL9Hb9B/c8HI5BFFKmjgBu2XpQuF98WZ9XkO+dBGgk5XgwiQz7o1SmpglNWId3581UcS0SFAWfoIhMHPfg==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-whitespace": "^0.3.0", + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/condense-newlines/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, "node_modules/connect": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", @@ -11573,6 +11632,57 @@ "node": "^16.13.0 || >=18.0.0" } }, + "node_modules/editorconfig": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "dev": true, + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -12922,6 +13032,18 @@ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -14451,6 +14573,12 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, "node_modules/inline-style-prefixer": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-6.0.4.tgz", @@ -14708,6 +14836,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -14987,6 +15124,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-whitespace": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-whitespace/-/is-whitespace-0.3.0.tgz", + "integrity": "sha512-RydPhl4S6JwAyj0JJjshWJEFG6hNye3pZFBRZaTUfZFwGHxzppNaNOVgQuS/E/SlhrApuMXrpnK1EEIXfdo3Dg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -15754,6 +15900,80 @@ "resolved": "https://registry.npmjs.org/jquery-i18next/-/jquery-i18next-1.2.1.tgz", "integrity": "sha512-UNcw3rgxoKjGEg4w23FEn2h3OlPJU7rPzsgDuXDBZktIzeiVbJohs9Cv9hj8oP8KNfBRKOoErL/OVxg2FaAR4g==" }, + "node_modules/js-beautify": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", + "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==", + "dev": true, + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.3.3", + "js-cookie": "^3.0.5", + "nopt": "^7.2.0" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-beautify/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/js-beautify/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-beautify/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/js-cookie": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", @@ -17945,6 +18165,21 @@ "url": "https://github.com/sponsors/antelle" } }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/normalize-package-data": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", @@ -18909,6 +19144,20 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pretty/-/pretty-2.0.0.tgz", + "integrity": "sha512-G9xUchgTEiNpormdYBl+Pha50gOUovT18IvAe7EYMZ1/f9W/WWMPRn+xI68yXNMUk3QXHDwo/1wV/4NejVNe1w==", + "dev": true, + "dependencies": { + "condense-newlines": "^0.2.1", + "extend-shallow": "^2.0.1", + "js-beautify": "^1.6.12" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -19040,6 +19289,12 @@ "node": ">=0.10" } }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -27445,6 +27700,12 @@ "fastq": "^1.6.0" } }, + "@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true + }, "@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -29247,6 +29508,12 @@ "@types/node": "*" } }, + "@types/pretty": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/pretty/-/pretty-2.0.3.tgz", + "integrity": "sha512-xR96pShNlrxLd3gZqzCnbaAmbYhiRYjW51CDFjektZemqpBZBAAkMwxm4gBraJP/xSgKcsQhLXdlXOwDNWo4VQ==", + "dev": true + }, "@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", @@ -30332,6 +30599,12 @@ "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" }, + "abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true + }, "abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -31769,6 +32042,38 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "condense-newlines": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/condense-newlines/-/condense-newlines-0.2.1.tgz", + "integrity": "sha512-P7X+QL9Hb9B/c8HI5BFFKmjgBu2XpQuF98WZ9XkO+dBGgk5XgwiQz7o1SmpglNWId3581UcS0SFAWfoIhMHPfg==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-whitespace": "^0.3.0", + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "requires": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, "connect": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", @@ -32449,6 +32754,44 @@ } } }, + "editorconfig": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "dev": true, + "requires": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true + }, + "minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -33435,6 +33778,15 @@ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, "external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -34547,6 +34899,12 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, "inline-style-prefixer": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-6.0.4.tgz", @@ -34733,6 +35091,12 @@ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==" }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -34897,6 +35261,12 @@ "call-bind": "^1.0.2" } }, + "is-whitespace": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-whitespace/-/is-whitespace-0.3.0.tgz", + "integrity": "sha512-RydPhl4S6JwAyj0JJjshWJEFG6hNye3pZFBRZaTUfZFwGHxzppNaNOVgQuS/E/SlhrApuMXrpnK1EEIXfdo3Dg==", + "dev": true + }, "is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -35454,6 +35824,59 @@ "resolved": "https://registry.npmjs.org/jquery-i18next/-/jquery-i18next-1.2.1.tgz", "integrity": "sha512-UNcw3rgxoKjGEg4w23FEn2h3OlPJU7rPzsgDuXDBZktIzeiVbJohs9Cv9hj8oP8KNfBRKOoErL/OVxg2FaAR4g==" }, + "js-beautify": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", + "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==", + "dev": true, + "requires": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.3.3", + "js-cookie": "^3.0.5", + "nopt": "^7.2.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + } + }, + "js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "dev": true + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "js-cookie": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", @@ -37117,6 +37540,15 @@ "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==" }, + "nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "requires": { + "abbrev": "^2.0.0" + } + }, "normalize-package-data": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", @@ -37784,6 +38216,17 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "pretty": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pretty/-/pretty-2.0.0.tgz", + "integrity": "sha512-G9xUchgTEiNpormdYBl+Pha50gOUovT18IvAe7EYMZ1/f9W/WWMPRn+xI68yXNMUk3QXHDwo/1wV/4NejVNe1w==", + "dev": true, + "requires": { + "condense-newlines": "^0.2.1", + "extend-shallow": "^2.0.1", + "js-beautify": "^1.6.12" + } + }, "pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -37885,6 +38328,12 @@ "integrity": "sha512-qYNxyMj1JeW54i/EWEFsM1cVwxJbtgPp8+0Wg9XjNaK6VE/c4oRi6PNu5p7w1mNXEIQIjV5Wwn8v8Gz82/QzdQ==", "dev": true }, + "proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", diff --git a/package.json b/package.json index a80f4a28e9..758708054d 100644 --- a/package.json +++ b/package.json @@ -146,6 +146,7 @@ "@types/moment-duration-format": "2.2.6", "@types/offscreencanvas": "2019.7.2", "@types/pixelmatch": "5.2.5", + "@types/pretty": "2.0.3", "@types/punycode": "2.1.0", "@types/react": "17.0.14", "@types/react-dom": "17.0.14", @@ -182,6 +183,7 @@ "jsonwebtoken": "9.0.2", "metro-react-native-babel-preset": "0.77.0", "patch-package": "6.4.7", + "pretty": "2.0.0", "process": "0.11.10", "sass": "1.26.8", "style-loader": "3.3.1", diff --git a/react/features/display-name/components/web/StageParticipantNameLabel.tsx b/react/features/display-name/components/web/StageParticipantNameLabel.tsx index c8d3adfc37..1eb9242890 100644 --- a/react/features/display-name/components/web/StageParticipantNameLabel.tsx +++ b/react/features/display-name/components/web/StageParticipantNameLabel.tsx @@ -106,7 +106,8 @@ const StageParticipantNameLabel = () => { classes.badgeContainer, toolboxVisible && classes.containerElevated, _isScreenShareParticipant && classes.screenSharing - ) }> + ) } + data-testid = 'stage-display-name' > ); diff --git a/tests/helpers/Participant.ts b/tests/helpers/Participant.ts index d527578d8f..263f08848f 100644 --- a/tests/helpers/Participant.ts +++ b/tests/helpers/Participant.ts @@ -7,11 +7,12 @@ import { urlObjectToString } from '../../react/features/base/util/uri'; import Filmstrip from '../pageobjects/Filmstrip'; import IframeAPI from '../pageobjects/IframeAPI'; import ParticipantsPane from '../pageobjects/ParticipantsPane'; +import SettingsDialog from '../pageobjects/SettingsDialog'; import Toolbar from '../pageobjects/Toolbar'; import VideoQualityDialog from '../pageobjects/VideoQualityDialog'; import { LOG_PREFIX, logInfo } from './browserLogger'; -import { IContext } from './types'; +import { IContext, IJoinOptions } from './types'; /** * Participant. @@ -112,21 +113,25 @@ export class Participant { * Joins conference. * * @param {IContext} context - The context. - * @param {boolean} skipInMeetingChecks - Whether to skip in meeting checks. + * @param {IJoinOptions} options - Options for joining. * @returns {Promise} */ - async joinConference(context: IContext, skipInMeetingChecks = false): Promise { + async joinConference(context: IContext, options: IJoinOptions = {}): Promise { const config = { room: context.roomName, configOverwrite: this.config, interfaceConfigOverwrite: { SHOW_CHROME_EXTENSION_BANNER: false - }, - userInfo: { - displayName: this._name } }; + if (!options.skipDisplayName) { + // @ts-ignore + config.userInfo = { + displayName: this._name + }; + } + if (context.iframeAPI) { config.room = 'iframeAPITest.html'; } @@ -168,7 +173,7 @@ export class Participant { await this.waitToJoinMUC(); - await this.postLoadProcess(skipInMeetingChecks); + await this.postLoadProcess(options.skipInMeetingChecks); } /** @@ -178,7 +183,7 @@ export class Participant { * @returns {Promise} * @private */ - private async postLoadProcess(skipInMeetingChecks: boolean): Promise { + private async postLoadProcess(skipInMeetingChecks = false): Promise { const driver = this.driver; const parallel = []; @@ -227,7 +232,7 @@ export class Participant { */ async waitForPageToLoad(): Promise { return this.driver.waitUntil( - () => this.driver.execute(() => document.readyState === 'complete'), + async () => await this.driver.execute(() => document.readyState === 'complete'), { timeout: 30_000, // 30 seconds timeoutMsg: 'Timeout waiting for Page Load Request to complete.' @@ -238,8 +243,8 @@ export class Participant { /** * Checks if the participant is in the meeting. */ - isInMuc() { - return this.driver.execute(() => APP.conference.isJoined()); + async isInMuc() { + return await this.driver.execute(() => typeof APP !== 'undefined' && APP.conference?.isJoined()); } /** @@ -266,7 +271,7 @@ export class Participant { const driver = this.driver; return driver.waitUntil(async () => - driver.execute(() => APP.conference.getConnectionState() === 'connected'), { + await driver.execute(() => APP.conference.getConnectionState() === 'connected'), { timeout: 15_000, timeoutMsg: 'expected ICE to be connected for 15s' }); @@ -281,7 +286,7 @@ export class Participant { const driver = this.driver; return driver.waitUntil(async () => - driver.execute(() => { + await driver.execute(() => { const stats = APP.conference.getStats(); const bitrateMap = stats?.bitrate || {}; const rtpStats = { @@ -306,7 +311,7 @@ export class Participant { const driver = this.driver; return driver.waitUntil(async () => - driver.execute(count => APP.conference.getNumberOfParticipantsWithTracks() >= count, number), { + await driver.execute(count => APP.conference.getNumberOfParticipantsWithTracks() >= count, number), { timeout: 15_000, timeoutMsg: 'expected remote streams in 15s' }); @@ -348,6 +353,15 @@ export class Participant { return new VideoQualityDialog(this); } + /** + * Returns the settings Dialog. + * + * @returns {SettingsDialog} + */ + getSettingsDialog(): SettingsDialog { + return new SettingsDialog(this); + } + /** * Switches to the iframe API context */ @@ -371,6 +385,13 @@ export class Participant { return new IframeAPI(this); } + /** + * Hangups the participant by leaving the page. base.html is an empty page on all deployments. + */ + async hangup() { + await this.driver.url('/base.html'); + } + /** * Returns the local display name. */ @@ -383,4 +404,92 @@ export class Participant { return await localDisplayName.getText(); } + + /** + * Gets avatar SRC attribute for the one displayed on local video thumbnail. + */ + async getLocalVideoAvatar() { + const avatar + = this.driver.$('//span[@id="localVideoContainer"]//img[contains(@class,"userAvatar")]'); + + return await avatar.isExisting() ? await avatar.getAttribute('src') : null; + } + + /** + * Gets avatar SRC attribute for the one displayed on large video. + */ + async getLargeVideoAvatar() { + const avatar = this.driver.$('//img[@id="dominantSpeakerAvatar"]'); + + return await avatar.isExisting() ? await avatar.getAttribute('src') : null; + } + + /** + * Returns resource part of the JID of the user who is currently displayed in the large video area. + */ + async getLargeVideoResource() { + return await this.driver.execute(() => APP.UI.getLargeVideoID()); + } + + /** + * Makes sure that the avatar is displayed in the local thumbnail and that the video is not displayed. + * There are 3 options for avatar: + * - defaultAvatar: true - the default avatar (with grey figure) is used + * - image: true - the avatar is an image set in the settings + * - defaultAvatar: false, image: false - the avatar is produced from the initials of the display name + */ + async assertThumbnailShowsAvatar( + participant: Participant, reverse = false, defaultAvatar = false, image = false): Promise { + const id = participant === this + ? 'localVideoContainer' : `participant_${await participant.getEndpointId()}`; + + const xpath = defaultAvatar + ? `//span[@id='${id}']//div[contains(@class,'userAvatar') and contains(@class, 'defaultAvatar')]` + : `//span[@id="${id}"]//${image ? 'img' : 'div'}[contains(@class,"userAvatar")]`; + + await this.driver.$(xpath).waitForDisplayed({ + reverse, + timeout: 2000, + timeoutMsg: `Avatar is ${reverse ? '' : 'not'} displayed in the local thumbnail for ${participant.name}` + }); + + await this.driver.$(`//span[@id="${id}"]//video`).waitForDisplayed({ + reverse: !reverse, + timeout: 2000, + timeoutMsg: `Video is ${reverse ? 'not' : ''} displayed in the local thumbnail for ${participant.name}` + }); + } + + /** + * Makes sure that the default avatar is used. + */ + async assertDefaultAvatarExist(participant: Participant): Promise { + const id = participant === this + ? 'localVideoContainer' : `participant_${await participant.getEndpointId()}`; + + await this.driver.$( + `//span[@id='${id}']//div[contains(@class,'userAvatar') and contains(@class, 'defaultAvatar')]`) + .waitForExist({ + timeout: 2000, + timeoutMsg: `Default avatar does not exist for ${participant.name}` + }); + } + + /** + * Makes sure that the local video is displayed in the local thumbnail and that the avatar is not displayed. + */ + async asserLocalThumbnailShowsVideo(): Promise { + await this.assertThumbnailShowsAvatar(this, true); + } + + /** + * Make sure a display name is visible on the stage. + * @param value + */ + async assertDisplayNameVisibleOnStage(value: string) { + const displayNameEl = this.driver.$('div[data-testid="stage-display-name"]'); + + expect(await displayNameEl.isDisplayed()).toBeTrue(); + expect(await displayNameEl.getText()).toBe(value); + } } diff --git a/tests/helpers/participants.ts b/tests/helpers/participants.ts index 1b5d78151b..d411d6c4f3 100644 --- a/tests/helpers/participants.ts +++ b/tests/helpers/participants.ts @@ -5,7 +5,7 @@ import { v4 as uuidv4 } from 'uuid'; import { Participant } from './Participant'; import WebhookProxy from './WebhookProxy'; -import { IContext } from './types'; +import { IContext, IJoinOptions } from './types'; /** * Generate a random room name. @@ -31,16 +31,20 @@ function generateRandomRoomName(): string { * Ensure that there is on participant. * * @param {IContext} context - The context. + * @param {IJoinOptions} options - The options to use when joining the participant. * @returns {Promise} */ -export async function ensureOneParticipant(context: IContext): Promise { +export async function ensureOneParticipant(context: IContext, options?: IJoinOptions): Promise { if (!context.roomName) { context.roomName = generateRandomRoomName(); } context.p1 = new Participant('participant1'); - await context.p1.joinConference(context, true); + await context.p1.joinConference(context, { + ...options, + skipInMeetingChecks: true + }); } /** @@ -80,9 +84,10 @@ export async function ensureThreeParticipants(context: IContext): Promise * Ensure that there are two participants. * * @param {Object} context - The context. + * @param {IJoinOptions} options - The options to join. * @returns {Promise} */ -export async function ensureTwoParticipants(context: IContext): Promise { +export async function ensureTwoParticipants(context: IContext, options?: IJoinOptions): Promise { if (!context.roomName) { context.roomName = generateRandomRoomName(); } @@ -98,12 +103,15 @@ export async function ensureTwoParticipants(context: IContext): Promise { // make sure the first participant is moderator, if supported by deployment await _joinParticipant(p1DisplayName, context.p1, p => { context.p1 = p; - }, true, token); + }, { + ...options, + skipInMeetingChecks: true + }, token); await Promise.all([ _joinParticipant('participant2', context.p2, p => { context.p2 = p; - }), + }, options), context.p1.waitForRemoteStreams(1), context.p2.waitForRemoteStreams(1) ]); @@ -114,24 +122,28 @@ export async function ensureTwoParticipants(context: IContext): Promise { * @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 {boolean} options - Join options. * @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, + options: IJoinOptions = {}, jwtToken?: string) { if (p) { - await p.switchInPage(); + if (context.iframeAPI) { + 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(); + if (context.iframeAPI) { + // 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'); @@ -144,7 +156,7 @@ async function _joinParticipant( // eslint-disable-line max-params // set the new participant instance, pass it to setter setter(newParticipant); - return newParticipant.joinConference(context, skipInMeetingChecks); + await newParticipant.joinConference(context, options); } /** @@ -157,13 +169,25 @@ async function _joinParticipant( // eslint-disable-line max-params * the mute state of {@code testee}. * @returns {Promise} */ -export async function toggleMuteAndCheck(testee: Participant, observer: Participant): Promise { +export async function muteAudioAndCheck(testee: Participant, observer: Participant): Promise { await testee.getToolbar().clickAudioMuteButton(); await observer.getFilmstrip().assertAudioMuteIconIsDisplayed(testee); await testee.getFilmstrip().assertAudioMuteIconIsDisplayed(testee); } +/** + * Starts the video on testee and check on observer. + * @param testee + * @param observer + */ +export async function unMuteVideoAndCheck(testee: Participant, observer: Participant): Promise { + await testee.getToolbar().clickVideoUnmuteButton(); + + await observer.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee, true); + await testee.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee, true); +} + /** * Get a JWT token for a moderator. */ diff --git a/tests/helpers/types.ts b/tests/helpers/types.ts index e75c3c9e91..4f292f72d2 100644 --- a/tests/helpers/types.ts +++ b/tests/helpers/types.ts @@ -13,3 +13,16 @@ export type IContext = { roomName: string; webhooksProxy: WebhookProxy; }; + +export type IJoinOptions = { + + /** + * Whether to skip setting display name. + */ + skipDisplayName?: boolean; + + /** + * Whether to skip in meeting checks like ice connected and send receive data. For single in meeting participant. + */ + skipInMeetingChecks?: boolean; +}; diff --git a/tests/pageobjects/BaseDialog.ts b/tests/pageobjects/BaseDialog.ts index bab64b9618..603a16d967 100644 --- a/tests/pageobjects/BaseDialog.ts +++ b/tests/pageobjects/BaseDialog.ts @@ -1,6 +1,7 @@ import { Participant } from '../helpers/Participant'; const CLOSE_BUTTON = 'modal-header-close-button'; +const OK_BUTTON = 'modal-dialog-ok-button'; /** * Base class for all dialogs. @@ -23,4 +24,11 @@ export default class BaseDialog { async clickCloseButton(): Promise { await this.participant.driver.$(`#${CLOSE_BUTTON}`).click(); } + + /** + * Clicks on the ok button. + */ + async clickOkButton(): Promise { + await this.participant.driver.$(`#${OK_BUTTON}`).click(); + } } diff --git a/tests/pageobjects/Filmstrip.ts b/tests/pageobjects/Filmstrip.ts index e9cc14919b..04f5ab5151 100644 --- a/tests/pageobjects/Filmstrip.ts +++ b/tests/pageobjects/Filmstrip.ts @@ -44,37 +44,6 @@ export default class Filmstrip { }); } - /** - * 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. @@ -86,4 +55,26 @@ export default class Filmstrip { return await remoteDisplayName.getText(); } + + /** + * Pins a participant by clicking on their thumbnail. + * @param participant The participant. + */ + async pinParticipant(participant: Participant) { + const id = participant === this.participant + ? 'localVideoContainer' : `participant_${await participant.getEndpointId()}`; + + await this.participant.driver.$(`//span[@id="${id}"]`).click(); + } + + /** + * Gets avatar SRC attribute for the one displayed on small video thumbnail. + * @param endpointId + */ + async getAvatar(endpointId: string) { + const elem = this.participant.driver.$( + `//span[@id='participant_${endpointId}']//img[contains(@class,'userAvatar')]`); + + return await elem.isExisting() ? elem.getAttribute('src') : null; + } } diff --git a/tests/pageobjects/IframeAPI.ts b/tests/pageobjects/IframeAPI.ts index 12f25f21fd..11a4458a90 100644 --- a/tests/pageobjects/IframeAPI.ts +++ b/tests/pageobjects/IframeAPI.ts @@ -20,7 +20,7 @@ export default class IframeAPI { * @param event */ async getEventResult(event: string): Promise { - return this.participant.driver.execute( + return await this.participant.driver.execute( eventName => { const result = window.jitsiAPI.test[eventName]; @@ -37,28 +37,29 @@ export default class 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); + return await this.participant.driver.execute( + (event, prefix) => { + 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; + }); + }, eventName, LOG_PREFIX); } /** * Returns an array of available rooms and details of it. */ async getRoomsInfo() { - return this.participant.driver.execute(() => window.jitsiAPI.getRoomsInfo()); + return await 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()); + return await this.participant.driver.execute(() => window.jitsiAPI.getNumberOfParticipants()); } /** @@ -67,7 +68,7 @@ export default class IframeAPI { * @param args The arguments. */ async executeCommand(command: string, ...args: any[]) { - return this.participant.driver.execute( + return await this.participant.driver.execute( (commandName, commandArgs) => window.jitsiAPI.executeCommand(commandName, ...commandArgs) , command, args); @@ -77,14 +78,14 @@ export default class IframeAPI { * Returns the current state of the participant's pane. */ async isParticipantsPaneOpen() { - return this.participant.driver.execute(() => window.jitsiAPI.isParticipantsPaneOpen()); + return await this.participant.driver.execute(() => window.jitsiAPI.isParticipantsPaneOpen()); } /** * Removes the embedded Jitsi Meet conference. */ async dispose() { - return this.participant.driver.execute(() => window.jitsiAPI.dispose()); + return await this.participant.driver.execute(() => window.jitsiAPI.dispose()); } } diff --git a/tests/pageobjects/ParticipantsPane.ts b/tests/pageobjects/ParticipantsPane.ts index a792eac9f6..64bd73fe8e 100644 --- a/tests/pageobjects/ParticipantsPane.ts +++ b/tests/pageobjects/ParticipantsPane.ts @@ -44,4 +44,35 @@ export default class ParticipantsPane { await this.participant.driver.$(`.${PARTICIPANTS_PANE}`).waitForDisplayed({ reverse: true }); } + + /** + * 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.isOpen(); + + if (!isOpen) { + await this.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.close(); + } + } } diff --git a/tests/pageobjects/SettingsDialog.ts b/tests/pageobjects/SettingsDialog.ts new file mode 100644 index 0000000000..e38ad03311 --- /dev/null +++ b/tests/pageobjects/SettingsDialog.ts @@ -0,0 +1,62 @@ +import BaseDialog from './BaseDialog'; + +const EMAIL_FIELD = '#setEmail'; +const SETTINGS_DIALOG_CONTENT = '.settings-pane'; +const X_PATH_PROFILE_TAB = '//div[contains(@class, "settings-dialog")]//*[text()="Profile"]'; + +/** + * The settings dialog. + */ +export default class SettingsDialog extends BaseDialog { + /** + * Waits for the settings dialog to be visible. + */ + async waitForDisplay() { + await this.participant.driver.$(SETTINGS_DIALOG_CONTENT).waitForDisplayed(); + } + + /** + * Displays a specific tab in the settings dialog. + * @param xpath + * @private + */ + private async openTab(xpath: string) { + const elem = this.participant.driver.$(xpath); + + await elem.waitForClickable(); + await elem.click(); + } + + /** + * Selects the Profile tab to be displayed. + */ + async openProfileTab() { + await this.openTab(X_PATH_PROFILE_TAB); + } + + /** + * Enters the passed in email into the email field. + * @param email + */ + async setEmail(email: string) { + await this.openProfileTab(); + + await this.participant.driver.$(EMAIL_FIELD).setValue(email); + } + + /** + * Returns the participant's email displayed in the settings dialog. + */ + async getEmail() { + await this.openProfileTab(); + + return await this.participant.driver.$(EMAIL_FIELD).getValue(); + } + + /** + * Clicks the OK button on the settings dialog to close the dialog and save any changes made. + */ + async submit() { + await this.clickOkButton(); + } +} diff --git a/tests/pageobjects/Toolbar.ts b/tests/pageobjects/Toolbar.ts index 6ae797deb1..11d85051fd 100644 --- a/tests/pageobjects/Toolbar.ts +++ b/tests/pageobjects/Toolbar.ts @@ -7,6 +7,7 @@ const CLOSE_PARTICIPANTS_PANE = 'Close participants pane'; const OVERFLOW_MENU = 'More actions menu'; const OVERFLOW = 'More actions'; const PARTICIPANTS = 'Open participants pane'; +const PROFILE = 'Edit your profile'; const VIDEO_QUALITY = 'Manage video quality'; const VIDEO_MUTE = 'Stop camera'; const VIDEO_UNMUTE = 'Start camera'; @@ -134,6 +135,13 @@ export default class Toolbar { return this.clickButtonInOverflowMenu(VIDEO_QUALITY); } + /** + * Clicks on the profile toolbar button which opens or closes the profile panel. + */ + async clickProfileButton(): Promise { + return this.clickButtonInOverflowMenu(PROFILE); + } + /** * Ensure the overflow menu is open and clicks on a specified button. * @param accessibilityLabel The accessibility label of the button to be clicked. @@ -203,4 +211,15 @@ export default class Toolbar { timeoutMsg: `Overflow menu is not ${visible ? 'visible' : 'hidden'}` }); } + + /** + * Gets the participant's avatar image element located in the toolbar. + */ + async getProfileImage() { + await this.openOverflowMenu(); + + const elem = this.participant.driver.$(`[aria-label^="${PROFILE}"] img`); + + return await elem.isExisting() ? await elem.getAttribute('src') : null; + } } diff --git a/tests/specs/2way/audioOnly.spec.ts b/tests/specs/2way/audioOnly.spec.ts index d0e778b7b9..17344cd020 100644 --- a/tests/specs/2way/audioOnly.spec.ts +++ b/tests/specs/2way/audioOnly.spec.ts @@ -19,9 +19,7 @@ describe('Audio only - ', () => { 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 }); + await context.p1.assertThumbnailShowsAvatar(context.p1); }); /** @@ -51,10 +49,10 @@ describe('Audio only - ', () => { */ async function verifyVideoMute(muted: boolean) { // Verify the observer sees the testee in the desired muted state. - await context.p2.getFilmstrip().assertVideoMuteIconIsDisplayed(context.p1, !muted); + await context.p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(context.p1, !muted); // Verify the testee sees itself in the desired muted state. - await context.p1.getFilmstrip().assertVideoMuteIconIsDisplayed(context.p1, !muted); + await context.p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(context.p1, !muted); } /** diff --git a/tests/specs/3way/activeSpeaker.spec.ts b/tests/specs/3way/activeSpeaker.spec.ts index 0e637307ab..180531b276 100644 --- a/tests/specs/3way/activeSpeaker.spec.ts +++ b/tests/specs/3way/activeSpeaker.spec.ts @@ -1,14 +1,14 @@ /* global APP */ import type { Participant } from '../../helpers/Participant'; -import { ensureThreeParticipants, toggleMuteAndCheck } from '../../helpers/participants'; +import { ensureThreeParticipants, muteAudioAndCheck } from '../../helpers/participants'; describe('ActiveSpeaker ', () => { it('testActiveSpeaker', async () => { await ensureThreeParticipants(context); - await toggleMuteAndCheck(context.p1, context.p2); - await toggleMuteAndCheck(context.p2, context.p1); - await toggleMuteAndCheck(context.p3, context.p1); + await muteAudioAndCheck(context.p1, context.p2); + await muteAudioAndCheck(context.p2, context.p1); + await muteAudioAndCheck(context.p3, context.p1); // participant1 becomes active speaker - check from participant2's perspective await testActiveSpeaker(context.p1, context.p2, context.p3); @@ -60,7 +60,8 @@ async function testActiveSpeaker( const otherParticipant1Driver = otherParticipant1.driver; await otherParticipant1Driver.waitUntil( - () => otherParticipant1Driver.execute((id: string) => APP.UI.getLargeVideoID() === id, speakerEndpoint), + async () => await otherParticipant1Driver.execute( + id => APP.UI.getLargeVideoID() === id, speakerEndpoint), { timeout: 30_000, // 30 seconds timeoutMsg: 'Active speaker not displayed on large video.' diff --git a/tests/specs/3way/avatarTest.spec.ts b/tests/specs/3way/avatarTest.spec.ts new file mode 100644 index 0000000000..47607449fa --- /dev/null +++ b/tests/specs/3way/avatarTest.spec.ts @@ -0,0 +1,189 @@ +import { + ensureThreeParticipants, + ensureTwoParticipants, + unMuteVideoAndCheck +} from '../../helpers/participants'; + +const EMAIL = 'support@jitsi.org'; +const HASH = '38f014e4b7dde0f64f8157d26a8c812e'; + +describe('Avatar - ', () => { + it('setup the meeting', async () => { + // Start p1 + await ensureTwoParticipants(context, { + skipDisplayName: true + }); + }); + + it('change and check', async () => { + // check default avatar for p1 on p2 + await context.p2.assertDefaultAvatarExist(context.p1); + + await context.p1.getToolbar().clickProfileButton(); + + const settings = context.p1.getSettingsDialog(); + + await settings.waitForDisplay(); + await settings.setEmail(EMAIL); + await settings.submit(); + + // check if the local avatar in the toolbar menu has changed + await context.p1.driver.waitUntil( + async () => (await context.p1.getToolbar().getProfileImage())?.includes(HASH), { + timeout: 3000, // give more time for the initial download of the image + timeoutMsg: 'Avatar has not changed for p1' + }); + + // check if the avatar in the local thumbnail has changed + expect(await context.p1.getLocalVideoAvatar()).toContain(HASH); + + const p1EndpointId = await context.p1.getEndpointId(); + + await context.p2.driver.waitUntil( + async () => (await context.p2.getFilmstrip().getAvatar(p1EndpointId))?.includes(HASH), { + timeout: 5000, + timeoutMsg: 'Avatar has not changed for p1 on p2' + }); + + // check if the avatar in the large video has changed + expect(await context.p2.getLargeVideoAvatar()).toContain(HASH); + + // we check whether the default avatar of participant2 is displayed on both sides + await context.p1.assertDefaultAvatarExist(context.p2); + await context.p2.assertDefaultAvatarExist(context.p2); + + // the problem on FF where we can send keys to the input field, + // and the m from the text can mute the call, check whether we are muted + await context.p2.getFilmstrip().assertAudioMuteIconIsDisplayed(context.p1, true); + }); + + it('when video muted', async () => { + await context.p2.hangup(); + + // Mute p1's video + await context.p1.getToolbar().clickVideoMuteButton(); + + await context.p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(context.p1); + + await context.p1.driver.waitUntil( + async () => (await context.p1.getLargeVideoAvatar())?.includes(HASH), { + timeout: 2000, + timeoutMsg: 'Avatar on large video did not change' + }); + + const p1LargeSrc = await context.p1.getLargeVideoAvatar(); + const p1ThumbSrc = await context.p1.getLocalVideoAvatar(); + + // Check if avatar on large video is the same as on local thumbnail + expect(p1ThumbSrc).toBe(p1LargeSrc); + + // Join p2 + await ensureTwoParticipants(context, { + skipDisplayName: true + }); + + // Verify that p1 is muted from the perspective of p2 + await context.p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(context.p1); + + await context.p2.getFilmstrip().pinParticipant(context.p1); + + // Check if p1's avatar is on large video now + await context.p2.driver.waitUntil( + async () => await context.p2.getLargeVideoAvatar() === p1LargeSrc, { + timeout: 2000, + timeoutMsg: 'Avatar on large video did not change' + }); + + // p1 pins p2's video + await context.p1.getFilmstrip().pinParticipant(context.p2); + + // Check if avatar is displayed on p1's local video thumbnail + await context.p1.assertThumbnailShowsAvatar(context.p1, false, false, true); + + // Unmute - now local avatar should be hidden and local video displayed + await unMuteVideoAndCheck(context.p1, context.p2); + + await context.p1.asserLocalThumbnailShowsVideo(); + + // Now both p1 and p2 have video muted + await context.p1.getToolbar().clickVideoMuteButton(); + await context.p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(context.p1); + await context.p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(context.p1); + + await context.p2.getToolbar().clickVideoMuteButton(); + await context.p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(context.p2); + await context.p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(context.p2); + + // Start the third participant + await ensureThreeParticipants(context); + + // Pin local video and verify avatars are displayed + await context.p3.getFilmstrip().pinParticipant(context.p3); + + await context.p3.assertThumbnailShowsAvatar(context.p1, false, false, true); + await context.p3.assertThumbnailShowsAvatar(context.p2, false, true); + + const p1EndpointId = await context.p1.getEndpointId(); + const p2EndpointId = await context.p2.getEndpointId(); + + expect(await context.p3.getFilmstrip().getAvatar(p1EndpointId)).toBe(p1ThumbSrc); + + // Click on p1's video + await context.p3.getFilmstrip().pinParticipant(context.p1); + + // The avatar should be on large video and display name instead of an avatar, local video displayed + await context.p3.driver.waitUntil( + async () => await context.p3.getLargeVideoResource() === p1EndpointId, { + timeout: 2000, + timeoutMsg: `Large video did not switch to ${context.p1.name}` + }); + + await context.p3.assertDisplayNameVisibleOnStage( + await context.p3.getFilmstrip().getRemoteDisplayName(p1EndpointId)); + + // p2 has the default avatar + await context.p3.assertThumbnailShowsAvatar(context.p2, false, true); + await context.p3.assertThumbnailShowsAvatar(context.p3, true); + + // Click on p2's video + await context.p3.getFilmstrip().pinParticipant(context.p2); + + // The avatar should be on large video and display name instead of an avatar, local video displayed + await context.p3.driver.waitUntil( + async () => await context.p3.getLargeVideoResource() === p2EndpointId, { + timeout: 2000, + timeoutMsg: `Large video did not switch to ${context.p2.name}` + }); + + await context.p3.assertDisplayNameVisibleOnStage( + await context.p3.getFilmstrip().getRemoteDisplayName(p2EndpointId) + ); + + await context.p3.assertThumbnailShowsAvatar(context.p1, false, false, true); + await context.p3.assertThumbnailShowsAvatar(context.p3, true); + + await context.p3.hangup(); + + // Unmute p1's and p2's videos + await context.p1.getToolbar().clickVideoUnmuteButton(); + + await context.p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(context.p1, true); + await context.p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(context.p1, true); + }); + + it('email persistence', async () => { + await context.p1.getToolbar().clickProfileButton(); + + expect(await context.p1.getSettingsDialog().getEmail()).toBe(EMAIL); + + await context.p1.hangup(); + + await ensureTwoParticipants(context, { + skipDisplayName: true + }); + + await context.p1.getToolbar().clickProfileButton(); + + expect(await context.p1.getSettingsDialog().getEmail()).toBe(EMAIL); + }); +}); diff --git a/tests/wdio.conf.ts b/tests/wdio.conf.ts index 22967dd4a1..8a02fb71c0 100644 --- a/tests/wdio.conf.ts +++ b/tests/wdio.conf.ts @@ -3,6 +3,7 @@ import { multiremotebrowser } from '@wdio/globals'; import { Buffer } from 'buffer'; import path from 'node:path'; import process from 'node:process'; +import pretty from 'pretty'; import { getLogs, initLogger, logInfo } from './helpers/browserLogger'; import { IContext } from './helpers/types'; @@ -58,7 +59,7 @@ export const config: WebdriverIO.MultiremoteConfig = { ], maxInstances: 1, - baseUrl: process.env.BASE_URL || 'https://alpha.jitsi.net/torture', + baseUrl: process.env.BASE_URL || 'https://alpha.jitsi.net/torture/', tsConfigPath: './tsconfig.json', // Default timeout for all waitForXXX commands. @@ -249,7 +250,7 @@ export const config: WebdriverIO.MultiremoteConfig = { AllureReporter.addAttachment(`console-logs-${instance}`, getLogs(bInstance) || '', 'text/plain'); allProcessing.push(bInstance.getPageSource().then(source => { - AllureReporter.addAttachment(`html-source-${instance}`, source, 'text/plain'); + AllureReporter.addAttachment(`html-source-${instance}`, pretty(source), 'text/plain'); })); });