Carlos Santos b055ef0333 update file exclusion patterns in workspace settings
webcomponent: Added missing and necessary js file

Update .gitignore to specify backend public directory exclusion

webcomponent: Add error handling for invalid base URL in OpenViduMeet component

webcomponent: Update Jest configuration for improved testing setup

webcomponent: Enhance iframe attribute tests and add support for optional query parameters

webcomponent: Refactor documentation copying in build_webcomponent_doc function for improved readability and add absolute path resolution

Add E2EE_KEY property to WebComponentProperty enum for end-to-end encryption support

meet.sh: Enhance build_rest_api_doc function with output file handling and user confirmation for overwriting

frontend: replace removeRoomSecretGuard with removeQueryParamsGuard for enhanced query parameter management

frontend: add E2EE key handling in room service and update query params guard

Updated pnpm-lock.yaml

Enables end-to-end encryption (E2EE)

Adds E2EE functionality to meeting rooms.

Significant changes:
- Allows encryption of the participant name
- Introduces setting and getting E2EE keys
- Ensures recording is disabled when encryption is enabled

webcomponent: Added e2e test for checking the e2ee funcionality

frontend: Sanitize participant name before request for a token

fix: clean up formatting in openvidu-meet.code-workspace
2025-11-10 17:54:33 +01:00

