chore: dev-stack switches, shared e2e harness, and desktop shell rules
- `LIVE_RELOAD=false npm run dev` keeps the renderer alive across a machine suspend; the reload client otherwise destroys the session under test. - `dev-peer.sh` plus a separate userdata dir runs a second local peer. - `tools/voice-probe.js` samples peer state and RTP counters from a live window, persisting to localStorage so a renderer reload cannot erase it. - e2e helpers for voice pairs, peer-role election, and a TURN relay. - Electron single-instance and dev-client-load decisions move into rules files with colocated specs.
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
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<PeerRoleEdge[]> {
|
||||
return await page.evaluate(() => {
|
||||
interface AngularDebugApi {
|
||||
getComponent: (element: Element) => Record<string, unknown>;
|
||||
}
|
||||
interface PeerDataShape {
|
||||
connection?: { connectionState?: string };
|
||||
isInitiator?: boolean;
|
||||
}
|
||||
interface RealtimeShape {
|
||||
peerManager?: { activePeerConnections?: Map<string, PeerDataShape> };
|
||||
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<number> {
|
||||
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<string, PeerRoleEdge[]>): void {
|
||||
const directed = new Map<string, boolean>();
|
||||
const pairs = new Set<string>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user