import { expect, type Page } from '@playwright/test'; export interface PeerRoleEdge { /** Remote peer id, in the identity space of the signal server routing that peer. */ peerId: string; /** Our own actor id in that same identity space. */ localActorId: string | null; isInitiator: boolean; connectionState: string; } /** * Read who elected themselves initiator for every active peer, together with the local * actor id in that peer's identity space. Both halves of a pair must describe the same * two ids, which is what makes the election comparable in the first place. */ export async function readPeerRoleEdges(page: Page): Promise { return await page.evaluate(() => { interface AngularDebugApi { getComponent: (element: Element) => Record; } interface PeerDataShape { connection?: { connectionState?: string }; isInitiator?: boolean; } interface RealtimeShape { peerManager?: { activePeerConnections?: Map }; signalingTransportHandler?: { getIdentifyCredentialsForPeer?: (peerId: string) => { oderId?: string } | null; }; } const host = document.querySelector('app-rooms-side-panel'); const debugApi = (window as { ng?: AngularDebugApi }).ng; if (!host || !debugApi?.getComponent) { return []; } const realtime = debugApi.getComponent(host)['realtime'] as RealtimeShape | undefined; const peers = realtime?.peerManager?.activePeerConnections; if (!peers) { return []; } const edges: PeerRoleEdge[] = []; peers.forEach((peerData, peerId) => { const credentials = realtime?.signalingTransportHandler?.getIdentifyCredentialsForPeer?.(peerId); edges.push({ connectionState: peerData.connection?.connectionState ?? 'unknown', isInitiator: peerData.isInitiator === true, localActorId: credentials?.oderId ?? null, peerId }); }); return edges; }); } /** * How many RTCPeerConnections this page has created since load. A clean session creates * exactly one per remote peer; a rebuilt peer - for example a non-initiator that gave up * waiting for an offer that was never elected to be sent - adds another. */ export async function countCreatedPeerConnections(page: Page): Promise { return await page.evaluate(() => ((window as unknown as { __rtcConnections?: RTCPeerConnection[] }).__rtcConnections ?? []).length ); } /** * Every connected pair must have exactly one initiator. Comparing ids from two different * identity spaces breaks the antisymmetry of the election, so both peers offer (glare) or * neither does until a takeover timer fires. */ export function expectExactlyOneInitiatorPerPair(edgesByClient: Record): void { const directed = new Map(); const pairs = new Set(); for (const [clientName, edges] of Object.entries(edgesByClient)) { for (const edge of edges) { expect( edge.localActorId, `${clientName} has no local actor id in the identity space of peer ${edge.peerId}` ).toBeTruthy(); const localActorId = edge.localActorId as string; directed.set(`${localActorId}->${edge.peerId}`, edge.isInitiator); pairs.add([localActorId, edge.peerId].sort().join('<->')); } } expect(pairs.size, 'expected at least one peer pair').toBeGreaterThan(0); for (const pair of pairs) { const [first, second] = pair.split('<->'); const forward = directed.get(`${first}->${second}`); const backward = directed.get(`${second}->${first}`); expect(forward, `missing peer connection ${first} -> ${second}`).not.toBeUndefined(); expect(backward, `missing peer connection ${second} -> ${first}`).not.toBeUndefined(); expect( [forward, backward].filter(Boolean), `expected exactly one initiator for pair ${pair}` ).toHaveLength(1); } }