import { test, expect } from '../../fixtures/multi-client'; import { installWebRTCTracking, waitForPeerConnected, isPeerStillConnected, getAudioStatsDelta, waitForAudioFlow, waitForAudioStatsPresent, dumpRtcDiagnostics } from '../../helpers/webrtc-helpers'; import { RegisterPage } from '../../pages/register.page'; import { ServerSearchPage } from '../../pages/server-search.page'; import { ChatRoomPage } from '../../pages/chat-room.page'; /** * Full user journey: register → create server → join → voice → verify audio * for 10+ seconds of stable connectivity. * * Uses two independent browser contexts (Alice & Bob) to simulate real * multi-user WebRTC voice chat. */ const ALICE = { username: `alice_${Date.now()}`, displayName: 'Alice', password: 'TestPass123!' }; const BOB = { username: `bob_${Date.now()}`, displayName: 'Bob', password: 'TestPass123!' }; const SERVER_NAME = `E2E Test Server ${Date.now()}`; const VOICE_CHANNEL = 'General'; test.describe('Full user journey: register → server → voice chat', () => { test('two users register, create server, join voice, and stay connected 10+ seconds with audio', async ({ createClient }) => { test.setTimeout(180_000); // 3 min - covers registration, server creation, voice establishment, and 10s stability check const alice = await createClient(); const bob = await createClient(); // Install WebRTC tracking before any navigation await installWebRTCTracking(alice.page); await installWebRTCTracking(bob.page); // Forward browser console for debugging alice.page.on('console', msg => console.log('[Alice]', msg.text())); bob.page.on('console', msg => console.log('[Bob]', msg.text())); // ── Step 1: Register both users ────────────────────────────────── await test.step('Alice registers an account', async () => { const registerPage = new RegisterPage(alice.page); await registerPage.goto(); await expect(registerPage.submitButton).toBeVisible(); await registerPage.register(ALICE.username, ALICE.displayName, ALICE.password); // After registration, app should navigate to /search await expect(alice.page).toHaveURL(/\/search/, { timeout: 15_000 }); }); await test.step('Bob registers an account', async () => { const registerPage = new RegisterPage(bob.page); await registerPage.goto(); await expect(registerPage.submitButton).toBeVisible(); await registerPage.register(BOB.username, BOB.displayName, BOB.password); await expect(bob.page).toHaveURL(/\/search/, { timeout: 15_000 }); }); // ── Step 2: Alice creates a server ─────────────────────────────── await test.step('Alice creates a new server', async () => { const searchPage = new ServerSearchPage(alice.page); await searchPage.createServer(SERVER_NAME, { description: 'E2E test server for voice testing' }); // After server creation, app navigates to the room await expect(alice.page).toHaveURL(/\/room\//, { timeout: 15_000 }); }); // ── Step 3: Bob joins the server ───────────────────────────────── await test.step('Bob finds and joins the server', async () => { const searchPage = new ServerSearchPage(bob.page); // Search for the server await searchPage.searchInput.fill(SERVER_NAME); // Wait for search results and click the server const serverCard = bob.page.locator('button', { hasText: SERVER_NAME }).first(); await expect(serverCard).toBeVisible({ timeout: 10_000 }); await serverCard.click(); // Bob should be in the room now await expect(bob.page).toHaveURL(/\/room\//, { timeout: 15_000 }); }); // ── Step 4: Create a voice channel (if one doesn't exist) ──────── await test.step('Alice ensures a voice channel is available', async () => { const chatRoom = new ChatRoomPage(alice.page); const existingVoiceChannel = alice.page.locator('app-rooms-side-panel') .getByRole('button', { name: VOICE_CHANNEL, exact: true }); const voiceChannelExists = await existingVoiceChannel.count() > 0; if (!voiceChannelExists) { // Click "Create Voice Channel" plus button await chatRoom.openCreateVoiceChannelDialog(); await chatRoom.createChannel(VOICE_CHANNEL); // Wait for the channel to appear await expect(existingVoiceChannel).toBeVisible({ timeout: 10_000 }); } }); // ── Step 5: Both users join the voice channel ──────────────────── await test.step('Alice joins the voice channel', async () => { const chatRoom = new ChatRoomPage(alice.page); await chatRoom.joinVoiceChannel(VOICE_CHANNEL); // Voice controls should appear (indicates voice is connected) await expect(alice.page.locator('app-voice-controls')).toBeVisible({ timeout: 15_000 }); }); await test.step('Bob joins the voice channel', async () => { const chatRoom = new ChatRoomPage(bob.page); await chatRoom.joinVoiceChannel(VOICE_CHANNEL); await expect(bob.page.locator('app-voice-controls')).toBeVisible({ timeout: 15_000 }); }); // ── Step 6: Verify WebRTC connection establishes ───────────────── await test.step('WebRTC peer connection reaches "connected" state', async () => { await waitForPeerConnected(alice.page, 30_000); await waitForPeerConnected(bob.page, 30_000); // Wait for audio RTP pipeline to appear before measuring deltas - // renegotiation after initial connect can temporarily remove stats. await waitForAudioStatsPresent(alice.page, 20_000); await waitForAudioStatsPresent(bob.page, 20_000); }); // ── Step 7: Verify audio is flowing in both directions ─────────── await test.step('Audio packets are flowing between Alice and Bob', async () => { const aliceDelta = await waitForAudioFlow(alice.page, 30_000); const bobDelta = await waitForAudioFlow(bob.page, 30_000); if (aliceDelta.outboundBytesDelta === 0 || aliceDelta.inboundBytesDelta === 0 || bobDelta.outboundBytesDelta === 0 || bobDelta.inboundBytesDelta === 0) { console.log('[Alice RTC Diagnostics]\n' + await dumpRtcDiagnostics(alice.page)); console.log('[Bob RTC Diagnostics]\n' + await dumpRtcDiagnostics(bob.page)); } expectAudioFlow(aliceDelta, 'Alice'); expectAudioFlow(bobDelta, 'Bob'); }); // ── Step 8: Verify UI states are correct ───────────────────────── await test.step('Voice UI shows correct state for both users', async () => { const aliceRoom = new ChatRoomPage(alice.page); const bobRoom = new ChatRoomPage(bob.page); // Both should see voice controls with "Connected" status await expect(alice.page.locator('app-voice-controls')).toBeVisible(); await expect(bob.page.locator('app-voice-controls')).toBeVisible(); // Both should see the voice workspace or at least voice users listed // Check that both users appear in the voice channel user list const aliceSeesBob = aliceRoom.channelsSidePanel.getByText(BOB.displayName).first(); const bobSeesAlice = bobRoom.channelsSidePanel.getByText(ALICE.displayName).first(); await expect(aliceSeesBob).toBeVisible({ timeout: 10_000 }); await expect(bobSeesAlice).toBeVisible({ timeout: 10_000 }); }); // ── Step 9: Stay connected for 10+ seconds, verify stability ───── await test.step('Connection remains stable for 10+ seconds', async () => { // Check connectivity at 0s, 5s, and 10s intervals for (const checkpoint of [ 0, 5_000, 5_000 ]) { if (checkpoint > 0) { await alice.page.waitForTimeout(checkpoint); } const aliceConnected = await isPeerStillConnected(alice.page); const bobConnected = await isPeerStillConnected(bob.page); expect(aliceConnected, 'Alice should still be connected').toBe(true); expect(bobConnected, 'Bob should still be connected').toBe(true); } // After 10s total, verify audio is still flowing const aliceDelta = await waitForAudioFlow(alice.page, 15_000); const bobDelta = await waitForAudioFlow(bob.page, 15_000); expectAudioFlow(aliceDelta, 'Alice after 10s'); expectAudioFlow(bobDelta, 'Bob after 10s'); }); // ── Step 10: Verify mute/unmute works correctly ────────────────── await test.step('Mute toggle works correctly', async () => { const aliceRoom = new ChatRoomPage(alice.page); // Alice mutes - click the first button in voice controls (mute button) await aliceRoom.muteButton.click(); // After muting, Alice's outbound audio should stop increasing // When muted, bytesSent may still show small comfort noise or zero growth // The key assertion is that Bob's inbound for Alice's stream stops or reduces await getAudioStatsDelta(alice.page, 2_000); // Alice unmutes await aliceRoom.muteButton.click(); // After unmuting, outbound should resume const unmutedDelta = await waitForAudioFlow(alice.page, 15_000); expectAudioFlow(unmutedDelta, 'Alice after unmuting'); }); // ── Step 11: Clean disconnect ──────────────────────────────────── await test.step('Alice disconnects from voice', async () => { const aliceRoom = new ChatRoomPage(alice.page); // Click the disconnect/hang-up button await aliceRoom.disconnectButton.click(); // Connected controls should collapse for Alice after disconnect await expect(aliceRoom.disconnectButton).not.toBeVisible({ timeout: 10_000 }); }); }); }); function expectAudioFlow(delta: { outboundBytesDelta: number; inboundBytesDelta: number; outboundPacketsDelta: number; inboundPacketsDelta: number; }, label: string): void { expect( delta.outboundBytesDelta > 0 || delta.outboundPacketsDelta > 0, `${label} should be sending audio` ).toBe(true); expect( delta.inboundBytesDelta > 0 || delta.inboundPacketsDelta > 0, `${label} should be receiving audio` ).toBe(true); }