/* eslint-disable @typescript-eslint/no-explicit-any */ import { type Page } from '@playwright/test'; /** * Install RTCPeerConnection monkey-patch on a page BEFORE navigating. * Tracks all created peer connections and their remote tracks so tests * can inspect WebRTC state via `page.evaluate()`. * * Call immediately after page creation, before any `goto()`. */ export async function installWebRTCTracking(page: Page): Promise { await page.addInitScript(() => { const connections: RTCPeerConnection[] = []; (window as any).__rtcConnections = connections; (window as any).__rtcRemoteTracks = [] as { kind: string; id: string; readyState: string }[]; const OriginalRTCPeerConnection = window.RTCPeerConnection; (window as any).RTCPeerConnection = function(this: RTCPeerConnection, ...args: any[]) { const pc: RTCPeerConnection = new OriginalRTCPeerConnection(...args); connections.push(pc); pc.addEventListener('connectionstatechange', () => { (window as any).__lastRtcState = pc.connectionState; }); pc.addEventListener('track', (event: RTCTrackEvent) => { (window as any).__rtcRemoteTracks.push({ kind: event.track.kind, id: event.track.id, readyState: event.track.readyState }); }); return pc; } as any; (window as any).RTCPeerConnection.prototype = OriginalRTCPeerConnection.prototype; Object.setPrototypeOf((window as any).RTCPeerConnection, OriginalRTCPeerConnection); }); } /** * Wait until at least one RTCPeerConnection reaches the 'connected' state. */ export async function waitForPeerConnected(page: Page, timeout = 30_000): Promise { await page.waitForFunction( () => (window as any).__rtcConnections?.some( (pc: RTCPeerConnection) => pc.connectionState === 'connected' ) ?? false, { timeout } ); } /** * Check that a peer connection is still in 'connected' state (not failed/disconnected). */ export async function isPeerStillConnected(page: Page): Promise { return page.evaluate( () => (window as any).__rtcConnections?.some( (pc: RTCPeerConnection) => pc.connectionState === 'connected' ) ?? false ); } /** * Get outbound and inbound audio RTP stats from the first peer connection. */ export async function getAudioStats(page: Page): Promise<{ outbound: { bytesSent: number; packetsSent: number } | null; inbound: { bytesReceived: number; packetsReceived: number } | null; }> { return page.evaluate(async () => { const connections = (window as any).__rtcConnections as RTCPeerConnection[] | undefined; if (!connections?.length) return { outbound: null, inbound: null }; let outbound: { bytesSent: number; packetsSent: number } | null = null; let inbound: { bytesReceived: number; packetsReceived: number } | null = null; for (const pc of connections) { if (pc.connectionState !== 'connected') continue; const stats = await pc.getStats(); stats.forEach((report: any) => { const reportMediaType = report.kind ?? report.mediaType; if (report.type === 'outbound-rtp' && reportMediaType === 'audio' && !outbound) { outbound = { bytesSent: report.bytesSent ?? 0, packetsSent: report.packetsSent ?? 0 }; } if (report.type === 'inbound-rtp' && reportMediaType === 'audio' && !inbound) { inbound = { bytesReceived: report.bytesReceived ?? 0, packetsReceived: report.packetsReceived ?? 0 }; } }); if (outbound && inbound) break; } return { outbound, inbound }; }); } /** * Snapshot audio stats, wait `durationMs`, snapshot again, and return the delta. * Useful for verifying audio is actively flowing (bytes increasing). */ export async function getAudioStatsDelta(page: Page, durationMs = 3_000): Promise<{ outboundBytesDelta: number; inboundBytesDelta: number; }> { const before = await getAudioStats(page); await page.waitForTimeout(durationMs); const after = await getAudioStats(page); return { outboundBytesDelta: (after.outbound?.bytesSent ?? 0) - (before.outbound?.bytesSent ?? 0), inboundBytesDelta: (after.inbound?.bytesReceived ?? 0) - (before.inbound?.bytesReceived ?? 0) }; }