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>
128 lines
4.8 KiB
TypeScript
128 lines
4.8 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|