182 lines
6.1 KiB
TypeScript
182 lines
6.1 KiB
TypeScript
import { expect, type Page } from '@playwright/test';
|
|
import { test, type Client } from '../../fixtures/multi-client';
|
|
import {
|
|
closeOpenDataChannels,
|
|
dispatchDataChannelErrors,
|
|
dumpRtcDiagnostics,
|
|
getOpenDataChannelCount,
|
|
installAutoResumeAudioContext,
|
|
installWebRTCTracking,
|
|
waitForAllPeerAudioFlow,
|
|
waitForAudioStatsPresent,
|
|
waitForConnectedPeerCount,
|
|
waitForOpenDataChannelCount
|
|
} from '../../helpers/webrtc-helpers';
|
|
import { RegisterPage } from '../../pages/register.page';
|
|
import { ServerSearchPage } from '../../pages/server-search.page';
|
|
import { ChatRoomPage } from '../../pages/chat-room.page';
|
|
|
|
interface VoiceClient extends Client {
|
|
displayName: string;
|
|
username: string;
|
|
}
|
|
|
|
const USER_PASSWORD = 'TestPass123!';
|
|
const VOICE_CHANNEL = 'General';
|
|
|
|
test.describe('Voice data-channel recovery', () => {
|
|
test('keeps two users hearing each other after a data-channel error and close', async ({ createClient }) => {
|
|
test.setTimeout(240_000);
|
|
|
|
const clients = await createVoiceScenario(createClient, 2, `DC Recovery Duo ${Date.now()}`);
|
|
const [alice, bob] = clients;
|
|
|
|
await assertMeshAudio(clients, 1, 'initial two-user voice');
|
|
|
|
await test.step('A non-fatal data-channel error does not interrupt audio', async () => {
|
|
const dispatched = await dispatchDataChannelErrors(alice.page);
|
|
|
|
expect(dispatched).toBeGreaterThan(0);
|
|
await waitForOpenDataChannelCount(alice.page, 1, 15_000);
|
|
await waitForOpenDataChannelCount(bob.page, 1, 15_000);
|
|
await assertMeshAudio(clients, 1, 'after synthetic data-channel error');
|
|
});
|
|
|
|
await test.step('A closed data channel is rebuilt and audio resumes both ways', async () => {
|
|
const closed = await closeOpenDataChannels(alice.page);
|
|
|
|
expect(closed).toBeGreaterThan(0);
|
|
await waitForConnectedPeerCount(alice.page, 1, 60_000);
|
|
await waitForConnectedPeerCount(bob.page, 1, 60_000);
|
|
await waitForOpenDataChannelCount(alice.page, 1, 60_000);
|
|
await waitForOpenDataChannelCount(bob.page, 1, 60_000);
|
|
await assertMeshAudio(clients, 1, 'after data-channel close recovery');
|
|
});
|
|
});
|
|
|
|
test('heals a three-user voice mesh when one client loses every data channel', async ({ createClient }) => {
|
|
test.setTimeout(300_000);
|
|
|
|
const clients = await createVoiceScenario(createClient, 3, `DC Recovery Trio ${Date.now()}`);
|
|
const bob = clients[1];
|
|
|
|
await assertMeshAudio(clients, 2, 'initial three-user mesh');
|
|
|
|
await test.step('Bob loses all control channels and the full mesh recovers', async () => {
|
|
const closed = await closeOpenDataChannels(bob.page);
|
|
|
|
expect(closed).toBe(2);
|
|
|
|
for (const client of clients) {
|
|
await waitForConnectedPeerCount(client.page, 2, 90_000);
|
|
await waitForOpenDataChannelCount(client.page, 2, 90_000);
|
|
}
|
|
|
|
await assertMeshAudio(clients, 2, 'after full control-channel recovery');
|
|
});
|
|
});
|
|
});
|
|
|
|
async function createVoiceScenario(
|
|
createClient: () => Promise<Client>,
|
|
userCount: number,
|
|
serverName: string
|
|
): Promise<VoiceClient[]> {
|
|
const clients: VoiceClient[] = [];
|
|
|
|
for (let index = 0; index < userCount; index++) {
|
|
const client = await createClient();
|
|
const displayName = `DC Voice ${index + 1}`;
|
|
|
|
await installDeterministicVoiceSettings(client.page);
|
|
await installWebRTCTracking(client.page);
|
|
await installAutoResumeAudioContext(client.page);
|
|
|
|
clients.push({
|
|
...client,
|
|
displayName,
|
|
username: `dc_voice_${Date.now()}_${index + 1}`
|
|
});
|
|
}
|
|
|
|
await test.step('Register clients', async () => {
|
|
for (const client of clients) {
|
|
const registerPage = new RegisterPage(client.page);
|
|
|
|
await registerPage.goto();
|
|
await registerPage.register(client.username, client.displayName, USER_PASSWORD);
|
|
await expect(client.page).toHaveURL(/\/search/, { timeout: 20_000 });
|
|
}
|
|
});
|
|
|
|
await test.step('Create and join server', async () => {
|
|
const hostSearch = new ServerSearchPage(clients[0].page);
|
|
|
|
await hostSearch.createServer(serverName, { description: 'Data-channel recovery voice test' });
|
|
await expect(clients[0].page).toHaveURL(/\/room\//, { timeout: 20_000 });
|
|
|
|
for (const client of clients.slice(1)) {
|
|
const searchPage = new ServerSearchPage(client.page);
|
|
|
|
await searchPage.joinServerFromSearch(serverName);
|
|
await expect(client.page).toHaveURL(/\/room\//, { timeout: 20_000 });
|
|
}
|
|
});
|
|
|
|
await test.step('Join everyone to voice', async () => {
|
|
const hostRoom = new ChatRoomPage(clients[0].page);
|
|
|
|
await hostRoom.ensureVoiceChannelExists(VOICE_CHANNEL);
|
|
|
|
for (const client of clients) {
|
|
const room = new ChatRoomPage(client.page);
|
|
|
|
await room.joinVoiceChannel(VOICE_CHANNEL);
|
|
await expect(room.voiceControls).toBeVisible({ timeout: 20_000 });
|
|
}
|
|
|
|
const expectedRemotePeers = clients.length - 1;
|
|
|
|
for (const client of clients) {
|
|
await waitForConnectedPeerCount(client.page, expectedRemotePeers, 90_000);
|
|
await waitForOpenDataChannelCount(client.page, expectedRemotePeers, 90_000);
|
|
await waitForAudioStatsPresent(client.page, 30_000);
|
|
}
|
|
});
|
|
|
|
return clients;
|
|
}
|
|
|
|
async function installDeterministicVoiceSettings(page: Page): Promise<void> {
|
|
await page.addInitScript(() => {
|
|
localStorage.setItem('metoyou_voice_settings', JSON.stringify({
|
|
inputVolume: 100,
|
|
outputVolume: 100,
|
|
audioBitrate: 96,
|
|
latencyProfile: 'balanced',
|
|
includeSystemAudio: false,
|
|
noiseReduction: false,
|
|
screenShareQuality: 'balanced',
|
|
askScreenShareQuality: false
|
|
}));
|
|
});
|
|
}
|
|
|
|
async function assertMeshAudio(
|
|
clients: readonly VoiceClient[],
|
|
expectedRemotePeers: number,
|
|
label: string
|
|
): Promise<void> {
|
|
for (const client of clients) {
|
|
try {
|
|
await waitForAllPeerAudioFlow(client.page, expectedRemotePeers, 60_000);
|
|
} catch (error) {
|
|
const dataChannelCount = await getOpenDataChannelCount(client.page);
|
|
|
|
console.log(`[${client.displayName} ${label} data channels] ${dataChannelCount}`);
|
|
console.log(`[${client.displayName} ${label} RTC]\n${await dumpRtcDiagnostics(client.page)}`);
|
|
throw error;
|
|
}
|
|
}
|
|
}
|