878 lines
33 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { expect, test } from '@playwright/test';
import { MeetRecordingAccess } from '../../../../typings/src/room-config';
import { MEET_TESTAPP_URL } from '../config';
import {
closeMoreOptionsMenu,
countElementsInIframe,
createTestRoom,
deleteAllRecordings,
deleteAllRooms,
interactWithElementInIframe,
joinRoomAs,
leaveRoom,
openMoreOptionsMenu,
prepareForJoiningRoom,
updateRoomConfig,
waitForElementInIframe
} from '../helpers/function-helpers';
let subscribedToAppErrors = false;
test.describe('E2EE UI Tests', () => {
let roomId: string;
let participantName: string;
// ==========================================
// SETUP & TEARDOWN
// ==========================================
test.beforeAll(async () => {
// Create a test room before all tests
roomId = await createTestRoom('test-room-e2ee');
});
test.beforeEach(async ({ page }) => {
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 = `P-${Math.random().toString(36).substring(2, 9)}`;
});
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();
});
// ==========================================
// E2EE LOBBY UI TESTS
// ==========================================
test.describe('E2EE Lobby Elements', () => {
test('should show E2EE key input and badge in lobby when E2EE is enabled', async ({ page }) => {
// Enable E2EE
await updateRoomConfig(roomId, {
chat: { enabled: true },
recording: { enabled: false, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
virtualBackground: { enabled: true },
e2ee: { enabled: true }
});
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
await page.click('#join-as-speaker');
const component = page.locator('openvidu-meet');
await expect(component).toBeVisible();
// Wait for participant name input
await waitForElementInIframe(page, '#participant-name-input', { state: 'visible' });
// Check that E2EE badge is visible
const e2eeBadge = await waitForElementInIframe(page, '.encryption-badge', { state: 'visible' });
await expect(e2eeBadge).toBeVisible();
await expect(e2eeBadge).toContainText('end-to-end encrypted');
// Check that E2EE key input is visible
const e2eeKeyInput = await waitForElementInIframe(page, '#participant-e2eekey-input', {
state: 'visible'
});
await expect(e2eeKeyInput).toBeVisible();
// Check that the input has correct attributes
await expect(e2eeKeyInput).toHaveAttribute('type', 'password');
await expect(e2eeKeyInput).toHaveAttribute('required');
});
test('should hide E2EE elements in lobby when E2EE is disabled', async ({ page }) => {
// Disable E2EE
await updateRoomConfig(roomId, {
chat: { enabled: true },
recording: { enabled: true, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
virtualBackground: { enabled: true },
e2ee: { enabled: false }
});
await page.goto(MEET_TESTAPP_URL);
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
await page.click('#join-as-speaker');
const component = page.locator('openvidu-meet');
await expect(component).toBeVisible();
// Wait for participant name input
await waitForElementInIframe(page, '#participant-name-input', { state: 'visible' });
// Check that E2EE badge is hidden
const e2eeBadge = await waitForElementInIframe(page, '.encryption-badge', { state: 'hidden' });
await expect(e2eeBadge).toBeHidden();
// Check that E2EE key input is hidden
const e2eeKeyInput = await waitForElementInIframe(page, '#participant-e2eekey-input', {
state: 'hidden'
});
await expect(e2eeKeyInput).toBeHidden();
});
});
// ==========================================
// E2EE MEETING TESTS
// ==========================================
test.describe('E2EE in Meeting', () => {
test.afterEach(async ({ page }) => {
try {
await leaveRoom(page);
} catch (error) {
// Ignore errors if already left
}
});
test('should allow participants to see and hear each other with correct E2EE key', async ({
page,
context
}) => {
// Enable E2EE
await updateRoomConfig(roomId, {
chat: { enabled: true },
recording: { enabled: false, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
virtualBackground: { enabled: true },
e2ee: { enabled: true }
});
// Create a second page for participant 2
const page2 = await context.newPage();
// Participant 1 joins with E2EE key
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
await page.click('#join-as-speaker');
await waitForElementInIframe(page, '#participant-name-input', { state: 'visible' });
await interactWithElementInIframe(page, '#participant-name-input', {
action: 'fill',
value: participantName
});
// Fill E2EE key
const e2eeKey = 'test-encryption-key-123';
await interactWithElementInIframe(page, '#participant-e2eekey-input', {
action: 'fill',
value: e2eeKey
});
await interactWithElementInIframe(page, '#participant-name-submit', { action: 'click' });
// Wait for prejoin page and join
await waitForElementInIframe(page, 'ov-pre-join', { state: 'visible' });
await interactWithElementInIframe(page, '#join-button', { action: 'click' });
await waitForElementInIframe(page, 'ov-session', { state: 'visible' });
// Participant 2 joins with same E2EE key
const participant2Name = `P2-${Math.random().toString(36).substring(2, 9)}`;
await prepareForJoiningRoom(page2, MEET_TESTAPP_URL, roomId);
await page2.click('#join-as-speaker');
await waitForElementInIframe(page2, '#participant-name-input', { state: 'visible' });
await interactWithElementInIframe(page2, '#participant-name-input', {
action: 'fill',
value: participant2Name
});
// Fill same E2EE key
await interactWithElementInIframe(page2, '#participant-e2eekey-input', {
action: 'fill',
value: e2eeKey
});
await interactWithElementInIframe(page2, '#participant-name-submit', { action: 'click' });
// Wait for prejoin page and join
await waitForElementInIframe(page2, 'ov-pre-join', { state: 'visible' });
await interactWithElementInIframe(page2, '#join-button', { action: 'click' });
await waitForElementInIframe(page2, 'ov-session', { state: 'visible' });
// Wait a bit for media to flow
await page.waitForTimeout(2000);
// Check that both participants can see each other's video elements
const videoCount1 = await countElementsInIframe(page, '.OV_video-element');
expect(videoCount1).toBeGreaterThanOrEqual(2);
const videoCount2 = await countElementsInIframe(page2, '.OV_video-element');
expect(videoCount2).toBeGreaterThanOrEqual(2);
// Check that no encryption error poster is shown
const encryptionError1 = await waitForElementInIframe(page, '.encryption-error-poster', {
state: 'hidden'
});
await expect(encryptionError1).toBeHidden();
const encryptionError2 = await waitForElementInIframe(page2, '.encryption-error-poster', {
state: 'hidden'
});
await expect(encryptionError2).toBeHidden();
// Expect video to be flowing (by checking the video element has video tracks)
const videoElements = await waitForElementInIframe(page, '.OV_video-element', {
state: 'visible',
all: true
});
for (const videoElement of videoElements) {
const videoTracks = await videoElement.evaluate((el) => (el as any).srcObject?.getVideoTracks());
expect(videoTracks.length).toBeGreaterThan(0);
}
// Cleanup participant 2
await leaveRoom(page2);
await page2.close();
});
test('should show encryption error poster when using wrong E2EE key', async ({ page, context }) => {
// Enable E2EE
await updateRoomConfig(roomId, {
chat: { enabled: true },
recording: { enabled: false, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
virtualBackground: { enabled: true },
e2ee: { enabled: true }
});
// Create a second page for participant 2
const page2 = await context.newPage();
// Participant 1 joins with E2EE key
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
await page.click('#join-as-speaker');
await waitForElementInIframe(page, '#participant-name-input', { state: 'visible' });
await interactWithElementInIframe(page, '#participant-name-input', {
action: 'fill',
value: participantName
});
// Fill E2EE key
const e2eeKey1 = 'correct-key-abc';
await interactWithElementInIframe(page, '#participant-e2eekey-input', {
action: 'fill',
value: e2eeKey1
});
await interactWithElementInIframe(page, '#participant-name-submit', { action: 'click' });
// Wait for prejoin page and join
await waitForElementInIframe(page, 'ov-pre-join', { state: 'visible' });
await interactWithElementInIframe(page, '#join-button', { action: 'click' });
await waitForElementInIframe(page, 'ov-session', { state: 'visible' });
// Participant 2 joins with DIFFERENT E2EE key
const participant2Name = `P2-${Math.random().toString(36).substring(2, 9)}`;
await prepareForJoiningRoom(page2, MEET_TESTAPP_URL, roomId);
await page2.click('#join-as-speaker');
await waitForElementInIframe(page2, '#participant-name-input', { state: 'visible' });
await interactWithElementInIframe(page2, '#participant-name-input', {
action: 'fill',
value: participant2Name
});
// Fill DIFFERENT E2EE key
const e2eeKey2 = 'wrong-key-xyz';
await interactWithElementInIframe(page2, '#participant-e2eekey-input', {
action: 'fill',
value: e2eeKey2
});
await interactWithElementInIframe(page2, '#participant-name-submit', { action: 'click' });
// Wait for prejoin page and join
await waitForElementInIframe(page2, 'ov-pre-join', { state: 'visible' });
await interactWithElementInIframe(page2, '#join-button', { action: 'click' });
await waitForElementInIframe(page2, 'ov-session', { state: 'visible' });
// Wait for encryption error to be detected
await page.waitForTimeout(3000);
// Check that encryption error poster is shown on both sides
// Each participant should see an encryption error for the other's video
const videoPosterCount = await countElementsInIframe(page, '.encryption-error-poster');
//! FIXME: Temporarily expecting 2 posters due to audio and video streams (needs to be fixed in ov-components)
expect(videoPosterCount).toBe(2);
const videoPosterCount2 = await countElementsInIframe(page2, '.encryption-error-poster');
//! FIXME: Temporarily expecting 2 posters due to audio and video streams (needs to be fixed in ov-components)
expect(videoPosterCount2).toBe(2);
// Add additional participant with correct key to verify they can see/hear each other
const page3 = await context.newPage();
const participant3Name = `P3-${Math.random().toString(36).substring(2, 9)}`;
await prepareForJoiningRoom(page3, MEET_TESTAPP_URL, roomId);
await page3.click('#join-as-speaker');
await waitForElementInIframe(page3, '#participant-name-input', { state: 'visible' });
await interactWithElementInIframe(page3, '#participant-name-input', {
action: 'fill',
value: participant3Name
});
// Fill CORRECT E2EE key
await interactWithElementInIframe(page3, '#participant-e2eekey-input', {
action: 'fill',
value: e2eeKey1
});
await interactWithElementInIframe(page3, '#participant-name-submit', { action: 'click' });
// Wait for prejoin page and join
await waitForElementInIframe(page3, 'ov-pre-join', { state: 'visible' });
await interactWithElementInIframe(page3, '#join-button', { action: 'click' });
await waitForElementInIframe(page3, 'ov-session', { state: 'visible' });
// Wait a bit for media to flow
await page3.waitForTimeout(2000);
// Check that participant 3 can see participant 1's video
const videoCount3 = await countElementsInIframe(page3, '.OV_video-element');
expect(videoCount3).toBeGreaterThanOrEqual(2);
const videoPosterCount3 = await countElementsInIframe(page3, '.encryption-error-poster');
//! FIXME: Temporarily expecting 2 posters due to audio and video streams (needs to be fixed in ov-components)
expect(videoPosterCount3).toBe(2);
// Cleanup participant 2
await Promise.all([leaveRoom(page2), leaveRoom(page3)]);
await Promise.all([page2.close(), page3.close()]);
});
test('should decrypt participant names and chat messages with correct E2EE key', async ({ page, context }) => {
// Enable E2EE
await updateRoomConfig(roomId, {
chat: { enabled: true },
recording: { enabled: false, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
virtualBackground: { enabled: true },
e2ee: { enabled: true }
});
const e2eeKey = 'shared-encryption-key-456';
const participant1Name = `Alice-${Math.random().toString(36).substring(2, 9)}`;
const participant2Name = `Bob-${Math.random().toString(36).substring(2, 9)}`;
// Participant 1 joins with E2EE key
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
await page.click('#join-as-speaker');
await waitForElementInIframe(page, '#participant-name-input', { state: 'visible' });
await interactWithElementInIframe(page, '#participant-name-input', {
action: 'fill',
value: participant1Name
});
await interactWithElementInIframe(page, '#participant-e2eekey-input', {
action: 'fill',
value: e2eeKey
});
await interactWithElementInIframe(page, '#participant-name-submit', { action: 'click' });
await waitForElementInIframe(page, 'ov-pre-join', { state: 'visible' });
await interactWithElementInIframe(page, '#join-button', { action: 'click' });
await waitForElementInIframe(page, 'ov-session', { state: 'visible' });
// Participant 2 joins with same E2EE key
const page2 = await context.newPage();
await prepareForJoiningRoom(page2, MEET_TESTAPP_URL, roomId);
await page2.click('#join-as-speaker');
await waitForElementInIframe(page2, '#participant-name-input', { state: 'visible' });
await interactWithElementInIframe(page2, '#participant-name-input', {
action: 'fill',
value: participant2Name
});
await interactWithElementInIframe(page2, '#participant-e2eekey-input', {
action: 'fill',
value: e2eeKey
});
await interactWithElementInIframe(page2, '#participant-name-submit', { action: 'click' });
await waitForElementInIframe(page2, 'ov-pre-join', { state: 'visible' });
await interactWithElementInIframe(page2, '#join-button', { action: 'click' });
await waitForElementInIframe(page2, 'ov-session', { state: 'visible' });
// Wait for participants to connect
await page.waitForTimeout(2000);
// ===== CHECK PARTICIPANT NAMES IN VIDEO GRID =====
// Participant 1 should see Participant 2's name decrypted
const participantNameElements = await Promise.all([
waitForElementInIframe(page, '#participant-name', {
state: 'attached',
all: true
}),
waitForElementInIframe(page2, '#participant-name', {
state: 'attached',
all: true
})
]);
for (const participantNameElement of participantNameElements.flat()) {
const name = await participantNameElement.evaluate((el) => el.textContent);
expect(name.includes(participant1Name) || name.includes(participant2Name)).toBeTruthy();
expect(name).not.toContain('*');
}
// ===== CHECK NAMES IN PARTICIPANTS PANEL =====
// Open participants panel
await Promise.all([
interactWithElementInIframe(page, '#participants-panel-btn', { action: 'click' }),
interactWithElementInIframe(page2, '#participants-panel-btn', { action: 'click' })
]);
await Promise.all([
waitForElementInIframe(page, 'ov-participants-panel', { state: 'visible' }),
waitForElementInIframe(page2, 'ov-participants-panel', { state: 'visible' })
]);
// Check that both names are visible and decrypted in the panel
const participantsPanelNames = await Promise.all([
waitForElementInIframe(page, '.participant-item-name span', {
state: 'attached',
all: true
}),
waitForElementInIframe(page2, '.participant-item-name span', {
state: 'attached',
all: true
})
]);
for (const participantPanelName of participantsPanelNames.flat()) {
const name = await participantPanelName.evaluate((el) => el.textContent);
expect(name.includes(participant1Name) || name.includes(participant2Name)).toBeTruthy();
expect(name).not.toContain('*');
}
// Close participants panel
await Promise.all([
interactWithElementInIframe(page, '#participants-panel-btn', { action: 'click' }),
interactWithElementInIframe(page2, '#participants-panel-btn', { action: 'click' })
]);
await Promise.all([
waitForElementInIframe(page, 'ov-participants-panel', { state: 'hidden' }),
waitForElementInIframe(page2, 'ov-participants-panel', { state: 'hidden' })
]);
// ===== CHECK OWN NAME IN SETTINGS PANEL =====
// Open settings panel
await Promise.all([openMoreOptionsMenu(page), openMoreOptionsMenu(page2)]);
await Promise.all([
interactWithElementInIframe(page, '#toolbar-settings-btn', { action: 'click' }),
interactWithElementInIframe(page2, '#toolbar-settings-btn', { action: 'click' })
]);
await Promise.all([
waitForElementInIframe(page, 'ov-settings-panel', { state: 'visible' }),
waitForElementInIframe(page2, 'ov-settings-panel', { state: 'visible' })
]);
// Check that own name is visible and decrypted
const ownNameInputs = await Promise.all([
waitForElementInIframe(page, '#participant-name-input', {
state: 'visible'
}),
waitForElementInIframe(page2, '#participant-name-input', { state: 'visible' })
]);
const ownName1 = await ownNameInputs[0].evaluate((el: HTMLInputElement) => el.value);
const ownName2 = await ownNameInputs[1].evaluate((el: HTMLInputElement) => el.value);
expect(ownName1).toBe(participant1Name);
expect(ownName1).not.toContain('*');
expect(ownName2).toBe(participant2Name);
expect(ownName2).not.toContain('*');
// Close settings panel
await Promise.all([
interactWithElementInIframe(page, '.panel-close-button', { action: 'click' }),
interactWithElementInIframe(page2, '.panel-close-button', { action: 'click' })
]);
await Promise.all([
waitForElementInIframe(page, 'ov-settings-panel', { state: 'hidden' }),
waitForElementInIframe(page2, 'ov-settings-panel', { state: 'hidden' })
]);
await Promise.all([closeMoreOptionsMenu(page), closeMoreOptionsMenu(page2)]);
// ===== CHECK CHAT MESSAGES =====
// Open chat
await Promise.all([
interactWithElementInIframe(page, '#chat-panel-btn', { action: 'click' }),
interactWithElementInIframe(page2, '#chat-panel-btn', { action: 'click' })
]);
await Promise.all([
waitForElementInIframe(page, 'ov-chat-panel', { state: 'visible' }),
waitForElementInIframe(page2, 'ov-chat-panel', { state: 'visible' })
]);
// ===== MESSAGE: PARTICIPANT 1 → PARTICIPANT 2 =====
const testMessage1 = `Hello from ${participant1Name}!`;
await Promise.all([
interactWithElementInIframe(page, '#chat-input', { action: 'fill', value: testMessage1 }),
waitForElementInIframe(page2, 'ov-chat-panel', { state: 'visible' })
]);
await interactWithElementInIframe(page, '#send-btn', { action: 'click' });
// Wait for message to be sent
await page.waitForTimeout(1000);
// Open chat on page 2
const chatMessages2 = await waitForElementInIframe(page2, '.chat-message', { state: 'visible' });
// Verify message content
const messageText2 = await chatMessages2.evaluate((el) => el.textContent || '');
expect(messageText2).toContain(testMessage1);
expect(messageText2).not.toContain('*');
// ===== MESSAGE: PARTICIPANT 2 → PARTICIPANT 1 =====
const testMessage2 = `Hi from ${participant2Name}!`;
// Send message in page2 iframe
await interactWithElementInIframe(page2, '#chat-input', { action: 'fill', value: testMessage2 });
await interactWithElementInIframe(page2, '#send-btn', { action: 'click' });
// Wait briefly for message delivery
await page.waitForTimeout(1000);
// Wait for message on participant 1s side
const chatMessages1 = await waitForElementInIframe(page, '.chat-message', { state: 'visible' });
// Collect all chat messages in the chat panel
const allMessages1 = await chatMessages1.evaluate((el) =>
Array.from(el.closest('ov-chat-panel')?.querySelectorAll('.chat-message') || []).map(
(e) => e.textContent || ''
)
);
// Verify received message
expect(allMessages1.join(' ')).toContain(testMessage2);
expect(allMessages1.join(' ')).not.toContain('*');
// Cleanup
await leaveRoom(page2);
await page2.close();
});
test('should show masked names and unreadable messages for participant with wrong E2EE key', async ({
page,
context
}) => {
// Enable E2EE
await updateRoomConfig(roomId, {
chat: { enabled: true },
recording: { enabled: false, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
virtualBackground: { enabled: true },
e2ee: { enabled: true }
});
const correctKey = 'correct-shared-key-789';
const wrongKey = 'wrong-key-999';
const participant1Name = `Charlie-${Math.random().toString(36).substring(2, 9)}`;
const participant2Name = `David-${Math.random().toString(36).substring(2, 9)}`;
const participant3Name = `Eve-${Math.random().toString(36).substring(2, 9)}`;
const [page2, page3] = await Promise.all([context.newPage(), context.newPage()]);
// Prepare for all participants to join the room
await Promise.all([
prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId),
prepareForJoiningRoom(page2, MEET_TESTAPP_URL, roomId),
prepareForJoiningRoom(page3, MEET_TESTAPP_URL, roomId)
]);
// Join as speaker in all pages
await Promise.all([
page.click('#join-as-speaker'),
page2.click('#join-as-speaker'),
page3.click('#join-as-speaker')
]);
// Wait for name and E2EE key inputs to be visible in all pages
await Promise.all([
waitForElementInIframe(page, '#participant-name-input', { state: 'visible' }),
waitForElementInIframe(page, '#participant-e2eekey-input', { state: 'visible' }),
waitForElementInIframe(page2, '#participant-name-input', { state: 'visible' }),
waitForElementInIframe(page2, '#participant-e2eekey-input', { state: 'visible' }),
waitForElementInIframe(page3, '#participant-name-input', { state: 'visible' }),
waitForElementInIframe(page3, '#participant-e2eekey-input', { state: 'visible' })
]);
// Fill participant names
await Promise.all([
interactWithElementInIframe(page, '#participant-name-input', {
action: 'fill',
value: participant1Name
}),
interactWithElementInIframe(page2, '#participant-name-input', {
action: 'fill',
value: participant2Name
}),
interactWithElementInIframe(page3, '#participant-name-input', {
action: 'fill',
value: participant3Name
})
]);
// Fill E2EE keys (two correct, one wrong)
await Promise.all([
interactWithElementInIframe(page, '#participant-e2eekey-input', { action: 'fill', value: correctKey }),
interactWithElementInIframe(page2, '#participant-e2eekey-input', { action: 'fill', value: correctKey }),
interactWithElementInIframe(page3, '#participant-e2eekey-input', { action: 'fill', value: wrongKey })
]);
// Join all participants
await Promise.all([
interactWithElementInIframe(page, '#participant-name-submit', { action: 'click' }),
interactWithElementInIframe(page2, '#participant-name-submit', { action: 'click' }),
interactWithElementInIframe(page3, '#participant-name-submit', { action: 'click' })
]);
// Wait for prejoin page in all pages
await Promise.all([
waitForElementInIframe(page, 'ov-pre-join', { state: 'visible' }),
waitForElementInIframe(page2, 'ov-pre-join', { state: 'visible' }),
waitForElementInIframe(page3, 'ov-pre-join', { state: 'visible' })
]);
// Click join button in all pages
await Promise.all([
interactWithElementInIframe(page, '#join-button', { action: 'click' }),
interactWithElementInIframe(page2, '#join-button', { action: 'click' }),
interactWithElementInIframe(page3, '#join-button', { action: 'click' })
]);
// Wait for session to be visible in all pages
await Promise.all([
waitForElementInIframe(page, 'ov-session', { state: 'visible' }),
waitForElementInIframe(page2, 'ov-session', { state: 'visible' }),
waitForElementInIframe(page3, 'ov-session', { state: 'visible' })
]);
// Wait for participants to connect
await page.waitForTimeout(1000);
// Check that participant 3 sees encryption error posters for others
// ===== CHECK MASKED NAMES IN VIDEO GRID FOR PARTICIPANT 3 =====
const participantNameElements3 = await waitForElementInIframe(
page3,
'#layout .participant-name-container #participant-name',
{
state: 'attached',
all: true
}
);
const participantNames3 = await Promise.all(
participantNameElements3.map((el) => el.evaluate((e) => e.textContent))
);
console.log('Participant Names Seen by Participant 3:', participantNames3);
console.log('Expected: 3 names (own + 2 masked), got:', participantNames3.length);
// Should have exactly 3 participants
expect(participantNames3.length).toBe(3);
// Should NOT all be masked (own name should be visible)
expect(participantNames3.every((name) => name?.includes('******'))).toBeFalsy();
// Should have exactly 2 masked names
const maskedNames = participantNames3.filter((name) => name?.includes('******'));
expect(maskedNames.length).toBe(2);
// Should see own name
expect(participantNames3).toContain(participant3Name);
// Should NOT see the actual names of P1 and P2
expect(participantNames3.join(' ')).not.toContain(participant1Name);
expect(participantNames3.join(' ')).not.toContain(participant2Name);
// ===== CHECK MASKED NAMES IN PARTICIPANTS PANEL =====
await interactWithElementInIframe(page3, '#participants-panel-btn', { action: 'click' });
await waitForElementInIframe(page3, 'ov-participants-panel', { state: 'visible' });
const participantsPanelNames3 = await waitForElementInIframe(page3, '.participant-name-text', {
state: 'visible',
all: true
});
const panelNamesText3 = await Promise.all(
participantsPanelNames3.map((el) => el.evaluate((e) => e.textContent))
);
console.log('Panel Names Seen by Participant 3:', panelNamesText3);
console.log('Expected: 3 names (own + 2 masked), got:', panelNamesText3.length);
// Should have exactly 3 participants in panel
expect(panelNamesText3.length).toBe(3);
// Should NOT all be masked (own name should be visible)
expect(panelNamesText3.every((name) => name?.includes('******'))).toBeFalsy();
// Should have exactly 2 masked names
const maskedPanelNames = panelNamesText3.filter((name) => name?.includes('******'));
expect(maskedPanelNames.length).toBe(2);
// Should see own name
expect(panelNamesText3).toContain(participant3Name);
// Should NOT see the actual names of P1 and P2
expect(panelNamesText3.join(' ')).not.toContain(participant1Name);
expect(panelNamesText3.join(' ')).not.toContain(participant2Name);
await interactWithElementInIframe(page3, '#participants-panel-btn', { action: 'click' });
await waitForElementInIframe(page3, 'ov-participants-panel', { state: 'hidden' });
// ===== CHECK OWN NAME IN SETTINGS PANEL =====
await openMoreOptionsMenu(page3);
await interactWithElementInIframe(page3, '#toolbar-settings-btn', { action: 'click' });
await waitForElementInIframe(page3, 'ov-settings-panel', { state: 'visible' });
const ownNameInput3 = await waitForElementInIframe(page3, '#participant-name-input', { state: 'visible' });
const ownName3 = await ownNameInput3.evaluate((el: HTMLInputElement) => el.value);
expect(ownName3).toBe(participant3Name);
expect(ownName3).not.toContain('******');
await interactWithElementInIframe(page3, '.panel-close-button', { action: 'click' });
await waitForElementInIframe(page3, 'ov-settings-panel', { state: 'hidden' });
await closeMoreOptionsMenu(page3);
// ===== SEND MESSAGE FROM PARTICIPANT 1 =====
const secretMessage = `Secret message from ${participant1Name}`;
await Promise.all([
interactWithElementInIframe(page, '#chat-panel-btn', { action: 'click' }),
waitForElementInIframe(page, 'ov-chat-panel', { state: 'visible' })
]);
// Send message
await interactWithElementInIframe(page, '#chat-input', { action: 'fill', value: secretMessage });
await interactWithElementInIframe(page, '#send-btn', { action: 'click' });
// Wait for message to be sent and received
await Promise.all([
waitForElementInIframe(page2, '#chat-panel-btn .mat-badge-content', { state: 'visible' }),
waitForElementInIframe(page3, '#chat-panel-btn .mat-badge-content', { state: 'visible' })
]);
// ===== CHECK CHAT MESSAGES ARE UNREADABLE =====
await interactWithElementInIframe(page3, '#chat-panel-btn', { action: 'click' });
await waitForElementInIframe(page3, 'ov-chat-panel', { state: 'visible' });
await page3.waitForTimeout(1000);
const chatMessagesCount = await countElementsInIframe(page3, '.chat-message');
expect(chatMessagesCount).toBeGreaterThan(0);
const chatMessages3 = await waitForElementInIframe(page3, '.chat-message', {
state: 'visible',
all: true
});
const messagesText3 = await Promise.all(chatMessages3.map((el) => el.evaluate((e) => e.textContent)));
console.log('Chat Messages Seen by Participant 3:', messagesText3);
console.log('Expected: All messages masked, got:', messagesText3.length, 'messages');
// All messages should contain the mask
expect(messagesText3.every((text) => text?.includes('******'))).toBeTruthy();
// Should NOT contain the actual secret message
expect(messagesText3.join(' ')).not.toContain(secretMessage);
// ===== VERIFY PARTICIPANTS 1 AND 2 CAN STILL SEE EACH OTHER =====
const participantNameElements1 = await waitForElementInIframe(page, '.participant-name', {
state: 'visible',
all: true
});
const participantNames1 = await Promise.all(
participantNameElements1.map((el) => el.evaluate((e) => e.textContent))
);
expect(participantNames1.join(' ')).toContain(participant2Name);
const participantNameElements2 = await waitForElementInIframe(page2, '.participant-name', {
state: 'visible',
all: true
});
const participantNames2 = await Promise.all(
participantNameElements2.map((el) => el.evaluate((e) => e.textContent))
);
expect(participantNames2.join(' ')).toContain(participant1Name);
// Cleanup
await Promise.all([leaveRoom(page2), leaveRoom(page3)]);
await Promise.all([page2.close(), page3.close()]);
});
});
// ==========================================
// E2EE AND RECORDING INTERACTION TESTS
// ==========================================
test.describe('E2EE and Recording', () => {
test.afterEach(async ({ page }) => {
try {
await leaveRoom(page, 'moderator');
} catch (error) {
// Ignore errors if already left
}
});
test('should hide recording button when E2EE is enabled', async ({ page }) => {
// Enable E2EE (which should auto-disable recording)
await updateRoomConfig(roomId, {
chat: { enabled: true },
recording: { enabled: false, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
virtualBackground: { enabled: true },
e2ee: { enabled: true }
});
await page.goto(MEET_TESTAPP_URL);
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
// Join as moderator to access recording controls
await page.click('#join-as-moderator');
const component = page.locator('openvidu-meet');
await expect(component).toBeVisible();
// Fill participant name
await waitForElementInIframe(page, '#participant-name-input', { state: 'visible' });
await interactWithElementInIframe(page, '#participant-name-input', {
action: 'fill',
value: participantName
});
// Fill E2EE key
const e2eeKey = 'test-key-recording';
await interactWithElementInIframe(page, '#participant-e2eekey-input', {
action: 'fill',
value: e2eeKey
});
await interactWithElementInIframe(page, '#participant-name-submit', { action: 'click' });
// Wait for prejoin page and join
await waitForElementInIframe(page, 'ov-pre-join', { state: 'visible' });
await interactWithElementInIframe(page, '#join-button', { action: 'click' });
await waitForElementInIframe(page, 'ov-session', { state: 'visible' });
// Open more options menu
await openMoreOptionsMenu(page);
// Check that recording button is not visible
const recordingButton = await waitForElementInIframe(page, '#recording-btn', { state: 'hidden' });
await expect(recordingButton).toBeHidden();
await closeMoreOptionsMenu(page);
// Also check that recording activities panel is not available
const activitiesButton = await waitForElementInIframe(page, '#activities-panel-btn', { state: 'hidden' });
await expect(activitiesButton).toBeHidden();
});
});
});