import { expect, type Page } from '@playwright/test'; import { test, type Client } from '../../fixtures/multi-client'; import { installDeterministicVoiceSettings } from '../../helpers/voice-session'; import { dumpRtcDiagnostics, installAutoResumeAudioContext, installWebRTCTracking, waitForAudioStatsPresent, waitForConnectedPeerCount, waitForInboundVideoFlow, waitForOutboundVideoFlow } from '../../helpers/webrtc-helpers'; import { ChatRoomPage } from '../../pages/chat-room.page'; import { RegisterPage } from '../../pages/register.page'; import { ServerSearchPage } from '../../pages/server-search.page'; const USER_PASSWORD = 'TestPass123!'; const VOICE_CHANNEL = 'General'; /** * Screen share is pull-based: the sharer only attaches tracks to peers that asked * for them. This covers the case the request model has to get right - a viewer who * arrives after the share already started. */ test.describe('Late joiner screen share', () => { test('a user who joins voice mid-share still receives the screen', async ({ createClient }) => { test.setTimeout(240_000); const serverName = `Late Share ${Date.now()}`; const sharer = await createVoiceClient(createClient, 'sharer'); const viewer = await createVoiceClient(createClient, 'viewer'); await test.step('Both users register and join the server', async () => { await new ServerSearchPage(sharer.page).createServer(serverName, { description: 'Late joiner screen share test' }); await expect(sharer.page).toHaveURL(/\/room\//, { timeout: 20_000 }); await new ServerSearchPage(viewer.page).joinServerFromSearch(serverName); await expect(viewer.page).toHaveURL(/\/room\//, { timeout: 20_000 }); }); await test.step('The sharer starts sharing while alone in voice', async () => { const room = new ChatRoomPage(sharer.page); await room.ensureVoiceChannelExists(VOICE_CHANNEL); await room.joinVoiceChannel(VOICE_CHANNEL); await expect(room.voiceControls).toBeVisible({ timeout: 20_000 }); await openVoiceWorkspace(sharer.page); await room.startScreenShare(); await expect(room.isScreenShareActive).toBeVisible({ timeout: 15_000 }); }); await test.step('The viewer joins voice after the share is already running', async () => { const room = new ChatRoomPage(viewer.page); await room.joinVoiceChannel(VOICE_CHANNEL); await expect(room.voiceControls).toBeVisible({ timeout: 20_000 }); await waitForConnectedPeerCount(viewer.page, 1, 90_000); await waitForConnectedPeerCount(sharer.page, 1, 90_000); await waitForAudioStatsPresent(viewer.page, 30_000); await openVoiceWorkspace(viewer.page); }); await test.step('The in-progress screen reaches the late joiner', async () => { try { const outbound = await waitForOutboundVideoFlow(sharer.page, 60_000); const inbound = await waitForInboundVideoFlow(viewer.page, 60_000); expect( outbound.outboundBytesDelta > 0 || outbound.outboundPacketsDelta > 0, 'The sharer never sent screen video to the late joiner' ).toBe(true); expect( inbound.inboundBytesDelta > 0 || inbound.inboundPacketsDelta > 0, 'The late joiner never received the in-progress screen share' ).toBe(true); } catch (error) { console.log(`[sharer RTC]\n${await dumpRtcDiagnostics(sharer.page)}`); console.log(`[viewer RTC]\n${await dumpRtcDiagnostics(viewer.page)}`); throw error; } }); await test.step('The late joiner renders a remote screen tile', async () => { await expect(viewer.page.locator('app-voice-workspace-stream-tile').first()) .toBeVisible({ timeout: 30_000 }); }); }); }); /** Expand the voice workspace, which is what turns on remote screen-share requests. */ async function openVoiceWorkspace(page: Page): Promise { const viewButton = page.locator('app-rooms-side-panel') .getByRole('button', { name: /view/i }) .first(); await expect(viewButton).toBeVisible({ timeout: 20_000 }); await viewButton.click(); await expect(page.locator('app-voice-workspace')).toBeVisible({ timeout: 20_000 }); } async function createVoiceClient( createClient: () => Promise, role: string ): Promise { const client = await createClient(); await installDeterministicVoiceSettings(client.page); await installWebRTCTracking(client.page); await installAutoResumeAudioContext(client.page); const registerPage = new RegisterPage(client.page); await registerPage.goto(); await registerPage.register( `late_share_${role}_${Date.now()}`, `Late Share ${role}`, USER_PASSWORD ); await expect(client.page).toHaveURL(/\/dashboard/, { timeout: 20_000 }); return client; }