From 9508687984b9a3aa6eced03736dbe729c1ff5526 Mon Sep 17 00:00:00 2001 From: Carlos Santos <4a.santos@gmail.com> Date: Wed, 11 Jun 2025 15:23:44 +0200 Subject: [PATCH] webcomponent: add UI feature preferences tests and enhance helper functions --- frontend/webcomponent/package.json | 1 + .../webcomponent/tests/e2e/core/room.test.ts | 12 +- .../tests/e2e/ui-feature-preferences.test.ts | 386 ++++++++++++++++++ .../tests/helpers/function-helpers.ts | 199 +++++++++ 4 files changed, 588 insertions(+), 10 deletions(-) create mode 100644 frontend/webcomponent/tests/e2e/ui-feature-preferences.test.ts diff --git a/frontend/webcomponent/package.json b/frontend/webcomponent/package.json index a297e66..67b1f1f 100644 --- a/frontend/webcomponent/package.json +++ b/frontend/webcomponent/package.json @@ -8,6 +8,7 @@ "test:unit": "jest --forceExit --testPathPattern \"tests/unit\" --ci", "test:e2e": "playwright test", "test:e2e-core": "playwright test tests/e2e/core/", + "test:e2e-ui-features": "playwright test tests/e2e/ui-feature-preferences.test.ts", "lint": "eslint 'src/**/*.ts'" }, "keywords": [], diff --git a/frontend/webcomponent/tests/e2e/core/room.test.ts b/frontend/webcomponent/tests/e2e/core/room.test.ts index 92180e6..da0171a 100644 --- a/frontend/webcomponent/tests/e2e/core/room.test.ts +++ b/frontend/webcomponent/tests/e2e/core/room.test.ts @@ -7,6 +7,7 @@ import { interactWithElementInIframe, joinRoomAs, leaveRoom, + openMoreOptionsMenu, prepareForJoiningRoom, saveScreenshot, startScreenSharing, @@ -86,7 +87,6 @@ test.describe('Room Functionality Tests', () => { test('should start a videoconference and display video elements', async ({ page, browser }) => { // First participant joins await joinRoomAs('publisher', participantName, page); - await waitForElementInIframe(page, 'ov-session'); // Check local video element const localVideo = await waitForElementInIframe(page, '.OV_stream.local'); @@ -100,7 +100,6 @@ test.describe('Room Functionality Tests', () => { await joinRoomAs('moderator', 'moderator', moderatorPage); // Verify session established and remote video appears - await waitForElementInIframe(moderatorPage, 'ov-session'); await waitForElementInIframe(moderatorPage, '.OV_stream.remote'); // Cleanup @@ -154,7 +153,6 @@ test.describe('Room Functionality Tests', () => { test.describe('UI Panels and Components', () => { test('should show and interact with chat panel', async ({ page }) => { await joinRoomAs('publisher', participantName, page); - await waitForElementInIframe(page, 'ov-session'); // Open chat panel await waitForElementInIframe(page, '#chat-panel-btn'); @@ -177,7 +175,6 @@ test.describe('Room Functionality Tests', () => { test('should show activities panel', async ({ page }) => { await joinRoomAs('publisher', participantName, page); - await waitForElementInIframe(page, 'ov-session'); // Open activities panel await waitForElementInIframe(page, '#activities-panel-btn'); @@ -192,7 +189,6 @@ test.describe('Room Functionality Tests', () => { test('should show participants panel', async ({ page }) => { await joinRoomAs('publisher', participantName, page); - await waitForElementInIframe(page, 'ov-session'); // Open participants panel await waitForElementInIframe(page, '#participants-panel-btn'); @@ -207,11 +203,8 @@ test.describe('Room Functionality Tests', () => { test('should show settings panel', async ({ page }) => { await joinRoomAs('publisher', participantName, page); - await waitForElementInIframe(page, 'ov-session'); - // Open more options menu - await interactWithElementInIframe(page, '#more-options-btn', { action: 'click' }); - await page.waitForTimeout(500); // Wait for menu animation + await openMoreOptionsMenu(page); // Open settings panel await interactWithElementInIframe(page, '#toolbar-settings-btn', { action: 'click' }); @@ -231,7 +224,6 @@ test.describe('Room Functionality Tests', () => { test.describe('Advanced Features', () => { test('should apply virtual background and detect visual changes', async ({ page }) => { await joinRoomAs('publisher', participantName, page); - await waitForElementInIframe(page, 'ov-session'); // Wait for video element to be ready await waitForElementInIframe(page, '.OV_video-element'); diff --git a/frontend/webcomponent/tests/e2e/ui-feature-preferences.test.ts b/frontend/webcomponent/tests/e2e/ui-feature-preferences.test.ts new file mode 100644 index 0000000..cf7b401 --- /dev/null +++ b/frontend/webcomponent/tests/e2e/ui-feature-preferences.test.ts @@ -0,0 +1,386 @@ +import { test, expect } from '@playwright/test'; +import { + applyVirtualBackground, + closeMoreOptionsMenu, + createTestRoom, + deleteTestRoom, + interactWithElementInIframe, + isVirtualBackgroundApplied, + joinRoomAs, + leaveRoom, + openMoreOptionsMenu, + prepareForJoiningRoom, + saveScreenshot, + startStopRecording, + waitForElementInIframe, + waitForVirtualBackgroundToApply +} from '../helpers/function-helpers'; +import { MeetRecordingAccess } from '../../../../typings/src/room-preferences'; + +let subscribedToAppErrors = false; + +test.describe('UI Feature Preferences Tests', () => { + const testAppUrl = 'http://localhost:5080'; + const testRoomPrefix = 'ui-feature-testing-room'; + const meetApiUrl = 'http://localhost:6080/meet/internal-api/v1'; + let participantName: string; + let roomId: string; + + // Helper function to login and get admin cookie + const loginAsAdmin = async () => { + const response = await fetch(`${meetApiUrl}/auth/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + username: 'admin', + password: 'admin' + }) + }); + + if (!response.ok || response.status !== 200) { + console.error('Login failed:', await response.text()); + throw new Error(`Failed to login: ${response.status}`); + } + + const cookies = response.headers.get('set-cookie') || ''; + if (!cookies) { + throw new Error('No cookies received from login'); + } + + // Extract the access token cookie + const accessTokenCookie = cookies.split(';').find((cookie) => cookie.trim().startsWith('OvMeetAccessToken=')); + + if (!accessTokenCookie) { + throw new Error('Access token cookie not found'); + } + + return accessTokenCookie.trim(); + }; + + // Helper function to update room preferences via REST API + const updateRoomPreferences = async (preferences: any) => { + const adminCookie = await loginAsAdmin(); + const response = await fetch(`${meetApiUrl}/rooms/${roomId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Cookie: adminCookie + }, + body: JSON.stringify(preferences) + }); + + if (!response.ok) { + throw new Error(`Failed to update room preferences: ${response.status} ${await response.text()}`); + } + + return response.json(); + }; + + // ========================================== + // SETUP & TEARDOWN + // ========================================== + + test.beforeAll(async () => { + // Login as admin to get authentication cookie + // adminCookie = await loginAsAdmin(); + // Create test room + }); + + 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 }) => { + // Cleanup: delete the test room + const tempContext = await browser.newContext(); + const tempPage = await tempContext.newPage(); + await tempPage.goto(testAppUrl); + await tempPage.waitForSelector('#delete-all-rooms'); + await tempPage.click('#delete-all-rooms'); + await tempPage.close(); + await tempContext.close(); + }); + + test.afterEach(async ({ page }) => { + try { + await leaveRoom(page); + } catch (error) {} + }); + + // ========================================== + // CHAT FEATURE TESTS + // ========================================== + + test.describe('Chat Feature', () => { + test('should show chat button when chat is enabled', async ({ page }) => { + roomId = await createTestRoom(testRoomPrefix, { + chatPreferences: { enabled: true }, + recordingPreferences: { + enabled: true, + allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER + }, + virtualBackgroundPreferences: { enabled: true } + }); + + await page.reload(); + await prepareForJoiningRoom(page, testAppUrl, testRoomPrefix); + + await joinRoomAs('publisher', participantName, page); + + // Check that chat button is visible + const chatButton = await waitForElementInIframe(page, '#chat-panel-btn', { state: 'visible' }); + await expect(chatButton).toBeVisible(); + }); + + test('should hide chat button when chat is disabled', async ({ page }) => { + // Disable chat via API + roomId = await createTestRoom(testRoomPrefix, { + chatPreferences: { enabled: false }, + recordingPreferences: { + enabled: true, + allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER + }, + virtualBackgroundPreferences: { enabled: true } + }); + + await page.reload(); + await prepareForJoiningRoom(page, testAppUrl, testRoomPrefix); + await joinRoomAs('publisher', participantName, page); + + // Check that chat button is not visible + const chatButton = page.frameLocator('openvidu-meet >>> iframe').locator('#chat-panel-btn'); + await expect(chatButton).toBeHidden(); + }); + }); + + // ========================================== + // RECORDING FEATURE TESTS + // ========================================== + + test.describe('Recording Feature', () => { + test('should show recording button when recording is enabled for moderator', async ({ page }) => { + roomId = await createTestRoom(testRoomPrefix, { + chatPreferences: { enabled: true }, + recordingPreferences: { + enabled: true, + allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER + }, + virtualBackgroundPreferences: { enabled: true } + }); + + await page.reload(); + await prepareForJoiningRoom(page, testAppUrl, testRoomPrefix); + + await joinRoomAs('moderator', participantName, page); + + await openMoreOptionsMenu(page); + + // Check that recording button is visible for moderator + await waitForElementInIframe(page, '#recording-btn', { state: 'visible' }); + + await closeMoreOptionsMenu(page); + await waitForElementInIframe(page, '#activities-panel-btn', { + state: 'visible' + }); + + await interactWithElementInIframe(page, '#activities-panel-btn', { action: 'click' }); + await page.waitForTimeout(500); + await waitForElementInIframe(page, 'ov-recording-activity', { state: 'visible' }); + }); + + test('should hide recording button when recording is disabled', async ({ page }) => { + // Disable recording via API + roomId = await createTestRoom(testRoomPrefix, { + chatPreferences: { enabled: true }, + recordingPreferences: { + enabled: false, + allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER + }, + virtualBackgroundPreferences: { enabled: true } + }); + + await page.reload(); + await prepareForJoiningRoom(page, testAppUrl, testRoomPrefix); + await joinRoomAs('moderator', participantName, page); + + // Check that recording button is not visible + await interactWithElementInIframe(page, '#more-options-btn', { action: 'click' }); + await page.waitForTimeout(500); + await waitForElementInIframe(page, '#recording-btn', { state: 'hidden' }); + await closeMoreOptionsMenu(page); + await waitForElementInIframe(page, '#activities-panel-btn', { + state: 'hidden' + }); + }); + + test('should not show recording button for publisher when recording is enabled', async ({ page }) => { + // Ensure recording is enabled but only for moderators + roomId = await createTestRoom(testRoomPrefix, { + chatPreferences: { enabled: true }, + recordingPreferences: { + enabled: true, + allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR + }, + virtualBackgroundPreferences: { enabled: true } + }); + + await page.reload(); + await prepareForJoiningRoom(page, testAppUrl, testRoomPrefix); + await joinRoomAs('publisher', participantName, page); + + // Check that recording button is not visible for publisher + const recordingButton = page.frameLocator('openvidu-meet >>> iframe').locator('#recording-btn'); + await expect(recordingButton).toBeHidden(); + }); + }); + + // ========================================== + // VIRTUAL BACKGROUND FEATURE TESTS + // ========================================== + + test.describe('Virtual Background Feature', () => { + test('should show virtual background button when enabled', async ({ page }) => { + // Ensure virtual backgrounds are enabled + roomId = await createTestRoom(testRoomPrefix, { + chatPreferences: { enabled: true }, + recordingPreferences: { + enabled: true, + allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER + }, + virtualBackgroundPreferences: { enabled: true } + }); + + await page.reload(); + await prepareForJoiningRoom(page, testAppUrl, testRoomPrefix); + await joinRoomAs('publisher', participantName, page); + + // Click more options to reveal virtual background button + await openMoreOptionsMenu(page); + + // Check that virtual background button is visible + await waitForElementInIframe(page, '#virtual-bg-btn', { state: 'visible' }); + await interactWithElementInIframe(page, '#virtual-bg-btn', { action: 'click' }); + + await waitForElementInIframe(page, 'ov-background-effects-panel', { state: 'visible' }); + }); + + test('should hide virtual background button when disabled', async ({ page }) => { + // Disable virtual backgrounds via API + roomId = await createTestRoom(testRoomPrefix, { + chatPreferences: { enabled: true }, + recordingPreferences: { + enabled: true, + allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER + }, + virtualBackgroundPreferences: { enabled: false } + }); + + await page.reload(); + await prepareForJoiningRoom(page, testAppUrl, testRoomPrefix); + await joinRoomAs('publisher', participantName, page); + + // Click more options to reveal virtual background button + await openMoreOptionsMenu(page); + + // Check that virtual background button is visible + await waitForElementInIframe(page, '#virtual-bg-btn', { state: 'hidden' }); + await closeMoreOptionsMenu(page); + }); + + test('should not apply virtual background when saved in local storage and feature is disabled', async ({ + page + }) => { + // Ensure virtual backgrounds are enabled + roomId = await createTestRoom(testRoomPrefix, { + chatPreferences: { enabled: true }, + recordingPreferences: { + enabled: true, + allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER + }, + virtualBackgroundPreferences: { enabled: true } + }); + + await page.reload(); + await prepareForJoiningRoom(page, testAppUrl, testRoomPrefix); + await joinRoomAs('publisher', participantName, page); + + await applyVirtualBackground(page, '2'); + + await waitForVirtualBackgroundToApply(page); + + // Now disable virtual backgrounds + const { preferences: updatedPreferences } = await updateRoomPreferences({ + chatPreferences: { enabled: true }, + recordingPreferences: { + enabled: true, + allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER + }, + virtualBackgroundPreferences: { enabled: false } + }); + + expect(updatedPreferences.virtualBackgroundPreferences.enabled).toBe(false); + await leaveRoom(page); + await page.reload(); + + await prepareForJoiningRoom(page, testAppUrl, testRoomPrefix); + await joinRoomAs('publisher', participantName, page); + await page.waitForTimeout(2000); + const isVBApplied = await isVirtualBackgroundApplied(page); + + expect(isVBApplied).toBe(false); + }); + }); + + // ========================================== + // ROLE-BASED FEATURE TESTS + // ========================================== + + // test.describe('Role-based Feature Access', () => { + // test('should show different features for moderator vs publisher', async ({ page, browser }) => { + // // Setup recording to be available for moderators only + // await updateRoomPreferences({ + // ...getDefaultRoomPreferences(), + // recordingPreferences: { + // enabled: true, + // allowAccessTo: 'admin-moderator' + // } + // }); + + // // Test as moderator + // await joinRoomAs('moderator', `moderator-${participantName}`, page); + + // // Moderator should see recording button + // const moderatorRecordingButton = await waitForElementInIframe(page, '#recording-btn', { state: 'visible' }); + // await expect(moderatorRecordingButton).toBeVisible(); + + // await leaveRoom(page); + + // // Test as publisher in a new context + // const publisherContext = await browser.newContext(); + // const publisherPage = await publisherContext.newPage(); + // await prepareForJoiningRoom(publisherPage, testAppUrl, testRoomPrefix); + + // await joinRoomAs('publisher', `publisher-${participantName}`, publisherPage); + + // // Publisher should not see recording button + // const publisherRecordingButton = publisherPage + // .frameLocator('openvidu-meet >>> iframe') + // .locator('#recording-btn'); + // await expect(publisherRecordingButton).toBeHidden(); + + // await leaveRoom(publisherPage); + // await publisherContext.close(); + // }); + // }); +}); diff --git a/frontend/webcomponent/tests/helpers/function-helpers.ts b/frontend/webcomponent/tests/helpers/function-helpers.ts index d439c31..e91e03a 100644 --- a/frontend/webcomponent/tests/helpers/function-helpers.ts +++ b/frontend/webcomponent/tests/helpers/function-helpers.ts @@ -1,5 +1,8 @@ +import { MeetRecordingAccess, MeetRoomPreferences } from '../../../../typings/src/room-preferences'; import { Page, Locator, FrameLocator } from '@playwright/test'; import { expect } from '@playwright/test'; +import { PNG } from 'pngjs'; +import * as fs from 'fs'; /** * Gets a FrameLocator for an iframe inside a Shadow DOM @@ -84,7 +87,69 @@ export async function interactWithElementInIframe( throw new Error(`Unsupported action: ${action}`); } } +// Helper function to get default room preferences +const getDefaultRoomPreferences = (): MeetRoomPreferences => ({ + recordingPreferences: { + enabled: true, + allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER + }, + chatPreferences: { enabled: true }, + virtualBackgroundPreferences: { enabled: true } +}); +// Helper function to create a room for testing +export const createTestRoom = async ( + roomIdPrefix: string, + preferences: MeetRoomPreferences = getDefaultRoomPreferences() +) => { + const response = await fetch(`http://localhost:6080/meet/api/v1/rooms`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': 'meet-api-key' + }, + body: JSON.stringify({ + roomIdPrefix, + autoDeletionDate: new Date(Date.now() + 61 * 60 * 1000).getTime(), // 1 hour from now + preferences + }) + }); + + if (!response.ok) { + const errorResponse = await response.json(); + console.error('Error creating room:', errorResponse); + throw new Error(`Failed to create room: ${response.status}`); + } + + const room = await response.json(); + return room.roomId; +}; + +// Helper function to delete a room +export const deleteTestRoom = async (roomIdToDelete: string) => { + await fetch(`http://localhost:6080/meet/api/v1/rooms/${roomIdToDelete}`, { + method: 'DELETE', + headers: { + 'x-api-key': 'meet-api-key' + } + }); +}; + +export const startStopRecording = async (page: Page, action: 'start' | 'stop') => { + const buttonSelector = action === 'start' ? '#recording-btn' : '#stop-recording-btn'; + if (action === 'start') { + await openMoreOptionsMenu(page); + } + await waitForElementInIframe(page, buttonSelector, { state: 'visible' }); + await interactWithElementInIframe(page, buttonSelector, { action: 'click' }); + await page.waitForTimeout(500); // Wait for recording action to complete + if (action === 'start') { + await page.waitForSelector('.webhook-recordingUpdated', { timeout: 10000 }); + } + if (action === 'stop') { + await page.waitForSelector('.webhook-recordingEnded', { timeout: 10000 }); + } +}; export const prepareForJoiningRoom = async (page: Page, url: string, roomPrefix: string) => { await page.goto(url); await page.waitForSelector('.rooms-container'); @@ -110,6 +175,7 @@ export const joinRoomAs = async (role: 'moderator' | 'publisher', pName: string, // wait for prejoin page to load and join the room await waitForElementInIframe(page, 'ov-pre-join', { state: 'visible' }); await interactWithElementInIframe(page, '#join-button', { action: 'click' }); + await waitForElementInIframe(page, 'ov-session', { state: 'visible' }); }; export const leaveRoom = async (page: Page) => { @@ -146,7 +212,140 @@ export const removeVirtualBackground = async (page: Page) => { await interactWithElementInIframe(page, '#no_effect-btn', { action: 'click' }); await page.waitForTimeout(500); // Wait for background to be removed }; + +/** + * Analyzes the current video frame to determine if the virtual background has been applied + * by checking if most pixels are different from Chrome's synthetic green background + */ +export const isVirtualBackgroundApplied = async ( + page: Page, + videoSelector: string = '.OV_video-element', + options: { + minChangedPixelsPercent?: number; // Minimum % of non-green pixels to consider background applied + saveDebugImages?: boolean; // Save images for debugging + } = {} +): Promise => { + const { + minChangedPixelsPercent = 70, // At least 20% of pixels should be non-green + saveDebugImages = false + } = options; + + try { + // Capture current video frame + const screenshotPath = `test-results/vbg_check_${Date.now()}.png`; + await saveScreenshot(page, screenshotPath, videoSelector); + + // Read the captured image + const currentFrame = PNG.sync.read(fs.readFileSync(screenshotPath)); + const { width, height } = currentFrame; + + // Count green pixels (sample every 5th pixel for performance) + let greenPixels = 0; + let totalSampled = 0; + + for (let y = 0; y < height; y += 5) { + for (let x = 0; x < width; x += 5) { + const idx = (width * y + x) << 2; + + const r = currentFrame.data[idx]; + const g = currentFrame.data[idx + 1]; + const b = currentFrame.data[idx + 2]; + + totalSampled++; + + if (isChromeSyntheticGreen(r, g, b)) { + greenPixels++; + } + } + } + + const greenPercentage = (greenPixels / totalSampled) * 100; + const nonGreenPercentage = 100 - greenPercentage; + const backgroundApplied = nonGreenPercentage >= minChangedPixelsPercent; + + console.log( + `Video Analysis: ${nonGreenPercentage.toFixed(1)}% non-green pixels - Background applied: ${backgroundApplied}` + ); + + // Cleanup + if (!saveDebugImages) { + fs.unlinkSync(screenshotPath); + } + + return backgroundApplied; + } catch (error) { + console.error('Error checking virtual background:', error); + return false; + } +}; + +/** + * Detects if a pixel is part of Chrome's synthetic green screen + * Chrome's fake video can vary, but typically has these characteristics: + * - Green channel is dominant + * - Overall brightness suggests synthetic content + * - Color tends to be uniformly distributed + */ +const isChromeSyntheticGreen = (r: number, g: number, b: number): boolean => { + // Method 1: Classic bright green detection (loose tolerances) + const isBrightGreen = g > 150 && g > r + 50 && g > b + 50 && r < 100 && b < 100; + + // Method 2: Detect greenish hues with high saturation + const isGreenish = g > Math.max(r, b) && g > 100; + const hasLowRedBlue = r + b < g * 0.6; + const isGreenDominant = isGreenish && hasLowRedBlue; + + // Method 3: Check for uniform synthetic-looking colors + // Chrome often uses specific green values + const isTypicalChromeGreen = + (g >= 240 && r <= 50 && b <= 50) || // Very bright green + (g >= 200 && r <= 80 && b <= 80) || // Bright green + (g >= 160 && r <= 60 && b <= 60); // Medium green + + // Method 4: HSV-like check - high green saturation + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const saturation = max > 0 ? (max - min) / max : 0; + const isHighSaturationGreen = g === max && saturation > 0.5 && g > 120; + + // Return true if any of the methods detect green + return isBrightGreen || isGreenDominant || isTypicalChromeGreen || isHighSaturationGreen; +}; + +/** + * Helper function that waits for virtual background to be applied + */ +export const waitForVirtualBackgroundToApply = async (page: Page, maxWaitTime: number = 5000): Promise => { + const startTime = Date.now(); + + while (Date.now() - startTime < maxWaitTime) { + const isApplied = await isVirtualBackgroundApplied(page); + + if (isApplied) { + console.log('✅ Virtual background detected'); + return true; + } + + await page.waitForTimeout(500); // Check every 500ms + } + + console.log('❌ Virtual background not detected after waiting'); + return false; +}; + export const saveScreenshot = async (page: Page, filename: string, selector: string) => { const element = await waitForElementInIframe(page, selector); await element.screenshot({ path: filename }); }; + +export const openMoreOptionsMenu = async (page: Page) => { + await waitForElementInIframe(page, '#toolbar', { state: 'visible' }); + // Open more options menu + await interactWithElementInIframe(page, '#more-options-btn', { action: 'click' }); + await page.waitForTimeout(500); // Wait for menu animation +}; + +export const closeMoreOptionsMenu = async (page: Page) => { + await interactWithElementInIframe(page, 'body', { action: 'click' }); + await page.waitForTimeout(500); // Wait for menu to close +};