import { expect, test } from '@playwright/test'; import { MEET_TESTAPP_URL } from '../config'; import { configureLayoutMode, createTestRoom, deleteAllRecordings, deleteAllRooms, joinRoomAs, muteAudio, prepareForJoiningRoom, waitForElementInIframe } from '../helpers/function-helpers'; import { disconnectAllBrowserFakeParticipants, disconnectAllFakeParticipants, disconnectBrowserFakeParticipant, forceMuteParticipantAudio, getVisibleParticipantNames, getVisibleParticipantsCount, joinBrowserFakeParticipant, joinFakeParticipant, stopScreenShareBrowserFakeParticipant, waitForParticipantCount, waitForParticipantSwap, waitForParticipantVisible } from '../helpers/participant.helper'; test.describe('Custom Layout Tests', () => { let subscribedToAppErrors = false; let roomId: string; let participantName: string; test.beforeEach(async ({ page }) => { // Create a new room for each test to avoid state pollution roomId = await createTestRoom('smart-mosaic-test-room'); if (!subscribedToAppErrors) { page.on('console', (msg) => { const type = msg.type(); const tag = type === 'error' ? 'ERROR' : type === 'warning' ? 'WARNING' : 'LOG'; console.log('[' + tag + ']', msg.text()); }); subscribedToAppErrors = true; } participantName = `Local-${Math.random().toString(36).substring(2, 7)}`; }); test.afterEach(async () => { // Clean up fake participants after each test await Promise.all([disconnectAllBrowserFakeParticipants(), disconnectAllFakeParticipants()]); }); test.afterAll(async ({ browser }) => { const tempContext = await browser.newContext(); const tempPage = await tempContext.newPage(); await deleteAllRooms(tempPage); await deleteAllRecordings(tempPage); await tempContext.close(); await tempPage.close(); }); // ========================================================================= // SMART MOSAIC LAYOUT TESTS // These tests verify that the Smart Mosaic layout correctly displays // participants based on their speaking activity, showing only the most // recent active speakers up to the configured limit. // ========================================================================= test.describe('Smart Mosaic Layout - Speaker Priority', () => { test('displays only active speaker when limit is 1 and other participant is muted', async ({ page }) => { // Scenario: 3 participants (local + remote A speaking + remote B muted), limit = 1 // Expected: Grid shows local + remote A only (2 participants total) // Audio: Remote A uses continuous_speech.ogg, Remote B has no audio // Local participant joins the room await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId); await joinRoomAs('moderator', participantName, page); // Wait for session to be ready await waitForElementInIframe(page, 'ov-session', { state: 'visible' }); await muteAudio(page); // Mute local to avoid interference // Configure Smart Mosaic layout with limit = 1 await configureLayoutMode(page, 'smart-mosaic', 1); // Join fake participant A (speaking with continuous audio) await Promise.all([ joinBrowserFakeParticipant(roomId, 'RemoteA-Speaker', { audioFile: 'continuous_speech.wav' }), // Join fake participant B (muted/silent - no audio) joinFakeParticipant(roomId, 'RemoteB-Silent') ]); // Wait for participants to appear and speaker detection to process await waitForParticipantVisible(page, 'RemoteA-Speaker'); // Verify the grid shows exactly 2 participants (local + 1 remote speaker) const participantCount = await getVisibleParticipantsCount(page); expect(participantCount).toBe(2); // Step 8: Verify the visible participants are local and RemoteA (the speaker) const visibleIdentities = await getVisibleParticipantNames(page); // expect(visibleIdentities).toContain(participantName); // Local participant expect(visibleIdentities).toContain('RemoteA-Speaker'); // Active speaker expect(visibleIdentities).not.toContain('RemoteB-Silent'); // Silent participant should NOT be visible }); test('reorders participants based on speech activity changes', async ({ page }) => { // Scenario: 3 participants, A speaks first (0-5s), then B speaks (5s onwards) // Expected: Initially A is prioritized, after B speaks, B becomes prioritized // Audio: A uses speech_5s_then_silence.ogg, B uses silence_5s_then_speech.ogg // Local participant joins the room await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId); await joinRoomAs('moderator', participantName, page); // Wait for session to be ready await waitForElementInIframe(page, 'ov-session', { state: 'visible' }); await muteAudio(page); // Mute local to avoid interference // Configure Smart Mosaic layout with limit = 1 // Only 1 remote participant should be visible at a time (plus local) await configureLayoutMode(page, 'smart-mosaic', 1); // Join fake participant A (speaks first 5s, then silent) // Join fake participant B (silent first 5s, then speaks) // Use browser-based fake participant to ensure VAD triggers correctly (lk CLI always send active speakers events when using audio files) await joinBrowserFakeParticipant(roomId, 'RemoteA-SpeaksFirst', { audioFile: 'speech_5s_then_silence.wav' }); await waitForParticipantVisible(page, 'RemoteA-SpeaksFirst'); await joinBrowserFakeParticipant(roomId, 'RemoteB-SpeaksLater', { audioFile: 'silence_5s_then_speech.wav' }); // Verify that RemoteA is visible (he's speaking in first 5s) let [visibleIdentities, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); expect(visibleIdentities).toContain('RemoteA-SpeaksFirst'); expect(visibleIdentities).not.toContain('RemoteB-SpeaksLater'); // Verify we have exactly 2 participants visible (local + 1 remote) expect(participantCount).toBe(2); expect(participantCount).toBe(2); // Wait for the speech transition (A stops at 5s, B starts at 5s) // Wait additional time for B to start speaking and be detected await waitForParticipantVisible(page, 'RemoteB-SpeaksLater'); // Verify that RemoteB is now visible (he started speaking) [visibleIdentities, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); expect(visibleIdentities).toContain('RemoteB-SpeaksLater'); expect(visibleIdentities).not.toContain('RemoteA-SpeaksFirst'); // Verify still exactly 2 participants visible (local + 1 remote) expect(participantCount).toBe(2); // Verify local participant remained visible throughout // The local participant should always be visible regardless of speaking state expect(visibleIdentities.length).toBe(2); // Local + current active speaker }); test('rotates participants showing most recent speakers when limit exceeded', async ({ page }) => { // Scenario: 4 participants with limit = 2, speaking order A → B → C // Expected: After rotation, grid shows local + B + C (last 2 speakers) // Audio: A speaks 0-3s, B speaks 5-8s, C speaks 10-13s // Local participant joins the room await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId); await joinRoomAs('moderator', participantName, page); // Wait for session to be ready await waitForElementInIframe(page, 'ov-session', { state: 'visible' }); await muteAudio(page); // Mute local to avoid interference // Configure Smart Mosaic layout with limit = 2 // 2 remote participants should be visible at a time (plus local = 3 total) await configureLayoutMode(page, 'smart-mosaic', 2); // Join three browser-based fake participants with sequential audio // speaker_seq_A.wav: speaks at 0-3s // speaker_seq_B.wav: speaks at 5-8s // speaker_seq_C.wav: speaks at 10-13s await joinBrowserFakeParticipant(roomId, 'RemoteA-First', { audioFile: 'speaker_seq_A.wav' }); await joinBrowserFakeParticipant(roomId, 'RemoteB-Second', { audioFile: 'speaker_seq_B.wav' }); await joinBrowserFakeParticipant(roomId, 'RemoteC-Third', { audioFile: 'speaker_seq_C.wav' }); // Wait for A to become visible (speaks first at 0-3s) await waitForParticipantVisible(page, 'RemoteA-First'); // Initially A and B should be visible (limit = 2, A is speaking, B fills the slot) let [visibleIdentities, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); console.log('After A starts speaking:', visibleIdentities); expect(visibleIdentities).toContain('RemoteA-First'); expect(participantCount).toBe(3); // Local + 2 remotes // Wait for C to become visible (speaks at 10-13s) // This is the key assertion - when C starts speaking, it should replace A (oldest speaker) await waitForParticipantSwap(page, 'RemoteC-Third', 'RemoteA-First', 30000); // Verify final state - B and C should be visible (most recent 2 speakers) [visibleIdentities, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); console.log('After C speaks (rotation complete):', visibleIdentities); expect(participantCount).toBe(3); // Local + 2 remotes expect(visibleIdentities).toContain('RemoteB-Second'); expect(visibleIdentities).toContain('RemoteC-Third'); expect(visibleIdentities).not.toContain('RemoteA-First'); // A was rotated out }); test('displays local and three most active speakers when limit is 3', async ({ page }) => { // Scenario: 5 participants (local + A, B, C speaking + D always silent), limit = 3 // Expected: Grid shows local + A + B + C, D is never shown // Audio: A, B, C use continuous_speech.wav, D has no audio // Local participant joins the room await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId); await joinRoomAs('moderator', participantName, page); // Wait for session to be ready await waitForElementInIframe(page, 'ov-session', { state: 'visible' }); await muteAudio(page); // Mute local to avoid interference // Configure Smart Mosaic layout with limit = 3 // 3 remote participants should be visible at a time (plus local = 4 total) await configureLayoutMode(page, 'smart-mosaic', 3); // Join silent participant first using lk CLI (no audio triggers VAD) await joinFakeParticipant(roomId, 'RemoteD-Silent'); // Join three browser-based fake participants with continuous speech audio // These will all be detected as active speakers await Promise.all([ joinBrowserFakeParticipant(roomId, 'RemoteA-Speaker', { audioFile: 'continuous_speech.wav' }), joinBrowserFakeParticipant(roomId, 'RemoteB-Speaker', { audioFile: 'continuous_speech.wav' }), joinBrowserFakeParticipant(roomId, 'RemoteC-Speaker', { audioFile: 'continuous_speech.wav' }) ]); // Wait for speaker detection to process all participants await Promise.all([ waitForParticipantVisible(page, 'RemoteA-Speaker'), waitForParticipantVisible(page, 'RemoteB-Speaker'), waitForParticipantVisible(page, 'RemoteC-Speaker'), waitForParticipantCount(page, 4) // Local + 3 remotes ]); }); test('handles simultaneous speech and reorders when only one continues', async ({ page }) => { // Scenario: 3 remote participants + local, limit = 2 // All 3 speak simultaneously for first 5s, then only A continues speaking // Expected: Initially any 2 of the 3 are visible (all speaking) // After 5s, only A continues → A should remain visible as active speaker // Audio: A uses simultaneous_then_solo.wav (15s speech) // B, C use simultaneous_then_stop.wav (5s speech then silence) // Local participant joins the room await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId); await joinRoomAs('moderator', participantName, page); // Wait for session to be ready await waitForElementInIframe(page, 'ov-session', { state: 'visible' }); await muteAudio(page); // Mute local to avoid interference // Configure Smart Mosaic layout with limit = 2 // Only 2 remote participants visible at a time await configureLayoutMode(page, 'smart-mosaic', 2); // Join three browser-based fake participants // A continues speaking (15s), B and C stop after 5s await Promise.all([ joinBrowserFakeParticipant(roomId, 'RemoteA-ContinuesSpeaking', { audioFile: 'continuous_speech.wav' }), joinBrowserFakeParticipant(roomId, 'RemoteB-StopsSpeaking', { audioFile: 'simultaneous_then_stop.wav' //5s speech + 25s silence }) ]); // Wait for simultaneous speech period (first 5s - all speaking) await Promise.all([ waitForParticipantVisible(page, 'RemoteA-ContinuesSpeaking'), waitForParticipantVisible(page, 'RemoteB-StopsSpeaking') ]); await joinBrowserFakeParticipant(roomId, 'RemoteC-StopsSpeaking', { audioFile: 'simultaneous_then_stop.wav' //5s speech + 25s silence }); let [visibleIdentities, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); // During simultaneous speech, we should see exactly 3 participants (local + 2 remotes) expect(participantCount).toBe(3); // At least A should be visible (continues speaking) // The other visible one could be B or C (both are speaking) expect(visibleIdentities).toContain('RemoteA-ContinuesSpeaking'); expect(visibleIdentities).toContain('RemoteB-StopsSpeaking'); expect(visibleIdentities).not.toContain('RemoteC-StopsSpeaking'); // Wait for B and C to stop speaking (after 5s mark) // Only A continues speaking, so A should remain as priority speaker await page.waitForTimeout(6000); // Wait until ~11s mark (well past the 5s cutoff) [visibleIdentities, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); console.log('After B and C stop speaking (~11s):', visibleIdentities); // A should definitely be visible (still speaking) expect(visibleIdentities).toContain('RemoteA-ContinuesSpeaking'); // Verify participant count is still 3 (local + 2 remotes) // Even though only A is speaking, the layout maintains 2 remotes expect(participantCount).toBe(3); }); test('stabilizes layout when limit reached with continuous speakers', async ({ page }) => { // Scenario: 2 remote participants + local, limit = 1 // Participants A and B speak continuously // Expected: Layout stabilizes showing local + 1 most recent active speakers // Audio: A and B use continuous_speech.wav // Local participant joins the room await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId); await joinRoomAs('moderator', participantName, page); // Wait for session to be ready await waitForElementInIframe(page, 'ov-session', { state: 'visible' }); await muteAudio(page); // Mute local to avoid interference // Configure Smart Mosaic layout with limit = 1 // Only 1 remote participant visible at a time await configureLayoutMode(page, 'smart-mosaic', 1); // Join two browser-based fake participants with continuous speech audio await joinBrowserFakeParticipant(roomId, 'RemoteA-Continuous', { audioFile: 'continuous_speech.wav' }); await waitForParticipantVisible(page, 'RemoteA-Continuous'); // Verify the grid shows exactly 2 participants (local + 1 remote speaker) let [participantCount, visibleNames] = await Promise.all([ getVisibleParticipantsCount(page), getVisibleParticipantNames(page) ]); expect(participantCount).toBe(2); expect(visibleNames).toContain('RemoteA-Continuous'); await joinBrowserFakeParticipant(roomId, 'RemoteB-Continuous', { audioFile: 'continuous_speech.wav' }); // Verify participant count remains stable 20 times for (let i = 0; i < 20; i++) { [participantCount, visibleNames] = await Promise.all([ getVisibleParticipantsCount(page), getVisibleParticipantNames(page) ]); expect(visibleNames).toContain('RemoteA-Continuous'); expect(visibleNames).not.toContain('RemoteB-Continuous'); expect(participantCount).toBe(2); await page.waitForTimeout(50); } }); test('prioritizes newly joined speaking participant over silent ones', async ({ page }) => { // Scenario: Local + 2 silent participants (A, B) in room, limit = 1 // New participant C joins and starts speaking immediately // Expected: C immediately appears in the grid, replacing one of the silent participants // Audio: A, B are silent (lk CLI), C uses continuous_speech.wav // Local participant joins the room await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId); await joinRoomAs('moderator', participantName, page); // Wait for session to be ready await waitForElementInIframe(page, 'ov-session', { state: 'visible' }); await muteAudio(page); // Mute local to avoid interference // Configure Smart Mosaic layout with limit = 1 // Only 1 remote participant visible at a time await configureLayoutMode(page, 'smart-mosaic', 1); // Join two silent participants using lk CLI (no VAD triggers) await Promise.all([ joinFakeParticipant(roomId, 'RemoteA-Silent'), joinFakeParticipant(roomId, 'RemoteB-Silent') ]); // Wait for silent participants to appear await page.waitForTimeout(2000); // New participant C joins and starts speaking immediately await joinBrowserFakeParticipant(roomId, 'RemoteC-NewSpeaker', { audioFile: 'silence_5s_then_speech.wav' }); await page.waitForTimeout(2000); let [visibleNames, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); expect(visibleNames).not.toContain('RemoteC-NewSpeaker'); // Wait for speaker detection to process await waitForParticipantVisible(page, 'RemoteC-NewSpeaker'); [visibleNames, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); // Verify C is now visible (speaking has priority) expect(visibleNames).toContain('RemoteC-NewSpeaker'); expect(participantCount).toBe(2); // Local + 1 remote }); }); test.describe('Smart Mosaic Layout - Participant Join/Leave Handling', () => { test('updates visible participants when speaker leaves', async ({ page }) => { // Scenario: Local + 3 remote participants (A, B, C) with limit = 2 // A and B are visible speakers, C is silent // A leaves the room → B should remain visible, C should NOT appear // Audio: A and B use continuous_speech.wav, C is silent // Step 1: Local participant joins the room await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId); await joinRoomAs('moderator', participantName, page); // Step 2: Wait for session to be ready await waitForElementInIframe(page, 'ov-session', { state: 'visible' }); // Step 3: Configure Smart Mosaic layout with limit = 2 await configureLayoutMode(page, 'smart-mosaic', 2); // Step 4: Join three browser-based fake participants await Promise.all([ joinBrowserFakeParticipant(roomId, 'RemoteA-Speaker', { audioFile: 'continuous_speech.wav' }), joinBrowserFakeParticipant(roomId, 'RemoteB-Speaker', { audioFile: 'continuous_speech.wav' }) ]); await joinFakeParticipant(roomId, 'RemoteC-Silent'); // Step 5: Wait for speaker detection to process await Promise.all([ waitForParticipantVisible(page, 'RemoteA-Speaker'), waitForParticipantVisible(page, 'RemoteB-Speaker') ]); // Verify A and B are visible let [visibleNames, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); console.log('Before A leaves, visible participants:', visibleNames); expect(visibleNames).toContain('RemoteA-Speaker'); expect(visibleNames).toContain('RemoteB-Speaker'); expect(participantCount).toBe(3); // Local + 2 remotes // Step 6: Disconnect participant A (visible speaker) await disconnectBrowserFakeParticipant(roomId, 'RemoteA-Speaker'); // Step 7: Wait for layout to update await page.waitForTimeout(1000); [visibleNames, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); console.log('After A leaves, visible participants:', visibleNames); // Step 8: Verify B remains visible, C does NOT appear expect(visibleNames).toContain('RemoteB-Speaker'); // expect(visibleNames).toContain('RemoteC-Silent'); expect(visibleNames).not.toContain('RemoteA-Speaker'); expect(participantCount).toBe(3); // Local + 2 remotes }); test('does not show silent participants when they join', async ({ page }) => { // Scenario: Local + 2 remote participants (A speaking, B silent) with limit = 1 // A is visible speaker, B is silent // C joins as silent participant → should NOT appear in the grid // Audio: A uses continuous_speech.wav, B and C are silent // Step 1: Local participant joins the room await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId); await joinRoomAs('moderator', participantName, page); // Step 2: Wait for session to be ready await waitForElementInIframe(page, 'ov-session', { state: 'visible' }); // Step 3: Configure Smart Mosaic layout with limit = 1 await configureLayoutMode(page, 'smart-mosaic', 1); // Step 4: Join two remote participants await joinBrowserFakeParticipant(roomId, 'RemoteA-Speaker', { audioFile: 'continuous_speech.wav' }); await joinFakeParticipant(roomId, 'RemoteB-Silent'); // Step 5: Wait for speaker detection to process await page.waitForTimeout(2000); // Verify A is visible let [visibleNames, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); console.log('Before C joins, visible participants:', visibleNames); expect(visibleNames).toContain('RemoteA-Speaker'); expect(participantCount).toBe(2); // Local + 1 remote // Step 6: Join new silent participant C await joinFakeParticipant(roomId, 'RemoteC-Silent'); // Step 7: Wait for layout to update await page.waitForTimeout(3000); [visibleNames, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); console.log('After C joins, visible participants:', visibleNames); // Step 8: Verify C does NOT appear in the grid expect(visibleNames).toContain('RemoteA-Speaker'); expect(participantCount).toBe(2); // Local + 1 remote }); }); test.describe('Mosaic Layout and Smart Mosaic Layout Switching', () => { test('switches from Smart Mosaic to Mosaic showing all participants', async ({ page }) => { // Scenario: Start in Smart Mosaic layout with limit = 2, switch to Mosaic // Expected: After switching, all participants become visible in the grid // Audio: Participants A, B, C, D use continuous_speech.wav // Step 1: Local participant joins the room await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId); await joinRoomAs('moderator', participantName, page); // Step 2: Wait for session to be ready await waitForElementInIframe(page, 'ov-session', { state: 'visible' }); // Step 3: Join four browser-based fake participants with continuous speech audio await Promise.all([ joinBrowserFakeParticipant(roomId, 'RemoteA-Speaker', { audioFile: 'continuous_speech.wav' }), joinBrowserFakeParticipant(roomId, 'RemoteB-Speaker', { audioFile: 'continuous_speech.wav' }), joinBrowserFakeParticipant(roomId, 'RemoteC-Speaker', { audioFile: 'continuous_speech.wav' }), joinBrowserFakeParticipant(roomId, 'RemoteD-Speaker', { audioFile: 'continuous_speech.wav' }) ]); // Step 4: Wait for all participants to appear await waitForParticipantCount(page, 5); // Local + 4 remotes // Step 5: Configure Smart Mosaic layout with limit = 2 await configureLayoutMode(page, 'smart-mosaic', 2); // Verify only 2 remote participants are visible let [visibleNames, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); console.log('In Smart Mosaic layout, visible participants:', visibleNames); expect(participantCount).toBe(3); // Local + 2 remotes // Step 6: Switch to Mosaic layout (all participants visible) await configureLayoutMode(page, 'mosaic'); // Step 7: Wait for layout to update await page.waitForTimeout(3000); [visibleNames, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); console.log('After switching to Mosaic, visible participants:', visibleNames); // Step 8: Verify all 4 remote participants are now visible expect(participantCount).toBe(5); // Local + 4 remotes }); test('switches from Mosaic to Smart Mosaic limiting by speakers', async ({ page }) => { // Scenario: Start in Mosaic layout with 4 participants visible, switch to Smart Mosaic with limit = 2 // Expected: After switching, only the 2 most recent active speakers remain visible // Audio: Participants A, B, C, D use continuous_speech.wav // Wait for local participant to join the room await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId); await joinRoomAs('moderator', participantName, page); // Wait for session to be ready await waitForElementInIframe(page, 'ov-session', { state: 'visible' }); // Wait for four browser-based fake participants with continuous speech audio to join await Promise.all([ joinBrowserFakeParticipant(roomId, 'RemoteA-Speaker', { audioFile: 'continuous_speech.wav' }), joinBrowserFakeParticipant(roomId, 'RemoteB-Speaker', { audioFile: 'continuous_speech.wav' }), joinBrowserFakeParticipant(roomId, 'RemoteC-Speaker', { audioFile: 'continuous_speech.wav' }), joinBrowserFakeParticipant(roomId, 'RemoteD-Speaker', { audioFile: 'continuous_speech.wav' }) ]); // Wait for all participants to appear await waitForParticipantCount(page, 5); // Local + 4 remotes // Switch to Smart Mosaic layout with limit = 2 await configureLayoutMode(page, 'smart-mosaic', 2); // Wait for layout to update speaker visibility await page.waitForTimeout(3000); const [visibleNames, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); console.log('After switching to Smart Mosaic, visible participants:', visibleNames); // Verify only 2 remote participants are visible (most recent speakers) expect(participantCount).toBe(3); // Local + 2 remotes }); }); // ========================================================================= // SMART MOSAIC LAYOUT - AUDIO FILTERING TESTS // These tests verify the hysteresis mechanisms that filter out: // 1. Low volume audio (below audioLevel threshold of 0.15) // 2. Brief sounds (below minimum speaking duration of 1.5s) // ========================================================================= test.describe('Smart Mosaic Layout - Audio Level and Duration Filtering', () => { test('filters out low volume audio below threshold', async ({ page }) => { // Scenario: 3 participants - Local + Remote A (normal volume) + Remote B (low volume ~10%) // Expected: Only Remote A appears in the grid, Remote B is filtered due to low audioLevel // Audio: A uses continuous_speech.wav, B uses low_volume_speech.wav (10% volume) // Local participant joins the room await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId); await joinRoomAs('moderator', participantName, page); // Wait for session to be ready await waitForElementInIframe(page, 'ov-session', { state: 'visible' }); // Configure Smart Mosaic layout with limit = 1 await configureLayoutMode(page, 'smart-mosaic', 1); // Join two participants: one with normal volume, one with very low volume await joinFakeParticipant(roomId, 'RemoteA-Silence'); // Wait for speaker detection to process await waitForParticipantCount(page, 2); await joinBrowserFakeParticipant(roomId, 'RemoteB-LowVolume', { audioFile: 'low_volume_speech.wav' // 10% volume - below 0.15 threshold }); // Wait additional time for B's low volume to be evaluated await page.waitForTimeout(4000); for (let i = 0; i < 5; i++) { const [visibleNames, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); console.log('Visible participants with volume filtering:', visibleNames); // Remote B (low volume) should NOT be prioritized as active speaker expect(visibleNames).not.toContain('RemoteB-LowVolume'); expect(participantCount).toBe(2); // Local + 1 remote } }); test('maintains stability with multiple low-volume speakers', async ({ page }) => { // Scenario: 3 participants all with low volume audio, limit = 2 // Expected: Layout remains stable without constant swapping (all filtered by audioLevel threshold) // Audio: All use low_volume_speech.wav // Local participant joins the room await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId); await joinRoomAs('moderator', participantName, page); // Wait for session to be ready await waitForElementInIframe(page, 'ov-session', { state: 'visible' }); await muteAudio(page); // Mute local to avoid interference // Configure Smart Mosaic layout with limit = 1 await configureLayoutMode(page, 'smart-mosaic', 1); await joinFakeParticipant(roomId, 'Remote-Initial'); await waitForParticipantCount(page, 2); // Join three participants all with low volume await Promise.all([ joinBrowserFakeParticipant(roomId, 'RemoteA-LowVol', { audioFile: 'ambient_pink_noise.wav' }), joinBrowserFakeParticipant(roomId, 'RemoteB-LowVol', { audioFile: 'ambient_pink_noise.wav' }) ]); await page.waitForTimeout(3000); // Record initial visible participants const initialVisibleNames = await getVisibleParticipantNames(page); console.log('Initial visible participants:', initialVisibleNames); // Check layout stability over time - should not swap since all are below threshold let swapCount = 0; let previousNames = [...initialVisibleNames]; for (let i = 0; i < 10; i++) { await page.waitForTimeout(500); const currentNames = await getVisibleParticipantNames(page); // Check if any swap occurred const hasSwap = !previousNames.every((name) => currentNames.includes(name)); if (hasSwap) { swapCount++; console.log(`Swap detected at check ${i + 1}:`, previousNames, '->', currentNames); } previousNames = [...currentNames]; } console.log(`Total swaps detected: ${swapCount}`); // Layout should be stable - no swaps should occur since all are filtered expect(swapCount).toBe(0); }); test('filters out brief sounds under minimum duration', async ({ page }) => { // Scenario: 3 participants - Local + Remote A (continuous speech) + Remote B (0.5s cough only) // Expected: Remote A appears as active speaker, Remote B's brief cough is filtered out // Audio: A uses continuous_speech.wav, B uses brief_cough.wav (0.5s sound) // Local participant joins the room await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId); await joinRoomAs('moderator', participantName, page); // Wait for session to be ready await waitForElementInIframe(page, 'ov-session', { state: 'visible' }); await muteAudio(page); // Mute local to avoid interference // Configure Smart Mosaic layout with limit = 1 await configureLayoutMode(page, 'smart-mosaic', 1); // Join silence participant first await joinFakeParticipant(roomId, 'RemoteA-Speaker'); // Wait for A to be detected as speaker await waitForParticipantCount(page, 2); // Now join participant B with brief cough sound await joinBrowserFakeParticipant(roomId, 'RemoteB-Cough', { audioFile: 'brief_cough_at_5s.wav' // 0.5s sound - below minimum duration }); // Wait for the brief sound to be processed await page.waitForTimeout(5000); for (let i = 0; i < 5; i++) { const [visibleNames, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); // Remote A should remain visible throughout expect(visibleNames).not.toContain('RemoteB-Cough'); expect(participantCount).toBe(2); // Local + 1 remote await page.waitForTimeout(500); } }); test('does not swap speaker for 1 second sound bursts', async ({ page }) => { // Scenario: 3 participants - Local + Remote A (speaking) + Remote B (1s sound burst) // Expected: A remains visible, B's 1 second sound is filtered (< 1.5s threshold) // Audio: A uses continuous_speech.wav, B uses brief_sound_1s.wav // Local participant joins the room await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId); await joinRoomAs('moderator', participantName, page); // Wait for session to be ready await waitForElementInIframe(page, 'ov-session', { state: 'visible' }); // Configure Smart Mosaic layout with limit = 1 await configureLayoutMode(page, 'smart-mosaic', 1); // Join participant A with continuous speech await joinFakeParticipant(roomId, 'RemoteA-Continuous'); await waitForParticipantCount(page, 2); // Join participant B with 1 second sound burst await joinBrowserFakeParticipant(roomId, 'RemoteB-BriefSound', { audioFile: 'brief_sound_1s_at_5s.wav' // 1s sound }); // Wait 5s to allow B's speaks await page.waitForTimeout(5000); // Track visible participants over time to ensure no swap occurs for (let i = 0; i < 5; i++) { const visibleNames = await getVisibleParticipantNames(page); // A should remain visible throughout expect(visibleNames).not.toContain('RemoteB-BriefSound'); console.log(`Check ${i + 1}: Visible participants:`, visibleNames); await page.waitForTimeout(500); } }); }); test.describe('Smart Mosaic Layout - Mute participants', () => { test('should be able to mute/unmute participants and keep muted ones in view', async ({ page }) => { // Scenario: 2 remote participants + local, limit = 1 // Remote A speaks first and is visible // Remote A is muted → Remote B speaks and becomes visible // Remote A is unmuted and speaks again → Remote A becomes visible again // Audio: A and B use silence_5s_then_speech.wav // Local participant joins the room await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId); await joinRoomAs('moderator', participantName, page); // Wait for session to be ready await waitForElementInIframe(page, 'ov-session', { state: 'visible' }); await muteAudio(page); // Mute local to avoid interference // Configure Smart Mosaic layout with limit = 1 await configureLayoutMode(page, 'smart-mosaic', 1); // Join Remote A who speaks first await joinBrowserFakeParticipant(roomId, 'RemoteA-Speaking', { audioFile: 'continuous_speech.wav' }); // Wait for A to become visible await waitForParticipantVisible(page, 'RemoteA-Speaking'); // Verify Remote A is visible let [visibleIdentities, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); expect(visibleIdentities).toContain('RemoteA-Speaking'); expect(participantCount).toBe(2); // Local + 1 remote // Mute Remote A await forceMuteParticipantAudio(page, 'RemoteA-Speaking'); // Join Remote B who speaks next await joinBrowserFakeParticipant(roomId, 'RemoteB-SpeakingNext', { audioFile: 'continuous_speech.wav' }); // Verify Remote A is visible although muted, [visibleIdentities, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); expect(visibleIdentities).not.toContain('RemoteB-SpeakingNext'); expect(visibleIdentities).toContain('RemoteA-Speaking'); expect(participantCount).toBe(2); // Local + 1 remote // Unmute Remote A await forceMuteParticipantAudio(page, 'RemoteA-Speaking'); // Verify Remote A is visible again [visibleIdentities, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); expect(visibleIdentities).toContain('RemoteA-Speaking'); expect(participantCount).toBe(2); // Local + 1 remote }); test('should hidden participants who are audio muted', async ({ page }) => { // Local participant joins the room await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId); await joinRoomAs('moderator', participantName, page); // Wait for session to be ready await waitForElementInIframe(page, 'ov-session', { state: 'visible' }); await muteAudio(page); // Mute local to avoid interference // Configure Smart Mosaic layout with limit = 1 await configureLayoutMode(page, 'smart-mosaic', 1); // Join Remote A who speaks first const pageA = await joinBrowserFakeParticipant(roomId, 'RemoteA-Speaking', { audioFile: 'continuous_speech.wav' }); // Wait for A to become visible await waitForParticipantVisible(page, 'RemoteA-Speaking'); // Verify Remote A is visible let [visibleIdentities, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); await joinBrowserFakeParticipant(roomId, 'RemoteB-Speaking', { audioFile: 'continuous_speech.wav' }); await page.waitForTimeout(2000); expect(visibleIdentities).toContain('RemoteA-Speaking'); expect(participantCount).toBe(2); // Local + 1 remote // Audio mute Remote A await muteAudio(pageA); // Wait for layout to update await waitForParticipantVisible(page, 'RemoteB-Speaking'); // Verify Remote A is hidden [visibleIdentities, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); expect(visibleIdentities).not.toContain('RemoteA-Speaking'); expect(visibleIdentities).toContain('RemoteB-Speaking'); expect(participantCount).toBe(2); // Local + 1 remote }); }); test.describe('Smart Mosaic Layout - Screen Sharing visibility', () => { test('retain screen sharing participant regardless limit', async ({ page }) => { // Scenario: 2 remote participants + local, limit = 1 // Remote A shares screen (no audio) → should be visible // Remote B speaks → should become visible, replacing A // Remote A stops sharing → should not be visible unless speaking // Audio: B uses continuous_speech.wav // Local participant joins the room await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId); await joinRoomAs('moderator', participantName, page); // Wait for session to be ready await waitForElementInIframe(page, 'ov-session', { state: 'visible' }); await muteAudio(page); // Mute local to avoid interference // Configure Smart Mosaic layout with limit = 1 await configureLayoutMode(page, 'smart-mosaic', 1); // Join Remote A who shares screen await joinBrowserFakeParticipant(roomId, 'RemoteA-ScreenShare', { screenShare: true, audioFile: 'complete_silence.wav' // No audio }); await waitForParticipantVisible(page, 'RemoteA-ScreenShare'); // Verify Remote A is visible due to screen sharing let [visibleIdentities, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); expect(visibleIdentities).toContain('RemoteA-ScreenShare'); expect(visibleIdentities).toContain('RemoteA-ScreenShare_SCREEN'); expect(participantCount).toBe(3); // Local + screen share + 1 remote // Join Remote B who speaks await joinBrowserFakeParticipant(roomId, 'RemoteB-Speaking', { audioFile: 'continuous_speech.wav' }); // Wait for B to become visible await waitForParticipantVisible(page, 'RemoteB-Speaking'); // Verify Remote B is visible, replacing A [visibleIdentities, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); expect(visibleIdentities).toContain('RemoteB-Speaking'); expect(visibleIdentities).not.toContain('RemoteA-ScreenShare'); expect(visibleIdentities).toContain('RemoteA-ScreenShare_SCREEN'); expect(participantCount).toBe(3); // Local + screen + 1 remote // Stop Remote A's screen sharing await stopScreenShareBrowserFakeParticipant(roomId, 'RemoteA-ScreenShare'); // Wait for layout to update await page.waitForTimeout(2000); // Verify Remote A is no longer visible [visibleIdentities, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); expect(visibleIdentities).toContain('RemoteB-Speaking'); expect(visibleIdentities).not.toContain('RemoteA-ScreenShare'); expect(participantCount).toBe(2); // Local + 1 remote }); test('show screen sharing participant over silent ones', async ({ page }) => { // Scenario: 3 remote participants + local, limit = 2 // Remote A shares screen (no audio) → should be visible // Remote B and C are silent → should NOT be visible // Audio: B and C are silent // Local participant joins the room await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId); await joinRoomAs('moderator', participantName, page); // Wait for session to be ready await waitForElementInIframe(page, 'ov-session', { state: 'visible' }); await muteAudio(page); // Mute local to avoid interference // Configure Smart Mosaic layout with limit = 2 await configureLayoutMode(page, 'smart-mosaic', 2); // Join two silent participants await Promise.all([ joinFakeParticipant(roomId, 'RemoteB-Silent'), joinFakeParticipant(roomId, 'RemoteC-Silent') ]); await waitForParticipantCount(page, 3); // Join Remote A who shares screen await joinBrowserFakeParticipant(roomId, 'RemoteA-ScreenShare', { screenShare: true, audioFile: 'complete_silence.wav' // No audio }); await waitForParticipantVisible(page, 'RemoteA-ScreenShare_SCREEN'); // Wait for layout to update await page.waitForTimeout(3000); // Verify Remote A is visible due to screen sharing const [visibleIdentities, participantCount] = await Promise.all([ getVisibleParticipantNames(page), getVisibleParticipantsCount(page) ]); expect(visibleIdentities).not.toContain('RemoteA-ScreenShare'); expect(visibleIdentities).toContain('RemoteA-ScreenShare_SCREEN'); expect(participantCount).toBe(4); // Local + screen share + 1 remote }); }); });