fix: Bug - Voice states doesn't get cleared for all users on leave
Broadcast a cleared voice_state when voice-active sockets drop and reset mute/deafen flags on disconnect or reconnect so stale session state cannot leak to other clients. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
127
e2e/tests/voice/voice-mute-state-reset.spec.ts
Normal file
127
e2e/tests/voice/voice-mute-state-reset.spec.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { test, expect } from '../../fixtures/multi-client';
|
||||
import {
|
||||
MULTI_DEVICE_PASSWORD,
|
||||
MULTI_DEVICE_VOICE_CHANNEL,
|
||||
closeClient,
|
||||
loginSecondDeviceIntoServer,
|
||||
uniqueMultiDeviceName
|
||||
} from '../../helpers/multi-device-session';
|
||||
import { RegisterPage } from '../../pages/register.page';
|
||||
import { ServerSearchPage } from '../../pages/server-search.page';
|
||||
import { ChatRoomPage } from '../../pages/chat-room.page';
|
||||
|
||||
async function waitForVoiceMuteState(
|
||||
page: import('@playwright/test').Page,
|
||||
displayName: string,
|
||||
expectedMuted: boolean,
|
||||
timeout = 45_000
|
||||
): Promise<void> {
|
||||
await page.waitForFunction(
|
||||
({ expectedDisplayName, expectedMuted: muted }) => {
|
||||
interface VoiceStateShape { isMuted?: boolean }
|
||||
interface UserShape { displayName: string; voiceState?: VoiceStateShape }
|
||||
interface ChannelShape { id: string; type: 'text' | 'voice' }
|
||||
interface RoomShape { channels?: ChannelShape[] }
|
||||
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 false;
|
||||
}
|
||||
|
||||
const component = debugApi.getComponent(host);
|
||||
const currentRoom = (component['currentRoom'] as (() => RoomShape | null) | undefined)?.() ?? null;
|
||||
const voiceChannel = currentRoom?.channels?.find((channel) => channel.type === 'voice');
|
||||
|
||||
if (!voiceChannel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const roster = (component['voiceUsersInRoom'] as ((roomId: string) => UserShape[]) | undefined)?.(voiceChannel.id) ?? [];
|
||||
const entry = roster.find((userEntry) => userEntry.displayName === expectedDisplayName);
|
||||
|
||||
return entry?.voiceState?.isMuted === muted;
|
||||
},
|
||||
{ expectedDisplayName: displayName, expectedMuted },
|
||||
{ timeout }
|
||||
);
|
||||
}
|
||||
|
||||
test.describe('Voice mute state reset', () => {
|
||||
test.describe.configure({ timeout: 300_000, retries: 1 });
|
||||
|
||||
test('clears stale mute state after abrupt disconnect and voice rejoin', async ({ createClient }) => {
|
||||
const suffix = uniqueMultiDeviceName('voice-mute-reset');
|
||||
const hostCredentials = {
|
||||
username: `host_${suffix}`,
|
||||
displayName: 'Voice Host',
|
||||
password: MULTI_DEVICE_PASSWORD
|
||||
};
|
||||
const guestCredentials = {
|
||||
username: `guest_${suffix}`,
|
||||
displayName: 'Voice Guest',
|
||||
password: MULTI_DEVICE_PASSWORD
|
||||
};
|
||||
const serverName = `Voice Mute Reset ${suffix}`;
|
||||
|
||||
let hostClient = await createClient();
|
||||
|
||||
const guestClient = await createClient();
|
||||
|
||||
await test.step('host creates the shared server', async () => {
|
||||
const registerPage = new RegisterPage(hostClient.page);
|
||||
|
||||
await registerPage.goto();
|
||||
await registerPage.register(hostCredentials.username, hostCredentials.displayName, hostCredentials.password);
|
||||
await expect(hostClient.page).toHaveURL(/\/dashboard/, { timeout: 15_000 });
|
||||
|
||||
const search = new ServerSearchPage(hostClient.page);
|
||||
|
||||
await search.createServer(serverName, { description: 'Voice mute reset coverage' });
|
||||
await expect(hostClient.page).toHaveURL(/\/room\//, { timeout: 15_000 });
|
||||
});
|
||||
|
||||
const hostRoom = new ChatRoomPage(hostClient.page);
|
||||
|
||||
await hostRoom.ensureVoiceChannelExists(MULTI_DEVICE_VOICE_CHANNEL);
|
||||
|
||||
await test.step('guest joins the server', async () => {
|
||||
const registerPage = new RegisterPage(guestClient.page);
|
||||
|
||||
await registerPage.goto();
|
||||
await registerPage.register(guestCredentials.username, guestCredentials.displayName, guestCredentials.password);
|
||||
await expect(guestClient.page).toHaveURL(/\/dashboard/, { timeout: 15_000 });
|
||||
|
||||
const search = new ServerSearchPage(guestClient.page);
|
||||
|
||||
await search.joinServerFromSearch(serverName);
|
||||
await expect(guestClient.page).toHaveURL(/\/room\//, { timeout: 20_000 });
|
||||
});
|
||||
|
||||
await test.step('host joins voice muted and guest observes the muted state', async () => {
|
||||
await hostRoom.joinVoiceChannel(MULTI_DEVICE_VOICE_CHANNEL);
|
||||
await expect(hostRoom.voiceControls).toBeVisible({ timeout: 20_000 });
|
||||
await hostRoom.muteButton.click();
|
||||
|
||||
await waitForVoiceMuteState(guestClient.page, hostCredentials.displayName, true);
|
||||
});
|
||||
|
||||
await test.step('abrupt host disconnect clears stale mute before rejoin', async () => {
|
||||
await closeClient(hostClient);
|
||||
|
||||
hostClient = await createClient();
|
||||
await loginSecondDeviceIntoServer(hostClient.page, hostCredentials, serverName);
|
||||
|
||||
const reopenedRoom = new ChatRoomPage(hostClient.page);
|
||||
|
||||
await reopenedRoom.joinVoiceChannel(MULTI_DEVICE_VOICE_CHANNEL);
|
||||
await expect(reopenedRoom.voiceControls).toBeVisible({ timeout: 20_000 });
|
||||
|
||||
await waitForVoiceMuteState(guestClient.page, hostCredentials.displayName, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user