fix: Bug - User automatically leaves voice after short period of time
All checks were successful
Queue Release Build / prepare (push) Successful in 22s
Deploy Web Apps / deploy (push) Successful in 7m32s
Queue Release Build / build-windows (push) Successful in 27m41s
Queue Release Build / build-linux (push) Successful in 44m56s
Queue Release Build / build-android (push) Successful in 18m52s
Queue Release Build / finalize (push) Successful in 21s

Ignore stale P2P self-disconnect voice-state echoes while this client actively owns voice, refresh noise-reduction input on re-join, and repair dual-signal E2E harness expectations.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-12 04:04:31 +02:00
parent dac5cb42a5
commit c3c2f01cc6
10 changed files with 345 additions and 226 deletions

View File

@@ -0,0 +1,72 @@
import { expect, type Page } from '@playwright/test';
/** Read how many signaling managers are currently connected for this page. */
export async function getConnectedSignalManagerCount(page: Page): Promise<number> {
return page.evaluate(() => {
interface AngularDebugApi {
getComponent: (element: Element) => Record<string, unknown>;
}
const host = document.querySelector('app-rooms-side-panel');
const debugApi = (window as { ng?: AngularDebugApi }).ng;
if (!host || !debugApi?.getComponent) {
return 0;
}
const component = debugApi.getComponent(host);
const realtime = component['realtime'] as {
signalingTransportHandler?: {
getConnectedSignalingManagers?: () => unknown[];
};
} | undefined;
return realtime?.signalingTransportHandler?.getConnectedSignalingManagers?.().length ?? 0;
});
}
/**
* Dual-signal setups create one RTCPeerConnection per remote peer per active
* signaling manager, so the harness tracks `remotePeerCount * signalCount`
* connected peer connections.
*/
export async function waitForConnectedRemotePeerMesh(
page: Page,
remotePeerCount: number,
timeout = 45_000
): Promise<void> {
const signalCount = Math.max(await getConnectedSignalManagerCount(page), 1);
const expectedCount = remotePeerCount * signalCount;
const minimumCount = Math.max(remotePeerCount, expectedCount - signalCount);
await page.waitForFunction(
(min) => ((window as unknown as {
__rtcConnections?: RTCPeerConnection[];
}).__rtcConnections ?? []).filter(
(pc) => pc.connectionState === 'connected'
).length >= min,
minimumCount,
{ timeout }
);
}
export async function getMinimumConnectedPeerMeshCount(
page: Page,
remotePeerCount: number
): Promise<number> {
const signalCount = Math.max(await getConnectedSignalManagerCount(page), 1);
const expectedCount = remotePeerCount * signalCount;
return Math.max(remotePeerCount, expectedCount - signalCount);
}
export async function waitForConnectedSignalManagerCount(
page: Page,
expectedCount: number,
timeout = 30_000
): Promise<void> {
await expect.poll(async () => await getConnectedSignalManagerCount(page), {
timeout,
intervals: [500, 1_000]
}).toBe(expectedCount);
}

View File

@@ -0,0 +1,49 @@
import { type Page } from '@playwright/test';
/** Wait until the side-panel roster under a voice channel lists the expected user count. */
export async function waitForVoiceRosterCount(
page: Page,
channelName: string,
expectedCount: number,
timeout = 45_000
): Promise<void> {
await page.waitForFunction(
({ expected, name }) => {
const buttons = document.querySelectorAll(
`app-rooms-side-panel button[data-channel-type="voice"][data-channel-name="${name}"]`
);
for (const button of buttons) {
const panel = button.closest('app-rooms-side-panel');
if (!panel || panel.getBoundingClientRect().width === 0) {
continue;
}
const rosterDiv = button.nextElementSibling;
if (!rosterDiv) {
continue;
}
const displayNames = new Set<string>();
rosterDiv.querySelectorAll('[appThemeNode="roomVoiceUserItem"] span.text-sm').forEach((element) => {
const label = element.textContent?.trim();
if (label) {
displayNames.add(label);
}
});
if (displayNames.size === expected) {
return true;
}
}
return false;
},
{ expected: expectedCount, name: channelName },
{ timeout }
);
}

View File

