Some checks failed
Deploy Web Apps / deploy (push) Has been cancelled
Queue Release Build / prepare (push) Successful in 21s
Queue Release Build / build-linux (push) Successful in 27m44s
Queue Release Build / build-windows (push) Successful in 32m16s
Queue Release Build / finalize (push) Successful in 1m54s
261 lines
10 KiB
TypeScript
261 lines
10 KiB
TypeScript
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);
|
|
}
|