mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 03:12:29 +00:00
feat(tests): Add network request capture and analysis for WDIO tests
Enables capturing all network requests during test execution to validate
URL allowlists, debug network failures, and verify requests match expected
patterns from comprehensive URL documentation.
Implementation uses puppeteer-core to connect directly to Chrome DevTools
Protocol, supporting both local testing and remote Selenium Grid deployments.
Key features:
- Captures URLs, status codes, resource types, and timing for all requests
- Exports to JSON with comprehensive statistics
- Analysis tool generates reports in multiple formats (text, CSV, JSON)
- Configurable grid support via environment variable or custom capability
- Works with multiremote mode (multiple browser instances)
- Zero-config for local development (opt-in via CAPTURE_NETWORK=true)
Added npm scripts:
- test-network / test-network-single: Run tests with capture enabled
- analyze-network: Generate reports from captured data
Dependencies added:
- puppeteer-core: Direct CDP access for network monitoring
- wdio-chromedriver-service: Chrome driver with debugging support
This commit is contained in:
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
|
||||
461
tests/NETWORK-CAPTURE.md
Normal file
461
tests/NETWORK-CAPTURE.md
Normal file
@@ -0,0 +1,461 @@
|
||||
# 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
|
||||
|
||||
When running tests on a Selenium/WebDriver grid with remote nodes, you need to tell NetworkCapture how to reach the remote Chrome instances.
|
||||
|
||||
**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 - Multiple Nodes)**
|
||||
|
||||
Use when tests run on different nodes and you need per-browser hostname:
|
||||
|
||||
```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)
|
||||
2. Environment variable `GRID_NODE_HOSTNAME`
|
||||
3. 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 env variable:
|
||||
NetworkCapture: Using grid node hostname from env: node-1.your-grid.com:65243
|
||||
|
||||
✓ Grid with 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';
|
||||
@@ -36,7 +37,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=0'
|
||||
];
|
||||
|
||||
if (process.env.RESOLVER_RULES) {
|
||||
@@ -127,6 +132,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 +248,19 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
await bInstance.execute(() => console.log(`${new Date().toISOString()} keep-alive`));
|
||||
}, 20_000));
|
||||
|
||||
// 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 +299,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