mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-01-21 06:00:17 +00:00
Compare commits
2 Commits
dependabot
...
feat/netwo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0358777b72 | ||
|
|
7ab6c283a6 |
257
package-lock.json
generated
257
package-lock.json
generated
@@ -181,6 +181,7 @@
|
||||
"patch-package": "6.4.7",
|
||||
"pretty": "2.0.0",
|
||||
"process": "0.11.10",
|
||||
"puppeteer-core": "24.29.1",
|
||||
"sass": "1.26.8",
|
||||
"style-loader": "3.3.1",
|
||||
"traverse": "0.6.6",
|
||||
@@ -5109,18 +5110,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers": {
|
||||
"version": "2.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz",
|
||||
"integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==",
|
||||
"version": "2.10.13",
|
||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.13.tgz",
|
||||
"integrity": "sha512-a9Ruw3j3qlnB5a/zHRTkruppynxqaeE4H9WNj5eYGRWqw0ZauZ23f4W2ARf3hghF5doozyD+CRtt7XSYuYRI/Q==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.1",
|
||||
"debug": "^4.4.3",
|
||||
"extract-zip": "^2.0.1",
|
||||
"progress": "^2.0.3",
|
||||
"proxy-agent": "^6.5.0",
|
||||
"semver": "^7.7.2",
|
||||
"tar-fs": "^3.0.8",
|
||||
"semver": "^7.7.3",
|
||||
"tar-fs": "^3.1.1",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
@@ -5131,9 +5132,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers/node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -5156,9 +5157,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@puppeteer/browsers/node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
@@ -11273,6 +11274,20 @@
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chromium-bidi": {
|
||||
"version": "10.5.1",
|
||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-10.5.1.tgz",
|
||||
"integrity": "sha512-rlj6OyhKhVTnk4aENcUme3Jl9h+cq4oXu4AzBcvr8RMmT6BR4a3zSNT9dbIfXr9/BS6ibzRyDhowuw4n2GgzsQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"mitt": "^3.0.1",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"devtools-protocol": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/chromium-edge-launcher": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz",
|
||||
@@ -19659,6 +19674,13 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/mitt": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
@@ -21441,6 +21463,79 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer-core": {
|
||||
"version": "24.29.1",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.29.1.tgz",
|
||||
"integrity": "sha512-ErJ9qKCK+bdLvBa7QVSQTBSPm8KZbl1yC/WvhrZ0ut27hDf2QBzjDsn1IukzE1i1KtZ7NYGETOV4W1beoo9izA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "2.10.13",
|
||||
"chromium-bidi": "10.5.1",
|
||||
"debug": "^4.4.3",
|
||||
"devtools-protocol": "0.0.1521046",
|
||||
"typed-query-selector": "^2.12.0",
|
||||
"webdriver-bidi-protocol": "0.3.8",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer-core/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer-core/node_modules/devtools-protocol": {
|
||||
"version": "0.0.1521046",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz",
|
||||
"integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/puppeteer-core/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/puppeteer-core/node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
@@ -24829,9 +24924,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.10.tgz",
|
||||
"integrity": "sha512-C1SwlQGNLe/jPNqapK8epDsXME7CAJR5RL3GcE6KWx1d9OUByzoHVcbu1VPI8tevg9H8Alae0AApHHFGzrD5zA==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz",
|
||||
"integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -25458,6 +25553,13 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/typed-query-selector": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz",
|
||||
"integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
|
||||
@@ -26005,6 +26107,13 @@
|
||||
"node": ">=18.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webdriver-bidi-protocol": {
|
||||
"version": "0.3.8",
|
||||
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.3.8.tgz",
|
||||
"integrity": "sha512-21Yi2GhGntMc671vNBCjiAeEVknXjVRoyu+k+9xOMShu+ZQfpGQwnBqbNz/Sv4GXZ6JmutlPAi2nIJcrymAWuQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/webdriver/node_modules/ws": {
|
||||
"version": "8.18.2",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
|
||||
@@ -27144,6 +27253,16 @@
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/zxcvbn": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz",
|
||||
@@ -30342,24 +30461,24 @@
|
||||
}
|
||||
},
|
||||
"@puppeteer/browsers": {
|
||||
"version": "2.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz",
|
||||
"integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==",
|
||||
"version": "2.10.13",
|
||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.13.tgz",
|
||||
"integrity": "sha512-a9Ruw3j3qlnB5a/zHRTkruppynxqaeE4H9WNj5eYGRWqw0ZauZ23f4W2ARf3hghF5doozyD+CRtt7XSYuYRI/Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "^4.4.1",
|
||||
"debug": "^4.4.3",
|
||||
"extract-zip": "^2.0.1",
|
||||
"progress": "^2.0.3",
|
||||
"proxy-agent": "^6.5.0",
|
||||
"semver": "^7.7.2",
|
||||
"tar-fs": "^3.0.8",
|
||||
"semver": "^7.7.3",
|
||||
"tar-fs": "^3.1.1",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -30372,9 +30491,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
@@ -34734,6 +34853,16 @@
|
||||
"integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==",
|
||||
"dev": true
|
||||
},
|
||||
"chromium-bidi": {
|
||||
"version": "10.5.1",
|
||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-10.5.1.tgz",
|
||||
"integrity": "sha512-rlj6OyhKhVTnk4aENcUme3Jl9h+cq4oXu4AzBcvr8RMmT6BR4a3zSNT9dbIfXr9/BS6ibzRyDhowuw4n2GgzsQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mitt": "^3.0.1",
|
||||
"zod": "^3.24.1"
|
||||
}
|
||||
},
|
||||
"chromium-edge-launcher": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz",
|
||||
@@ -40731,6 +40860,12 @@
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="
|
||||
},
|
||||
"mitt": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||
"dev": true
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
@@ -41963,6 +42098,50 @@
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
|
||||
"integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA=="
|
||||
},
|
||||
"puppeteer-core": {
|
||||
"version": "24.29.1",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.29.1.tgz",
|
||||
"integrity": "sha512-ErJ9qKCK+bdLvBa7QVSQTBSPm8KZbl1yC/WvhrZ0ut27hDf2QBzjDsn1IukzE1i1KtZ7NYGETOV4W1beoo9izA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@puppeteer/browsers": "2.10.13",
|
||||
"chromium-bidi": "10.5.1",
|
||||
"debug": "^4.4.3",
|
||||
"devtools-protocol": "0.0.1521046",
|
||||
"typed-query-selector": "^2.12.0",
|
||||
"webdriver-bidi-protocol": "0.3.8",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "^2.1.3"
|
||||
}
|
||||
},
|
||||
"devtools-protocol": {
|
||||
"version": "0.0.1521046",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz",
|
||||
"integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==",
|
||||
"dev": true
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
@@ -44282,9 +44461,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"tar-fs": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.10.tgz",
|
||||
"integrity": "sha512-C1SwlQGNLe/jPNqapK8epDsXME7CAJR5RL3GcE6KWx1d9OUByzoHVcbu1VPI8tevg9H8Alae0AApHHFGzrD5zA==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz",
|
||||
"integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"bare-fs": "^4.0.1",
|
||||
@@ -44719,6 +44898,12 @@
|
||||
"reflect.getprototypeof": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"typed-query-selector": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz",
|
||||
"integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==",
|
||||
"dev": true
|
||||
},
|
||||
"typescript": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
|
||||
@@ -45073,6 +45258,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"webdriver-bidi-protocol": {
|
||||
"version": "0.3.8",
|
||||
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.3.8.tgz",
|
||||
"integrity": "sha512-21Yi2GhGntMc671vNBCjiAeEVknXjVRoyu+k+9xOMShu+ZQfpGQwnBqbNz/Sv4GXZ6JmutlPAi2nIJcrymAWuQ==",
|
||||
"dev": true
|
||||
},
|
||||
"webdriverio": {
|
||||
"version": "9.16.0",
|
||||
"resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.16.0.tgz",
|
||||
@@ -45838,6 +46029,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"dev": true
|
||||
},
|
||||
"zxcvbn": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz",
|
||||
|
||||
@@ -187,6 +187,7 @@
|
||||
"patch-package": "6.4.7",
|
||||
"pretty": "2.0.0",
|
||||
"process": "0.11.10",
|
||||
"puppeteer-core": "24.29.1",
|
||||
"sass": "1.26.8",
|
||||
"style-loader": "3.3.1",
|
||||
"traverse": "0.6.6",
|
||||
@@ -225,7 +226,12 @@
|
||||
"test-grid": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.conf.ts",
|
||||
"test-grid-single": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.conf.ts --spec",
|
||||
"test-grid-ff": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.firefox.conf.ts",
|
||||
"test-grid-ff-single": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.firefox.conf.ts --spec"
|
||||
"test-grid-ff-single": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.firefox.conf.ts --spec",
|
||||
"test-network": "CAPTURE_NETWORK=true DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.conf.ts",
|
||||
"test-network-single": "CAPTURE_NETWORK=true DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.conf.ts --spec",
|
||||
"test-network-dev": "CAPTURE_NETWORK=true DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.dev.conf.ts",
|
||||
"test-network-dev-single": "CAPTURE_NETWORK=true DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.dev.conf.ts --spec",
|
||||
"analyze-network": "npx tsx tests/helpers/networkAnalysis.ts"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "17.0.14",
|
||||
|
||||
208
tests/NETWORK-CAPTURE-GRID-SETUP.md
Normal file
208
tests/NETWORK-CAPTURE-GRID-SETUP.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# Network Capture Grid Setup Guide
|
||||
|
||||
Quick reference for configuring network capture with Selenium/WebDriver grid.
|
||||
|
||||
## TL;DR
|
||||
|
||||
**Local testing (default):** No configuration needed ✓
|
||||
|
||||
**Grid testing:** Set `GRID_NODE_HOSTNAME` environment variable or `custom:nodeHostname` capability
|
||||
|
||||
---
|
||||
|
||||
## Configuration Matrix
|
||||
|
||||
| Scenario | Configuration | Command Example |
|
||||
|----------|---------------|-----------------|
|
||||
| **Local testing** | None (default) | `CAPTURE_NETWORK=true npm run test-dev-single -- tests/specs/2way/audioOnlyTest.spec.ts` |
|
||||
| **Grid - Single node** | `GRID_NODE_HOSTNAME=node-1.grid.com` | `CAPTURE_NETWORK=true GRID_NODE_HOSTNAME=node-1.grid.com npm run test-grid` |
|
||||
| **Grid - Multiple nodes** | Custom capability `'custom:nodeHostname'` | See below |
|
||||
|
||||
---
|
||||
|
||||
## Setup Steps for Grid
|
||||
|
||||
### Step 1: Configure Chrome on Grid Nodes
|
||||
|
||||
Add these Chrome arguments on ALL grid nodes:
|
||||
|
||||
```bash
|
||||
--remote-debugging-address=0.0.0.0 # Expose debugging on network (not just localhost)
|
||||
--remote-debugging-port=0 # Auto-assign available port
|
||||
```
|
||||
|
||||
**Example Selenium Grid node config:**
|
||||
```json
|
||||
{
|
||||
"capabilities": [{
|
||||
"browserName": "chrome",
|
||||
"goog:chromeOptions": {
|
||||
"args": [
|
||||
"--remote-debugging-address=0.0.0.0",
|
||||
"--remote-debugging-port=0"
|
||||
]
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Choose Configuration Method
|
||||
|
||||
#### Method A: Environment Variable (Simple)
|
||||
|
||||
Use when all tests run on the same node:
|
||||
|
||||
```bash
|
||||
export GRID_NODE_HOSTNAME=node-1.your-grid.com
|
||||
CAPTURE_NETWORK=true npm run test-grid
|
||||
```
|
||||
|
||||
#### Method B: Custom Capability (Advanced)
|
||||
|
||||
Use when tests run on different nodes. Your grid hub/router must set the capability based on which node the browser is assigned to.
|
||||
|
||||
**In your grid hub logic** (pseudocode):
|
||||
```javascript
|
||||
// When assigning browser to a node:
|
||||
if (assignedToNode === 'node-1.your-grid.com') {
|
||||
capabilities['custom:nodeHostname'] = 'node-1.your-grid.com';
|
||||
} else if (assignedToNode === 'node-2.your-grid.com') {
|
||||
capabilities['custom:nodeHostname'] = 'node-2.your-grid.com';
|
||||
}
|
||||
```
|
||||
|
||||
**Or in wdio.conf.ts** (if node is known at config time):
|
||||
```typescript
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
'custom:nodeHostname': process.env.NODE_HOSTNAME || 'node-1.your-grid.com'
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
When tests start, check console output:
|
||||
|
||||
```
|
||||
✓ Local: NetworkCapture: Using local debugger address: localhost:65243
|
||||
✓ Grid (env): NetworkCapture: Using grid node hostname from env: node-1.grid.com:65243
|
||||
✓ Grid (cap): NetworkCapture: Using grid node hostname from capability: node-1.grid.com:65243
|
||||
```
|
||||
|
||||
If you see connection errors, the hostname/port is wrong or Chrome isn't exposing debugging on the network.
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
**⚠️ IMPORTANT:** Exposing Chrome's debugging port on `0.0.0.0` allows anyone on the network to connect and control the browser.
|
||||
|
||||
**Recommended security measures:**
|
||||
|
||||
1. **Firewall rules:** Only allow connections from test runner IPs
|
||||
```bash
|
||||
# Example iptables rule on grid node:
|
||||
iptables -A INPUT -p tcp --dport 9222:9322 -s 10.0.1.100 -j ACCEPT
|
||||
iptables -A INPUT -p tcp --dport 9222:9322 -j DROP
|
||||
```
|
||||
|
||||
2. **VPN/Private network:** Run grid on isolated network
|
||||
|
||||
3. **SSH tunneling:** Tunnel debugging ports through SSH
|
||||
```bash
|
||||
# On test runner machine:
|
||||
ssh -L 9222:localhost:9222 node-1.your-grid.com
|
||||
# Then use GRID_NODE_HOSTNAME=localhost in tests
|
||||
```
|
||||
|
||||
4. **Temporary exposure:** Only enable debugging when running network capture tests
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: "Chrome debugger address not found in capabilities"
|
||||
|
||||
**Cause:** Chrome didn't start with `--remote-debugging-port`
|
||||
|
||||
**Fix:** Ensure Chrome args include `--remote-debugging-port=0` (should already be in wdio.conf.ts)
|
||||
|
||||
### Error: "Failed to fetch debugger info: 404" or "ECONNREFUSED"
|
||||
|
||||
**Cause:** NetworkCapture can't reach Chrome's debugging port
|
||||
|
||||
**Possible fixes:**
|
||||
1. Check `GRID_NODE_HOSTNAME` matches actual node hostname
|
||||
2. Verify Chrome is running with `--remote-debugging-address=0.0.0.0`
|
||||
3. Check firewall allows connections to debugging port
|
||||
4. Test manually: `curl http://node-1.your-grid.com:9222/json/version`
|
||||
|
||||
### Error: "webSocketDebuggerUrl not found"
|
||||
|
||||
**Cause:** Chrome's debugging endpoint isn't responding correctly
|
||||
|
||||
**Fix:**
|
||||
1. Verify Chrome version supports remote debugging
|
||||
2. Check Chrome didn't crash during startup
|
||||
3. Try restarting the grid node
|
||||
|
||||
### Tests work locally but fail on grid
|
||||
|
||||
**Cause:** Forgot to set `GRID_NODE_HOSTNAME` or `custom:nodeHostname`
|
||||
|
||||
**Fix:** Add grid configuration (see Step 2 above)
|
||||
|
||||
---
|
||||
|
||||
## Priority Order (for reference)
|
||||
|
||||
When resolving the debugger address, NetworkCapture checks in this order:
|
||||
|
||||
1. **Custom capability** `'custom:nodeHostname'` ← Highest priority
|
||||
2. **Environment variable** `GRID_NODE_HOSTNAME`
|
||||
3. **Default** Use debuggerAddress as-is (localhost) ← Local testing
|
||||
|
||||
This allows you to:
|
||||
- Use env var for simple setups
|
||||
- Override per-browser with capability for complex setups
|
||||
- No config needed for local development
|
||||
|
||||
---
|
||||
|
||||
## Example: Real Grid Setup
|
||||
|
||||
**Infrastructure:**
|
||||
- Grid hub: `grid-hub.company.com`
|
||||
- Node 1: `chrome-node-1.company.com`
|
||||
- Node 2: `chrome-node-2.company.com`
|
||||
- Test runner: `test-runner.company.com`
|
||||
|
||||
**Node configuration (both nodes):**
|
||||
```bash
|
||||
# Chrome startup args:
|
||||
--remote-debugging-address=0.0.0.0
|
||||
--remote-debugging-port=0
|
||||
|
||||
# Firewall (allow test runner only):
|
||||
iptables -A INPUT -p tcp --dport 9222:9322 -s test-runner.company.com -j ACCEPT
|
||||
iptables -A INPUT -p tcp --dport 9222:9322 -j DROP
|
||||
```
|
||||
|
||||
**Test execution:**
|
||||
```bash
|
||||
# On test-runner.company.com
|
||||
# Tests will be distributed across both nodes
|
||||
export GRID_NODE_HOSTNAME=chrome-node-1.company.com # If all tests go to node 1
|
||||
# OR configure custom:nodeHostname capability in grid hub
|
||||
CAPTURE_NETWORK=true npm run test-grid
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Questions?
|
||||
|
||||
- See full documentation: `tests/NETWORK-CAPTURE.md`
|
||||
- Check implementation: `tests/helpers/NetworkCapture.ts`
|
||||
- Grid configuration: Your grid provider's documentation
|
||||
486
tests/NETWORK-CAPTURE.md
Normal file
486
tests/NETWORK-CAPTURE.md
Normal file
@@ -0,0 +1,486 @@
|
||||
# Network Request Capture for WDIO Tests
|
||||
|
||||
This feature enables capturing and analyzing all network requests made during WebDriverIO test execution. It uses Chrome DevTools Protocol (CDP) to intercept and log network activity.
|
||||
|
||||
## Features
|
||||
|
||||
- **URL Capture**: Records all URLs requested during tests
|
||||
- **Basic Statistics**: Success/failure counts, status codes, response timing
|
||||
- **Domain Analysis**: Groups requests by domain
|
||||
- **Resource Type Tracking**: Categorizes by resource type (Document, Script, XHR, etc.)
|
||||
- **Cache Detection**: Identifies cached vs. fresh requests
|
||||
- **Failure Tracking**: Records failed requests with error messages
|
||||
- **Multiple Export Formats**: JSON, CSV, plain text
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Chrome Browser**: Network capture uses Chrome DevTools Protocol (CDP)
|
||||
- **puppeteer-core**: Direct CDP access library (already installed)
|
||||
- **wdio-chromedriver-service**: Chrome browser driver (already configured)
|
||||
- **Chrome remote debugging**: Enabled via `--remote-debugging-port=0` (already configured)
|
||||
|
||||
**Note**: This implementation uses puppeteer-core to connect directly to Chrome's debugging port, which allows it to work with WebDriverIO's multiremote mode (p1, p2, p3, p4 browser instances).
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Running Tests with Network Capture
|
||||
|
||||
Enable network capture by setting the `CAPTURE_NETWORK=true` environment variable:
|
||||
|
||||
```bash
|
||||
# Run all tests with network capture
|
||||
npm run test-network
|
||||
|
||||
# Run single test with network capture
|
||||
npm run test-network-single -- tests/specs/2way/audioOnlyTest.spec.ts
|
||||
|
||||
# Run development environment test with network capture
|
||||
npm run test-network-dev-single -- tests/specs/2way/audioOnlyTest.spec.ts
|
||||
```
|
||||
|
||||
### Output Files
|
||||
|
||||
Network capture data is saved to `test-results/` directory:
|
||||
|
||||
```
|
||||
test-results/
|
||||
├── network-p1-0-0-audioOnlyTest.json # Participant 1 capture
|
||||
├── network-p2-0-0-audioOnlyTest.json # Participant 2 capture
|
||||
└── ...
|
||||
```
|
||||
|
||||
Each file contains:
|
||||
- `captureDate`: Timestamp of capture
|
||||
- `stats`: Summary statistics (total, succeeded, failed, by domain, etc.)
|
||||
- `requests`: Array of all captured requests with details
|
||||
|
||||
### Analyzing Captured Data
|
||||
|
||||
Use the analysis utility to generate reports:
|
||||
|
||||
```bash
|
||||
# Print summary to console
|
||||
npm run analyze-network -- test-results/network-p1-0-0-audioOnlyTest.json
|
||||
|
||||
# Export unique URLs to text file
|
||||
npm run analyze-network -- test-results/network-p1-0-0-audioOnlyTest.json --urls urls.txt
|
||||
|
||||
# Export all requests to CSV
|
||||
npm run analyze-network -- test-results/network-p1-0-0-audioOnlyTest.json --csv requests.csv
|
||||
|
||||
# Export domain summary to JSON
|
||||
npm run analyze-network -- test-results/network-p1-0-0-audioOnlyTest.json --domains domains.json
|
||||
|
||||
# Combine multiple exports
|
||||
npm run analyze-network -- test-results/network-p1-0-0-audioOnlyTest.json \
|
||||
--urls urls.txt \
|
||||
--csv requests.csv \
|
||||
--domains domains.json
|
||||
```
|
||||
|
||||
## Analysis Output
|
||||
|
||||
### Console Summary
|
||||
|
||||
The analysis tool prints a comprehensive summary:
|
||||
|
||||
```
|
||||
=== Network Capture Analysis ===
|
||||
Capture Date: 2025-11-06T10:30:00.000Z
|
||||
|
||||
--- Summary ---
|
||||
Total Requests: 127
|
||||
Succeeded: 124 (97.6%)
|
||||
Failed: 3 (2.4%)
|
||||
Cached: 15 (11.8%)
|
||||
|
||||
--- Requests by Domain (Top 10) ---
|
||||
localhost:8080: 45
|
||||
meet-jit-si-turnrelay.jitsi.net: 12
|
||||
alpha.jitsi.net: 8
|
||||
...
|
||||
|
||||
--- Requests by Status Code ---
|
||||
200: 110
|
||||
304: 14
|
||||
404: 2
|
||||
Failed: 1
|
||||
|
||||
--- Requests by Resource Type ---
|
||||
Document: 2
|
||||
Script: 35
|
||||
XHR: 40
|
||||
WebSocket: 5
|
||||
...
|
||||
|
||||
--- Failed Requests (3) ---
|
||||
[GET] https://example.com/missing.js
|
||||
Reason: net::ERR_CONNECTION_REFUSED
|
||||
...
|
||||
```
|
||||
|
||||
### Export Formats
|
||||
|
||||
**URLs Text File** (`--urls`)
|
||||
```
|
||||
https://localhost:8080/config.js
|
||||
https://localhost:8080/lib-jitsi-meet.js
|
||||
https://alpha.jitsi.net/http-bind
|
||||
...
|
||||
```
|
||||
|
||||
**CSV File** (`--csv`)
|
||||
```csv
|
||||
"URL","Method","Status","Success","ResourceType","FromCache","FailureText"
|
||||
"https://localhost:8080/config.js","GET","200","true","Script","false",""
|
||||
...
|
||||
```
|
||||
|
||||
**Domain Summary JSON** (`--domains`)
|
||||
```json
|
||||
{
|
||||
"uniqueDomains": [
|
||||
"localhost:8080",
|
||||
"alpha.jitsi.net",
|
||||
"meet-jit-si-turnrelay.jitsi.net"
|
||||
],
|
||||
"requestsByDomain": {
|
||||
"localhost:8080": 45,
|
||||
"alpha.jitsi.net": 8,
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Verify URL Allowlist
|
||||
|
||||
Compare captured URLs against expected patterns:
|
||||
|
||||
```bash
|
||||
npm run test-network-single -- tests/specs/2way/audioOnlyTest.spec.ts
|
||||
npm run analyze-network -- test-results/network-p1-*.json --urls captured-urls.txt
|
||||
|
||||
# Compare with your allowlist
|
||||
diff captured-urls.txt expected-urls.txt
|
||||
```
|
||||
|
||||
### 2. Debug Network Failures
|
||||
|
||||
Identify failing requests during tests:
|
||||
|
||||
```bash
|
||||
npm run test-network-dev-single -- tests/specs/failing-test.spec.ts
|
||||
npm run analyze-network -- test-results/network-*.json
|
||||
# Check "Failed Requests" section
|
||||
```
|
||||
|
||||
### 3. Performance Analysis
|
||||
|
||||
Analyze request patterns and identify bottlenecks:
|
||||
|
||||
```bash
|
||||
npm run analyze-network -- test-results/network-*.json --csv all-requests.csv
|
||||
# Import CSV into spreadsheet for timing analysis
|
||||
```
|
||||
|
||||
### 4. Validate CSP/CORS
|
||||
|
||||
Ensure all requests succeed and no CORS errors occur:
|
||||
|
||||
```bash
|
||||
npm run test-network-single -- tests/specs/your-test.spec.ts
|
||||
npm run analyze-network -- test-results/network-*.json
|
||||
# Check for CORS-related failures
|
||||
```
|
||||
|
||||
### 5. Compare URL Patterns Across Tests
|
||||
|
||||
Capture multiple tests and compare:
|
||||
|
||||
```bash
|
||||
npm run test-network-single -- tests/specs/2way/audioOnlyTest.spec.ts
|
||||
npm run test-network-single -- tests/specs/3way/test.spec.ts
|
||||
|
||||
npm run analyze-network -- test-results/network-p1-*-audioOnlyTest.json --urls audio-urls.txt
|
||||
npm run analyze-network -- test-results/network-p1-*-test.json --urls 3way-urls.txt
|
||||
|
||||
diff audio-urls.txt 3way-urls.txt
|
||||
```
|
||||
|
||||
## Programmatic Usage
|
||||
|
||||
### Using NetworkCapture in Custom Scripts
|
||||
|
||||
```typescript
|
||||
import { NetworkCapture } from './tests/helpers/NetworkCapture';
|
||||
|
||||
// In your test or script
|
||||
const capture = new NetworkCapture(browser);
|
||||
await capture.start();
|
||||
|
||||
// ... perform actions ...
|
||||
|
||||
await capture.stop();
|
||||
|
||||
// Get statistics
|
||||
const stats = capture.getStats();
|
||||
console.log(`Total requests: ${stats.total}`);
|
||||
|
||||
// Get failed URLs
|
||||
const failed = capture.getFailedUrls();
|
||||
console.log('Failed:', failed);
|
||||
|
||||
// Export to file
|
||||
capture.exportToJSON('my-capture.json');
|
||||
```
|
||||
|
||||
### Using NetworkAnalyzer
|
||||
|
||||
```typescript
|
||||
import { NetworkAnalyzer } from './tests/helpers/networkAnalysis';
|
||||
|
||||
const analyzer = new NetworkAnalyzer('test-results/network-p1-0-0-test.json');
|
||||
|
||||
// Get unique domains
|
||||
const domains = analyzer.getUniqueDomains();
|
||||
|
||||
// Get failed requests
|
||||
const failed = analyzer.getFailedRequests();
|
||||
|
||||
// Export reports
|
||||
analyzer.printSummary();
|
||||
analyzer.exportUrlsToFile('urls.txt');
|
||||
analyzer.exportToCSV('requests.csv');
|
||||
analyzer.exportDomainSummary('domains.json');
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
- **`CAPTURE_NETWORK=true`**: Enable network capture (required)
|
||||
- **`GRID_NODE_HOSTNAME=<hostname>`**: Grid node hostname for remote testing (optional, see Grid Support below)
|
||||
- Works with all WDIO configurations: `wdio.conf.ts`, `wdio.dev.conf.ts`, etc.
|
||||
|
||||
### Grid Support
|
||||
|
||||
The network capture feature supports both **local testing** (default) and **remote grid nodes**.
|
||||
|
||||
#### Local Testing (Default)
|
||||
|
||||
No configuration needed. Works out of the box:
|
||||
|
||||
```bash
|
||||
CAPTURE_NETWORK=true npm run test-dev-single -- tests/specs/2way/audioOnlyTest.spec.ts
|
||||
```
|
||||
|
||||
#### Grid Testing Configuration
|
||||
|
||||
**✨ Automatic Discovery (Recommended)**
|
||||
|
||||
For Selenium Grid 4 or Grid 3, node addresses are **automatically discovered**:
|
||||
|
||||
```bash
|
||||
# Just set GRID_HOST_URL - nodes are discovered automatically!
|
||||
CAPTURE_NETWORK=true GRID_HOST_URL=http://your-grid-hub:4444 npm run test-grid
|
||||
```
|
||||
|
||||
The system will:
|
||||
- Query Grid 4 GraphQL API (`/graphql`) to get node URI
|
||||
- Fall back to Grid 3 REST API (`/grid/api/testsession`) if Grid 4 not available
|
||||
- Extract hostname from node URI automatically
|
||||
|
||||
**Manual Configuration (Fallback)**
|
||||
|
||||
If automatic discovery fails or you want manual control:
|
||||
|
||||
**Option 1: Environment Variable (Simple - Single Node)**
|
||||
|
||||
Use when all tests run on the same grid node:
|
||||
|
||||
```bash
|
||||
CAPTURE_NETWORK=true GRID_NODE_HOSTNAME=node-1.your-grid.com npm run test-grid-single -- tests/specs/2way/audioOnlyTest.spec.ts
|
||||
```
|
||||
|
||||
**Option 2: Custom Capability (Advanced - Per-Browser)**
|
||||
|
||||
Use when you want to manually specify hostname per browser:
|
||||
|
||||
```javascript
|
||||
// In your grid configuration or wdio.conf.ts:
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
'goog:chromeOptions': {
|
||||
args: ['--remote-debugging-port=0', '--remote-debugging-address=0.0.0.0']
|
||||
},
|
||||
'custom:nodeHostname': 'node-1.your-grid.com' // Add this
|
||||
}
|
||||
```
|
||||
|
||||
**Priority Order:**
|
||||
1. Custom capability `'custom:nodeHostname'` (highest priority - manual override)
|
||||
2. **Automatic discovery via `GRID_HOST_URL`** (NEW - zero config for Grid 4/3)
|
||||
3. Environment variable `GRID_NODE_HOSTNAME` (fallback)
|
||||
4. Default: `localhost` (local testing)
|
||||
|
||||
#### Grid Node Requirements
|
||||
|
||||
For grid support, Chrome on nodes must be configured to expose debugging on the network:
|
||||
|
||||
```bash
|
||||
# In your grid node Chrome configuration:
|
||||
--remote-debugging-address=0.0.0.0 # Allow connections from network (not just localhost)
|
||||
--remote-debugging-port=0 # Auto-assign port (already configured)
|
||||
```
|
||||
|
||||
**Security Note:** Only expose debugging ports on trusted networks. Consider firewall rules or SSH tunneling for production grids.
|
||||
|
||||
#### Example Grid Setup
|
||||
|
||||
**Scenario:** Tests run on grid with 2 nodes
|
||||
|
||||
**Node Configuration:**
|
||||
```yaml
|
||||
# node-1.your-grid.com
|
||||
chrome_args:
|
||||
- --remote-debugging-address=0.0.0.0
|
||||
- --remote-debugging-port=0
|
||||
|
||||
# node-2.your-grid.com
|
||||
chrome_args:
|
||||
- --remote-debugging-address=0.0.0.0
|
||||
- --remote-debugging-port=0
|
||||
```
|
||||
|
||||
**Test Execution:**
|
||||
```bash
|
||||
# If all tests run on node-1:
|
||||
CAPTURE_NETWORK=true GRID_NODE_HOSTNAME=node-1.your-grid.com npm run test-grid
|
||||
|
||||
# If tests distribute across nodes, use custom capability in wdio.conf.ts
|
||||
# (grid must set 'custom:nodeHostname' based on which node browser is on)
|
||||
CAPTURE_NETWORK=true npm run test-grid
|
||||
```
|
||||
|
||||
#### Verifying Grid Configuration
|
||||
|
||||
Check the console output when tests start:
|
||||
|
||||
```
|
||||
✓ Local testing:
|
||||
NetworkCapture: Using local debugger address: localhost:65243
|
||||
|
||||
✓ Grid with automatic discovery (Grid 4):
|
||||
p1: Discovered running on grid node node-1.your-grid.com
|
||||
NetworkCapture: Using grid node hostname from capability: node-1.your-grid.com:65243
|
||||
|
||||
✓ Grid with automatic discovery (Grid 3):
|
||||
p1: Discovered running on grid node 172.18.0.3
|
||||
NetworkCapture: Using grid node hostname from capability: 172.18.0.3:65243
|
||||
|
||||
✓ Grid with env variable:
|
||||
NetworkCapture: Using grid node hostname from env: node-1.your-grid.com:65243
|
||||
|
||||
✓ Grid with custom capability:
|
||||
NetworkCapture: Using grid node hostname from capability: node-1.your-grid.com:65243
|
||||
```
|
||||
|
||||
### Limitations
|
||||
|
||||
- **Chrome Only**: Network capture uses CDP, which is Chrome-specific. Firefox tests are automatically skipped.
|
||||
- **Performance Overhead**: Minimal impact, but may add 1-2% to test execution time.
|
||||
- **Storage**: Each test generates 100KB-5MB JSON files depending on number of requests.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Network capture not starting
|
||||
|
||||
**Symptom**: No `network-*.json` files created
|
||||
|
||||
**Solutions**:
|
||||
1. Verify `CAPTURE_NETWORK=true` is set
|
||||
2. Check browser is Chrome (not Firefox)
|
||||
3. Look for errors in test output: "Failed to start network capture"
|
||||
4. Ensure WebDriverIO config properly loads `NetworkCapture`
|
||||
|
||||
### CDP connection errors
|
||||
|
||||
**Symptom**: "Error: Protocol error: Connection closed"
|
||||
|
||||
**Solutions**:
|
||||
1. Ensure Chrome/chromedriver versions are compatible
|
||||
2. Check if Chrome crashed during test (screenshot in test-results/)
|
||||
3. Try running with headful mode (remove `HEADLESS=true`)
|
||||
|
||||
### Large file sizes
|
||||
|
||||
**Symptom**: JSON files are unexpectedly large (>10MB)
|
||||
|
||||
**Solutions**:
|
||||
1. Long-running tests capture more requests
|
||||
2. Consider filtering out noise (e.g., keep-alive pings, polling)
|
||||
3. Break test into smaller test cases
|
||||
|
||||
### Missing requests
|
||||
|
||||
**Symptom**: Expected URLs not in capture
|
||||
|
||||
**Possible causes**:
|
||||
1. Requests completed before capture started (start earlier in test)
|
||||
2. Requests made from service worker (not intercepted by CDP)
|
||||
3. WebRTC data channels (not HTTP requests)
|
||||
4. Browser cache prevented request (check `fromCache` field)
|
||||
|
||||
## Architecture
|
||||
|
||||
### NetworkCapture Class
|
||||
|
||||
Located in `tests/helpers/NetworkCapture.ts`
|
||||
|
||||
- Uses puppeteer-core to connect directly to Chrome's debugging port
|
||||
- Works with WebDriverIO multiremote mode (multiple browser instances)
|
||||
- Enables CDP Network domain via CDPSession
|
||||
- Listens to CDP events:
|
||||
- `Network.requestWillBeSent` - captures outgoing requests
|
||||
- `Network.responseReceived` - captures responses
|
||||
- `Network.loadingFailed` - captures failures
|
||||
- Stores data in memory, exports on demand
|
||||
|
||||
**Why Puppeteer instead of @wdio/devtools-service?**
|
||||
The @wdio/devtools-service does not support multiremote mode (GitHub issue #5505 since 2020). Using puppeteer-core allows us to establish individual CDP connections for each browser instance (p1, p2, p3, p4).
|
||||
|
||||
### WDIO Integration
|
||||
|
||||
Located in `tests/wdio.conf.ts`
|
||||
|
||||
- **before hook**: Initializes NetworkCapture when `CAPTURE_NETWORK=true`
|
||||
- **after hook**: Stops capture and exports JSON files
|
||||
- Creates separate capture instance for each browser (p1, p2, p3, p4)
|
||||
|
||||
### NetworkAnalyzer Class
|
||||
|
||||
Located in `tests/helpers/networkAnalysis.ts`
|
||||
|
||||
- Parses JSON capture files
|
||||
- Generates statistics and reports
|
||||
- Exports to multiple formats
|
||||
- CLI entry point for analysis commands
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential additions:
|
||||
|
||||
- **HAR Export**: Full HTTP Archive format support
|
||||
- **Request/Response Bodies**: Optional capture of payload data
|
||||
- **Real-time Filtering**: Exclude domains/patterns during capture
|
||||
- **Automatic Comparison**: Built-in diff against `COMPREHENSIVE-URL-LIST.md`
|
||||
- **HTML Reports**: Visual charts and graphs
|
||||
- **WebDriver Bidi**: Cross-browser support when standardized
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [COMPREHENSIVE-URL-LIST.md](../COMPREHENSIVE-URL-LIST.md) - Complete list of possible URLs
|
||||
- [WebDriverIO CDP Docs](https://webdriver.io/docs/api/chromium/#cdp) - CDP API reference
|
||||
- [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) - CDP specification
|
||||
401
tests/helpers/NetworkCapture.ts
Normal file
401
tests/helpers/NetworkCapture.ts
Normal file
@@ -0,0 +1,401 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import puppeteer, { type CDPSession } from 'puppeteer-core';
|
||||
|
||||
/**
|
||||
* Represents a captured network request with its metadata.
|
||||
*/
|
||||
interface INetworkRequest {
|
||||
/** Failure reason if request failed. */
|
||||
failureText?: string;
|
||||
/** Whether response came from cache. */
|
||||
fromCache?: boolean;
|
||||
/** HTTP method (GET, POST, etc.). */
|
||||
method: string;
|
||||
/** Unique request identifier from CDP. */
|
||||
requestId: string;
|
||||
/** Type of resource (Document, Stylesheet, XHR, etc.). */
|
||||
resourceType?: string;
|
||||
/** HTTP status code (200, 404, etc.). */
|
||||
status?: number;
|
||||
/** Whether the request completed successfully. */
|
||||
success?: boolean;
|
||||
/** Timestamp when request was initiated (Unix time in ms). */
|
||||
timestamp: number;
|
||||
/** Full URL of the request. */
|
||||
url: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistics about captured network requests.
|
||||
*/
|
||||
interface INetworkStats {
|
||||
/** Requests grouped by domain. */
|
||||
byDomain: Record<string, number>;
|
||||
/** Requests grouped by resource type. */
|
||||
byResourceType: Record<string, number>;
|
||||
/** Requests grouped by HTTP status code. */
|
||||
byStatus: Record<number, number>;
|
||||
/** Number of requests served from cache. */
|
||||
cachedRequests: number;
|
||||
/** Number of failed requests. */
|
||||
failed: number;
|
||||
/** Number of successful requests. */
|
||||
succeeded: number;
|
||||
/** Total number of requests. */
|
||||
total: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class to capture network requests during WDIO tests using Chrome DevTools Protocol via Puppeteer.
|
||||
*
|
||||
* This implementation uses puppeteer-core to connect directly to Chrome's debugging port,
|
||||
* which allows it to work with WebDriverIO's multiremote mode (unlike @wdio/devtools-service).
|
||||
*
|
||||
* Usage:
|
||||
* ```typescript
|
||||
* const capture = new NetworkCapture(driver);
|
||||
* await capture.start();
|
||||
* // ... perform test actions ...
|
||||
* await capture.stop();
|
||||
* const stats = capture.getStats();
|
||||
* capture.exportToJSON('network-capture.json');
|
||||
* ```
|
||||
*/
|
||||
export class NetworkCapture {
|
||||
private cdpClient: CDPSession | null = null;
|
||||
private driver: WebdriverIO.Browser;
|
||||
private isCapturing: boolean;
|
||||
private requests: Map<string, INetworkRequest>;
|
||||
|
||||
/**
|
||||
* Creates a new NetworkCapture instance.
|
||||
*
|
||||
* @param {WebdriverIO.Browser} driver - WebDriverIO browser instance.
|
||||
*/
|
||||
constructor(driver: WebdriverIO.Browser) {
|
||||
this.driver = driver;
|
||||
this.requests = new Map();
|
||||
this.isCapturing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the actual debugger address to connect to.
|
||||
* Supports both local testing (default) and remote grid nodes.
|
||||
*
|
||||
* Configuration options (in priority order):
|
||||
* 1. Custom capability 'custom:nodeHostname' - explicit node hostname
|
||||
* 2. Environment variable GRID_NODE_HOSTNAME - static hostname for all nodes
|
||||
* 3. Default: use debuggerAddress as-is (localhost) - for local testing
|
||||
*
|
||||
* @param {string} debuggerAddress - Address from Chrome capabilities (e.g., "localhost:65243").
|
||||
* @returns {string} Resolved address to connect to.
|
||||
*
|
||||
* @example
|
||||
* // Local testing (default):
|
||||
* resolveDebuggerAddress('localhost:65243') → 'localhost:65243'
|
||||
*
|
||||
* // Grid with custom capability:
|
||||
* driver.capabilities['custom:nodeHostname'] = 'node-1.grid.com'
|
||||
* resolveDebuggerAddress('localhost:65243') → 'node-1.grid.com:65243'
|
||||
*
|
||||
* // Grid with environment variable:
|
||||
* GRID_NODE_HOSTNAME=node-1.grid.com
|
||||
* resolveDebuggerAddress('localhost:65243') → 'node-1.grid.com:65243'
|
||||
*/
|
||||
private resolveDebuggerAddress(debuggerAddress: string): string {
|
||||
// Option 1: Check for custom capability (highest priority)
|
||||
const customNodeHost = (this.driver.capabilities as any)['custom:nodeHostname'];
|
||||
|
||||
if (customNodeHost) {
|
||||
const [ , port ] = debuggerAddress.split(':');
|
||||
|
||||
console.log(`NetworkCapture: Using grid node hostname from capability: ${customNodeHost}:${port}`);
|
||||
|
||||
return `${customNodeHost}:${port}`;
|
||||
}
|
||||
|
||||
// Option 2: Check for environment variable
|
||||
const envNodeHost = process.env.GRID_NODE_HOSTNAME;
|
||||
|
||||
if (envNodeHost) {
|
||||
const [ , port ] = debuggerAddress.split(':');
|
||||
|
||||
console.log(`NetworkCapture: Using grid node hostname from env: ${envNodeHost}:${port}`);
|
||||
|
||||
return `${envNodeHost}:${port}`;
|
||||
}
|
||||
|
||||
// Option 3: Default - use debuggerAddress as-is (local testing)
|
||||
console.log(`NetworkCapture: Using local debugger address: ${debuggerAddress}`);
|
||||
|
||||
return debuggerAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts capturing network requests via CDP using Puppeteer.
|
||||
* Connects to Chrome's debugging port and enables the Network domain.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
if (this.isCapturing) {
|
||||
console.warn('NetworkCapture: Already capturing, ignoring start()');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the debugging address from Chrome capabilities
|
||||
const debuggerAddress = this.driver.capabilities['goog:chromeOptions']?.debuggerAddress;
|
||||
|
||||
if (!debuggerAddress) {
|
||||
throw new Error('Chrome debugger address not found in capabilities');
|
||||
}
|
||||
|
||||
// Resolve the actual address to connect to (supports both local and grid setups)
|
||||
const actualAddress = this.resolveDebuggerAddress(debuggerAddress);
|
||||
|
||||
// Fetch the WebSocket debugger URL from Chrome's /json/version endpoint
|
||||
const response = await fetch(`http://${actualAddress}/json/version`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch debugger info: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const versionInfo = await response.json() as { webSocketDebuggerUrl: string; };
|
||||
const wsUrl = versionInfo.webSocketDebuggerUrl;
|
||||
|
||||
if (!wsUrl) {
|
||||
throw new Error('webSocketDebuggerUrl not found in debugger info');
|
||||
}
|
||||
|
||||
// Connect Puppeteer to the existing Chrome instance using the correct WebSocket URL
|
||||
const browser = await puppeteer.connect({
|
||||
browserWSEndpoint: wsUrl,
|
||||
defaultViewport: null
|
||||
});
|
||||
|
||||
// Get the first page/target
|
||||
const targets = await browser.targets();
|
||||
const target = targets.find(t => t.type() === 'page') || targets[0];
|
||||
|
||||
if (!target) {
|
||||
throw new Error('No page target found');
|
||||
}
|
||||
|
||||
// Create CDP session
|
||||
this.cdpClient = await target.createCDPSession();
|
||||
|
||||
// Enable Network domain
|
||||
await this.cdpClient.send('Network.enable');
|
||||
|
||||
// Listen for network events
|
||||
this.cdpClient.on('Network.requestWillBeSent', this.handleRequestWillBeSent.bind(this));
|
||||
this.cdpClient.on('Network.responseReceived', this.handleResponseReceived.bind(this));
|
||||
this.cdpClient.on('Network.loadingFailed', this.handleLoadingFailed.bind(this));
|
||||
|
||||
this.isCapturing = true;
|
||||
console.log('NetworkCapture: Started capturing network requests via Puppeteer CDP');
|
||||
} catch (error) {
|
||||
console.error('NetworkCapture: Failed to start capturing:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops capturing network requests and closes CDP connection.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
if (!this.isCapturing || !this.cdpClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Disable Network domain
|
||||
await this.cdpClient.send('Network.disable');
|
||||
|
||||
// Detach CDP session
|
||||
await this.cdpClient.detach();
|
||||
|
||||
this.cdpClient = null;
|
||||
this.isCapturing = false;
|
||||
console.log(`NetworkCapture: Stopped capturing. Total requests: ${this.requests.size}`);
|
||||
} catch (error) {
|
||||
console.error('NetworkCapture: Failed to stop capturing:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles Network.requestWillBeSent CDP events.
|
||||
* Records initial request data.
|
||||
*
|
||||
* @param {any} params - CDP event parameters.
|
||||
*/
|
||||
private handleRequestWillBeSent(params: any): void {
|
||||
const { requestId, request, timestamp, type } = params;
|
||||
|
||||
this.requests.set(requestId, {
|
||||
failureText: undefined,
|
||||
fromCache: false,
|
||||
method: request.method,
|
||||
requestId,
|
||||
resourceType: type,
|
||||
status: undefined,
|
||||
success: undefined,
|
||||
timestamp: timestamp * 1000, // CDP uses seconds, convert to ms
|
||||
url: request.url
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles Network.responseReceived CDP events.
|
||||
* Updates request with response status and cache info.
|
||||
*
|
||||
* @param {any} params - CDP event parameters.
|
||||
*/
|
||||
private handleResponseReceived(params: any): void {
|
||||
const { requestId, response } = params;
|
||||
const request = this.requests.get(requestId);
|
||||
|
||||
if (request) {
|
||||
request.status = response.status;
|
||||
request.success = response.status >= 200 && response.status < 400;
|
||||
request.fromCache = response.fromDiskCache || response.fromServiceWorker || false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles Network.loadingFailed CDP events.
|
||||
* Marks request as failed and records reason.
|
||||
*
|
||||
* @param {any} params - CDP event parameters.
|
||||
*/
|
||||
private handleLoadingFailed(params: any): void {
|
||||
const { requestId, errorText } = params;
|
||||
const request = this.requests.get(requestId);
|
||||
|
||||
if (request) {
|
||||
request.success = false;
|
||||
request.failureText = errorText;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all captured network requests.
|
||||
*
|
||||
* @returns {INetworkRequest[]} Array of captured requests.
|
||||
*/
|
||||
getRequests(): INetworkRequest[] {
|
||||
return Array.from(this.requests.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns URLs of all captured requests.
|
||||
*
|
||||
* @returns {string[]} Array of URLs.
|
||||
*/
|
||||
getUrls(): string[] {
|
||||
return this.getRequests().map(req => req.url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns URLs that failed to load.
|
||||
*
|
||||
* @returns {string[]} Array of failed URLs with failure reasons.
|
||||
*/
|
||||
getFailedUrls(): Array<{ reason?: string; url: string; }> {
|
||||
return this.getRequests()
|
||||
.filter(req => req.success === false)
|
||||
.map(req => ({ reason: req.failureText, url: req.url }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates statistics about captured network requests.
|
||||
*
|
||||
* @returns {INetworkStats} Statistics object.
|
||||
*/
|
||||
getStats(): INetworkStats {
|
||||
const requests = this.getRequests();
|
||||
const stats: INetworkStats = {
|
||||
byDomain: {},
|
||||
byResourceType: {},
|
||||
byStatus: {},
|
||||
cachedRequests: 0,
|
||||
failed: 0,
|
||||
succeeded: 0,
|
||||
total: requests.length
|
||||
};
|
||||
|
||||
for (const request of requests) {
|
||||
// Count success/failure
|
||||
if (request.success === true) {
|
||||
stats.succeeded++;
|
||||
} else if (request.success === false) {
|
||||
stats.failed++;
|
||||
}
|
||||
|
||||
// Group by status code
|
||||
if (request.status) {
|
||||
stats.byStatus[request.status] = (stats.byStatus[request.status] || 0) + 1;
|
||||
}
|
||||
|
||||
// Group by domain
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const domain = url.hostname;
|
||||
|
||||
stats.byDomain[domain] = (stats.byDomain[domain] || 0) + 1;
|
||||
} catch (e) {
|
||||
// Invalid URL, skip domain grouping
|
||||
}
|
||||
|
||||
// Group by resource type
|
||||
if (request.resourceType) {
|
||||
stats.byResourceType[request.resourceType]
|
||||
= (stats.byResourceType[request.resourceType] || 0) + 1;
|
||||
}
|
||||
|
||||
// Count cached requests
|
||||
if (request.fromCache) {
|
||||
stats.cachedRequests++;
|
||||
}
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports captured requests to a JSON file.
|
||||
*
|
||||
* @param {string} filePath - Path where to save the JSON file.
|
||||
*/
|
||||
exportToJSON(filePath: string): void {
|
||||
const data = {
|
||||
captureDate: new Date().toISOString(),
|
||||
requests: this.getRequests(),
|
||||
stats: this.getStats()
|
||||
};
|
||||
|
||||
// Ensure directory exists
|
||||
const dir = path.dirname(filePath);
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
||||
console.log(`NetworkCapture: Exported ${data.requests.length} requests to ${filePath}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all captured requests.
|
||||
*/
|
||||
clear(): void {
|
||||
this.requests.clear();
|
||||
console.log('NetworkCapture: Cleared all captured requests');
|
||||
}
|
||||
}
|
||||
348
tests/helpers/networkAnalysis.ts
Normal file
348
tests/helpers/networkAnalysis.ts
Normal file
@@ -0,0 +1,348 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import * as fs from 'fs';
|
||||
|
||||
/**
|
||||
* Represents a captured network request from the NetworkCapture output.
|
||||
*/
|
||||
interface INetworkRequest {
|
||||
failureText?: string;
|
||||
fromCache?: boolean;
|
||||
method: string;
|
||||
requestId: string;
|
||||
resourceType?: string;
|
||||
status?: number;
|
||||
success?: boolean;
|
||||
timestamp: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistics about captured network requests.
|
||||
*/
|
||||
interface INetworkStats {
|
||||
byDomain: Record<string, number>;
|
||||
byResourceType: Record<string, number>;
|
||||
byStatus: Record<number, number>;
|
||||
cachedRequests: number;
|
||||
failed: number;
|
||||
succeeded: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Network capture data structure.
|
||||
*/
|
||||
interface ICaptureData {
|
||||
captureDate: string;
|
||||
requests: INetworkRequest[];
|
||||
stats: INetworkStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyzes network capture data and generates reports.
|
||||
*/
|
||||
export class NetworkAnalyzer {
|
||||
private data: ICaptureData;
|
||||
|
||||
/**
|
||||
* Creates a new NetworkAnalyzer from a JSON file.
|
||||
*
|
||||
* @param {string} jsonFilePath - Path to the network capture JSON file.
|
||||
*/
|
||||
constructor(jsonFilePath: string) {
|
||||
if (!fs.existsSync(jsonFilePath)) {
|
||||
throw new Error(`File not found: ${jsonFilePath}`);
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(jsonFilePath, 'utf-8');
|
||||
|
||||
this.data = JSON.parse(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all unique URLs from the capture.
|
||||
*
|
||||
* @returns {string[]} Array of unique URLs.
|
||||
*/
|
||||
getUniqueUrls(): string[] {
|
||||
const urls = new Set(this.data.requests.map(req => req.url));
|
||||
|
||||
return Array.from(urls).sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all unique domains from the capture.
|
||||
*
|
||||
* @returns {string[]} Array of unique domains.
|
||||
*/
|
||||
getUniqueDomains(): string[] {
|
||||
const domains = new Set<string>();
|
||||
|
||||
for (const request of this.data.requests) {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
|
||||
domains.add(url.hostname);
|
||||
} catch (e) {
|
||||
// Skip invalid URLs
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(domains).sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns requests grouped by domain.
|
||||
*
|
||||
* @returns {Record<string, INetworkRequest[]>} Requests grouped by domain.
|
||||
*/
|
||||
getRequestsByDomain(): Record<string, INetworkRequest[]> {
|
||||
const byDomain: Record<string, INetworkRequest[]> = {};
|
||||
|
||||
for (const request of this.data.requests) {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const domain = url.hostname;
|
||||
|
||||
if (!byDomain[domain]) {
|
||||
byDomain[domain] = [];
|
||||
}
|
||||
byDomain[domain].push(request);
|
||||
} catch (e) {
|
||||
// Skip invalid URLs
|
||||
}
|
||||
}
|
||||
|
||||
return byDomain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns failed requests with details.
|
||||
*
|
||||
* @returns {INetworkRequest[]} Array of failed requests.
|
||||
*/
|
||||
getFailedRequests(): INetworkRequest[] {
|
||||
return this.data.requests.filter(req => req.success === false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns requests by HTTP status code.
|
||||
*
|
||||
* @returns {Record<number, INetworkRequest[]>} Requests grouped by status code.
|
||||
*/
|
||||
getRequestsByStatus(): Record<number, INetworkRequest[]> {
|
||||
const byStatus: Record<number, INetworkRequest[]> = {};
|
||||
|
||||
for (const request of this.data.requests) {
|
||||
if (request.status) {
|
||||
if (!byStatus[request.status]) {
|
||||
byStatus[request.status] = [];
|
||||
}
|
||||
byStatus[request.status].push(request);
|
||||
}
|
||||
}
|
||||
|
||||
return byStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns requests by resource type.
|
||||
*
|
||||
* @returns {Record<string, INetworkRequest[]>} Requests grouped by resource type.
|
||||
*/
|
||||
getRequestsByResourceType(): Record<string, INetworkRequest[]> {
|
||||
const byType: Record<string, INetworkRequest[]> = {};
|
||||
|
||||
for (const request of this.data.requests) {
|
||||
if (request.resourceType) {
|
||||
if (!byType[request.resourceType]) {
|
||||
byType[request.resourceType] = [];
|
||||
}
|
||||
byType[request.resourceType].push(request);
|
||||
}
|
||||
}
|
||||
|
||||
return byType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints a summary report to console.
|
||||
*/
|
||||
printSummary(): void {
|
||||
console.log('\n=== Network Capture Analysis ===');
|
||||
console.log(`Capture Date: ${this.data.captureDate}`);
|
||||
console.log('\n--- Summary ---');
|
||||
console.log(`Total Requests: ${this.data.stats.total}`);
|
||||
console.log(`Succeeded: ${this.data.stats.succeeded} (${this.getPercentage(this.data.stats.succeeded, this.data.stats.total)}%)`);
|
||||
console.log(`Failed: ${this.data.stats.failed} (${this.getPercentage(this.data.stats.failed, this.data.stats.total)}%)`);
|
||||
console.log(`Cached: ${this.data.stats.cachedRequests} (${this.getPercentage(this.data.stats.cachedRequests, this.data.stats.total)}%)`);
|
||||
|
||||
console.log('\n--- Requests by Domain (Top 10) ---');
|
||||
const domainEntries = Object.entries(this.data.stats.byDomain)
|
||||
.sort(([ , a ], [ , b ]) => b - a)
|
||||
.slice(0, 10);
|
||||
|
||||
for (const [ domain, count ] of domainEntries) {
|
||||
console.log(` ${domain}: ${count}`);
|
||||
}
|
||||
|
||||
console.log('\n--- Requests by Status Code ---');
|
||||
const statusEntries = Object.entries(this.data.stats.byStatus)
|
||||
.sort(([ a ], [ b ]) => parseInt(a) - parseInt(b));
|
||||
|
||||
for (const [ status, count ] of statusEntries) {
|
||||
console.log(` ${status}: ${count}`);
|
||||
}
|
||||
|
||||
console.log('\n--- Requests by Resource Type ---');
|
||||
const typeEntries = Object.entries(this.data.stats.byResourceType)
|
||||
.sort(([ , a ], [ , b ]) => b - a);
|
||||
|
||||
for (const [ type, count ] of typeEntries) {
|
||||
console.log(` ${type}: ${count}`);
|
||||
}
|
||||
|
||||
const failedRequests = this.getFailedRequests();
|
||||
|
||||
if (failedRequests.length > 0) {
|
||||
console.log(`\n--- Failed Requests (${failedRequests.length}) ---`);
|
||||
for (const request of failedRequests) {
|
||||
console.log(` [${request.method}] ${request.url}`);
|
||||
if (request.failureText) {
|
||||
console.log(` Reason: ${request.failureText}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports unique URLs to a text file (one per line).
|
||||
*
|
||||
* @param {string} outputPath - Path where to save the file.
|
||||
*/
|
||||
exportUrlsToFile(outputPath: string): void {
|
||||
const urls = this.getUniqueUrls();
|
||||
|
||||
fs.writeFileSync(outputPath, urls.join('\n'));
|
||||
console.log(`Exported ${urls.length} unique URLs to ${outputPath}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports analysis to CSV format.
|
||||
*
|
||||
* @param {string} outputPath - Path where to save the CSV file.
|
||||
*/
|
||||
exportToCSV(outputPath: string): void {
|
||||
const headers = [ 'URL', 'Method', 'Status', 'Success', 'ResourceType', 'FromCache', 'FailureText' ];
|
||||
const rows = [ headers ];
|
||||
|
||||
for (const request of this.data.requests) {
|
||||
rows.push([
|
||||
request.url,
|
||||
request.method,
|
||||
request.status?.toString() || '',
|
||||
request.success?.toString() || '',
|
||||
request.resourceType || '',
|
||||
request.fromCache?.toString() || '',
|
||||
request.failureText || ''
|
||||
]);
|
||||
}
|
||||
|
||||
const csvContent = rows.map(row => row.map(cell => `"${cell}"`).join(',')).join('\n');
|
||||
|
||||
fs.writeFileSync(outputPath, csvContent);
|
||||
console.log(`Exported ${this.data.requests.length} requests to ${outputPath}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports domain summary to JSON.
|
||||
*
|
||||
* @param {string} outputPath - Path where to save the JSON file.
|
||||
*/
|
||||
exportDomainSummary(outputPath: string): void {
|
||||
const summary = {
|
||||
uniqueDomains: this.getUniqueDomains(),
|
||||
requestsByDomain: Object.fromEntries(
|
||||
Object.entries(this.data.stats.byDomain)
|
||||
.sort(([ , a ], [ , b ]) => b - a)
|
||||
)
|
||||
};
|
||||
|
||||
fs.writeFileSync(outputPath, JSON.stringify(summary, null, 2));
|
||||
console.log(`Exported domain summary to ${outputPath}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to calculate percentage.
|
||||
*
|
||||
* @param {number} value - The value.
|
||||
* @param {number} total - The total.
|
||||
* @returns {string} Percentage formatted to 1 decimal place.
|
||||
*/
|
||||
private getPercentage(value: number, total: number): string {
|
||||
if (total === 0) {
|
||||
return '0.0';
|
||||
}
|
||||
|
||||
return ((value / total) * 100).toFixed(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CLI entry point for network analysis.
|
||||
*/
|
||||
function main(): void {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error('Usage: networkAnalysis.ts <network-capture.json> [options]');
|
||||
console.error('\nOptions:');
|
||||
console.error(' --urls <file> Export unique URLs to text file');
|
||||
console.error(' --csv <file> Export requests to CSV file');
|
||||
console.error(' --domains <file> Export domain summary to JSON file');
|
||||
console.error('\nExample:');
|
||||
console.error(' npx tsx tests/helpers/networkAnalysis.ts test-results/network-p1-0-0-audioOnlyTest.json');
|
||||
console.error(' npx tsx tests/helpers/networkAnalysis.ts test-results/network-p1-0-0-audioOnlyTest.json --urls urls.txt --csv requests.csv');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const jsonFilePath = args[0];
|
||||
const analyzer = new NetworkAnalyzer(jsonFilePath);
|
||||
|
||||
// Print summary to console
|
||||
analyzer.printSummary();
|
||||
|
||||
// Handle optional exports
|
||||
for (let i = 1; i < args.length; i += 2) {
|
||||
const option = args[i];
|
||||
const outputPath = args[i + 1];
|
||||
|
||||
if (!outputPath) {
|
||||
console.error(`Missing output path for option ${option}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
switch (option) {
|
||||
case '--urls':
|
||||
analyzer.exportUrlsToFile(outputPath);
|
||||
break;
|
||||
case '--csv':
|
||||
analyzer.exportToCSV(outputPath);
|
||||
break;
|
||||
case '--domains':
|
||||
analyzer.exportDomainSummary(outputPath);
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown option: ${option}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run if executed directly
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import pretty from 'pretty';
|
||||
|
||||
import { NetworkCapture } from './helpers/NetworkCapture';
|
||||
import { getTestProperties, loadTestFiles } from './helpers/TestProperties';
|
||||
import { config as testsConfig } from './helpers/TestsConfig';
|
||||
import WebhookProxy from './helpers/WebhookProxy';
|
||||
@@ -20,6 +21,151 @@ const allure = require('allure-commandline');
|
||||
// we need it to be able to reuse jitsi-meet code in tests
|
||||
require.extensions['.web.ts'] = require.extensions['.ts'];
|
||||
|
||||
/**
|
||||
* Discovers which Selenium Grid node a browser session is running on.
|
||||
* Supports both Grid 4 (GraphQL) and Grid 3 (REST API).
|
||||
* Retries with exponential backoff if session not found (timing issue).
|
||||
*
|
||||
* @param {string} sessionId - WebDriver session ID.
|
||||
* @param {string} gridUrl - Grid hub URL (e.g., "http://grid-hub:4444").
|
||||
* @returns {Promise<string | null>} Node hostname/IP or null if discovery fails.
|
||||
*/
|
||||
async function discoverGridNode(sessionId: string, gridUrl: string): Promise<string | null> {
|
||||
// Retry up to 3 times with delays (session might not be registered immediately)
|
||||
const maxRetries = 3;
|
||||
const retryDelays = [ 500, 1000, 2000 ]; // ms
|
||||
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
if (attempt > 0) {
|
||||
console.log(`Discovery retry attempt ${attempt + 1}/${maxRetries} after ${retryDelays[attempt - 1]}ms delay`);
|
||||
await new Promise(resolve => setTimeout(resolve, retryDelays[attempt - 1]));
|
||||
}
|
||||
|
||||
// Try Grid 4 GraphQL API first
|
||||
const grid4Node = await discoverGridNodeGraphQL(sessionId, gridUrl);
|
||||
|
||||
if (grid4Node) {
|
||||
return grid4Node;
|
||||
}
|
||||
|
||||
// Fall back to Grid 3 REST API
|
||||
const grid3Node = await discoverGridNodeREST(sessionId, gridUrl);
|
||||
|
||||
if (grid3Node) {
|
||||
return grid3Node;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets base grid URL by stripping /wd/hub suffix if present.
|
||||
* GraphQL and Grid 3 REST APIs are at root level, not under /wd/hub.
|
||||
*
|
||||
* @param {string} gridUrl - Grid URL (may include /wd/hub).
|
||||
* @returns {string} Base URL without /wd/hub.
|
||||
*/
|
||||
function getBaseGridUrl(gridUrl: string): string {
|
||||
return gridUrl.replace(/\/wd\/hub\/?$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers grid node using Selenium Grid 4 GraphQL API.
|
||||
*
|
||||
* @param {string} sessionId - WebDriver session ID.
|
||||
* @param {string} gridUrl - Grid hub URL.
|
||||
* @returns {Promise<string | null>} Node hostname or null.
|
||||
*/
|
||||
async function discoverGridNodeGraphQL(sessionId: string, gridUrl: string): Promise<string | null> {
|
||||
try {
|
||||
const baseUrl = getBaseGridUrl(gridUrl);
|
||||
const graphqlUrl = `${baseUrl}/graphql`;
|
||||
|
||||
console.log(`Attempting Grid 4 discovery: ${graphqlUrl} with session ${sessionId}`);
|
||||
|
||||
const response = await fetch(graphqlUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
query: `{ session(id: "${sessionId}") { nodeUri } }`
|
||||
})
|
||||
});
|
||||
|
||||
console.log(`GraphQL response status: ${response.status} ${response.statusText}`);
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
console.log('GraphQL response data:', JSON.stringify(data, null, 2));
|
||||
|
||||
const nodeUri = data?.data?.session?.nodeUri;
|
||||
|
||||
if (nodeUri) {
|
||||
// Extract hostname from URI: "http://172.18.0.3:5555" -> "172.18.0.3"
|
||||
const url = new URL(nodeUri);
|
||||
|
||||
console.log(`Discovered grid node via GraphQL (Grid 4): ${url.hostname}`);
|
||||
|
||||
return url.hostname;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.log('GraphQL discovery error:', error);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers grid node using Selenium Grid 3 REST API.
|
||||
*
|
||||
* @param {string} sessionId - WebDriver session ID.
|
||||
* @param {string} gridUrl - Grid hub URL.
|
||||
* @returns {Promise<string | null>} Node hostname or null.
|
||||
*/
|
||||
async function discoverGridNodeREST(sessionId: string, gridUrl: string): Promise<string | null> {
|
||||
try {
|
||||
const baseUrl = getBaseGridUrl(gridUrl);
|
||||
const restUrl = `${baseUrl}/grid/api/testsession?session=${sessionId}`;
|
||||
|
||||
console.log(`Attempting Grid 3 discovery: ${restUrl}`);
|
||||
|
||||
const response = await fetch(restUrl);
|
||||
|
||||
console.log(`REST API response status: ${response.status} ${response.statusText}`);
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
console.log('REST API response data:', JSON.stringify(data, null, 2));
|
||||
|
||||
const proxyId = data?.proxyId;
|
||||
|
||||
if (proxyId && data.success) {
|
||||
// proxyId format: "http://192.168.1.100:5555"
|
||||
const url = new URL(proxyId);
|
||||
|
||||
console.log(`Discovered grid node via REST API (Grid 3): ${url.hostname}`);
|
||||
|
||||
return url.hostname;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.log('REST API discovery error:', error);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const chromeArgs = [
|
||||
'--allow-insecure-localhost',
|
||||
'--use-fake-ui-for-media-stream',
|
||||
@@ -36,7 +182,11 @@ const chromeArgs = [
|
||||
// Avoids - "You are checking for animations on an inactive tab, animations do not run for inactive tabs"
|
||||
// when executing waitForStable()
|
||||
'--disable-renderer-backgrounding',
|
||||
'--use-file-for-fake-audio-capture=tests/resources/fakeAudioStream.wav'
|
||||
'--use-file-for-fake-audio-capture=tests/resources/fakeAudioStream.wav',
|
||||
|
||||
// Enable remote debugging for CDP access via Puppeteer (required for NetworkCapture)
|
||||
// Port 0 means Chrome will choose an available port automatically
|
||||
'--remote-debugging-port=35699'
|
||||
];
|
||||
|
||||
if (process.env.RESOLVER_RULES) {
|
||||
@@ -127,6 +277,9 @@ const TEST_RESULTS_DIR = 'test-results';
|
||||
|
||||
const keepAlive: Array<any> = [];
|
||||
|
||||
/** Network capture instances for each browser (when CAPTURE_NETWORK is enabled). */
|
||||
const networkCaptures: Map<string, NetworkCapture> = new Map();
|
||||
|
||||
export const config: WebdriverIO.MultiremoteConfig = {
|
||||
|
||||
runner: 'local',
|
||||
@@ -240,6 +393,37 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
await bInstance.execute(() => console.log(`${new Date().toISOString()} keep-alive`));
|
||||
}, 20_000));
|
||||
|
||||
// Discover grid node if running on Selenium Grid
|
||||
// This enables automatic network capture support for grid deployments
|
||||
if (process.env.GRID_HOST_URL && !bInstance.isFirefox) {
|
||||
try {
|
||||
const nodeHostname = await discoverGridNode(bInstance.sessionId, process.env.GRID_HOST_URL);
|
||||
|
||||
if (nodeHostname) {
|
||||
// Store discovered hostname in capabilities for NetworkCapture and other uses
|
||||
(bInstance.capabilities as any)['custom:nodeHostname'] = nodeHostname;
|
||||
console.log(`${instance}: Discovered running on grid node ${nodeHostname}`);
|
||||
} else {
|
||||
console.warn(`${instance}: Failed to discover grid node, will use fallback methods`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${instance}: Error during grid node discovery:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Setup network capture if enabled
|
||||
if (process.env.CAPTURE_NETWORK === 'true' && !bInstance.isFirefox) {
|
||||
try {
|
||||
const capture = new NetworkCapture(bInstance);
|
||||
|
||||
await capture.start();
|
||||
networkCaptures.set(`${instance}-${cid}-${testName}`, capture);
|
||||
console.log(`Network capture started for ${instance}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to start network capture for ${instance}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (bInstance.isFirefox) {
|
||||
return;
|
||||
}
|
||||
@@ -278,11 +462,35 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
}
|
||||
},
|
||||
|
||||
after() {
|
||||
async after() {
|
||||
const { ctx }: any = global;
|
||||
|
||||
ctx?.webhooksProxy?.disconnect();
|
||||
keepAlive.forEach(clearInterval);
|
||||
|
||||
// Stop network capture and export data
|
||||
if (networkCaptures.size > 0) {
|
||||
await Promise.all(Array.from(networkCaptures.entries()).map(async ([ key, capture ]) => {
|
||||
try {
|
||||
await capture.stop();
|
||||
const outputPath = path.join(TEST_RESULTS_DIR, `network-${key}.json`);
|
||||
|
||||
capture.exportToJSON(outputPath);
|
||||
|
||||
const stats = capture.getStats();
|
||||
|
||||
console.log(`\nNetwork Capture Summary for ${key}:`);
|
||||
console.log(` Total requests: ${stats.total}`);
|
||||
console.log(` Succeeded: ${stats.succeeded}`);
|
||||
console.log(` Failed: ${stats.failed}`);
|
||||
console.log(` Cached: ${stats.cachedRequests}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to stop/export network capture for ${key}:`, error);
|
||||
}
|
||||
}));
|
||||
|
||||
networkCaptures.clear();
|
||||
}
|
||||
},
|
||||
|
||||
beforeSession(c, capabilities_, specs_, cid) {
|
||||
|
||||
Reference in New Issue
Block a user