@@ -8,10 +8,6 @@ interface ScreenShareMediaStream extends MediaStream {
__isScreenShare?: boolean;
}
function webRtcHarnessWindow(scope: Window = window): WebRtcTestHarnessWindow {
return scope as unknown as WebRtcTestHarnessWindow;
}
/**
* Install RTCPeerConnection monkey-patch on a page BEFORE navigating.
* Tracks all created peer connections and their remote tracks so tests
@@ -32,7 +28,7 @@ export async function installWebRTCTracking(target: BrowserContext | Page): Prom
source?: AudioScheduledSourceNode;
drawIntervalId?: number;
}[] = [];
const harness = webRtcHarnessWindow();
const harness = window as unknown as WebRtcTestHarnessWindow;
harness.__rtcConnections = connections;
harness.__rtcDataChannels = dataChannels;
@@ -160,6 +156,7 @@ export async function installWebRTCTracking(target: BrowserContext | Page): Prom
return resultStream;
};
});
}
@@ -181,7 +178,7 @@ export async function installWebRTCTracking(target: BrowserContext | Page): Prom
export async function installAutoResumeAudioContext(page: Page): Promise<void> {
await page.addInitScript(() => {
const OrigAudioContext = window.AudioContext;
const audioHarness = webRtcHarnessWindow();
const audioHarness = window as unknown as WebRtcTestHarnessWindow;
audioHarness.AudioContext = function(this: AudioContext, ...args: AudioContextArgs) {
const ctx: AudioContext = new OrigAudioContext(...args);
@@ -211,7 +208,7 @@ export async function installAutoResumeAudioContext(page: Page): Promise<void> {
export async function waitForPeerConnected(page: Page, timeout = 30_000): Promise<void> {
await page.waitForFunction(
() => webRtcHarnessWindow().__rtcConnections?.some(
() => (window as unknown as WebRtcTestHarnessWindow).__rtcConnections?.some(
(pc: RTCPeerConnection) => pc.connectionState === 'connected'
) ?? false,
undefined,
@@ -224,7 +221,7 @@ export async function waitForPeerConnected(page: Page, timeout = 30_000): Promis
*/
export async function isPeerStillConnected(page: Page): Promise<boolean> {
return page.evaluate(
() => webRtcHarnessWindow().__rtcConnections?.some(
() => (window as unknown as WebRtcTestHarnessWindow).__rtcConnections?.some(
(pc: RTCPeerConnection) => pc.connectionState === 'connected'
) ?? false
);
@@ -233,7 +230,7 @@ export async function isPeerStillConnected(page: Page): Promise<boolean> {
/** Returns the number of tracked peer connections in `connected` state. */
export async function getConnectedPeerCount(page: Page): Promise<number> {
return page.evaluate(
() => (webRtcHarnessWindow().__rtcConnections as RTCPeerConnection[] | undefined)?.filter(
() => ((window as unknown as WebRtcTestHarnessWindow).__rtcConnections as RTCPeerConnection[] | undefined)?.filter(
(pc) => pc.connectionState === 'connected'
).length ?? 0
);
@@ -241,19 +238,36 @@ export async function getConnectedPeerCount(page: Page): Promise<number> {
/** Wait until the expected number of peer connections are `connected`. */
export async function waitForConnectedPeerCount(page: Page, expectedCount: number, timeout = 45_000): Promise<void> {
await page.waitForFunction(
(count) => (webRtcHarnessWindow().__rtcConnections as RTCPeerConnection[] | undefined)?.filter(
(pc) => pc.connectionState === 'connected'
).length === count,
expectedCount,
{ timeout }
);
try {
await page.waitForFunction(
(count) => ((window as unknown as WebRtcTestHarnessWindow).__rtcConnections as RTCPeerConnection[] | undefined)?.filter(
(pc) => pc.connectionState === 'connected'
).length === count,
expectedCount,
{ timeout }
);
} catch (error) {
const diagnostics = await page.evaluate(() => {
const connections = (window as unknown as WebRtcTestHarnessWindow).__rtcConnections ?? [];
return {
connected: connections.filter((pc) => pc.connectionState === 'connected').length,
states: connections.map((pc) => pc.connectionState)
};
});
throw new Error(
`Expected ${expectedCount} connected peers within ${timeout}ms; `
+ `saw ${diagnostics.connected} connected (${diagnostics.states.join(', ') || 'none'})`,
{ cause: error }
);
}
}
/** Returns the number of tracked RTCDataChannels in the open state. */
export async function getOpenDataChannelCount(page: Page): Promise<number> {
return page.evaluate(
() => (webRtcHarnessWindow().__rtcDataChannels as RTCDataChannel[] | undefined)?.filter(
() => ((window as unknown as WebRtcTestHarnessWindow).__rtcDataChannels as RTCDataChannel[] | undefined)?.filter(
(channel) => channel.readyState === 'open'
).length ?? 0
);
@@ -262,7 +276,7 @@ export async function getOpenDataChannelCount(page: Page): Promise<number> {
/** Wait until the expected number of tracked RTCDataChannels are open. */
export async function waitForOpenDataChannelCount(page: Page, expectedCount: number, timeout = 45_000): Promise<void> {
await page.waitForFunction(
(count) => (webRtcHarnessWindow().__rtcDataChannels as RTCDataChannel[] | undefined)?.filter(
(count) => ((window as unknown as WebRtcTestHarnessWindow).__rtcDataChannels as RTCDataChannel[] | undefined)?.filter(
(channel) => channel.readyState === 'open'
).length === count,
expectedCount,
@@ -273,7 +287,7 @@ export async function waitForOpenDataChannelCount(page: Page, expectedCount: num
/** Close every currently-open RTCDataChannel and return how many were closed. */
export async function closeOpenDataChannels(page: Page): Promise<number> {
return page.evaluate(() => {
const channels = (webRtcHarnessWindow().__rtcDataChannels as RTCDataChannel[] | undefined) ?? [];
const channels = ((window as unknown as WebRtcTestHarnessWindow).__rtcDataChannels as RTCDataChannel[] | undefined) ?? [];
let closed = 0;
@@ -293,7 +307,7 @@ export async function closeOpenDataChannels(page: Page): Promise<number> {
/** Dispatch a synthetic data-channel error event on each open channel. */
export async function dispatchDataChannelErrors(page: Page): Promise<number> {
return page.evaluate(() => {
const channels = (webRtcHarnessWindow().__rtcDataChannels as RTCDataChannel[] | undefined) ?? [];
const channels = ((window as unknown as WebRtcTestHarnessWindow).__rtcDataChannels as RTCDataChannel[] | undefined) ?? [];
let dispatched = 0;
@@ -354,7 +368,7 @@ interface PerPeerAudioStat {
/** Get per-peer audio stats for every tracked RTCPeerConnection. */
export async function getPerPeerAudioStats(page: Page): Promise<PerPeerAudioStat[]> {
return page.evaluate(async () => {
const connections = webRtcHarnessWindow().__rtcConnections as RTCPeerConnection[] | undefined;
const connections = (window as unknown as WebRtcTestHarnessWindow).__rtcConnections as RTCPeerConnection[] | undefined;
if (!connections?.length) {
return [];
@@ -472,7 +486,7 @@ export async function getAudioStats(page: Page): Promise<{
inbound: { bytesReceived: number; packetsReceived: number } | null;
}> {
return page.evaluate(async () => {
const connections = webRtcHarnessWindow().__rtcConnections as RTCPeerConnection[] | undefined;
const connections = (window as unknown as WebRtcTestHarnessWindow).__rtcConnections as RTCPeerConnection[] | undefined;
if (!connections?.length)
return { outbound: null, inbound: null };
@@ -486,8 +500,8 @@ export async function getAudioStats(page: Page): Promise<{
hasInbound: boolean;
};
const hwm: Record<number, HWMEntry> = webRtcHarnessWindow().__rtcStatsHWM =
(webRtcHarnessWindow().__rtcStatsHWM as Record<number, HWMEntry> | undefined) ?? {};
const hwm: Record<number, HWMEntry> = (window as unknown as WebRtcTestHarnessWindow).__rtcStatsHWM =
((window as unknown as WebRtcTestHarnessWindow).__rtcStatsHWM as Record<number, HWMEntry> | undefined) ?? {};
for (let idx = 0; idx < connections.length; idx++) {
let stats: RTCStatsReport;
@@ -596,7 +610,7 @@ export async function getAudioStatsDelta(page: Page, durationMs = 3_000): Promis
export async function waitForAudioStatsPresent(page: Page, timeout = 15_000): Promise<void> {
await page.waitForFunction(
async () => {
const connections = webRtcHarnessWindow().__rtcConnections as RTCPeerConnection[] | undefined;
const connections = (window as unknown as WebRtcTestHarnessWindow).__rtcConnections as RTCPeerConnection[] | undefined;
if (!connections?.length)
return false;
@@ -705,7 +719,7 @@ export async function getVideoStats(page: Page): Promise<{
inbound: { bytesReceived: number; packetsReceived: number } | null;
}> {
return page.evaluate(async () => {
const connections = webRtcHarnessWindow().__rtcConnections as RTCPeerConnection[] | undefined;
const connections = (window as unknown as WebRtcTestHarnessWindow).__rtcConnections as RTCPeerConnection[] | undefined;
if (!connections?.length)
return { outbound: null, inbound: null };
@@ -719,8 +733,8 @@ export async function getVideoStats(page: Page): Promise<{
hasInbound: boolean;
}
const hwm: Record<number, VHWM> = webRtcHarnessWindow().__rtcVideoStatsHWM =
(webRtcHarnessWindow().__rtcVideoStatsHWM as Record<number, VHWM> | undefined) ?? {};
const hwm: Record<number, VHWM> = (window as unknown as WebRtcTestHarnessWindow).__rtcVideoStatsHWM =
((window as unknown as WebRtcTestHarnessWindow).__rtcVideoStatsHWM as Record<number, VHWM> | undefined) ?? {};
for (let idx = 0; idx < connections.length; idx++) {
let stats: RTCStatsReport;
@@ -804,7 +818,7 @@ export async function getVideoStats(page: Page): Promise<{
export async function waitForVideoStatsPresent(page: Page, timeout = 15_000): Promise<void> {
await page.waitForFunction(
async () => {
const connections = webRtcHarnessWindow().__rtcConnections as RTCPeerConnection[] | undefined;
const connections = (window as unknown as WebRtcTestHarnessWindow).__rtcConnections as RTCPeerConnection[] | undefined;
if (!connections?.length)
return false;
@@ -972,7 +986,7 @@ export async function waitForInboundVideoFlow(
*/
export async function dumpRtcDiagnostics(page: Page): Promise<string> {
return page.evaluate(async () => {
const conns = webRtcHarnessWindow().__rtcConnections as RTCPeerConnection[] | undefined;
const conns = (window as unknown as WebRtcTestHarnessWindow).__rtcConnections as RTCPeerConnection[] | undefined;
if (!conns?.length)
return 'No connections tracked';