feat(tests): Add automatic Selenium Grid node discovery for network capture

Implement automatic discovery of grid node addresses to eliminate manual
configuration when running network capture on Selenium Grid deployments.

Changes:
- Added GraphQL API discovery for Grid 4 (queries /graphql endpoint)
- Added REST API discovery for Grid 3 (queries /grid/api/testsession)
- Added getBaseGridUrl() to strip /wd/hub suffix before API queries
- Integrated discovery into before hook, stores result in custom:nodeHostname
- Updated documentation with automatic discovery examples

Configuration priority:
1. Custom capability 'custom:nodeHostname' (manual override)
2. Automatic discovery via GRID_HOST_URL (zero config for Grid 4/3)
3. Environment variable GRID_NODE_HOSTNAME (fallback)
4. Default: localhost (local testing)

Grid 4/3 APIs are at root level, not under /wd/hub path.
This commit is contained in:
Hristo Terezov
2025-11-07 11:06:53 -06:00
parent 7ab6c283a6
commit 0358777b72
2 changed files with 196 additions and 8 deletions

View File

@@ -278,7 +278,23 @@ CAPTURE_NETWORK=true npm run test-dev-single -- tests/specs/2way/audioOnlyTest.s
#### 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.
**✨ 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)**
@@ -288,9 +304,9 @@ Use when all tests run on the same grid node:
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)**
**Option 2: Custom Capability (Advanced - Per-Browser)**
Use when tests run on different nodes and you need per-browser hostname:
Use when you want to manually specify hostname per browser:
```javascript
// In your grid configuration or wdio.conf.ts:
@@ -304,9 +320,10 @@ capabilities: {
```
**Priority Order:**
1. Custom capability `'custom:nodeHostname'` (highest priority)
2. Environment variable `GRID_NODE_HOSTNAME`
3. Default: `localhost` (local testing)
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
@@ -355,10 +372,18 @@ 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 capability:
✓ Grid with custom capability:
NetworkCapture: Using grid node hostname from capability: node-1.your-grid.com:65243
```

View File

@@ -21,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',
@@ -41,7 +186,7 @@ const chromeArgs = [
// 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'
'--remote-debugging-port=35699'
];
if (process.env.RESOLVER_RULES) {
@@ -248,6 +393,24 @@ 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 {