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
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:
72
e2e/helpers/signal-manager.ts
Normal file
72
e2e/helpers/signal-manager.ts
Normal 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);
|
||||
}
|
||||
49
e2e/helpers/voice-roster.ts
Normal file
49
e2e/helpers/voice-roster.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user