From 1202ad8503e03376241cd4eb33c5170c69184d64 Mon Sep 17 00:00:00 2001 From: PrayagGP Date: Wed, 2 Jul 2025 00:48:13 +0530 Subject: [PATCH 01/18] fix(cc-widgets): add station-login tests --- playwright/station-login-test.spec.ts | 134 +++++++++++++++++--------- 1 file changed, 90 insertions(+), 44 deletions(-) diff --git a/playwright/station-login-test.spec.ts b/playwright/station-login-test.spec.ts index 2810ef4cc..42bb175dd 100644 --- a/playwright/station-login-test.spec.ts +++ b/playwright/station-login-test.spec.ts @@ -7,7 +7,7 @@ import { setupMultiLoginPage, loginViaAccessToken, } from './Utils/initUtils'; -import {stationLogout, telephonyLogin, verifyLoginMode, ensureUserStateVisible} from './Utils/stationLoginUtils'; +import {stationLogout, telephonyLogin} from './Utils/stationLoginUtils'; import { getCurrentState, changeUserState, @@ -16,13 +16,69 @@ import { validateConsoleStateChange, checkCallbackSequence, } from './Utils/userStateUtils'; -import {parseTimeString, waitForWebSocketDisconnection, waitForWebSocketReconnection} from './Utils/helperUtils'; import {USER_STATES, THEME_COLORS, LOGIN_MODE, LONG_WAIT} from './constants'; +import dotenv from 'dotenv'; + +dotenv.config(); let page: Page; let context: BrowserContext; let consoleMessages: string[] = []; +function parseTimeString(timeString: string): number { + const parts = timeString.split(':'); + const minutes = parseInt(parts[0], 10) || 0; + const seconds = parseInt(parts[1], 10) || 0; + return minutes * 60 + seconds; +} + +async function waitForWebSocketDisconnection(consoleMessages: string[], timeoutMs: number = 15000): Promise { + const startTime = Date.now(); + while (Date.now() - startTime < timeoutMs) { + const webSocketDisconnectLog = consoleMessages.find( + (msg) => + msg.includes('Failed to load resource: net::ERR_INTERNET_DISCONNECTED') || + msg.includes('[WebSocketStatus] event=checkOnlineStatus | online status= false') + ); + if (webSocketDisconnectLog) { + return true; + } + await new Promise((resolve) => setTimeout(resolve, 100)); + } + return false; +} + +async function waitForWebSocketReconnection(consoleMessages: string[], timeoutMs: number = 15000): Promise { + const startTime = Date.now(); + while (Date.now() - startTime < timeoutMs) { + const webSocketReconnectLog = consoleMessages.find((msg) => + msg.includes('[WebSocketStatus] event=checkOnlineStatus | online status= true') + ); + if (webSocketReconnectLog) { + return true; + } + await new Promise((resolve) => setTimeout(resolve, 100)); + } + return false; +} + +// Helper function to verify login mode +async function verifyLoginMode(page: Page, expectedMode: string): Promise { + await expect(page.getByTestId('login-option-select').locator('#select-base-triggerid')).toContainText(expectedMode); +} + +// Helper function to ensure user state widget is visible and login if needed +async function ensureUserStateVisible(page: Page, loginMode: string): Promise { + const isUserStateWidgetVisible = await page + .getByTestId('state-select') + .isVisible() + .catch(() => false); + if (!isUserStateWidgetVisible) { + await telephonyLogin(page, loginMode); + await expect(page.getByTestId('state-select')).toBeVisible({timeout: LONG_WAIT}); + } +} + test.describe('Station Login Tests - Dial Number Mode', () => { test.beforeAll(async ({browser}) => { context = await browser.newContext(); @@ -40,7 +96,7 @@ test.describe('Station Login Tests - Dial Number Mode', () => { if (isLogoutButtonVisible) { await stationLogout(page); } - await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); + await expect(page.getByTestId('station-login-widget')).toBeVisible(); }); test.afterAll(async () => { @@ -61,15 +117,15 @@ test.describe('Station Login Tests - Dial Number Mode', () => { }); test('should login with Dial Number mode and verify all fields are visible', async () => { - await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); + await expect(page.getByTestId('station-login-widget')).toBeVisible(); const loginModeSelector = page.getByTestId('login-option-select'); - await expect(loginModeSelector).toBeVisible({timeout: 2000}); + await expect(loginModeSelector).toBeVisible(); const phoneNumberInput = page.getByTestId('dial-number-input'); - await expect(phoneNumberInput).toBeVisible({timeout: 2000}); + await expect(phoneNumberInput).toBeVisible(); const teamSelectionDropdown = page.getByTestId('teams-select-dropdown'); - await expect(teamSelectionDropdown).toBeVisible({timeout: 2000}); + await expect(teamSelectionDropdown).toBeVisible(); const saveAndContinueButton = page.getByTestId('login-button'); - await expect(saveAndContinueButton).toBeVisible({timeout: 2000}); + await expect(saveAndContinueButton).toBeVisible(); await expect(saveAndContinueButton).toContainText('Save & Continue'); await telephonyLogin(page, LOGIN_MODE.DIAL_NUMBER); await expect(page.getByTestId('state-select')).toBeVisible({timeout: LONG_WAIT}); @@ -79,13 +135,13 @@ test.describe('Station Login Tests - Dial Number Mode', () => { test('should handle page reload and maintain Dial Number login state', async () => { await ensureUserStateVisible(page, LOGIN_MODE.DIAL_NUMBER); await agentRelogin(page); - await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); + await expect(page.getByTestId('station-login-widget')).toBeVisible(); await verifyLoginMode(page, 'Dial Number'); const dialNumber = process.env.PW_AGENT1_DIAL_NUMBER; if (dialNumber) { await expect(page.getByTestId('dial-number-input').locator('input')).toHaveValue(dialNumber); } - await expect(page.getByTestId('state-select')).toBeVisible({timeout: 2000}); + await expect(page.getByTestId('state-select')).toBeVisible(); }); test('should retain user state timer and switch to Meeting state after network disconnection with Dial Number mode', async () => { @@ -94,13 +150,13 @@ test.describe('Station Login Tests - Dial Number Mode', () => { await verifyCurrentState(page, USER_STATES.MEETING); const timerBeforeDisconnection = await getStateElapsedTime(page); const secondsBeforeDisconnection = parseTimeString(timerBeforeDisconnection); - await page.waitForTimeout(3000); + await page.waitForTimeout(15000); consoleMessages.length = 0; await page.context().setOffline(true); await page.waitForTimeout(3000); const isWebSocketDisconnected = await waitForWebSocketDisconnection(consoleMessages); expect(isWebSocketDisconnected).toBe(true); - await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); + await expect(page.getByTestId('station-login-widget')).toBeVisible(); await verifyLoginMode(page, 'Dial Number'); consoleMessages.length = 0; await page.context().setOffline(false); @@ -126,7 +182,7 @@ test.describe('Station Login Tests - Dial Number Mode', () => { await page.context().setOffline(true); const isWebSocketDisconnected = await waitForWebSocketDisconnection(consoleMessages); expect(isWebSocketDisconnected).toBe(true); - await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); + await expect(page.getByTestId('station-login-widget')).toBeVisible(); await verifyLoginMode(page, 'Dial Number'); consoleMessages.length = 0; await page.context().setOffline(false); @@ -144,7 +200,7 @@ test.describe('Station Login Tests - Dial Number Mode', () => { await verifyLoginMode(page, 'Dial Number'); }); - test('should support multi-login synchronization for Dial Number Mode ', async () => { + test('should support multi-session Dial Number login synchronization', async () => { await ensureUserStateVisible(page, LOGIN_MODE.DIAL_NUMBER); const multiSessionPage = await setupMultiLoginPage(context); await verifyLoginMode(multiSessionPage, 'Dial Number'); @@ -198,15 +254,15 @@ test.describe('Station Login Tests - Extension Mode', () => { }); test('should login with Extension mode and verify all fields are visible', async () => { - await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); + await expect(page.getByTestId('station-login-widget')).toBeVisible(); const loginModeSelector = page.getByTestId('login-option-select'); - await expect(loginModeSelector).toBeVisible({timeout: 2000}); + await expect(loginModeSelector).toBeVisible(); const phoneNumberInput = page.getByTestId('dial-number-input'); - await expect(phoneNumberInput).toBeVisible({timeout: 2000}); + await expect(phoneNumberInput).toBeVisible(); const teamSelectionDropdown = page.getByTestId('teams-select-dropdown'); - await expect(teamSelectionDropdown).toBeVisible({timeout: 2000}); + await expect(teamSelectionDropdown).toBeVisible(); const saveAndContinueButton = page.getByTestId('login-button'); - await expect(saveAndContinueButton).toBeVisible({timeout: 2000}); + await expect(saveAndContinueButton).toBeVisible(); await expect(saveAndContinueButton).toContainText('Save & Continue'); await telephonyLogin(page, LOGIN_MODE.EXTENSION); await expect(page.getByTestId('state-select')).toBeVisible({timeout: LONG_WAIT}); @@ -216,13 +272,13 @@ test.describe('Station Login Tests - Extension Mode', () => { test('should handle page reload and maintain Extension login state', async () => { await ensureUserStateVisible(page, LOGIN_MODE.EXTENSION); await agentRelogin(page); - await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); + await expect(page.getByTestId('station-login-widget')).toBeVisible(); await verifyLoginMode(page, 'Extension'); const extensionNumber = process.env.PW_AGENT1_EXTENSION; if (extensionNumber) { await expect(page.getByTestId('dial-number-input').locator('input')).toHaveValue(extensionNumber); } - await expect(page.getByTestId('state-select')).toBeVisible({timeout: 2000}); + await expect(page.getByTestId('state-select')).toBeVisible(); }); test('should retain user state timer and switch to Meeting state after network disconnection with Extension mode', async () => { @@ -234,14 +290,12 @@ test.describe('Station Login Tests - Extension Mode', () => { await page.waitForTimeout(3000); consoleMessages.length = 0; await page.context().setOffline(true); - await page.waitForTimeout(3000); const isWebSocketDisconnected = await waitForWebSocketDisconnection(consoleMessages); expect(isWebSocketDisconnected).toBe(true); - await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); + await expect(page.getByTestId('station-login-widget')).toBeVisible(); await verifyLoginMode(page, 'Extension'); consoleMessages.length = 0; await page.context().setOffline(false); - await page.waitForTimeout(3000); const isWebSocketReconnected = await waitForWebSocketReconnection(consoleMessages); expect(isWebSocketReconnected).toBe(true); await verifyCurrentState(page, USER_STATES.MEETING); @@ -263,7 +317,7 @@ test.describe('Station Login Tests - Extension Mode', () => { await page.context().setOffline(true); const isWebSocketDisconnected = await waitForWebSocketDisconnection(consoleMessages); expect(isWebSocketDisconnected).toBe(true); - await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); + await expect(page.getByTestId('station-login-widget')).toBeVisible(); await verifyLoginMode(page, 'Extension'); consoleMessages.length = 0; await page.context().setOffline(false); @@ -281,7 +335,7 @@ test.describe('Station Login Tests - Extension Mode', () => { await verifyLoginMode(page, 'Extension'); }); - test('should support multi-login synchronization for Extension Mode', async () => { + test('should support multi-session Extension login synchronization', async () => { await ensureUserStateVisible(page, LOGIN_MODE.EXTENSION); const multiSessionPage = await setupMultiLoginPage(context); await verifyLoginMode(multiSessionPage, 'Extension'); @@ -312,18 +366,12 @@ test.describe('Station Login Tests - Desktop Mode', () => { if (isLogoutButtonVisible) { await stationLogout(page); } - await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); + await expect(page.getByTestId('station-login-widget')).toBeVisible(); }); test.afterAll(async () => { if (page) { - const isLogoutButtonVisible = await page - .getByTestId('samples:station-logout-button') - .isVisible() - .catch(() => false); - if (isLogoutButtonVisible) { - await stationLogout(page); - } + await stationLogout(page); await context.close(); } }); @@ -333,13 +381,13 @@ test.describe('Station Login Tests - Desktop Mode', () => { }); test('should login with Desktop mode and verify all fields are visible', async () => { - await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); + await expect(page.getByTestId('station-login-widget')).toBeVisible(); const loginModeSelector = page.getByTestId('login-option-select'); - await expect(loginModeSelector).toBeVisible({timeout: 2000}); + await expect(loginModeSelector).toBeVisible(); const teamSelectionDropdown = page.getByTestId('teams-select-dropdown'); - await expect(teamSelectionDropdown).toBeVisible({timeout: 2000}); + await expect(teamSelectionDropdown).toBeVisible(); const saveAndContinueButton = page.getByTestId('login-button'); - await expect(saveAndContinueButton).toBeVisible({timeout: 2000}); + await expect(saveAndContinueButton).toBeVisible(); await expect(saveAndContinueButton).toContainText('Save & Continue'); await telephonyLogin(page, LOGIN_MODE.DESKTOP); await expect(page.getByTestId('state-select')).toBeVisible({timeout: 3000}); @@ -349,9 +397,9 @@ test.describe('Station Login Tests - Desktop Mode', () => { test('should handle page reload and maintain Desktop login state', async () => { await ensureUserStateVisible(page, LOGIN_MODE.DESKTOP); await agentRelogin(page); - await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); + await expect(page.getByTestId('station-login-widget')).toBeVisible(); await verifyLoginMode(page, 'Desktop'); - await expect(page.getByTestId('state-select')).toBeVisible({timeout: 2000}); + await expect(page.getByTestId('state-select')).toBeVisible(); }); test('should retain user state timer and switch to Meeting state after network disconnection with Desktop mode', async () => { @@ -365,12 +413,10 @@ test.describe('Station Login Tests - Desktop Mode', () => { await page.context().setOffline(true); const isWebSocketDisconnected = await waitForWebSocketDisconnection(consoleMessages); expect(isWebSocketDisconnected).toBe(true); - await page.waitForTimeout(3000); - await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); + await expect(page.getByTestId('station-login-widget')).toBeVisible(); await verifyLoginMode(page, 'Desktop'); consoleMessages.length = 0; await page.context().setOffline(false); - await page.waitForTimeout(3000); const isWebSocketReconnected = await waitForWebSocketReconnection(consoleMessages); expect(isWebSocketReconnected).toBe(true); await verifyCurrentState(page, USER_STATES.MEETING); @@ -392,7 +438,7 @@ test.describe('Station Login Tests - Desktop Mode', () => { await page.context().setOffline(true); const isWebSocketDisconnected = await waitForWebSocketDisconnection(consoleMessages); expect(isWebSocketDisconnected).toBe(true); - await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); + await expect(page.getByTestId('station-login-widget')).toBeVisible(); await verifyLoginMode(page, 'Desktop'); consoleMessages.length = 0; await page.context().setOffline(false); From 0d043fbbef5aacc59abea18270a1975d831e5c09 Mon Sep 17 00:00:00 2001 From: PrayagGP Date: Wed, 2 Jul 2025 11:52:02 +0530 Subject: [PATCH 02/18] fix(cc-widgets): rename multi-session to mulit-login --- playwright/station-login-test.spec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/playwright/station-login-test.spec.ts b/playwright/station-login-test.spec.ts index 42bb175dd..9a303e906 100644 --- a/playwright/station-login-test.spec.ts +++ b/playwright/station-login-test.spec.ts @@ -200,7 +200,7 @@ test.describe('Station Login Tests - Dial Number Mode', () => { await verifyLoginMode(page, 'Dial Number'); }); - test('should support multi-session Dial Number login synchronization', async () => { + test('should support multi-login synchronization for Dial Number Mode ', async () => { await ensureUserStateVisible(page, LOGIN_MODE.DIAL_NUMBER); const multiSessionPage = await setupMultiLoginPage(context); await verifyLoginMode(multiSessionPage, 'Dial Number'); @@ -296,6 +296,7 @@ test.describe('Station Login Tests - Extension Mode', () => { await verifyLoginMode(page, 'Extension'); consoleMessages.length = 0; await page.context().setOffline(false); + await page.waitForTimeout(3000); const isWebSocketReconnected = await waitForWebSocketReconnection(consoleMessages); expect(isWebSocketReconnected).toBe(true); await verifyCurrentState(page, USER_STATES.MEETING); @@ -335,7 +336,7 @@ test.describe('Station Login Tests - Extension Mode', () => { await verifyLoginMode(page, 'Extension'); }); - test('should support multi-session Extension login synchronization', async () => { + test('should support multi-login synchronization for Extension Mode', async () => { await ensureUserStateVisible(page, LOGIN_MODE.EXTENSION); const multiSessionPage = await setupMultiLoginPage(context); await verifyLoginMode(multiSessionPage, 'Extension'); @@ -417,6 +418,7 @@ test.describe('Station Login Tests - Desktop Mode', () => { await verifyLoginMode(page, 'Desktop'); consoleMessages.length = 0; await page.context().setOffline(false); + await page.waitForTimeout(3000); const isWebSocketReconnected = await waitForWebSocketReconnection(consoleMessages); expect(isWebSocketReconnected).toBe(true); await verifyCurrentState(page, USER_STATES.MEETING); From 1165d039b4b6cb3cbd43f77be29499a077909b80 Mon Sep 17 00:00:00 2001 From: PrayagGP Date: Wed, 2 Jul 2025 12:19:41 +0530 Subject: [PATCH 03/18] fix(cc-widgets): add delays while handling socket reconnection --- playwright/station-login-test.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/playwright/station-login-test.spec.ts b/playwright/station-login-test.spec.ts index 9a303e906..20e2f889f 100644 --- a/playwright/station-login-test.spec.ts +++ b/playwright/station-login-test.spec.ts @@ -150,7 +150,7 @@ test.describe('Station Login Tests - Dial Number Mode', () => { await verifyCurrentState(page, USER_STATES.MEETING); const timerBeforeDisconnection = await getStateElapsedTime(page); const secondsBeforeDisconnection = parseTimeString(timerBeforeDisconnection); - await page.waitForTimeout(15000); + await page.waitForTimeout(3000); consoleMessages.length = 0; await page.context().setOffline(true); await page.waitForTimeout(3000); @@ -290,6 +290,7 @@ test.describe('Station Login Tests - Extension Mode', () => { await page.waitForTimeout(3000); consoleMessages.length = 0; await page.context().setOffline(true); + await page.waitForTimeout(3000); const isWebSocketDisconnected = await waitForWebSocketDisconnection(consoleMessages); expect(isWebSocketDisconnected).toBe(true); await expect(page.getByTestId('station-login-widget')).toBeVisible(); @@ -414,6 +415,7 @@ test.describe('Station Login Tests - Desktop Mode', () => { await page.context().setOffline(true); const isWebSocketDisconnected = await waitForWebSocketDisconnection(consoleMessages); expect(isWebSocketDisconnected).toBe(true); + await page.waitForTimeout(3000); await expect(page.getByTestId('station-login-widget')).toBeVisible(); await verifyLoginMode(page, 'Desktop'); consoleMessages.length = 0; From 6c2f81179de8fb39ab07ef8e7ab107b2612fed00 Mon Sep 17 00:00:00 2001 From: PrayagGP Date: Wed, 2 Jul 2025 12:38:35 +0530 Subject: [PATCH 04/18] fix(cc-widgets): add timeouts for various expect statements --- playwright/station-login-test.spec.ts | 56 +++++++++++++-------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/playwright/station-login-test.spec.ts b/playwright/station-login-test.spec.ts index 20e2f889f..8ede1e07c 100644 --- a/playwright/station-login-test.spec.ts +++ b/playwright/station-login-test.spec.ts @@ -96,7 +96,7 @@ test.describe('Station Login Tests - Dial Number Mode', () => { if (isLogoutButtonVisible) { await stationLogout(page); } - await expect(page.getByTestId('station-login-widget')).toBeVisible(); + await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); }); test.afterAll(async () => { @@ -117,15 +117,15 @@ test.describe('Station Login Tests - Dial Number Mode', () => { }); test('should login with Dial Number mode and verify all fields are visible', async () => { - await expect(page.getByTestId('station-login-widget')).toBeVisible(); + await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); const loginModeSelector = page.getByTestId('login-option-select'); - await expect(loginModeSelector).toBeVisible(); + await expect(loginModeSelector).toBeVisible({timeout: 2000}); const phoneNumberInput = page.getByTestId('dial-number-input'); - await expect(phoneNumberInput).toBeVisible(); + await expect(phoneNumberInput).toBeVisible({timeout: 2000}); const teamSelectionDropdown = page.getByTestId('teams-select-dropdown'); - await expect(teamSelectionDropdown).toBeVisible(); + await expect(teamSelectionDropdown).toBeVisible({timeout: 2000}); const saveAndContinueButton = page.getByTestId('login-button'); - await expect(saveAndContinueButton).toBeVisible(); + await expect(saveAndContinueButton).toBeVisible({timeout: 2000}); await expect(saveAndContinueButton).toContainText('Save & Continue'); await telephonyLogin(page, LOGIN_MODE.DIAL_NUMBER); await expect(page.getByTestId('state-select')).toBeVisible({timeout: LONG_WAIT}); @@ -135,13 +135,13 @@ test.describe('Station Login Tests - Dial Number Mode', () => { test('should handle page reload and maintain Dial Number login state', async () => { await ensureUserStateVisible(page, LOGIN_MODE.DIAL_NUMBER); await agentRelogin(page); - await expect(page.getByTestId('station-login-widget')).toBeVisible(); + await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); await verifyLoginMode(page, 'Dial Number'); const dialNumber = process.env.PW_AGENT1_DIAL_NUMBER; if (dialNumber) { await expect(page.getByTestId('dial-number-input').locator('input')).toHaveValue(dialNumber); } - await expect(page.getByTestId('state-select')).toBeVisible(); + await expect(page.getByTestId('state-select')).toBeVisible({timeout: 2000}); }); test('should retain user state timer and switch to Meeting state after network disconnection with Dial Number mode', async () => { @@ -156,7 +156,7 @@ test.describe('Station Login Tests - Dial Number Mode', () => { await page.waitForTimeout(3000); const isWebSocketDisconnected = await waitForWebSocketDisconnection(consoleMessages); expect(isWebSocketDisconnected).toBe(true); - await expect(page.getByTestId('station-login-widget')).toBeVisible(); + await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); await verifyLoginMode(page, 'Dial Number'); consoleMessages.length = 0; await page.context().setOffline(false); @@ -182,7 +182,7 @@ test.describe('Station Login Tests - Dial Number Mode', () => { await page.context().setOffline(true); const isWebSocketDisconnected = await waitForWebSocketDisconnection(consoleMessages); expect(isWebSocketDisconnected).toBe(true); - await expect(page.getByTestId('station-login-widget')).toBeVisible(); + await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); await verifyLoginMode(page, 'Dial Number'); consoleMessages.length = 0; await page.context().setOffline(false); @@ -254,15 +254,15 @@ test.describe('Station Login Tests - Extension Mode', () => { }); test('should login with Extension mode and verify all fields are visible', async () => { - await expect(page.getByTestId('station-login-widget')).toBeVisible(); + await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); const loginModeSelector = page.getByTestId('login-option-select'); - await expect(loginModeSelector).toBeVisible(); + await expect(loginModeSelector).toBeVisible({timeout: 2000}); const phoneNumberInput = page.getByTestId('dial-number-input'); - await expect(phoneNumberInput).toBeVisible(); + await expect(phoneNumberInput).toBeVisible({timeout: 2000}); const teamSelectionDropdown = page.getByTestId('teams-select-dropdown'); - await expect(teamSelectionDropdown).toBeVisible(); + await expect(teamSelectionDropdown).toBeVisible({timeout: 2000}); const saveAndContinueButton = page.getByTestId('login-button'); - await expect(saveAndContinueButton).toBeVisible(); + await expect(saveAndContinueButton).toBeVisible({timeout: 2000}); await expect(saveAndContinueButton).toContainText('Save & Continue'); await telephonyLogin(page, LOGIN_MODE.EXTENSION); await expect(page.getByTestId('state-select')).toBeVisible({timeout: LONG_WAIT}); @@ -272,13 +272,13 @@ test.describe('Station Login Tests - Extension Mode', () => { test('should handle page reload and maintain Extension login state', async () => { await ensureUserStateVisible(page, LOGIN_MODE.EXTENSION); await agentRelogin(page); - await expect(page.getByTestId('station-login-widget')).toBeVisible(); + await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); await verifyLoginMode(page, 'Extension'); const extensionNumber = process.env.PW_AGENT1_EXTENSION; if (extensionNumber) { await expect(page.getByTestId('dial-number-input').locator('input')).toHaveValue(extensionNumber); } - await expect(page.getByTestId('state-select')).toBeVisible(); + await expect(page.getByTestId('state-select')).toBeVisible({timeout: 2000}); }); test('should retain user state timer and switch to Meeting state after network disconnection with Extension mode', async () => { @@ -293,7 +293,7 @@ test.describe('Station Login Tests - Extension Mode', () => { await page.waitForTimeout(3000); const isWebSocketDisconnected = await waitForWebSocketDisconnection(consoleMessages); expect(isWebSocketDisconnected).toBe(true); - await expect(page.getByTestId('station-login-widget')).toBeVisible(); + await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); await verifyLoginMode(page, 'Extension'); consoleMessages.length = 0; await page.context().setOffline(false); @@ -319,7 +319,7 @@ test.describe('Station Login Tests - Extension Mode', () => { await page.context().setOffline(true); const isWebSocketDisconnected = await waitForWebSocketDisconnection(consoleMessages); expect(isWebSocketDisconnected).toBe(true); - await expect(page.getByTestId('station-login-widget')).toBeVisible(); + await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); await verifyLoginMode(page, 'Extension'); consoleMessages.length = 0; await page.context().setOffline(false); @@ -368,7 +368,7 @@ test.describe('Station Login Tests - Desktop Mode', () => { if (isLogoutButtonVisible) { await stationLogout(page); } - await expect(page.getByTestId('station-login-widget')).toBeVisible(); + await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); }); test.afterAll(async () => { @@ -383,13 +383,13 @@ test.describe('Station Login Tests - Desktop Mode', () => { }); test('should login with Desktop mode and verify all fields are visible', async () => { - await expect(page.getByTestId('station-login-widget')).toBeVisible(); + await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); const loginModeSelector = page.getByTestId('login-option-select'); - await expect(loginModeSelector).toBeVisible(); + await expect(loginModeSelector).toBeVisible({timeout: 2000}); const teamSelectionDropdown = page.getByTestId('teams-select-dropdown'); - await expect(teamSelectionDropdown).toBeVisible(); + await expect(teamSelectionDropdown).toBeVisible({timeout: 2000}); const saveAndContinueButton = page.getByTestId('login-button'); - await expect(saveAndContinueButton).toBeVisible(); + await expect(saveAndContinueButton).toBeVisible({timeout: 2000}); await expect(saveAndContinueButton).toContainText('Save & Continue'); await telephonyLogin(page, LOGIN_MODE.DESKTOP); await expect(page.getByTestId('state-select')).toBeVisible({timeout: 3000}); @@ -399,9 +399,9 @@ test.describe('Station Login Tests - Desktop Mode', () => { test('should handle page reload and maintain Desktop login state', async () => { await ensureUserStateVisible(page, LOGIN_MODE.DESKTOP); await agentRelogin(page); - await expect(page.getByTestId('station-login-widget')).toBeVisible(); + await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); await verifyLoginMode(page, 'Desktop'); - await expect(page.getByTestId('state-select')).toBeVisible(); + await expect(page.getByTestId('state-select')).toBeVisible({timeout: 2000}); }); test('should retain user state timer and switch to Meeting state after network disconnection with Desktop mode', async () => { @@ -416,7 +416,7 @@ test.describe('Station Login Tests - Desktop Mode', () => { const isWebSocketDisconnected = await waitForWebSocketDisconnection(consoleMessages); expect(isWebSocketDisconnected).toBe(true); await page.waitForTimeout(3000); - await expect(page.getByTestId('station-login-widget')).toBeVisible(); + await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); await verifyLoginMode(page, 'Desktop'); consoleMessages.length = 0; await page.context().setOffline(false); @@ -442,7 +442,7 @@ test.describe('Station Login Tests - Desktop Mode', () => { await page.context().setOffline(true); const isWebSocketDisconnected = await waitForWebSocketDisconnection(consoleMessages); expect(isWebSocketDisconnected).toBe(true); - await expect(page.getByTestId('station-login-widget')).toBeVisible(); + await expect(page.getByTestId('station-login-widget')).toBeVisible({timeout: 2000}); await verifyLoginMode(page, 'Desktop'); consoleMessages.length = 0; await page.context().setOffline(false); From e62cf04bb8cd85bf7c87c68f67fc10458623a01c Mon Sep 17 00:00:00 2001 From: PrayagGP Date: Wed, 2 Jul 2025 16:20:24 +0530 Subject: [PATCH 05/18] fix(cc-widgets): add a second attempt of intialising widgets --- playwright/Utils/initUtils.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/playwright/Utils/initUtils.ts b/playwright/Utils/initUtils.ts index 167c4bcf1..9b9c6bcd0 100644 --- a/playwright/Utils/initUtils.ts +++ b/playwright/Utils/initUtils.ts @@ -1,6 +1,6 @@ -import { Page, expect, BrowserContext } from '@playwright/test'; +import {Page, expect, BrowserContext} from '@playwright/test'; import dotenv from 'dotenv'; -import { BASE_URL } from '../constants'; +import {BASE_URL} from '../constants'; dotenv.config(); @@ -67,10 +67,10 @@ export const oauthLogin = async (page: Page, agentId: string): Promise => await page.locator('#select-base-triggerid').getByText('Access Token').click(); await page.getByTestId('samples:login_option_oauth').getByText('Login with Webex').click(); await page.getByTestId('samples:login_with_webex_button').click(); - await page.getByRole('textbox', { name: 'name@example.com' }).fill(username); - await page.getByRole('link', { name: 'Sign in' }).click(); - await page.getByRole('textbox', { name: 'Password' }).fill(password); - await page.getByRole('button', { name: 'Sign in' }).click(); + await page.getByRole('textbox', {name: 'name@example.com'}).fill(username); + await page.getByRole('link', {name: 'Sign in'}).click(); + await page.getByRole('textbox', {name: 'Password'}).fill(password); + await page.getByRole('button', {name: 'Sign in'}).click(); }; /** @@ -138,15 +138,13 @@ export const initialiseWidgets = async (page: Page): Promise => { await page.getByTestId('samples:init-widgets-button').click(); try { - await page.getByTestId('station-login-widget').waitFor({ state: 'visible', timeout: 30000 }); + await page.getByTestId('station-login-widget').waitFor({state: 'visible', timeout: 50000}); } catch (error) { // First attempt failed, try clicking init widgets button again - await page.reload(); - await page.waitForTimeout(2000); // Wait for page to settle await page.getByTestId('samples:init-widgets-button').click(); - + try { - await page.getByTestId('station-login-widget').waitFor({ state: 'visible', timeout: 30000 }); + await page.getByTestId('station-login-widget').waitFor({state: 'visible', timeout: 50000}); } catch (secondError) { // Second attempt also failed, throw error throw new Error('Station login widget failed to become visible after two initialization attempts (100 seconds total)'); From 2783685a46d05364d0dea80e8fe92a2c4d42c6b4 Mon Sep 17 00:00:00 2001 From: PrayagGP Date: Wed, 2 Jul 2025 17:03:44 +0530 Subject: [PATCH 06/18] fix(cc-widgets): add descriptions for helper functions --- playwright/station-login-test.spec.ts | 65 +++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/playwright/station-login-test.spec.ts b/playwright/station-login-test.spec.ts index 8ede1e07c..06195e668 100644 --- a/playwright/station-login-test.spec.ts +++ b/playwright/station-login-test.spec.ts @@ -25,6 +25,17 @@ let page: Page; let context: BrowserContext; let consoleMessages: string[] = []; +/** + * Parses a time string in MM:SS format and converts it to total seconds + * @param timeString - Time string in format "MM:SS" (e.g., "01:30" for 1 minute 30 seconds) + * @returns Total number of seconds + * @example + * ```typescript + * parseTimeString("01:30"); // Returns 90 (1 minute 30 seconds) + * parseTimeString("00:45"); // Returns 45 (45 seconds) + * parseTimeString("10:00"); // Returns 600 (10 minutes) + * ``` + */ function parseTimeString(timeString: string): number { const parts = timeString.split(':'); const minutes = parseInt(parts[0], 10) || 0; @@ -32,6 +43,20 @@ function parseTimeString(timeString: string): number { return minutes * 60 + seconds; } +/** + * Waits for WebSocket disconnection by monitoring console messages for specific disconnection indicators + * @param consoleMessages - Array of console messages to monitor + * @param timeoutMs - Maximum time to wait for disconnection in milliseconds (default: 15000) + * @returns Promise - True if disconnection is detected, false if timeout is reached + * @description Monitors for network disconnection messages or WebSocket offline status changes + * @example + * ```typescript + * consoleMessages.length = 0; // Clear existing messages + * await page.context().setOffline(true); + * const isDisconnected = await waitForWebSocketDisconnection(consoleMessages); + * expect(isDisconnected).toBe(true); + * ``` + */ async function waitForWebSocketDisconnection(consoleMessages: string[], timeoutMs: number = 15000): Promise { const startTime = Date.now(); while (Date.now() - startTime < timeoutMs) { @@ -48,6 +73,20 @@ async function waitForWebSocketDisconnection(consoleMessages: string[], timeoutM return false; } +/** + * Waits for WebSocket reconnection by monitoring console messages for online status changes + * @param consoleMessages - Array of console messages to monitor + * @param timeoutMs - Maximum time to wait for reconnection in milliseconds (default: 15000) + * @returns Promise - True if reconnection is detected, false if timeout is reached + * @description Monitors for WebSocket online status change messages indicating successful reconnection + * @example + * ```typescript + * consoleMessages.length = 0; // Clear existing messages + * await page.context().setOffline(false); + * const isReconnected = await waitForWebSocketReconnection(consoleMessages); + * expect(isReconnected).toBe(true); + * ``` + */ async function waitForWebSocketReconnection(consoleMessages: string[], timeoutMs: number = 15000): Promise { const startTime = Date.now(); while (Date.now() - startTime < timeoutMs) { @@ -62,11 +101,37 @@ async function waitForWebSocketReconnection(consoleMessages: string[], timeoutMs return false; } +/** + * Verifies that the login mode selector displays the expected login mode + * @param page - The Playwright page object + * @param expectedMode - The expected login mode text to verify (e.g., 'Dial Number', 'Extension', 'Desktop') + * @description Checks the login option select element's trigger text to ensure it matches the expected mode + * @throws {Error} When the login mode doesn't match the expected value + * @example + * ```typescript + * await verifyLoginMode(page, LOGIN_MODE.DIAL_NUMBER); + * await verifyLoginMode(page, LOGIN_MODE.EXTENSION); + * await verifyLoginMode(page, LOGIN_MODE.DESKTOP); + * ``` + */ // Helper function to verify login mode async function verifyLoginMode(page: Page, expectedMode: string): Promise { await expect(page.getByTestId('login-option-select').locator('#select-base-triggerid')).toContainText(expectedMode); } +/** + * Ensures the user state widget is visible by checking its current state and logging in if necessary + * @param page - The Playwright page object + * @param loginMode - The login mode to use if login is required (from LOGIN_MODE constants) + * @description Checks if the state-select widget is visible; if not, performs telephony login and waits for it to appear + * @throws {Error} When telephony login fails or state widget doesn't become visible + * @example + * ```typescript + * await ensureUserStateVisible(page, LOGIN_MODE.DIAL_NUMBER); + * await ensureUserStateVisible(page, LOGIN_MODE.EXTENSION); + * await ensureUserStateVisible(page, LOGIN_MODE.DESKTOP); + * ``` + */ // Helper function to ensure user state widget is visible and login if needed async function ensureUserStateVisible(page: Page, loginMode: string): Promise { const isUserStateWidgetVisible = await page From 0f925573915df8382d48cd6b73269b8bbc376156 Mon Sep 17 00:00:00 2001 From: PrayagGP Date: Wed, 2 Jul 2025 20:52:05 +0530 Subject: [PATCH 07/18] fix(cc-widgets): update test.afterAll for Desktop mode --- playwright/station-login-test.spec.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/playwright/station-login-test.spec.ts b/playwright/station-login-test.spec.ts index 06195e668..b03a7887a 100644 --- a/playwright/station-login-test.spec.ts +++ b/playwright/station-login-test.spec.ts @@ -438,7 +438,13 @@ test.describe('Station Login Tests - Desktop Mode', () => { test.afterAll(async () => { if (page) { - await stationLogout(page); + const isLogoutButtonVisible = await page + .getByTestId('samples:station-logout-button') + .isVisible() + .catch(() => false); + if (isLogoutButtonVisible) { + await stationLogout(page); + } await context.close(); } }); From fc40e676e36479a93a293c54bdaebab5531b5367 Mon Sep 17 00:00:00 2001 From: PrayagGP Date: Fri, 4 Jul 2025 20:50:58 +0530 Subject: [PATCH 08/18] fix(cc-widgets): remove unnecessary code --- playwright/station-login-test.spec.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/playwright/station-login-test.spec.ts b/playwright/station-login-test.spec.ts index b03a7887a..ba2c40f77 100644 --- a/playwright/station-login-test.spec.ts +++ b/playwright/station-login-test.spec.ts @@ -17,9 +17,6 @@ import { checkCallbackSequence, } from './Utils/userStateUtils'; import {USER_STATES, THEME_COLORS, LOGIN_MODE, LONG_WAIT} from './constants'; -import dotenv from 'dotenv'; - -dotenv.config(); let page: Page; let context: BrowserContext; From 77ce4648ff7a34e3a5d1ebf169d3eddace169242 Mon Sep 17 00:00:00 2001 From: PrayagGP Date: Mon, 7 Jul 2025 10:51:38 +0530 Subject: [PATCH 09/18] fix(cc-widgets): shift functions from test file to util file --- playwright/Utils/helperUtils.ts | 390 +------------------------- playwright/station-login-test.spec.ts | 122 +------- 2 files changed, 3 insertions(+), 509 deletions(-) diff --git a/playwright/Utils/helperUtils.ts b/playwright/Utils/helperUtils.ts index 918c36d98..3b4abb7fa 100644 --- a/playwright/Utils/helperUtils.ts +++ b/playwright/Utils/helperUtils.ts @@ -1,10 +1,3 @@ -import { Page } from '@playwright/test'; -import { getCurrentState, changeUserState } from './userStateUtils'; -import { WRAPUP_REASONS, USER_STATES, RONA_OPTIONS, LOGIN_MODE, LoginMode, ThemeColor, userState, WrapupReason } from 'playwright/constants'; -import { submitWrapup } from './wrapupUtils'; -import { acceptExtensionCall, submitRonaPopup } from './incomingTaskUtils'; -import { loginViaAccessToken, disableMultiLogin, enableMultiLogin, initialiseWidgets, enableAllWidgets, } from './initUtils'; -import { stationLogout, telephonyLogin } from './stationLoginUtils'; /** * Parses a time string in MM:SS format and converts it to total seconds * @param timeString - Time string in format "MM:SS" (e.g., "01:30" for 1 minute 30 seconds) @@ -79,385 +72,4 @@ export async function waitForWebSocketReconnection(consoleMessages: string[], ti await new Promise((resolve) => setTimeout(resolve, 100)); } return false; -} - -/** - * Waits for a specific user state to be reached in the UI - * @param page - Playwright Page object - * @param expectedState - The expected user state to wait for - * @returns Promise - * @throws Error if the expected state is not reached within the timeout - * @description Continuously checks the current user state until it matches the expected state or times out - * @example - * ```typescript - * await waitForState(page, USER_STATES.AVAILABLE); - * // Waits until the user state changes to 'Available' - * ``` - */ - -export const waitForState = async ( - page: Page, - expectedState: userState, -): Promise => { - const start = Date.now(); - const timeoutMs: number = 10000 - while (true) { - const currentState = await getCurrentState(page); - if (currentState === expectedState) return; - if (Date.now() - start > timeoutMs) { - throw new Error(`Timed out waiting for state "${expectedState}", last state was "${currentState}"`); - } - await page.waitForTimeout(500); // Poll every 500ms - } -}; - - -/** - * Retrieves the last state from captured logs - * @param capturedLogs - Array of log messages - * @returns Promise - The last state name found in the logs, or a message if not found - * @description Filters logs for state change messages and extracts the last state name - * @example - * ```typescript - * const lastState = await getLastStateFromLogs(capturedLogs); - * console.log(lastState); // Outputs the last state name or a message if not found - * ``` - */ - - -export async function getLastStateFromLogs(capturedLogs: string[]) { - const stateChangeLogs = capturedLogs.filter(log => - log.includes('onStateChange invoked with state name:') - ); - - if (stateChangeLogs.length === 0) { - return 'No state change logs found'; - } - - const lastStateLog = stateChangeLogs[stateChangeLogs.length - 1]; - const match = lastStateLog.match(/onStateChange invoked with state name:\s*(.+)$/); - - if (!match) { - return 'No State change log found'; - } - - return match[1].trim(); -} - - -/** - * Waits for a specific state to appear in the captured logs - * @param capturedLogs - Array of log messages - * @param expectedState - The expected state to wait for - * @param timeoutMs - Maximum time to wait for the state in milliseconds (default: 10000) - * uses the manual logs for that, such as "onStateChange invoked with state name: AVAILABLE" - * @returns Promise - * @throws Error if the expected state is not found within the timeout - * @description Continuously checks the last state in logs until it matches the expected state or times out - * @example - * ```typescript - * await waitForStateLogs(capturedLogs, AVAILABLE); - * // Waits until the last state in logs changes to 'Available' - * ``` - */ - -export const waitForStateLogs = async ( - capturedLogs: string[], - expectedState: userState, - timeoutMs: number = 10000 -): Promise => { - const start = Date.now(); - while (true) { - // Check if the latest state in logs matches expectedState - try { - const lastState = await getLastStateFromLogs(capturedLogs); - if (lastState === expectedState) return; - } catch { - // Ignore error if no state log yet - } - if (Date.now() - start > timeoutMs) { - throw new Error(`Timed out waiting for state "${expectedState}" in logs`); - } - await new Promise(res => setTimeout(res, 300)); // Poll every 300ms - } -}; - - -/** - * Waits for a specific wrapup reason to appear in the captured logs - * @param capturedLogs - Array of log messages - * @param expectedReason - The expected wrapup reason to wait for - * @param timeoutMs - Maximum time to wait for the wrapup reason in milliseconds (default: 10000) - * Uses the manual logs for that, such as "onWrapup invoked with reason : Sale" - * @returns Promise - * @throws Error if the expected wrapup reason is not found within the timeout - * @description Continuously checks the last wrapup reason in logs until it matches the expected reason or times out - * @example - * ```typescript - * await waitForWrapupReasonLogs(capturedLogs, WRAPUP_REASONS.SALE); - * ``` - */ - -export const waitForWrapupReasonLogs = async ( - capturedLogs: string[], - expectedReason: WrapupReason, - timeoutMs: number = 10000 -): Promise => { - const start = Date.now(); - while (true) { - try { - const lastReason = await getLastWrapupReasonFromLogs(capturedLogs); - if (lastReason === expectedReason) return; - } catch { - // Ignore error if no wrapup log yet - } - if (Date.now() - start > timeoutMs) { - throw new Error(`Timed out waiting for wrapup reason "${expectedReason}" in logs`); - } - await new Promise(res => setTimeout(res, 300)); // Poll every 300ms - } -}; - - -/** - * Retrieves the last wrapup reason from captured logs - * @param capturedLogs - Array of log messages - * @returns Promise - The last wrapup reason found in the logs, or a message if not found - * @description Filters logs for wrapup messages and extracts the last wrapup reason - * Uses the manual logs for that, such as "onWrapup invoked with reason : Sale" - * @example - * ```typescript - * const lastWrapupReason = await getLastWrapupReasonFromLogs(capturedLogs); - * console.log(lastWrapupReason); // Outputs the last wrapup reason or a message if not found - * ``` - */ - - -export async function getLastWrapupReasonFromLogs(capturedLogs: string[]): Promise { - const wrapupLogs = capturedLogs.filter(log => - log.includes('onWrapup invoked with reason :') - ); - - if (wrapupLogs.length === 0) { - return 'No wrapup reason found'; - } - - const lastWrapupLog = wrapupLogs[wrapupLogs.length - 1]; - const match = lastWrapupLog.match(/onWrapup invoked with reason : (.+)$/); - - if (!match) { - return 'No wrapup reason found'; - } - - return match[1].trim(); -} - - -/** - * Compares two RGB color strings to check if they are within a specified tolerance - * @param receivedColor - The color received from the UI (e.g., "rgb(255, 0, 0)") - * @param expectedColor - The expected color to compare against (e.g., "rgb(250, 5, 0)") - * @param tolerance - The maximum allowed difference for each RGB component (default: 10) - * @returns boolean - True if colors are close enough, false otherwise - * @description Compares each RGB component of the two colors and checks if the absolute difference is within the specified tolerance - * @example - * ```typescript - * const isClose = isColorClose("rgb(255, 0, 0)", "rgb(250, 5, 0)"); - * expect(isClose).toBe(true); // Returns true if the colors are close enough - * ``` - */ - -export function isColorClose( - receivedColor: string, - expectedColor: ThemeColor, - tolerance: number = 10 -): boolean { - const receivedRgb = receivedColor.match(/\d+/g)?.map(Number) || []; - const expectedRgb = expectedColor.match(/\d+/g)?.map(Number) || []; - - for (let i = 0; i < 3; i++) { - if ( - typeof receivedRgb[i] !== 'number' || - typeof expectedRgb[i] !== 'number' - ) { - continue; // skip if not present - } - if (Math.abs(receivedRgb[i] - expectedRgb[i]) > tolerance) { - return false; - } - } - return true; -} - - -/** - * Handles stray incoming tasks by accepting them and performing wrap-up actions, to be used for clean up before tests - * @param page - Playwright Page object - * @param extensionPage - Optional extension page for handling calls (default: null) - * @returns Promise - * @description Continuously checks for incoming tasks, accepts them, and performs wrap-up actions until no more tasks are available - * @example - * ```typescript - * await handleStrayTasks(page, extensionPage); - * ``` - */ - -export const handleStrayTasks = async (page: Page, extensionPage: Page | null = null): Promise => { - await page.waitForTimeout(1000); - const incomingTaskDiv = page.getByTestId(/^samples:incoming-task(-\w+)?$/); - - while (true) { - let flag1 = false; - let flag2 = true; - while (true) { - const task = incomingTaskDiv.first(); - let isTaskVisible = await task.isVisible().catch(() => false); - if (!isTaskVisible) break; - const acceptButton = task.getByTestId('task:accept-button').first(); - const acceptButtonVisible = await acceptButton.isVisible().catch(() => false); - const isExtensionCall = await (await task.innerText()).includes('Ringing...'); - if (isExtensionCall) { - const extensionCallVisible = await extensionPage.locator('[data-test="right-action-button"]').waitFor({ state: 'visible', timeout: 40000 }).then(() => true).catch(() => false); - if (extensionCallVisible) { - await acceptExtensionCall(extensionPage); - flag1 = true - } else { - throw new Error('Accept button not visible and extension page is not available'); - } - } else { - try { - await acceptButton.click({ timeout: 5000 }); - } catch (error) { } - flag1 = true; - } - await page.waitForTimeout(1000); - } - const endButton = page.getByTestId('call-control:end-call').first(); - const endButtonVisible = await endButton.waitFor({ state: 'visible', timeout: 2000 }).then(() => true).catch(() => false); - if (endButtonVisible) { - await page.waitForTimeout(2000); - await endButton.click({ timeout: 5000 }); - await submitWrapup(page, WRAPUP_REASONS.SALE); - } else { - const wrapupBox = page.getByTestId('call-control:wrapup-button').first(); - const isWrapupBoxVisible = await wrapupBox.waitFor({ state: 'visible', timeout: 2000 }).then(() => true).catch(() => false); - if (isWrapupBoxVisible) { - await page.waitForTimeout(2000); - await submitWrapup(page, WRAPUP_REASONS.SALE); - await page.waitForTimeout(2000) - } else { - flag2 = false; - } - } - - if (!flag1 && !flag2) { - break; - } - } - -} - -/* -/ * Sets up the page for testing by logging in, enabling widgets, and handling user states, cleaning up stray tasks, submitting RONA popups - * @param page - Playwright Page object - * @param loginMode - The login mode to use (e.g., LOGIN_MODE.DESKTOP or LOGIN_MODE.EXTENSION) - * @param agentName - Name of the agent to be logged in, example: 'AGENT1' - * @param extensionPage - Optional extension page for handling calls in extension mode (default: null) - * The extension Page should have the webex calling web-client logged in - * @returns Promise - * @description Logs in via access token, enables all widgets, handles multi-login settings, initializes widgets, and manages user states - * @example - * ```typescript - * await pageSetup(page, LOGIN_MODE.DESKTOP); - * ``` - */ - -export const pageSetup = async (page: Page, loginMode: LoginMode, agentName: string, extensionPage: Page | null = null) => { - const maxRetries = 3; - await loginViaAccessToken(page, agentName); - await enableAllWidgets(page); - if (loginMode === LOGIN_MODE.DESKTOP) { - await disableMultiLogin(page); - } else { - await enableMultiLogin(page); - } - - for (let i = 0; i < maxRetries; i++) { - try { - await initialiseWidgets(page); - break; - } catch (error) { - - if (i == maxRetries - 1) { - throw new Error(`Failed to initialise widgets after ${maxRetries} attempts: ${error}`); - } - await page.reload(); - await page.waitForTimeout(2000); // Wait for page to settle - } - } - - let loginButtonExists = await page - .getByTestId('login-button') - .isVisible() - .catch(() => false); - if (loginButtonExists) { - await telephonyLogin(page, loginMode); - } else { - const stateSelectVisible = await page.getByTestId('state-select').waitFor({ state: 'visible', timeout: 30000 }).then(() => true).catch(() => false); - if (stateSelectVisible) { - const ronapopupVisible = await page.getByTestId('samples:rona-popup').waitFor({ state: 'visible', timeout: 5000 }).then(() => true).catch(() => false); - if (ronapopupVisible) { - await submitRonaPopup(page, RONA_OPTIONS.AVAILABLE); - } - const userState = await getCurrentState(page); - await changeUserState(page, USER_STATES.AVAILABLE); - await page.waitForTimeout(5000); - - const incomingTaskDiv = page.getByTestId(/^samples:incoming-task(-\w+)?$/).first(); - await incomingTaskDiv.waitFor({ state: 'visible', timeout: 5000 }).catch(() => false); - await handleStrayTasks(page, extensionPage); - } - loginButtonExists = await page - .getByTestId('login-button') - .isVisible() - .catch(() => false); - - - if (!loginButtonExists) await stationLogout(page); - await telephonyLogin(page, loginMode); - } - - let ronapopupVisible = await page.getByTestId('samples:rona-popup').waitFor({ state: 'visible', timeout: 5000 }).then(() => true).catch(() => false); - if (ronapopupVisible) { - await submitRonaPopup(page, RONA_OPTIONS.AVAILABLE); - } - - let stationLoginFailure = await page.getByTestId('station-login-failure-label').waitFor({ state: 'visible', timeout: 5000 }).then(() => true).catch(() => false); - for (let i = 0; i < maxRetries && stationLoginFailure; i++) { - loginButtonExists = await page - .getByTestId('login-button') - .isVisible() - .catch(() => false); - if (!loginButtonExists) await stationLogout(page); - await telephonyLogin(page, loginMode); - await page.getByTestId('state-select').waitFor({ state: 'visible', timeout: 30000 }); - stationLoginFailure = await page.getByTestId('station-login-failure-label').waitFor({ state: 'visible', timeout: 5000 }).then(() => true).catch(() => false); - if (i == maxRetries - 1 && stationLoginFailure) { - throw new Error(`Station Login Error Persists after ${maxRetries} attempts`); - } - } - - await page.getByTestId('state-select').waitFor({ state: 'visible', timeout: 30000 }); - - ronapopupVisible = await page.getByTestId('samples:rona-popup').waitFor({ state: 'visible', timeout: 3000 }).then(() => true).catch(() => false); - if (ronapopupVisible) { - await submitRonaPopup(page, RONA_OPTIONS.AVAILABLE); - } - - await changeUserState(page, USER_STATES.AVAILABLE); - await page.waitForTimeout(3000); - const incomingTaskDiv = page.getByTestId(/^samples:incoming-task(-\w+)?$/).first(); - await incomingTaskDiv.waitFor({ state: 'visible', timeout: 5000 }).catch(() => false); - await handleStrayTasks(page, extensionPage); - -} +} \ No newline at end of file diff --git a/playwright/station-login-test.spec.ts b/playwright/station-login-test.spec.ts index ba2c40f77..2810ef4cc 100644 --- a/playwright/station-login-test.spec.ts +++ b/playwright/station-login-test.spec.ts @@ -7,7 +7,7 @@ import { setupMultiLoginPage, loginViaAccessToken, } from './Utils/initUtils'; -import {stationLogout, telephonyLogin} from './Utils/stationLoginUtils'; +import {stationLogout, telephonyLogin, verifyLoginMode, ensureUserStateVisible} from './Utils/stationLoginUtils'; import { getCurrentState, changeUserState, @@ -16,131 +16,13 @@ import { validateConsoleStateChange, checkCallbackSequence, } from './Utils/userStateUtils'; +import {parseTimeString, waitForWebSocketDisconnection, waitForWebSocketReconnection} from './Utils/helperUtils'; import {USER_STATES, THEME_COLORS, LOGIN_MODE, LONG_WAIT} from './constants'; let page: Page; let context: BrowserContext; let consoleMessages: string[] = []; -/** - * Parses a time string in MM:SS format and converts it to total seconds - * @param timeString - Time string in format "MM:SS" (e.g., "01:30" for 1 minute 30 seconds) - * @returns Total number of seconds - * @example - * ```typescript - * parseTimeString("01:30"); // Returns 90 (1 minute 30 seconds) - * parseTimeString("00:45"); // Returns 45 (45 seconds) - * parseTimeString("10:00"); // Returns 600 (10 minutes) - * ``` - */ -function parseTimeString(timeString: string): number { - const parts = timeString.split(':'); - const minutes = parseInt(parts[0], 10) || 0; - const seconds = parseInt(parts[1], 10) || 0; - return minutes * 60 + seconds; -} - -/** - * Waits for WebSocket disconnection by monitoring console messages for specific disconnection indicators - * @param consoleMessages - Array of console messages to monitor - * @param timeoutMs - Maximum time to wait for disconnection in milliseconds (default: 15000) - * @returns Promise - True if disconnection is detected, false if timeout is reached - * @description Monitors for network disconnection messages or WebSocket offline status changes - * @example - * ```typescript - * consoleMessages.length = 0; // Clear existing messages - * await page.context().setOffline(true); - * const isDisconnected = await waitForWebSocketDisconnection(consoleMessages); - * expect(isDisconnected).toBe(true); - * ``` - */ -async function waitForWebSocketDisconnection(consoleMessages: string[], timeoutMs: number = 15000): Promise { - const startTime = Date.now(); - while (Date.now() - startTime < timeoutMs) { - const webSocketDisconnectLog = consoleMessages.find( - (msg) => - msg.includes('Failed to load resource: net::ERR_INTERNET_DISCONNECTED') || - msg.includes('[WebSocketStatus] event=checkOnlineStatus | online status= false') - ); - if (webSocketDisconnectLog) { - return true; - } - await new Promise((resolve) => setTimeout(resolve, 100)); - } - return false; -} - -/** - * Waits for WebSocket reconnection by monitoring console messages for online status changes - * @param consoleMessages - Array of console messages to monitor - * @param timeoutMs - Maximum time to wait for reconnection in milliseconds (default: 15000) - * @returns Promise - True if reconnection is detected, false if timeout is reached - * @description Monitors for WebSocket online status change messages indicating successful reconnection - * @example - * ```typescript - * consoleMessages.length = 0; // Clear existing messages - * await page.context().setOffline(false); - * const isReconnected = await waitForWebSocketReconnection(consoleMessages); - * expect(isReconnected).toBe(true); - * ``` - */ -async function waitForWebSocketReconnection(consoleMessages: string[], timeoutMs: number = 15000): Promise { - const startTime = Date.now(); - while (Date.now() - startTime < timeoutMs) { - const webSocketReconnectLog = consoleMessages.find((msg) => - msg.includes('[WebSocketStatus] event=checkOnlineStatus | online status= true') - ); - if (webSocketReconnectLog) { - return true; - } - await new Promise((resolve) => setTimeout(resolve, 100)); - } - return false; -} - -/** - * Verifies that the login mode selector displays the expected login mode - * @param page - The Playwright page object - * @param expectedMode - The expected login mode text to verify (e.g., 'Dial Number', 'Extension', 'Desktop') - * @description Checks the login option select element's trigger text to ensure it matches the expected mode - * @throws {Error} When the login mode doesn't match the expected value - * @example - * ```typescript - * await verifyLoginMode(page, LOGIN_MODE.DIAL_NUMBER); - * await verifyLoginMode(page, LOGIN_MODE.EXTENSION); - * await verifyLoginMode(page, LOGIN_MODE.DESKTOP); - * ``` - */ -// Helper function to verify login mode -async function verifyLoginMode(page: Page, expectedMode: string): Promise { - await expect(page.getByTestId('login-option-select').locator('#select-base-triggerid')).toContainText(expectedMode); -} - -/** - * Ensures the user state widget is visible by checking its current state and logging in if necessary - * @param page - The Playwright page object - * @param loginMode - The login mode to use if login is required (from LOGIN_MODE constants) - * @description Checks if the state-select widget is visible; if not, performs telephony login and waits for it to appear - * @throws {Error} When telephony login fails or state widget doesn't become visible - * @example - * ```typescript - * await ensureUserStateVisible(page, LOGIN_MODE.DIAL_NUMBER); - * await ensureUserStateVisible(page, LOGIN_MODE.EXTENSION); - * await ensureUserStateVisible(page, LOGIN_MODE.DESKTOP); - * ``` - */ -// Helper function to ensure user state widget is visible and login if needed -async function ensureUserStateVisible(page: Page, loginMode: string): Promise { - const isUserStateWidgetVisible = await page - .getByTestId('state-select') - .isVisible() - .catch(() => false); - if (!isUserStateWidgetVisible) { - await telephonyLogin(page, loginMode); - await expect(page.getByTestId('state-select')).toBeVisible({timeout: LONG_WAIT}); - } -} - test.describe('Station Login Tests - Dial Number Mode', () => { test.beforeAll(async ({browser}) => { context = await browser.newContext(); From fe92788c47659f9654ebe5d6e53721fbbe9d3eec Mon Sep 17 00:00:00 2001 From: PrayagGP Date: Mon, 14 Jul 2025 14:42:24 +0530 Subject: [PATCH 10/18] fix(cc-widgets): sync utility files with upstream - restore complete helperUtils and fix initUtils formatting --- playwright/Utils/helperUtils.ts | 390 +++++++++++++++++++++++++++++++- playwright/Utils/initUtils.ts | 20 +- 2 files changed, 400 insertions(+), 10 deletions(-) diff --git a/playwright/Utils/helperUtils.ts b/playwright/Utils/helperUtils.ts index 3b4abb7fa..918c36d98 100644 --- a/playwright/Utils/helperUtils.ts +++ b/playwright/Utils/helperUtils.ts @@ -1,3 +1,10 @@ +import { Page } from '@playwright/test'; +import { getCurrentState, changeUserState } from './userStateUtils'; +import { WRAPUP_REASONS, USER_STATES, RONA_OPTIONS, LOGIN_MODE, LoginMode, ThemeColor, userState, WrapupReason } from 'playwright/constants'; +import { submitWrapup } from './wrapupUtils'; +import { acceptExtensionCall, submitRonaPopup } from './incomingTaskUtils'; +import { loginViaAccessToken, disableMultiLogin, enableMultiLogin, initialiseWidgets, enableAllWidgets, } from './initUtils'; +import { stationLogout, telephonyLogin } from './stationLoginUtils'; /** * Parses a time string in MM:SS format and converts it to total seconds * @param timeString - Time string in format "MM:SS" (e.g., "01:30" for 1 minute 30 seconds) @@ -72,4 +79,385 @@ export async function waitForWebSocketReconnection(consoleMessages: string[], ti await new Promise((resolve) => setTimeout(resolve, 100)); } return false; -} \ No newline at end of file +} + +/** + * Waits for a specific user state to be reached in the UI + * @param page - Playwright Page object + * @param expectedState - The expected user state to wait for + * @returns Promise + * @throws Error if the expected state is not reached within the timeout + * @description Continuously checks the current user state until it matches the expected state or times out + * @example + * ```typescript + * await waitForState(page, USER_STATES.AVAILABLE); + * // Waits until the user state changes to 'Available' + * ``` + */ + +export const waitForState = async ( + page: Page, + expectedState: userState, +): Promise => { + const start = Date.now(); + const timeoutMs: number = 10000 + while (true) { + const currentState = await getCurrentState(page); + if (currentState === expectedState) return; + if (Date.now() - start > timeoutMs) { + throw new Error(`Timed out waiting for state "${expectedState}", last state was "${currentState}"`); + } + await page.waitForTimeout(500); // Poll every 500ms + } +}; + + +/** + * Retrieves the last state from captured logs + * @param capturedLogs - Array of log messages + * @returns Promise - The last state name found in the logs, or a message if not found + * @description Filters logs for state change messages and extracts the last state name + * @example + * ```typescript + * const lastState = await getLastStateFromLogs(capturedLogs); + * console.log(lastState); // Outputs the last state name or a message if not found + * ``` + */ + + +export async function getLastStateFromLogs(capturedLogs: string[]) { + const stateChangeLogs = capturedLogs.filter(log => + log.includes('onStateChange invoked with state name:') + ); + + if (stateChangeLogs.length === 0) { + return 'No state change logs found'; + } + + const lastStateLog = stateChangeLogs[stateChangeLogs.length - 1]; + const match = lastStateLog.match(/onStateChange invoked with state name:\s*(.+)$/); + + if (!match) { + return 'No State change log found'; + } + + return match[1].trim(); +} + + +/** + * Waits for a specific state to appear in the captured logs + * @param capturedLogs - Array of log messages + * @param expectedState - The expected state to wait for + * @param timeoutMs - Maximum time to wait for the state in milliseconds (default: 10000) + * uses the manual logs for that, such as "onStateChange invoked with state name: AVAILABLE" + * @returns Promise + * @throws Error if the expected state is not found within the timeout + * @description Continuously checks the last state in logs until it matches the expected state or times out + * @example + * ```typescript + * await waitForStateLogs(capturedLogs, AVAILABLE); + * // Waits until the last state in logs changes to 'Available' + * ``` + */ + +export const waitForStateLogs = async ( + capturedLogs: string[], + expectedState: userState, + timeoutMs: number = 10000 +): Promise => { + const start = Date.now(); + while (true) { + // Check if the latest state in logs matches expectedState + try { + const lastState = await getLastStateFromLogs(capturedLogs); + if (lastState === expectedState) return; + } catch { + // Ignore error if no state log yet + } + if (Date.now() - start > timeoutMs) { + throw new Error(`Timed out waiting for state "${expectedState}" in logs`); + } + await new Promise(res => setTimeout(res, 300)); // Poll every 300ms + } +}; + + +/** + * Waits for a specific wrapup reason to appear in the captured logs + * @param capturedLogs - Array of log messages + * @param expectedReason - The expected wrapup reason to wait for + * @param timeoutMs - Maximum time to wait for the wrapup reason in milliseconds (default: 10000) + * Uses the manual logs for that, such as "onWrapup invoked with reason : Sale" + * @returns Promise + * @throws Error if the expected wrapup reason is not found within the timeout + * @description Continuously checks the last wrapup reason in logs until it matches the expected reason or times out + * @example + * ```typescript + * await waitForWrapupReasonLogs(capturedLogs, WRAPUP_REASONS.SALE); + * ``` + */ + +export const waitForWrapupReasonLogs = async ( + capturedLogs: string[], + expectedReason: WrapupReason, + timeoutMs: number = 10000 +): Promise => { + const start = Date.now(); + while (true) { + try { + const lastReason = await getLastWrapupReasonFromLogs(capturedLogs); + if (lastReason === expectedReason) return; + } catch { + // Ignore error if no wrapup log yet + } + if (Date.now() - start > timeoutMs) { + throw new Error(`Timed out waiting for wrapup reason "${expectedReason}" in logs`); + } + await new Promise(res => setTimeout(res, 300)); // Poll every 300ms + } +}; + + +/** + * Retrieves the last wrapup reason from captured logs + * @param capturedLogs - Array of log messages + * @returns Promise - The last wrapup reason found in the logs, or a message if not found + * @description Filters logs for wrapup messages and extracts the last wrapup reason + * Uses the manual logs for that, such as "onWrapup invoked with reason : Sale" + * @example + * ```typescript + * const lastWrapupReason = await getLastWrapupReasonFromLogs(capturedLogs); + * console.log(lastWrapupReason); // Outputs the last wrapup reason or a message if not found + * ``` + */ + + +export async function getLastWrapupReasonFromLogs(capturedLogs: string[]): Promise { + const wrapupLogs = capturedLogs.filter(log => + log.includes('onWrapup invoked with reason :') + ); + + if (wrapupLogs.length === 0) { + return 'No wrapup reason found'; + } + + const lastWrapupLog = wrapupLogs[wrapupLogs.length - 1]; + const match = lastWrapupLog.match(/onWrapup invoked with reason : (.+)$/); + + if (!match) { + return 'No wrapup reason found'; + } + + return match[1].trim(); +} + + +/** + * Compares two RGB color strings to check if they are within a specified tolerance + * @param receivedColor - The color received from the UI (e.g., "rgb(255, 0, 0)") + * @param expectedColor - The expected color to compare against (e.g., "rgb(250, 5, 0)") + * @param tolerance - The maximum allowed difference for each RGB component (default: 10) + * @returns boolean - True if colors are close enough, false otherwise + * @description Compares each RGB component of the two colors and checks if the absolute difference is within the specified tolerance + * @example + * ```typescript + * const isClose = isColorClose("rgb(255, 0, 0)", "rgb(250, 5, 0)"); + * expect(isClose).toBe(true); // Returns true if the colors are close enough + * ``` + */ + +export function isColorClose( + receivedColor: string, + expectedColor: ThemeColor, + tolerance: number = 10 +): boolean { + const receivedRgb = receivedColor.match(/\d+/g)?.map(Number) || []; + const expectedRgb = expectedColor.match(/\d+/g)?.map(Number) || []; + + for (let i = 0; i < 3; i++) { + if ( + typeof receivedRgb[i] !== 'number' || + typeof expectedRgb[i] !== 'number' + ) { + continue; // skip if not present + } + if (Math.abs(receivedRgb[i] - expectedRgb[i]) > tolerance) { + return false; + } + } + return true; +} + + +/** + * Handles stray incoming tasks by accepting them and performing wrap-up actions, to be used for clean up before tests + * @param page - Playwright Page object + * @param extensionPage - Optional extension page for handling calls (default: null) + * @returns Promise + * @description Continuously checks for incoming tasks, accepts them, and performs wrap-up actions until no more tasks are available + * @example + * ```typescript + * await handleStrayTasks(page, extensionPage); + * ``` + */ + +export const handleStrayTasks = async (page: Page, extensionPage: Page | null = null): Promise => { + await page.waitForTimeout(1000); + const incomingTaskDiv = page.getByTestId(/^samples:incoming-task(-\w+)?$/); + + while (true) { + let flag1 = false; + let flag2 = true; + while (true) { + const task = incomingTaskDiv.first(); + let isTaskVisible = await task.isVisible().catch(() => false); + if (!isTaskVisible) break; + const acceptButton = task.getByTestId('task:accept-button').first(); + const acceptButtonVisible = await acceptButton.isVisible().catch(() => false); + const isExtensionCall = await (await task.innerText()).includes('Ringing...'); + if (isExtensionCall) { + const extensionCallVisible = await extensionPage.locator('[data-test="right-action-button"]').waitFor({ state: 'visible', timeout: 40000 }).then(() => true).catch(() => false); + if (extensionCallVisible) { + await acceptExtensionCall(extensionPage); + flag1 = true + } else { + throw new Error('Accept button not visible and extension page is not available'); + } + } else { + try { + await acceptButton.click({ timeout: 5000 }); + } catch (error) { } + flag1 = true; + } + await page.waitForTimeout(1000); + } + const endButton = page.getByTestId('call-control:end-call').first(); + const endButtonVisible = await endButton.waitFor({ state: 'visible', timeout: 2000 }).then(() => true).catch(() => false); + if (endButtonVisible) { + await page.waitForTimeout(2000); + await endButton.click({ timeout: 5000 }); + await submitWrapup(page, WRAPUP_REASONS.SALE); + } else { + const wrapupBox = page.getByTestId('call-control:wrapup-button').first(); + const isWrapupBoxVisible = await wrapupBox.waitFor({ state: 'visible', timeout: 2000 }).then(() => true).catch(() => false); + if (isWrapupBoxVisible) { + await page.waitForTimeout(2000); + await submitWrapup(page, WRAPUP_REASONS.SALE); + await page.waitForTimeout(2000) + } else { + flag2 = false; + } + } + + if (!flag1 && !flag2) { + break; + } + } + +} + +/* +/ * Sets up the page for testing by logging in, enabling widgets, and handling user states, cleaning up stray tasks, submitting RONA popups + * @param page - Playwright Page object + * @param loginMode - The login mode to use (e.g., LOGIN_MODE.DESKTOP or LOGIN_MODE.EXTENSION) + * @param agentName - Name of the agent to be logged in, example: 'AGENT1' + * @param extensionPage - Optional extension page for handling calls in extension mode (default: null) + * The extension Page should have the webex calling web-client logged in + * @returns Promise + * @description Logs in via access token, enables all widgets, handles multi-login settings, initializes widgets, and manages user states + * @example + * ```typescript + * await pageSetup(page, LOGIN_MODE.DESKTOP); + * ``` + */ + +export const pageSetup = async (page: Page, loginMode: LoginMode, agentName: string, extensionPage: Page | null = null) => { + const maxRetries = 3; + await loginViaAccessToken(page, agentName); + await enableAllWidgets(page); + if (loginMode === LOGIN_MODE.DESKTOP) { + await disableMultiLogin(page); + } else { + await enableMultiLogin(page); + } + + for (let i = 0; i < maxRetries; i++) { + try { + await initialiseWidgets(page); + break; + } catch (error) { + + if (i == maxRetries - 1) { + throw new Error(`Failed to initialise widgets after ${maxRetries} attempts: ${error}`); + } + await page.reload(); + await page.waitForTimeout(2000); // Wait for page to settle + } + } + + let loginButtonExists = await page + .getByTestId('login-button') + .isVisible() + .catch(() => false); + if (loginButtonExists) { + await telephonyLogin(page, loginMode); + } else { + const stateSelectVisible = await page.getByTestId('state-select').waitFor({ state: 'visible', timeout: 30000 }).then(() => true).catch(() => false); + if (stateSelectVisible) { + const ronapopupVisible = await page.getByTestId('samples:rona-popup').waitFor({ state: 'visible', timeout: 5000 }).then(() => true).catch(() => false); + if (ronapopupVisible) { + await submitRonaPopup(page, RONA_OPTIONS.AVAILABLE); + } + const userState = await getCurrentState(page); + await changeUserState(page, USER_STATES.AVAILABLE); + await page.waitForTimeout(5000); + + const incomingTaskDiv = page.getByTestId(/^samples:incoming-task(-\w+)?$/).first(); + await incomingTaskDiv.waitFor({ state: 'visible', timeout: 5000 }).catch(() => false); + await handleStrayTasks(page, extensionPage); + } + loginButtonExists = await page + .getByTestId('login-button') + .isVisible() + .catch(() => false); + + + if (!loginButtonExists) await stationLogout(page); + await telephonyLogin(page, loginMode); + } + + let ronapopupVisible = await page.getByTestId('samples:rona-popup').waitFor({ state: 'visible', timeout: 5000 }).then(() => true).catch(() => false); + if (ronapopupVisible) { + await submitRonaPopup(page, RONA_OPTIONS.AVAILABLE); + } + + let stationLoginFailure = await page.getByTestId('station-login-failure-label').waitFor({ state: 'visible', timeout: 5000 }).then(() => true).catch(() => false); + for (let i = 0; i < maxRetries && stationLoginFailure; i++) { + loginButtonExists = await page + .getByTestId('login-button') + .isVisible() + .catch(() => false); + if (!loginButtonExists) await stationLogout(page); + await telephonyLogin(page, loginMode); + await page.getByTestId('state-select').waitFor({ state: 'visible', timeout: 30000 }); + stationLoginFailure = await page.getByTestId('station-login-failure-label').waitFor({ state: 'visible', timeout: 5000 }).then(() => true).catch(() => false); + if (i == maxRetries - 1 && stationLoginFailure) { + throw new Error(`Station Login Error Persists after ${maxRetries} attempts`); + } + } + + await page.getByTestId('state-select').waitFor({ state: 'visible', timeout: 30000 }); + + ronapopupVisible = await page.getByTestId('samples:rona-popup').waitFor({ state: 'visible', timeout: 3000 }).then(() => true).catch(() => false); + if (ronapopupVisible) { + await submitRonaPopup(page, RONA_OPTIONS.AVAILABLE); + } + + await changeUserState(page, USER_STATES.AVAILABLE); + await page.waitForTimeout(3000); + const incomingTaskDiv = page.getByTestId(/^samples:incoming-task(-\w+)?$/).first(); + await incomingTaskDiv.waitFor({ state: 'visible', timeout: 5000 }).catch(() => false); + await handleStrayTasks(page, extensionPage); + +} diff --git a/playwright/Utils/initUtils.ts b/playwright/Utils/initUtils.ts index 9b9c6bcd0..167c4bcf1 100644 --- a/playwright/Utils/initUtils.ts +++ b/playwright/Utils/initUtils.ts @@ -1,6 +1,6 @@ -import {Page, expect, BrowserContext} from '@playwright/test'; +import { Page, expect, BrowserContext } from '@playwright/test'; import dotenv from 'dotenv'; -import {BASE_URL} from '../constants'; +import { BASE_URL } from '../constants'; dotenv.config(); @@ -67,10 +67,10 @@ export const oauthLogin = async (page: Page, agentId: string): Promise => await page.locator('#select-base-triggerid').getByText('Access Token').click(); await page.getByTestId('samples:login_option_oauth').getByText('Login with Webex').click(); await page.getByTestId('samples:login_with_webex_button').click(); - await page.getByRole('textbox', {name: 'name@example.com'}).fill(username); - await page.getByRole('link', {name: 'Sign in'}).click(); - await page.getByRole('textbox', {name: 'Password'}).fill(password); - await page.getByRole('button', {name: 'Sign in'}).click(); + await page.getByRole('textbox', { name: 'name@example.com' }).fill(username); + await page.getByRole('link', { name: 'Sign in' }).click(); + await page.getByRole('textbox', { name: 'Password' }).fill(password); + await page.getByRole('button', { name: 'Sign in' }).click(); }; /** @@ -138,13 +138,15 @@ export const initialiseWidgets = async (page: Page): Promise => { await page.getByTestId('samples:init-widgets-button').click(); try { - await page.getByTestId('station-login-widget').waitFor({state: 'visible', timeout: 50000}); + await page.getByTestId('station-login-widget').waitFor({ state: 'visible', timeout: 30000 }); } catch (error) { // First attempt failed, try clicking init widgets button again + await page.reload(); + await page.waitForTimeout(2000); // Wait for page to settle await page.getByTestId('samples:init-widgets-button').click(); - + try { - await page.getByTestId('station-login-widget').waitFor({state: 'visible', timeout: 50000}); + await page.getByTestId('station-login-widget').waitFor({ state: 'visible', timeout: 30000 }); } catch (secondError) { // Second attempt also failed, throw error throw new Error('Station login widget failed to become visible after two initialization attempts (100 seconds total)'); From 4baacb660fafbda7c3eb3a16154088df9bd1aeda Mon Sep 17 00:00:00 2001 From: PrayagGP Date: Mon, 14 Jul 2025 18:53:43 +0530 Subject: [PATCH 11/18] fix(cc-widgets): add tests and utils for basic task controls --- .../task/CallControl/call-control.tsx | 9 +- playwright/Utils/helperUtils.ts | 5 +- playwright/Utils/taskControlUtils.ts | 616 ++++++++++++++++++ playwright/basic-task-controls-test.spec.ts | 450 +++++++++++++ 4 files changed, 1077 insertions(+), 3 deletions(-) create mode 100644 playwright/Utils/taskControlUtils.ts create mode 100644 playwright/basic-task-controls-test.spec.ts diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx index b8afefe27..5781cd8c5 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx @@ -159,6 +159,7 @@ function CallControlComponent(props: CallControlComponentProps) { className: 'call-control-button', disabled: false, isVisible: controlVisibility.holdResume, + dataTestId: 'call-control:hold-toggle', }, { id: 'consult', @@ -168,6 +169,7 @@ function CallControlComponent(props: CallControlComponentProps) { disabled: false, menuType: 'Consult', isVisible: controlVisibility.consult, + dataTestId: 'call-control:consult', }, { id: 'transfer', @@ -177,6 +179,7 @@ function CallControlComponent(props: CallControlComponentProps) { disabled: false, menuType: 'Transfer', isVisible: controlVisibility.transfer, + dataTestId: 'call-control:transfer', }, { id: 'record', @@ -186,6 +189,7 @@ function CallControlComponent(props: CallControlComponentProps) { className: 'call-control-button', disabled: false, isVisible: controlVisibility.pauseResumeRecording, + dataTestId: 'call-control:recording-toggle', }, { id: 'end', @@ -195,6 +199,7 @@ function CallControlComponent(props: CallControlComponentProps) { className: 'call-control-button-cancel', disabled: isHeld, isVisible: controlVisibility.end, + dataTestId: 'call-control:end-call', }, ]; @@ -254,7 +259,7 @@ function CallControlComponent(props: CallControlComponentProps) { className={button.className} aria-label={button.tooltip} disabled={button.disabled || (consultInitiated && isTelephony)} - data-testid="ButtonCircle" + data-testid={button.dataTestId} onPress={() => handlePopoverOpen(button.menuType as CallControlMenuType)} > @@ -295,7 +300,7 @@ function CallControlComponent(props: CallControlComponentProps) { button.className + (button.disabled || (consultInitiated && isTelephony) ? ` ${button.className}-disabled` : '') } - data-testid={button.id === 'end' ? 'call-control:end-call' : button.id} + data-testid={button.dataTestId} onPress={button.onClick} disabled={button.disabled || (consultInitiated && isTelephony)} aria-label={button.tooltip} diff --git a/playwright/Utils/helperUtils.ts b/playwright/Utils/helperUtils.ts index 918c36d98..bad820643 100644 --- a/playwright/Utils/helperUtils.ts +++ b/playwright/Utils/helperUtils.ts @@ -1,6 +1,6 @@ import { Page } from '@playwright/test'; import { getCurrentState, changeUserState } from './userStateUtils'; -import { WRAPUP_REASONS, USER_STATES, RONA_OPTIONS, LOGIN_MODE, LoginMode, ThemeColor, userState, WrapupReason } from 'playwright/constants'; +import { WRAPUP_REASONS, USER_STATES, RONA_OPTIONS, LOGIN_MODE, LoginMode, ThemeColor, userState, WrapupReason } from '../constants'; import { submitWrapup } from './wrapupUtils'; import { acceptExtensionCall, submitRonaPopup } from './incomingTaskUtils'; import { loginViaAccessToken, disableMultiLogin, enableMultiLogin, initialiseWidgets, enableAllWidgets, } from './initUtils'; @@ -317,6 +317,9 @@ export const handleStrayTasks = async (page: Page, extensionPage: Page | null = const acceptButtonVisible = await acceptButton.isVisible().catch(() => false); const isExtensionCall = await (await task.innerText()).includes('Ringing...'); if (isExtensionCall) { + if (!extensionPage) { + throw new Error('Extension page is not available for handling extension call'); + } const extensionCallVisible = await extensionPage.locator('[data-test="right-action-button"]').waitFor({ state: 'visible', timeout: 40000 }).then(() => true).catch(() => false); if (extensionCallVisible) { await acceptExtensionCall(extensionPage); diff --git a/playwright/Utils/taskControlUtils.ts b/playwright/Utils/taskControlUtils.ts new file mode 100644 index 000000000..c963b2e69 --- /dev/null +++ b/playwright/Utils/taskControlUtils.ts @@ -0,0 +1,616 @@ +import { Page, expect } from '@playwright/test'; +import { TASK_TYPES } from '../constants'; + +/** + * Utility functions for task controls testing. + * Verifies visibility of task control buttons for different task types. + * + * @packageDocumentation + */ + +/** + * Verifies that all call task control buttons are visible and accessible. + * Checks for hold, recording, transfer, consult, and end buttons. + * @param page - The agent's main page + * @returns Promise + */ +export async function callTaskControlCheck(page: Page): Promise { + // Verify call control container is visible + await expect(page.getByTestId('call-control-container').nth(0)).toBeVisible({ timeout: 30000 }); + + // Verify hold/resume toggle button is visible + await expect(page.getByTestId('call-control:hold-toggle').nth(0)).toBeVisible(); + + // Verify recording toggle button is visible + await expect(page.getByTestId('call-control:recording-toggle').nth(0)).toBeVisible(); + + // Verify transfer button is visible + await expect(page.getByTestId('call-control:transfer').nth(0)).toBeVisible(); + + // Verify consult button is visible + await expect(page.getByTestId('call-control:consult').nth(0)).toBeVisible(); + + // Verify end call button is visible + await expect(page.getByTestId('call-control:end-call').nth(0)).toBeVisible(); +} + +/** + * Verifies that chat task control buttons are visible and accessible. + * Checks for transfer and end buttons only. + * @param page - The agent's main page + * @returns Promise + */ +export async function chatTaskControlCheck(page: Page): Promise { + // Verify chat control container or equivalent is visible + await expect(page.getByTestId('call-control-container').nth(0)).toBeVisible({ timeout: 30000 }); + + // Verify transfer button is visible + await expect(page.getByTestId('call-control:transfer').nth(0)).toBeVisible(); + + // Verify end button is visible (for chat tasks) + await expect(page.getByTestId('call-control:end-call').nth(0)).toBeVisible(); +} + +/** + * Verifies that email task control buttons are visible and accessible. + * Checks for transfer and end buttons only. + * @param page - The agent's main page + * @returns Promise + */ +export async function emailTaskControlCheck(page: Page): Promise { + // Verify email control container or equivalent is visible + await expect(page.getByTestId('call-control-container').nth(0)).toBeVisible({ timeout: 30000 }); + + // Verify transfer button is visible + await expect(page.getByTestId('call-control:transfer').nth(0)).toBeVisible(); + + // Verify end button is visible (for email tasks) + await expect(page.getByTestId('call-control:end-call').nth(0)).toBeVisible(); +} + +/** + * Verifies task control buttons based on the task type. + * @param page - The agent's main page + * @param taskType - The type of the task (e.g., TASK_TYPES.CALL, TASK_TYPES.CHAT) + * @returns Promise + */ +export async function verifyTaskControls(page: Page, taskType: string): Promise { + switch (taskType) { + case TASK_TYPES.CALL: + await callTaskControlCheck(page); + break; + case TASK_TYPES.CHAT: + await chatTaskControlCheck(page); + break; + case TASK_TYPES.EMAIL: + await emailTaskControlCheck(page); + break; + default: + throw new Error(`Task control check not implemented for task type: ${taskType}`); + } +} + +/** + * Toggles the hold state of a call by clicking the hold/resume button. + * This function will put the call on hold if it's currently active, or resume it if it's on hold. + * @param page - The agent's main page + * @returns Promise + */ +export async function holdCallToggle(page: Page): Promise { + // Wait for hold toggle button to be visible and clickable + const holdButton = page.getByTestId('call-control:hold-toggle').nth(0); + await expect(holdButton).toBeVisible({ timeout: 10000 }); + + // Click the hold toggle button + await holdButton.click(); +} + +/** + * Toggles the recording state of a call by clicking the recording pause/resume button. + * This function will pause recording if it's currently active, or resume it if it's paused. + * @param page - The agent's main page + * @returns Promise + */ +export async function recordCallToggle(page: Page): Promise { + // Wait for recording toggle button to be visible and clickable + const recordButton = page.getByTestId('call-control:recording-toggle').nth(0); + await expect(recordButton).toBeVisible({ timeout: 10000 }); + + // Click the recording toggle button + await recordButton.click(); +} + +/** + * Verifies the hold timer visibility and content based on expected state. + * @param page - The agent's main page + * @param shouldBeVisible - Whether the timer should be visible (true) or hidden (false) + * @param verifyContent - Whether to verify timer content (default: true when visible) + * @returns Promise + */ +export async function verifyHoldTimer(page: Page, shouldBeVisible: boolean, verifyContent: boolean = shouldBeVisible): Promise { + const holdTimerContainer = page.locator('.on-hold-chip-text'); + + if (shouldBeVisible) { + await expect(holdTimerContainer).toBeVisible({ timeout: 10000 }); + + if (verifyContent) { + // Verify "On hold" text is present + await expect(holdTimerContainer).toContainText('On hold'); + + // Verify timer format (should contain time like 00:XX) + await expect(holdTimerContainer).toContainText(/\d{2}:\d{2}/); + } + } else { + await expect(holdTimerContainer).toBeHidden({ timeout: 10000 }); + } +} + +// Global variable to store captured logs +let capturedLogs: string[] = []; + +/** + * Sets up console logging to capture callback logs for task controls. + * Captures onHoldResume, onRecordingToggle, onEnd callbacks and SDK success messages. + * @param page - The agent's main page + * @returns Function to remove the console handler + */ +export function setupConsoleLogging(page: Page): () => void { + capturedLogs.length = 0; + + const consoleHandler = (msg) => { + const logText = msg.text(); + if (logText.includes('onHoldResume invoked') || + logText.includes('onRecordingToggle invoked') || + logText.includes('onEnd invoked') || + logText.includes('WXCC_SDK_TASK_HOLD_SUCCESS') || + logText.includes('WXCC_SDK_TASK_RESUME_SUCCESS') || + logText.includes('WXCC_SDK_TASK_PAUSE_RECORDING_SUCCESS') || + logText.includes('WXCC_SDK_TASK_RESUME_RECORDING_SUCCESS')) { + capturedLogs.push(logText); + } + }; + + page.on('console', consoleHandler); + return () => page.off('console', consoleHandler); +} + +/** + * Clears the captured logs array. + * Should be called before each test or verification to ensure clean state. + */ +export function clearCapturedLogs(): void { + capturedLogs.length = 0; +} + +/** + * Verifies that hold/resume callback logs are present and contain expected values. + * @param expectedIsHeld - Expected hold state (true for hold, false for resume) + * @throws Error if verification fails with detailed error message + */ +export function verifyHoldLogs(expectedIsHeld: boolean): void { + const holdResumeLogs = capturedLogs.filter(log => log.includes('onHoldResume invoked')); + const statusLogs = capturedLogs.filter(log => + log.includes(expectedIsHeld ? 'WXCC_SDK_TASK_HOLD_SUCCESS' : 'WXCC_SDK_TASK_RESUME_SUCCESS') + ); + + if (holdResumeLogs.length === 0) { + throw new Error(`No 'onHoldResume invoked' logs found. Expected logs for isHeld: ${expectedIsHeld}. Captured logs: ${JSON.stringify(capturedLogs)}`); + } + + if (statusLogs.length === 0) { + const expectedStatus = expectedIsHeld ? 'WXCC_SDK_TASK_HOLD_SUCCESS' : 'WXCC_SDK_TASK_RESUME_SUCCESS'; + throw new Error(`No '${expectedStatus}' logs found. Captured logs: ${JSON.stringify(capturedLogs)}`); + } + + const lastHoldLog = holdResumeLogs[holdResumeLogs.length - 1]; + if (!lastHoldLog.includes(`isHeld: ${expectedIsHeld}`)) { + throw new Error(`Expected 'isHeld: ${expectedIsHeld}' in log but found: ${lastHoldLog}`); + } +} + +/** + * Verifies that recording pause/resume callback logs are present and contain expected values. + * @param expectedIsRecording - Expected recording state (true for recording, false for paused) + * @throws Error if verification fails with detailed error message + */ +export function verifyRecordingLogs(expectedIsRecording: boolean): void { + const recordingLogs = capturedLogs.filter(log => log.includes('onRecordingToggle invoked')); + const statusLogs = capturedLogs.filter(log => + log.includes(expectedIsRecording ? 'WXCC_SDK_TASK_RESUME_RECORDING_SUCCESS' : 'WXCC_SDK_TASK_PAUSE_RECORDING_SUCCESS') + ); + + if (recordingLogs.length === 0) { + throw new Error(`No 'onRecordingToggle invoked' logs found. Expected logs for isRecording: ${expectedIsRecording}. Captured logs: ${JSON.stringify(capturedLogs)}`); + } + + if (statusLogs.length === 0) { + const expectedStatus = expectedIsRecording ? 'WXCC_SDK_TASK_RESUME_RECORDING_SUCCESS' : 'WXCC_SDK_TASK_PAUSE_RECORDING_SUCCESS'; + throw new Error(`No '${expectedStatus}' logs found. Captured logs: ${JSON.stringify(capturedLogs)}`); + } + + const lastRecordingLog = recordingLogs[recordingLogs.length - 1]; + if (!lastRecordingLog.includes(`isRecording: ${expectedIsRecording}`)) { + throw new Error(`Expected 'isRecording: ${expectedIsRecording}' in log but found: ${lastRecordingLog}`); + } +} + +/** + * Verifies that onEnd callback logs are present when tasks are ended. + * @throws Error if verification fails with detailed error message + */ +export function verifyEndLogs(): void { + const endLogs = capturedLogs.filter(log => log.includes('onEnd invoked')); + + if (endLogs.length === 0) { + throw new Error(`No 'onEnd invoked' logs found. Captured logs: ${JSON.stringify(capturedLogs)}`); + } +} + +/** + * Verifies that remote audio from another tab/participant is properly configured and ready to play. + * This function checks for the presence of the remote audio element, its properties, and MediaStream connection. + * @param page - The agent's main page + * @returns Promise + * @throws Error if remote audio verification fails + */ +export async function verifyRemoteAudio(page: Page): Promise { + try { + // Verify audio element properties using JavaScript evaluation + // Handle multiple audio elements with same ID by checking all of them + const audioProperties = await page.evaluate(() => { + const audioElements = document.querySelectorAll('#remote-audio') as NodeListOf; + + if (audioElements.length === 0) { + throw new Error('No remote audio elements found'); + } + + const results: any[] = []; + let hasActiveStream = false; + + audioElements.forEach((audio, index) => { + const hasSourceObj = audio.srcObject !== null; + + let mediaStreamActive = false; + let tracksCount = 0; + let audioTracksCount = 0; + + if (hasSourceObj) { + const mediaStream = audio.srcObject as MediaStream; + mediaStreamActive = mediaStream.active; + tracksCount = mediaStream.getTracks().length; + audioTracksCount = mediaStream.getAudioTracks().length; + } + + const properties = { + elementIndex: index, + autoplay: audio.autoplay, + hasSourceObject: hasSourceObj, + readyState: audio.readyState, + paused: audio.paused, + muted: audio.muted, + volume: audio.volume, + mediaStreamActive: mediaStreamActive, + mediaStreamTracks: tracksCount, + audioTrackCount: audioTracksCount + }; + + results.push(properties); + + // Check if this element has an active stream + if (properties.hasSourceObject && properties.mediaStreamActive && properties.audioTrackCount > 0) { + hasActiveStream = true; + } + }); + + return { + totalElements: audioElements.length, + elements: results, + hasActiveAudioStream: hasActiveStream + }; + }); + + // Verify at least one audio element exists + expect(audioProperties.totalElements).toBeGreaterThan(0); + + // Verify at least one audio element has an active MediaStream with audio tracks + expect(audioProperties.hasActiveAudioStream).toBe(true); + + // Verify properties of elements that have audio streams + const activeElements = audioProperties.elements.filter(el => + el.hasSourceObject && el.mediaStreamActive && el.audioTrackCount > 0 + ); + + expect(activeElements.length).toBeGreaterThan(0); + + // For each active audio element, verify it's properly configured + activeElements.forEach((element, index) => { + // Verify autoplay is enabled for remote audio + expect(element.autoplay).toBe(true); + + // Verify audio is not muted (should be able to hear remote audio) + expect(element.muted).toBe(false); + + // Verify volume is at audible level + expect(element.volume).toBeGreaterThan(0); + }); + + } catch (error) { + throw new Error(`Remote audio verification failed: ${error.message}`); + } +} + +/** + * Verifies the #remote-audio element exists in the DOM. + * Executes: document.querySelector("#remote-audio") + * @param page - The agent's main page (browser receiving audio) + * @returns Promise + * @throws Error if remote audio element verification fails + */ +export async function verifyRemoteAudioElement(page: Page): Promise { + try { + // Execute the console command to check for remote audio element + const elementResult = await page.evaluate(() => { + const audioElem = document.querySelector("#remote-audio") as HTMLAudioElement; + + if (!audioElem) { + return null; + } + + return { + tagName: audioElem.tagName, + id: audioElem.id, + autoplay: audioElem.autoplay, + muted: audioElem.muted, + volume: audioElem.volume, + readyState: audioElem.readyState, + paused: audioElem.paused, + hasSourceObject: audioElem.srcObject !== null, + outerHTML: audioElem.outerHTML.substring(0, 200) // First 200 chars for debugging + }; + }); + + if (!elementResult) { + throw new Error('❌ #remote-audio element not found in DOM'); + } + + // Verify the element properties + expect(elementResult.tagName).toBe('AUDIO'); + expect(elementResult.id).toBe('remote-audio'); + expect(elementResult.hasSourceObject).toBe(true); + + } catch (error) { + throw new Error(`❌ Remote audio element verification failed: ${error.message}`); + } +} + +/** + * Verifies audio transfer from caller to browser by executing the exact console command. + * Executes: document.querySelector("#remote-audio").srcObject.getAudioTracks() + * Verifies the result contains MediaStreamTrack with GUID label and proper properties + * @param page - The agent's main page (browser receiving audio) + * @returns Promise + * @throws Error if remote audio tracks verification fails + */ +export async function verifyRemoteAudioTracks(page: Page): Promise { + try { + // First verify the element exists + await verifyRemoteAudioElement(page); + + // Execute the exact console command for audio tracks + const consoleResult = await page.evaluate(() => { + // This is the exact command from your console + const audioElem = document.querySelector("#remote-audio") as HTMLAudioElement; + + if (!audioElem) { + return []; + } + + if (!audioElem.srcObject) { + return []; + } + + const mediaStream = audioElem.srcObject as MediaStream; + const audioTracks = mediaStream.getAudioTracks(); + + // Convert MediaStreamTrack objects to serializable format (like console shows) + const result = audioTracks.map((track, index) => { + return { + index, + kind: track.kind, + id: track.id, + label: track.label, + enabled: track.enabled, + muted: track.muted, + readyState: track.readyState, + onended: track.onended, + onmute: track.onmute, + onunmute: track.onunmute + }; + }); + + return result; + }); + + // Verify we got an array with at least one MediaStreamTrack + expect(consoleResult.length).toBeGreaterThanOrEqual(1); + + // Find the first audio track (should match the structure you provided) + const audioTrack = consoleResult.find(track => track.kind === 'audio'); + + if (!audioTrack) { + const availableTracks = consoleResult.map(t => `{ kind: "${t.kind}", label: "${t.label}", id: "${t.id}" }`).join(', '); + throw new Error(`❌ No audio MediaStreamTrack found. Available tracks: [${availableTracks}]`); + } + + // Verify the track properties match the exact structure you provided + expect(audioTrack.kind).toBe('audio'); + expect(audioTrack.enabled).toBe(true); + expect(audioTrack.muted).toBe(false); + expect(audioTrack.readyState).toBe('live'); + expect(audioTrack.onended).toBeNull(); + expect(audioTrack.onmute).toBeNull(); + expect(audioTrack.onunmute).toBeNull(); + + // Verify both id and label are GUID format and match each other + const guidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + + expect(guidPattern.test(audioTrack.id)).toBe(true); + expect(guidPattern.test(audioTrack.label)).toBe(true); + expect(audioTrack.id).toBe(audioTrack.label); // id should match label exactly + + // Verify index is 0 (first track) + expect(audioTrack.index).toBe(0); + + } catch (error) { + throw new Error(`❌ Audio transfer verification failed: ${error.message}`); + } +} + +/** + * Verifies the presence of hold music audio element with autoplay and loop attributes. + * Looks for: + * This is checked on the caller page when call is put on hold + * @param page - The caller's page (where hold music should be playing) + * @returns Promise + * @throws Error if hold music element verification fails + */ +export async function verifyHoldMusicElement(page: Page): Promise { + try { + const holdMusicInfo = await page.evaluate(() => { + // Look for audio elements with both autoplay and loop attributes + const audioElements = document.querySelectorAll('audio[autoplay][loop]'); + + if (audioElements.length === 0) { + // Debug: Show all audio elements if none found with autoplay and loop + const allAudioElements = document.querySelectorAll('audio'); + + const allSources = Array.from(allAudioElements).map((audio, index) => { + const a = audio as HTMLAudioElement; + + return { + src: a.src, + autoplay: a.autoplay, + loop: a.loop, + hasAutoplayAttr: a.hasAttribute('autoplay'), + hasLoopAttr: a.hasAttribute('loop'), + outerHTML: a.outerHTML.substring(0, 200) // Show first 200 chars + }; + }); + + return { + holdMusicFound: false, + allAudioElements: allSources, + totalAudioElements: allAudioElements.length + }; + } + + // Map the audio elements with autoplay and loop attributes + const holdMusicElements = Array.from(audioElements).map((audio, index) => { + const a = audio as HTMLAudioElement; + + return { + index, + src: a.src, + autoplay: a.autoplay, + loop: a.loop, + hasAutoplayAttr: a.hasAttribute('autoplay'), + hasLoopAttr: a.hasAttribute('loop'), + paused: a.paused, + volume: a.volume, + muted: a.muted, + readyState: a.readyState, + outerHTML: a.outerHTML // Full element structure + }; + }); + + return { + holdMusicFound: true, + holdMusicElements, + totalHoldMusicElements: holdMusicElements.length + }; + }); + + if (!holdMusicInfo.holdMusicFound) { + throw new Error(`❌ No hold music audio elements found with autoplay and loop attributes. Total audio elements: ${holdMusicInfo.totalAudioElements}. All audio sources: ${JSON.stringify(holdMusicInfo.allAudioElements, null, 2)}`); + } + + // Verify at least one hold music element exists + expect(holdMusicInfo.totalHoldMusicElements).toBeGreaterThan(0); + + // Find the element that matches the pattern: + const targetElement = holdMusicInfo.holdMusicElements && + holdMusicInfo.holdMusicElements.find(audio => + audio.hasAutoplayAttr && + audio.hasLoopAttr && + audio.autoplay === true && + audio.loop === true + ); + + if (!targetElement) { + throw new Error(`❌ Hold music element with correct autoplay and loop attributes not found. Available elements: ${JSON.stringify(holdMusicInfo.holdMusicElements, null, 2)}`); + } + + // Verify the element matches the expected pattern: + expect(targetElement.autoplay).toBe(true); + expect(targetElement.loop).toBe(true); + expect(targetElement.hasAutoplayAttr).toBe(true); + expect(targetElement.hasLoopAttr).toBe(true); + + } catch (error) { + throw new Error(`❌ Hold music element verification failed: ${error.message}`); + } +} + +/** + * Executes the exact console command: document.querySelector("#remote-audio") + * This verifies that the remote audio element exists in the DOM + * @param page - The agent's main page (browser receiving audio) + * @returns Promise + * @throws Error if remote audio element is not found + */ +export async function executeRemoteAudioQuery(page: Page): Promise { + try { + // Execute the exact console command + const elementExists = await page.evaluate(() => { + const element = document.querySelector("#remote-audio"); + + if (!element) { + return null; + } + + // Return basic element info to verify it exists + return { + tagName: element.tagName, + id: element.id, + className: element.className, + nodeType: element.nodeType, + exists: true + }; + }); + + if (!elementExists) { + throw new Error('❌ document.querySelector("#remote-audio") returned null - element not found in DOM'); + } + + // Verify basic properties + expect(elementExists.exists).toBe(true); + expect(elementExists.tagName).toBe('AUDIO'); + expect(elementExists.id).toBe('remote-audio'); + expect(elementExists.nodeType).toBe(1); // ELEMENT_NODE + + } catch (error) { + throw new Error(`❌ Remote audio element query failed: ${error.message}`); + } +} + +/** + * Ends a task by clicking the end call button and waiting for it to be visible. + * This function can be used for any task type (call, chat, email) as they all use the same end button. + * @param page - The agent's main page + * @returns Promise + */ +export async function endTask(page: Page): Promise { + const endButton = page.getByTestId('call-control:end-call').nth(0); + await endButton.waitFor({ state: 'visible', timeout: 30000 }); + await endButton.click(); +} \ No newline at end of file diff --git a/playwright/basic-task-controls-test.spec.ts b/playwright/basic-task-controls-test.spec.ts new file mode 100644 index 000000000..b0b46504d --- /dev/null +++ b/playwright/basic-task-controls-test.spec.ts @@ -0,0 +1,450 @@ +import { test, expect, Page, BrowserContext } from '@playwright/test'; +import { + enableAllWidgets, + enableMultiLogin, + initialiseWidgets, + loginViaAccessToken, +} from './Utils/initUtils'; +import { stationLogout, telephonyLogin } from './Utils/stationLoginUtils'; +import { changeUserState, getCurrentState, verifyCurrentState } from './Utils/userStateUtils'; +import { + createCallTask, + createChatTask, + createEmailTask, + acceptIncomingTask, + loginExtension, + endChatTask, + acceptExtensionCall, + endCallTask +} from './Utils/incomingTaskUtils'; +import { verifyTaskControls, holdCallToggle, recordCallToggle, setupConsoleLogging, clearCapturedLogs, verifyHoldLogs, verifyRecordingLogs, verifyEndLogs, verifyHoldTimer, verifyRemoteAudioTracks, verifyHoldMusicElement, executeRemoteAudioQuery, endTask } from './Utils/taskControlUtils'; +import { submitWrapup } from './Utils/wrapupUtils'; +import { pageSetup } from './Utils/helperUtils'; +import { USER_STATES, LOGIN_MODE, TASK_TYPES, WRAPUP_REASONS } from './constants'; + +let page: Page; +let context: BrowserContext; +let callerPage: Page; +let chatPage: Page; +let context2: BrowserContext; +const maxRetries = 3; + +test.describe('Basic Task Controls Tests', () => { + test.beforeEach(() => { + clearCapturedLogs(); + }); + + test.beforeAll(async ({ browser }) => { + context = await browser.newContext(); + context2 = await browser.newContext(); + page = await context.newPage(); + chatPage = await context.newPage(); + callerPage = await context2.newPage(); + + await Promise.all([ + (async () => { + for (let i = 0; i < maxRetries; i++) { + try { + await loginExtension(callerPage, process.env.PW_AGENT2_USERNAME ?? '', process.env.PW_PASSWORD ?? ''); + break; + } catch (error) { + if (i == maxRetries - 1) { + throw new Error(`Failed to login extension after ${maxRetries} attempts: ${error}`); + } + } + } + })(), + (async () => { + await pageSetup(page, LOGIN_MODE.DESKTOP, 'AGENT1'); + setupConsoleLogging(page); + })(), + ]); + }); + + test.afterAll(async () => { + if(await getCurrentState(page) === USER_STATES.ENGAGED) { + // If still engaged, end the call to clean up + await endTask(page); + await page.waitForTimeout(3000); + await submitWrapup(page, WRAPUP_REASONS.RESOLVED); + await page.waitForTimeout(2000); + } + await stationLogout(page); + await context.close(); + await context2.close(); + }); + + test('Call task - create call and verify all control buttons are visible', async () => { + // Create call task + await createCallTask(callerPage); + await changeUserState(page, USER_STATES.AVAILABLE); + + // Wait for incoming call notification + const incomingTaskDiv = page.getByTestId('samples:incoming-task-telephony').first(); + await incomingTaskDiv.waitFor({ state: 'visible', timeout: 120000 }); + await page.waitForTimeout(3000); + + // Accept the incoming call + await acceptIncomingTask(page, TASK_TYPES.CALL); + await page.waitForTimeout(5000); + + // Verify agent state changed to engaged + await verifyCurrentState(page, USER_STATES.ENGAGED); + + // Use utility to check all call control buttons are visible + try { + await verifyTaskControls(page, TASK_TYPES.CALL); + } catch (error) { + throw new Error(`Call control buttons verification failed: ${error.message}`); + } + }); + + + test('Call task - verify remote audio tracks from caller to browser', async () => { + // Verify we're still in an engaged call from previous test + await verifyCurrentState(page, USER_STATES.ENGAGED); + + try { + // First execute the basic console query to verify element exists + await executeRemoteAudioQuery(page); + + // Then verify the audio tracks with the exact structure you provided + await verifyRemoteAudioTracks(page); + + } catch (error) { + throw new Error(`Remote audio tracks verification failed: ${error.message}`); + } + }); + + test('Call task - verify hold and resume functionality with callbacks, timer, and hold music', async () => { + // Verify we're still in an engaged call from previous test + await verifyCurrentState(page, USER_STATES.ENGAGED); + + try { + // Clear logs first to ensure clean state + clearCapturedLogs(); + + // Put call on hold from agent side + await holdCallToggle(page); + await page.waitForTimeout(3000); // Allow time for hold to take effect + + // Verify hold callback logs + verifyHoldLogs(true); + + // Verify hold music element is present on the CALLER page (where hold music plays) + // The hold music plays on the caller's side when agent puts call on hold + await verifyHoldMusicElement(callerPage); + + // Verify hold timer is visible and functioning + await verifyHoldTimer(page, true); + + clearCapturedLogs(); // Clear logs for next verification + + // Resume call from hold + await holdCallToggle(page); + await page.waitForTimeout(2000); + + // Verify resume callback logs + verifyHoldLogs(false); + verifyHoldTimer(page, false); + + clearCapturedLogs(); // Clear logs for next verification + + } catch (error) { + throw new Error(`Hold/Resume functionality with callbacks, timer, and hold music verification failed: ${error.message}`); + } + }); + + test('Call task - verify recording pause and resume functionality with callbacks', async () => { + // Verify we're still in an engaged call from previous tests + await verifyCurrentState(page, USER_STATES.ENGAGED); + + try { + // Pause the call recording + await recordCallToggle(page); + await page.waitForTimeout(2000); + + // Verify pause recording callback logs + verifyRecordingLogs(false); + clearCapturedLogs(); // Clear logs for next verification + + // Resume the call recording + await recordCallToggle(page); + await page.waitForTimeout(2000); + + // Verify resume recording callback logs + verifyRecordingLogs(true); + clearCapturedLogs(); // Clear logs for next verification + + } catch (error) { + throw new Error(`Recording pause/resume functionality verification failed: ${error.message}`); + } + }); + + test('Call task - end call and complete wrapup', async () => { + // Verify we're still in an engaged call from previous tests + await verifyCurrentState(page, USER_STATES.ENGAGED); + + try { + // End the call by clicking the end button + await endTask(page); + await page.waitForTimeout(3000); + + // Verify onEnd callback logs + verifyEndLogs(); + clearCapturedLogs(); // Clear logs for next verification + + // Submit wrapup + await submitWrapup(page, WRAPUP_REASONS.RESOLVED); + await page.waitForTimeout(2000); + + } catch (error) { + throw new Error(`Call task end and wrapup failed: ${error.message}`); + } + }); + + test('Chat task - verify transfer and end buttons are visible, end chat, and wrap up', async () => { + // Create chat task + await createChatTask(chatPage); + await changeUserState(page, USER_STATES.AVAILABLE); + + // Wait for incoming chat notification + const incomingTaskDiv = page.getByTestId('samples:incoming-task-chat').first(); + await incomingTaskDiv.waitFor({ state: 'visible', timeout: 120000 }); + await page.waitForTimeout(3000); + + // Accept the incoming chat + await acceptIncomingTask(page, TASK_TYPES.CHAT); + await page.waitForTimeout(5000); + + // Verify agent state changed to engaged + await verifyCurrentState(page, USER_STATES.ENGAGED); + + try { + // Use utility to check chat control buttons are visible + await verifyTaskControls(page, TASK_TYPES.CHAT); + + // End the chat by clicking the end button + await endTask(page); + await page.waitForTimeout(3000); + + // Verify onEnd callback logs + verifyEndLogs(); + clearCapturedLogs(); // Clear logs for next verification + + // Submit wrapup + await submitWrapup(page, WRAPUP_REASONS.RESOLVED); + await page.waitForTimeout(2000); + } catch (error) { + throw new Error(`Chat task control test failed: ${error.message}`); + } + }); + + test('Email task - verify transfer and end buttons are visible, end email, and wrap up', async () => { + // Create email task + await createEmailTask(); + await changeUserState(page, USER_STATES.AVAILABLE); + + // Wait for incoming email notification (emails may take longer) + const incomingTaskDiv = page.getByTestId('samples:incoming-task-email').first(); + await incomingTaskDiv.waitFor({ state: 'visible', timeout: 180000 }); // 3 minutes for email + await page.waitForTimeout(3000); + + // Accept the incoming email + await acceptIncomingTask(page, TASK_TYPES.EMAIL); + await page.waitForTimeout(5000); + + // Verify agent state changed to engaged + await verifyCurrentState(page, USER_STATES.ENGAGED); + + try { + // Use utility to check email control buttons are visible + await verifyTaskControls(page, TASK_TYPES.EMAIL); + + // End the email by clicking the end button + await endTask(page); + await page.waitForTimeout(3000); + + // Verify onEnd callback logs + verifyEndLogs(); + clearCapturedLogs(); // Clear logs for next verification + + // Submit wrapup + await submitWrapup(page, WRAPUP_REASONS.RESOLVED); + await page.waitForTimeout(2000); + } catch (error) { + throw new Error(`Email task control test failed: ${error.message}`); + } + }); +}); + +test.describe('Multi-Login Task Controls Tests', () => { + let session1Page: Page; + let session2Page: Page; + let session1Context: BrowserContext; + let session2Context: BrowserContext; + let callerPageMulti: Page; + let callerContextMulti: BrowserContext; + let extensionPage: Page; + let extensionContext: BrowserContext; + + test.beforeEach(() => { + clearCapturedLogs(); + }); + + test.beforeAll(async ({ browser }) => { + // Create separate browser contexts for multi-session testing + session1Context = await browser.newContext(); + session2Context = await browser.newContext(); + callerContextMulti = await browser.newContext(); + extensionContext = await browser.newContext(); + + session1Page = await session1Context.newPage(); + session2Page = await session2Context.newPage(); + callerPageMulti = await callerContextMulti.newPage(); + extensionPage = await extensionContext.newPage(); + + await Promise.all([ + (async () => { + for (let i = 0; i < maxRetries; i++) { + try { + await loginExtension(callerPageMulti, process.env.PW_AGENT2_USERNAME ?? '', process.env.PW_PASSWORD ?? ''); + break; + } catch (error) { + if (i == maxRetries - 1) { + throw new Error(`Failed to login extension for multi-session test after ${maxRetries} attempts: ${error}`); + } + } + } + })(), + (async () => { + await pageSetup(session1Page, LOGIN_MODE.EXTENSION, 'AGENT1', extensionPage); + setupConsoleLogging(session1Page); + })(), + (async () => { + await pageSetup(session2Page, LOGIN_MODE.EXTENSION, 'AGENT1', extensionPage); + setupConsoleLogging(session2Page); + })(), + (async () => { + + for (let i = 0; i < maxRetries; i++) { + try { + await loginExtension(extensionPage, process.env.PW_AGENT1_USERNAME ?? '', process.env.PW_PASSWORD ?? ''); + break; + } catch (error) { + if (i == maxRetries - 1) { + throw new Error(`Failed to login extension after ${maxRetries} attempts: ${error}`); + } + } + } + })(), + ]); + }); + + test.afterAll(async () => { + // If still engaged, end the call to clean up + if (await getCurrentState(session1Page) === USER_STATES.ENGAGED) { + await endTask(session1Page); + await session1Page.waitForTimeout(5000); + await submitWrapup(session1Page, WRAPUP_REASONS.RESOLVED); + await session1Page.waitForTimeout(2000); + } + await Promise.all([ + stationLogout(session1Page), + stationLogout(session2Page), + ]); + await session1Context.close(); + await session2Context.close(); + await callerContextMulti.close(); + await extensionContext.close(); + }); + + test('Multi-login call controls - verify controls are synchronized', async () => { + // Set both AGENT1 sessions to available state + await Promise.all([ + changeUserState(session1Page, USER_STATES.AVAILABLE), + ]); + await session1Page.waitForTimeout(2000); + + // Caller page creates call to extension page + await createCallTask(callerPageMulti); + + // Wait for incoming call notification on both AGENT1 sessions + const incomingTaskSession1 = session1Page.getByTestId('samples:incoming-task-telephony').first(); + const incomingTaskSession2 = session2Page.getByTestId('samples:incoming-task-telephony').first(); + + await Promise.all([ + incomingTaskSession1.waitFor({ state: 'visible', timeout: 40000 }), + incomingTaskSession2.waitFor({ state: 'visible', timeout: 40000 }), + ]); + + // Wait for extension caller to be visible and accept the call + await extensionPage.locator('[data-test="generic-person-item-base"]').waitFor({ state: 'visible', timeout: 20000 }); + await session1Page.waitForTimeout(3000); + await acceptExtensionCall(extensionPage); + await session1Page.waitForTimeout(2000); + + // Verify both AGENT1 sessions show engaged state + await Promise.all([ + verifyCurrentState(session1Page, USER_STATES.ENGAGED), + verifyCurrentState(session2Page, USER_STATES.ENGAGED), + ]); + + try { + // Verify call control buttons are visible on both AGENT1 sessions + await Promise.all([ + verifyTaskControls(session1Page, TASK_TYPES.CALL), + verifyTaskControls(session2Page, TASK_TYPES.CALL), + ]); + + // Setup console logging for both AGENT1 sessions + setupConsoleLogging(session1Page); + setupConsoleLogging(session2Page); + + // Put call on hold from session 1 (AGENT1) + await holdCallToggle(session1Page); + await session1Page.waitForTimeout(3000); + + // Verify hold timer is visible on both AGENT1 sessions + await Promise.all([ + verifyHoldTimer(session1Page, true), + verifyHoldTimer(session2Page, true), + ]); + + // Resume call from session 2 (AGENT1) + await holdCallToggle(session2Page); + await session2Page.waitForTimeout(3000); + + // Verify hold timer disappears on both AGENT1 sessions + await Promise.all([ + verifyHoldTimer(session1Page, false), + verifyHoldTimer(session2Page, false), + ]); + + // Pause recording from session 1 (AGENT1) + await recordCallToggle(session1Page); + await session1Page.waitForTimeout(2000); + + // Resume recording from session 2 (AGENT1) + await recordCallToggle(session2Page); + await session2Page.waitForTimeout(2000); + + // End call from extension page + await endCallTask(extensionPage); + await session1Page.waitForTimeout(2000); + + // Submit wrapup from session 1 (AGENT1) + await submitWrapup(session1Page, WRAPUP_REASONS.RESOLVED); + await session1Page.waitForTimeout(2000); + + // Verify both AGENT1 sessions return to available state + await Promise.all([ + verifyCurrentState(session1Page, USER_STATES.AVAILABLE), + verifyCurrentState(session2Page, USER_STATES.AVAILABLE), + ]); + + } catch (error) { + throw new Error(`Multi-session call controls synchronization failed: ${error.message}`); + } + }); +}); \ No newline at end of file From 651b817e7939874ab86c9e3b533839fe06d015fd Mon Sep 17 00:00:00 2001 From: Ritesh Singh Date: Tue, 15 Jul 2025 19:51:21 +0530 Subject: [PATCH 12/18] fix(cc-widgets): add tasklist-test.spec.ts --- .../task/CallControlCAD/call-control-cad.tsx | 2 +- .../src/components/task/Task/index.tsx | 7 +- playwright/tasklist-test.spec.ts | 376 ++++++++++++++++++ .../cc/samples-cc-react-app/src/App.tsx | 1 + 4 files changed, 382 insertions(+), 4 deletions(-) create mode 100644 playwright/tasklist-test.spec.ts diff --git a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx index fdacfd384..32675cc92 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx @@ -159,7 +159,7 @@ const CallControlCADComponent: React.FC = (props) =>
{renderCustomerName()}
- + {currentMediaType.labelName} -
diff --git a/packages/contact-center/cc-components/src/components/task/Task/index.tsx b/packages/contact-center/cc-components/src/components/task/Task/index.tsx index 7d9ed1014..d77df64ed 100644 --- a/packages/contact-center/cc-components/src/components/task/Task/index.tsx +++ b/packages/contact-center/cc-components/src/components/task/Task/index.tsx @@ -73,6 +73,7 @@ const Task: React.FC = ({ type={selected ? 'body-large-bold' : 'body-large-medium'} className={getTitleClassName()} id={isNonVoiceMedia ? tooltipTriggerId : undefined} + data-testid="task:title" > {title} @@ -121,7 +122,7 @@ const Task: React.FC = ({
{renderTitle()} {state && !isIncomingTask && ( - + {capitalizeFirstWord(state)} )} @@ -135,9 +136,9 @@ const Task: React.FC = ({ {/* Handle Time should render if it's an incoming call without ronaTimeout OR if it's not an incoming call */} {(isIncomingTask && !ronaTimeout) || !isIncomingTask ? startTimeStamp && ( - + Handle Time: {' '} - + ) : null} diff --git a/playwright/tasklist-test.spec.ts b/playwright/tasklist-test.spec.ts new file mode 100644 index 000000000..8742ef73e --- /dev/null +++ b/playwright/tasklist-test.spec.ts @@ -0,0 +1,376 @@ +import { test, Page, expect, BrowserContext } from '@playwright/test'; +import { changeUserState, getCurrentState } from './Utils/userStateUtils'; +import { createCallTask, createChatTask, loginExtension, acceptExtensionCall, createEmailTask, endExtensionCall, submitRonaPopup, acceptIncomingTask } from './Utils/incomingTaskUtils'; +import { TASK_TYPES, USER_STATES, LOGIN_MODE, THEME_COLORS, WRAPUP_REASONS, RONA_OPTIONS } from './constants'; +import { verifyTaskControls } from './Utils/taskControlUtils'; +import { submitWrapup } from './Utils/wrapupUtils'; +import { pageSetup, handleStrayTasks } from './Utils/helperUtils'; + + +let page: Page | null = null; +let context: BrowserContext | null = null; +let callerpage: Page | null = null; +let context2: BrowserContext | null = null; +let chatPage: Page | null = null; +let capturedLogs: string[] = []; + +const labelToMediaType: Record = { + Call: 'telephony', + Email: 'email', + Chat: 'chat', +}; + + +/** + * Reads the handle time of ith task in the task list. + * @param page Playwright Page object + * @param index Index of the task in the task list (default is 0 for the first task) + * @returns returns the handle time in seconds + */ + +async function getCurrentHandleTime(page: Page, index: number = 0): Promise { + const full = await page.getByTestId('task:handle-time').nth(index).textContent(); + + // 2. Pull out the MM:SS via regex + const match = full?.match(/(\d{2}:\d{2})/); + const timer = match ? match[1] : null; + + // 3. (Optional) convert to total seconds + const [m, s] = timer!.split(':').map(Number); + const totalSeconds = m * 60 + s; + return totalSeconds; +} + +/** + * wait for and accept a specific task in the task list. + * @param page : Playwright Page object + * @param testId : testId of the task to accept + * @param extensionMode : extension mode flag + * @param extensionPage : extension page if in extension mode + */ + +async function waitForAndAcceptSpecificTask(page: Page, testId: string): Promise { + const timeoutMs = 60000, pollInterval = 2000 + const start = Date.now(); + const type = testId.split('-').pop(); + while (Date.now() - start < timeoutMs) { + const taskDiv = page.getByTestId(testId).first(); + const isVisible = await taskDiv.isVisible().catch(() => false); + if (isVisible) { + const acceptButton = taskDiv.getByTestId('task:accept-button').first(); + await expect(acceptButton).toBeVisible({ timeout: 5000 }); + await acceptButton.click({timeout:3000}); + + return; + } + await page.waitForTimeout(pollInterval); + } + throw new Error(`No incoming task found for ${testId} after ${timeoutMs / 1000} seconds`); +} + +export const waitForState = async ( + page: Page, + expectedState: string, +): Promise => { + const start = Date.now(); + const timeoutMs: number = 10000 + while (true) { + const currentState = await getCurrentState(page); + if (currentState === expectedState) return; + if (Date.now() - start > timeoutMs) { + throw new Error(`Timed out waiting for state "${expectedState}", last state was "${currentState}"`); + } + await page.waitForTimeout(500); + } +}; + + + +async function getTaskType(): Promise { + const callTimer = page.getByTestId('cc-cad:call-timer').first(); + const fullText = await callTimer.textContent(); + const mediaLabel = fullText!.split(' - ')[0].trim(); + return mediaLabel; +} + + +function setupConsoleLogging(page: Page): () => void { + capturedLogs.length = 0; + + const consoleHandler = (msg) => { + const logText = msg.text(); + if ( + logText.startsWith('onTaskSelected invoked for task with title :') && + logText.includes(', and mediaType :') + ) { + capturedLogs.push(logText); + } + }; + + page.on('console', consoleHandler); + return () => page.off('console', consoleHandler); +} + + + +function escapeForRegExp(str?: string): string { + if (!str) { + return ''; + } + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +async function waitForConsoleLogs( + logs: string[], + title: string, + mediaType: string, + timeoutMs = 15000, + intervalMs = 500 +): Promise { + const escTitle = escapeForRegExp(title!); + const escMedia = escapeForRegExp(mediaType!); + const pattern = new RegExp( + '^onTaskSelected invoked for task with title : ' + + escTitle + + ', and mediaType : ' + + escMedia + + '$' + ); + + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + // check if any captured log matches the pattern + if (logs.some(log => pattern.test(log))) return; + await new Promise(r => setTimeout(r, intervalMs)); + } + + throw new Error( + `Timed out waiting for console log matching "${pattern.source}"` + ); +} + + + +test.describe('Task List Tests for different types of Task', () => { + test.beforeEach(() => { + capturedLogs.length = 0; + }) + + test.beforeAll(async ({ browser }) => { + context = await browser.newContext(); + context2 = await browser.newContext(); + page = await context.newPage(); + callerpage = await context2.newPage(); + chatPage = await context.newPage(); + setupConsoleLogging(page); + + await Promise.all([ + (async () => { + await loginExtension(callerpage, process.env.PW_AGENT2_USERNAME, process.env.PW_PASSWORD); + })(), + (async () => { + await pageSetup(page, LOGIN_MODE.DESKTOP, 'AGENT1'); + })(), + ]) + }) + + + test('Verify Task List for incoming Call', async () => { + await createCallTask(callerpage); + await changeUserState(page, USER_STATES.AVAILABLE); + let incomingTaskDiv = page.getByTestId('samples:incoming-task-telephony').first(); + await incomingTaskDiv.waitFor({ state: 'visible', timeout: 40000 }); + await page.waitForTimeout(1000); + const taskListItem = page.getByTestId('task-list').getByRole('listitem').first(); + expect(taskListItem).toBeVisible(); + const taskListAcceptButton = taskListItem.getByTestId('task:accept-button').first(); + const taskListDeclineButton = taskListItem.getByTestId('task:decline-button').first(); + const title = await incomingTaskDiv.getByTestId('task:title').first().textContent(); + expect(await taskListItem.getByTestId('task:title').textContent()).toBe(title); + await expect(incomingTaskDiv.getByTestId('task:accept-button')).toBeVisible(); + await expect(incomingTaskDiv.getByTestId('task:decline-button')).toBeVisible(); + await expect(taskListAcceptButton).toBeVisible(); + await expect(taskListDeclineButton).toBeVisible(); + await taskListAcceptButton.click(); + await page.waitForTimeout(1000); + await expect(taskListAcceptButton).not.toBeVisible(); + await expect(taskListDeclineButton).not.toBeVisible(); + await page.waitForTimeout(5000); + try { + await verifyTaskControls(page, TASK_TYPES.CALL); + } catch (error) { + throw new Error(`Call control buttons verification failed: ${error.message}`); + } + await waitForState(page, USER_STATES.ENGAGED); + await taskListItem.click(); + await waitForConsoleLogs(capturedLogs, title, 'telephony'); + expect(await taskListItem.getByTestId('task:title').textContent()).toBe(title); + await expect(taskListItem.locator('[icon-name="handset-filled"]')).toBeVisible(); + await expect(taskListItem.getByTestId('task:item-state')).toHaveText('Connected'); + await expect(taskListItem.getByTestId('task:handle-time')).toBeVisible(); + await page.getByTestId('call-control:end-call').first().waitFor({ state: 'visible', timeout: 5000 }); + await page.getByTestId('call-control:end-call').first().click(); + await page.waitForTimeout(500); + await submitWrapup(page, WRAPUP_REASONS.SALE); + await waitForState(page, USER_STATES.AVAILABLE); + }) + + test('Verify Task List for incoming Chat Task', async () => { + await createChatTask(chatPage); + await changeUserState(page, USER_STATES.AVAILABLE); + const incomingTaskDiv = page.getByTestId('samples:incoming-task-chat').first(); + await incomingTaskDiv.waitFor({ state: 'visible', timeout: 60000 }); + await page.waitForTimeout(1000); + const taskListItem = page.getByTestId('task-list').getByRole('listitem').first(); + expect(taskListItem).toBeVisible(); + const taskListAcceptButton = taskListItem.getByTestId('task:accept-button').first(); + const taskListDeclineButton = taskListItem.getByTestId('task:decline-button').first(); + const title = await incomingTaskDiv.getByTestId('task:title').textContent(); + expect(await taskListItem.getByTestId('task:title').textContent()).toBe(title); + await expect(incomingTaskDiv.getByTestId('task:accept-button')).toBeVisible(); + await expect(incomingTaskDiv.getByTestId('task:decline-button')).not.toBeVisible(); + await expect(taskListAcceptButton).toBeVisible(); + await expect(taskListDeclineButton).not.toBeVisible(); + await taskListAcceptButton.click(); + await page.waitForTimeout(1000); + await waitForState(page, USER_STATES.ENGAGED); + const prevtimer = await getCurrentHandleTime(page); + await page.waitForTimeout(5000); + const currentTimer = await getCurrentHandleTime(page); + expect(currentTimer).toBeGreaterThan(prevtimer); + expect(Math.abs(currentTimer - prevtimer + 1)).toBeGreaterThanOrEqual(5); + try { + await verifyTaskControls(page, TASK_TYPES.CHAT); + } catch (error) { + throw new Error(`Call control buttons verification failed: ${error.message}`); + } + await waitForConsoleLogs(capturedLogs, title, 'chat'); + await expect(taskListAcceptButton).not.toBeVisible(); + await expect(taskListDeclineButton).not.toBeVisible(); + expect(await taskListItem.getByTestId('task:title').textContent()).toBe(title); + await expect(taskListItem.locator('[icon-name="chat-filled"]')).toBeVisible(); + await expect(taskListItem.getByTestId('task:item-state')).toHaveText('Connected'); + await expect(taskListItem.getByTestId('task:handle-time')).toBeVisible(); + await page.getByTestId('call-control:end-call').first().waitFor({ state: 'visible', timeout: 5000 }); + await page.getByTestId('call-control:end-call').first().click(); + await page.waitForTimeout(2000); + await submitWrapup(page, WRAPUP_REASONS.SALE); + await waitForState(page, USER_STATES.AVAILABLE); + }) + + test('Verify Task List for incoming Email Task', async () => { + await createEmailTask(); + await changeUserState(page, USER_STATES.AVAILABLE); + const incomingTaskDiv = page.getByTestId('samples:incoming-task-email').first(); + await incomingTaskDiv.waitFor({ state: 'visible', timeout: 60000 }); + await page.waitForTimeout(1000); + const taskListItem = page.getByTestId('task-list').getByRole('listitem').first(); + expect(taskListItem).toBeVisible(); + const taskListAcceptButton = taskListItem.getByTestId('task:accept-button').first(); + const taskListDeclineButton = taskListItem.getByTestId('task:decline-button').first(); + const title = await incomingTaskDiv.getByTestId('task:title').textContent(); + expect(await taskListItem.getByTestId('task:title').textContent()).toBe(title); + await expect(incomingTaskDiv.getByTestId('task:accept-button')).toBeVisible(); + await expect(incomingTaskDiv.getByTestId('task:decline-button')).not.toBeVisible(); + await expect(taskListAcceptButton).toBeVisible(); + await expect(taskListDeclineButton).not.toBeVisible(); + await taskListAcceptButton.click(); + await page.waitForTimeout(1000); + const prevtimer = await getCurrentHandleTime(page); + await page.waitForTimeout(5000); + + const currentTimer = await getCurrentHandleTime(page); + expect(currentTimer).toBeGreaterThan(prevtimer); + expect(Math.abs(currentTimer - prevtimer + 1)).toBeGreaterThanOrEqual(5); + + try { + await verifyTaskControls(page, TASK_TYPES.EMAIL); + } catch (error) { + throw new Error(`Call control buttons verification failed: ${error.message}`); + } + + await expect(taskListAcceptButton).not.toBeVisible(); + await expect(taskListDeclineButton).not.toBeVisible(); + await waitForState(page, USER_STATES.ENGAGED); + await waitForConsoleLogs(capturedLogs, title, 'email'); + expect(await taskListItem.getByTestId('task:title').textContent()).toBe(title); + await expect(taskListItem.locator('[icon-name="email-filled"]')).toBeVisible(); + await expect(taskListItem.getByTestId('task:item-state')).toHaveText('Connected'); + await expect(taskListItem.getByTestId('task:handle-time')).toBeVisible(); + await page.getByTestId('call-control:end-call').first().waitFor({ state: 'visible', timeout: 5000 }); + await page.getByTestId('call-control:end-call').first().click(); + await page.waitForTimeout(2000); + await submitWrapup(page, WRAPUP_REASONS.SALE); + await waitForState(page, USER_STATES.AVAILABLE); + }); + + test('Task List Test with Multiple Taks', async () => { + await changeUserState(page, USER_STATES.MEETING); + await waitForState(page, USER_STATES.MEETING); + await Promise.all([ + createCallTask(callerpage), + createChatTask(chatPage), + createEmailTask() + ]) + + await changeUserState(page, USER_STATES.AVAILABLE); + + await Promise.all([ + waitForAndAcceptSpecificTask(page, 'samples:incoming-task-telephony'), + waitForAndAcceptSpecificTask(page, 'samples:incoming-task-chat'), + waitForAndAcceptSpecificTask(page, 'samples:incoming-task-email'), + ]); + await page.waitForTimeout(3000); + + for (let i = 0; i < 3; i++) { + const taskListItem = page + .getByTestId('task-list') + .getByRole('listitem') + .nth(i); + + await taskListItem.waitFor({ state: 'visible', timeout: 5000 }); + expect(taskListItem).toBeVisible(); + await taskListItem.click(); + const prevtimer = await getCurrentHandleTime(page, i); + await page.waitForTimeout(5000); + const currentTimer = await getCurrentHandleTime(page, i); + expect(currentTimer).toBeGreaterThan(prevtimer); + expect(Math.abs(currentTimer - prevtimer + 1)).toBeGreaterThanOrEqual(5); + const inferredType = await getTaskType(); + try { + await verifyTaskControls(page, inferredType); + } catch (error) { + throw new Error(`Call control buttons verification failed: ${error.message}`); + } + + await waitForConsoleLogs(capturedLogs, await taskListItem.getByTestId('task:title').textContent()!, labelToMediaType[inferredType]); + capturedLogs.length = 0; + } + + await handleStrayTasks(page); + }) + + test.afterAll(async () => { + if (context) { + await context.close(); + context = null; + } + if (context2) { + await context2.close(); + context2 = null; + } + if (page) { + await page.close(); + page = null; + } + if (callerpage) { + await callerpage.close(); + callerpage = null; + } + if (chatPage) { + await chatPage.close(); + chatPage = null; + } + + }) +}) diff --git a/widgets-samples/cc/samples-cc-react-app/src/App.tsx b/widgets-samples/cc/samples-cc-react-app/src/App.tsx index 5d0ea298d..d493d8e38 100644 --- a/widgets-samples/cc/samples-cc-react-app/src/App.tsx +++ b/widgets-samples/cc/samples-cc-react-app/src/App.tsx @@ -165,6 +165,7 @@ const onTaskDeclined = (task,reason) => { const onTaskSelected = ({task, isClicked}) => { console.log('onTaskSelected invoked for task:', task, 'isClicked:', isClicked); + console.log(`onTaskSelected invoked for task with title : ${task?.data?.interaction?.callAssociatedDetails?.ani}, and mediaType : ${task?.data?.mediaType}`); }; const onHoldResume = ({isHeld, task}) => { From 58faa94eaf4d11e6c668314ad9516193e85dc756 Mon Sep 17 00:00:00 2001 From: Ritesh Singh Date: Tue, 15 Jul 2025 22:27:36 +0530 Subject: [PATCH 13/18] fix(cc-widgets): refactor --- playwright/tasklist-test.spec.ts | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/playwright/tasklist-test.spec.ts b/playwright/tasklist-test.spec.ts index 8742ef73e..6f4f4af24 100644 --- a/playwright/tasklist-test.spec.ts +++ b/playwright/tasklist-test.spec.ts @@ -4,7 +4,7 @@ import { createCallTask, createChatTask, loginExtension, acceptExtensionCall, cr import { TASK_TYPES, USER_STATES, LOGIN_MODE, THEME_COLORS, WRAPUP_REASONS, RONA_OPTIONS } from './constants'; import { verifyTaskControls } from './Utils/taskControlUtils'; import { submitWrapup } from './Utils/wrapupUtils'; -import { pageSetup, handleStrayTasks } from './Utils/helperUtils'; +import { pageSetup, handleStrayTasks, waitForState } from './Utils/helperUtils'; let page: Page | null = null; @@ -59,8 +59,8 @@ async function waitForAndAcceptSpecificTask(page: Page, testId: string): Promise if (isVisible) { const acceptButton = taskDiv.getByTestId('task:accept-button').first(); await expect(acceptButton).toBeVisible({ timeout: 5000 }); - await acceptButton.click({timeout:3000}); - + await acceptButton.click({ timeout: 3000 }); + return; } await page.waitForTimeout(pollInterval); @@ -68,23 +68,6 @@ async function waitForAndAcceptSpecificTask(page: Page, testId: string): Promise throw new Error(`No incoming task found for ${testId} after ${timeoutMs / 1000} seconds`); } -export const waitForState = async ( - page: Page, - expectedState: string, -): Promise => { - const start = Date.now(); - const timeoutMs: number = 10000 - while (true) { - const currentState = await getCurrentState(page); - if (currentState === expectedState) return; - if (Date.now() - start > timeoutMs) { - throw new Error(`Timed out waiting for state "${expectedState}", last state was "${currentState}"`); - } - await page.waitForTimeout(500); - } -}; - - async function getTaskType(): Promise { const callTimer = page.getByTestId('cc-cad:call-timer').first(); @@ -139,7 +122,6 @@ async function waitForConsoleLogs( const start = Date.now(); while (Date.now() - start < timeoutMs) { - // check if any captured log matches the pattern if (logs.some(log => pattern.test(log))) return; await new Promise(r => setTimeout(r, intervalMs)); } From ff53ab022aa5976802eed369938d563cb2edc1a5 Mon Sep 17 00:00:00 2001 From: PrayagGP Date: Wed, 16 Jul 2025 01:43:21 +0530 Subject: [PATCH 14/18] fix(cc-widgets): add utils and tests to verify hold and recording icons --- playwright/Utils/taskControlUtils.ts | 364 +++++--------------- playwright/basic-task-controls-test.spec.ts | 105 ++++-- 2 files changed, 162 insertions(+), 307 deletions(-) diff --git a/playwright/Utils/taskControlUtils.ts b/playwright/Utils/taskControlUtils.ts index c963b2e69..cd7608a4a 100644 --- a/playwright/Utils/taskControlUtils.ts +++ b/playwright/Utils/taskControlUtils.ts @@ -123,11 +123,12 @@ export async function recordCallToggle(page: Page): Promise { /** * Verifies the hold timer visibility and content based on expected state. * @param page - The agent's main page - * @param shouldBeVisible - Whether the timer should be visible (true) or hidden (false) - * @param verifyContent - Whether to verify timer content (default: true when visible) + * @param options - Configuration object + * @param options.shouldBeVisible - Whether the timer should be visible (true) or hidden (false) + * @param options.verifyContent - Whether to verify timer content (default: true when visible) * @returns Promise */ -export async function verifyHoldTimer(page: Page, shouldBeVisible: boolean, verifyContent: boolean = shouldBeVisible): Promise { +export async function verifyHoldTimer(page: Page, { shouldBeVisible, verifyContent = shouldBeVisible }: { shouldBeVisible: boolean; verifyContent?: boolean }): Promise { const holdTimerContainer = page.locator('.on-hold-chip-text'); if (shouldBeVisible) { @@ -145,6 +146,60 @@ export async function verifyHoldTimer(page: Page, shouldBeVisible: boolean, veri } } +/** + * Verifies the icon of the hold toggle button based on current hold state. + * - When call is NOT on hold: expects 'pause-bold' icon (to put call on hold) + * - When call IS on hold: expects 'play-bold' icon (to resume call) + * @param page - The agent's main page + * @param options - Configuration object + * @param options.expectedIsHeld - Expected hold state (true if call is on hold, false if active) + * @returns Promise + * @throws Error if icon verification fails + */ +export async function verifyHoldButtonIcon(page: Page, { expectedIsHeld }: { expectedIsHeld: boolean }): Promise { + const holdButton = page.getByTestId('call-control:hold-toggle').nth(0); + await expect(holdButton).toBeVisible({ timeout: 10000 }); + + // Get the icon element within the hold button + const iconElement = holdButton.locator('mdc-icon').nth(0); + await expect(iconElement).toBeVisible({ timeout: 5000 }); + + // Verify the correct icon based on hold state + const expectedIcon = expectedIsHeld ? 'play-bold' : 'pause-bold'; + const actualIcon = await iconElement.getAttribute('name'); + + if (actualIcon !== expectedIcon) { + throw new Error(`Hold button icon mismatch. Expected: '${expectedIcon}' (isHeld: ${expectedIsHeld}), but found: '${actualIcon}'`); + } +} + +/** + * Verifies the icon of the record toggle button based on current recording state. + * - When recording is ACTIVE: expects 'record-paused-bold' icon (to pause recording) + * - When recording is PAUSED: expects 'record-bold' icon (to resume recording) + * @param page - The agent's main page + * @param options - Configuration object + * @param options.expectedIsRecording - Expected recording state (true if recording, false if paused) + * @returns Promise + * @throws Error if icon verification fails + */ +export async function verifyRecordButtonIcon(page: Page, { expectedIsRecording }: { expectedIsRecording: boolean }): Promise { + const recordButton = page.getByTestId('call-control:recording-toggle').nth(0); + await expect(recordButton).toBeVisible({ timeout: 10000 }); + + // Get the icon element within the record button + const iconElement = recordButton.locator('mdc-icon').nth(0); + await expect(iconElement).toBeVisible({ timeout: 5000 }); + + // Verify the correct icon based on recording state + const expectedIcon = expectedIsRecording ? 'record-paused-bold' : 'record-bold'; + const actualIcon = await iconElement.getAttribute('name'); + + if (actualIcon !== expectedIcon) { + throw new Error(`Record button icon mismatch. Expected: '${expectedIcon}' (isRecording: ${expectedIsRecording}), but found: '${actualIcon}'`); + } +} + // Global variable to store captured logs let capturedLogs: string[] = []; @@ -184,10 +239,11 @@ export function clearCapturedLogs(): void { /** * Verifies that hold/resume callback logs are present and contain expected values. - * @param expectedIsHeld - Expected hold state (true for hold, false for resume) + * @param options - Configuration object + * @param options.expectedIsHeld - Expected hold state (true for hold, false for resume) * @throws Error if verification fails with detailed error message */ -export function verifyHoldLogs(expectedIsHeld: boolean): void { +export function verifyHoldLogs({ expectedIsHeld }: { expectedIsHeld: boolean }): void { const holdResumeLogs = capturedLogs.filter(log => log.includes('onHoldResume invoked')); const statusLogs = capturedLogs.filter(log => log.includes(expectedIsHeld ? 'WXCC_SDK_TASK_HOLD_SUCCESS' : 'WXCC_SDK_TASK_RESUME_SUCCESS') @@ -210,10 +266,11 @@ export function verifyHoldLogs(expectedIsHeld: boolean): void { /** * Verifies that recording pause/resume callback logs are present and contain expected values. - * @param expectedIsRecording - Expected recording state (true for recording, false for paused) + * @param options - Configuration object + * @param options.expectedIsRecording - Expected recording state (true for recording, false for paused) * @throws Error if verification fails with detailed error message */ -export function verifyRecordingLogs(expectedIsRecording: boolean): void { +export function verifyRecordingLogs({ expectedIsRecording }: { expectedIsRecording: boolean }): void { const recordingLogs = capturedLogs.filter(log => log.includes('onRecordingToggle invoked')); const statusLogs = capturedLogs.filter(log => log.includes(expectedIsRecording ? 'WXCC_SDK_TASK_RESUME_RECORDING_SUCCESS' : 'WXCC_SDK_TASK_PAUSE_RECORDING_SUCCESS') @@ -246,156 +303,16 @@ export function verifyEndLogs(): void { } } -/** - * Verifies that remote audio from another tab/participant is properly configured and ready to play. - * This function checks for the presence of the remote audio element, its properties, and MediaStream connection. - * @param page - The agent's main page - * @returns Promise - * @throws Error if remote audio verification fails - */ -export async function verifyRemoteAudio(page: Page): Promise { - try { - // Verify audio element properties using JavaScript evaluation - // Handle multiple audio elements with same ID by checking all of them - const audioProperties = await page.evaluate(() => { - const audioElements = document.querySelectorAll('#remote-audio') as NodeListOf; - - if (audioElements.length === 0) { - throw new Error('No remote audio elements found'); - } - - const results: any[] = []; - let hasActiveStream = false; - - audioElements.forEach((audio, index) => { - const hasSourceObj = audio.srcObject !== null; - - let mediaStreamActive = false; - let tracksCount = 0; - let audioTracksCount = 0; - - if (hasSourceObj) { - const mediaStream = audio.srcObject as MediaStream; - mediaStreamActive = mediaStream.active; - tracksCount = mediaStream.getTracks().length; - audioTracksCount = mediaStream.getAudioTracks().length; - } - - const properties = { - elementIndex: index, - autoplay: audio.autoplay, - hasSourceObject: hasSourceObj, - readyState: audio.readyState, - paused: audio.paused, - muted: audio.muted, - volume: audio.volume, - mediaStreamActive: mediaStreamActive, - mediaStreamTracks: tracksCount, - audioTrackCount: audioTracksCount - }; - - results.push(properties); - - // Check if this element has an active stream - if (properties.hasSourceObject && properties.mediaStreamActive && properties.audioTrackCount > 0) { - hasActiveStream = true; - } - }); - - return { - totalElements: audioElements.length, - elements: results, - hasActiveAudioStream: hasActiveStream - }; - }); - - // Verify at least one audio element exists - expect(audioProperties.totalElements).toBeGreaterThan(0); - - // Verify at least one audio element has an active MediaStream with audio tracks - expect(audioProperties.hasActiveAudioStream).toBe(true); - - // Verify properties of elements that have audio streams - const activeElements = audioProperties.elements.filter(el => - el.hasSourceObject && el.mediaStreamActive && el.audioTrackCount > 0 - ); - - expect(activeElements.length).toBeGreaterThan(0); - - // For each active audio element, verify it's properly configured - activeElements.forEach((element, index) => { - // Verify autoplay is enabled for remote audio - expect(element.autoplay).toBe(true); - - // Verify audio is not muted (should be able to hear remote audio) - expect(element.muted).toBe(false); - - // Verify volume is at audible level - expect(element.volume).toBeGreaterThan(0); - }); - - } catch (error) { - throw new Error(`Remote audio verification failed: ${error.message}`); - } -} - -/** - * Verifies the #remote-audio element exists in the DOM. - * Executes: document.querySelector("#remote-audio") - * @param page - The agent's main page (browser receiving audio) - * @returns Promise - * @throws Error if remote audio element verification fails - */ -export async function verifyRemoteAudioElement(page: Page): Promise { - try { - // Execute the console command to check for remote audio element - const elementResult = await page.evaluate(() => { - const audioElem = document.querySelector("#remote-audio") as HTMLAudioElement; - - if (!audioElem) { - return null; - } - - return { - tagName: audioElem.tagName, - id: audioElem.id, - autoplay: audioElem.autoplay, - muted: audioElem.muted, - volume: audioElem.volume, - readyState: audioElem.readyState, - paused: audioElem.paused, - hasSourceObject: audioElem.srcObject !== null, - outerHTML: audioElem.outerHTML.substring(0, 200) // First 200 chars for debugging - }; - }); - - if (!elementResult) { - throw new Error('❌ #remote-audio element not found in DOM'); - } - - // Verify the element properties - expect(elementResult.tagName).toBe('AUDIO'); - expect(elementResult.id).toBe('remote-audio'); - expect(elementResult.hasSourceObject).toBe(true); - - } catch (error) { - throw new Error(`❌ Remote audio element verification failed: ${error.message}`); - } -} - /** * Verifies audio transfer from caller to browser by executing the exact console command. * Executes: document.querySelector("#remote-audio").srcObject.getAudioTracks() - * Verifies the result contains MediaStreamTrack with GUID label and proper properties + * Verifies that exactly 1 audio MediaStreamTrack is present with GUID label and proper properties * @param page - The agent's main page (browser receiving audio) * @returns Promise * @throws Error if remote audio tracks verification fails */ export async function verifyRemoteAudioTracks(page: Page): Promise { - try { - // First verify the element exists - await verifyRemoteAudioElement(page); - + try { // Execute the exact console command for audio tracks const consoleResult = await page.evaluate(() => { // This is the exact command from your console @@ -431,35 +348,22 @@ export async function verifyRemoteAudioTracks(page: Page): Promise { return result; }); - // Verify we got an array with at least one MediaStreamTrack - expect(consoleResult.length).toBeGreaterThanOrEqual(1); + // Verify we got exactly 1 audio track (no more, no less) + expect(consoleResult.length).toBe(1); - // Find the first audio track (should match the structure you provided) - const audioTrack = consoleResult.find(track => track.kind === 'audio'); + // Get the single audio track (since we verified there's exactly 1) + const audioTrack = consoleResult[0]; - if (!audioTrack) { - const availableTracks = consoleResult.map(t => `{ kind: "${t.kind}", label: "${t.label}", id: "${t.id}" }`).join(', '); - throw new Error(`❌ No audio MediaStreamTrack found. Available tracks: [${availableTracks}]`); + // Verify it's an audio track + if (audioTrack.kind !== 'audio') { + throw new Error(`❌ Expected audio track but found ${audioTrack.kind} track. Track details: { kind: "${audioTrack.kind}", label: "${audioTrack.label}", id: "${audioTrack.id}" }`); } - // Verify the track properties match the exact structure you provided + // Verify essential track properties for audio transfer expect(audioTrack.kind).toBe('audio'); expect(audioTrack.enabled).toBe(true); expect(audioTrack.muted).toBe(false); expect(audioTrack.readyState).toBe('live'); - expect(audioTrack.onended).toBeNull(); - expect(audioTrack.onmute).toBeNull(); - expect(audioTrack.onunmute).toBeNull(); - - // Verify both id and label are GUID format and match each other - const guidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; - - expect(guidPattern.test(audioTrack.id)).toBe(true); - expect(guidPattern.test(audioTrack.label)).toBe(true); - expect(audioTrack.id).toBe(audioTrack.label); // id should match label exactly - - // Verify index is 0 (first track) - expect(audioTrack.index).toBe(0); } catch (error) { throw new Error(`❌ Audio transfer verification failed: ${error.message}`); @@ -476,133 +380,33 @@ export async function verifyRemoteAudioTracks(page: Page): Promise { */ export async function verifyHoldMusicElement(page: Page): Promise { try { - const holdMusicInfo = await page.evaluate(() => { + const holdMusicExists = await page.evaluate(() => { // Look for audio elements with both autoplay and loop attributes const audioElements = document.querySelectorAll('audio[autoplay][loop]'); if (audioElements.length === 0) { - // Debug: Show all audio elements if none found with autoplay and loop - const allAudioElements = document.querySelectorAll('audio'); - - const allSources = Array.from(allAudioElements).map((audio, index) => { - const a = audio as HTMLAudioElement; - - return { - src: a.src, - autoplay: a.autoplay, - loop: a.loop, - hasAutoplayAttr: a.hasAttribute('autoplay'), - hasLoopAttr: a.hasAttribute('loop'), - outerHTML: a.outerHTML.substring(0, 200) // Show first 200 chars - }; - }); - - return { - holdMusicFound: false, - allAudioElements: allSources, - totalAudioElements: allAudioElements.length - }; + return false; } - // Map the audio elements with autoplay and loop attributes - const holdMusicElements = Array.from(audioElements).map((audio, index) => { + // Check if at least one element has the correct attributes + return Array.from(audioElements).some(audio => { const a = audio as HTMLAudioElement; - - return { - index, - src: a.src, - autoplay: a.autoplay, - loop: a.loop, - hasAutoplayAttr: a.hasAttribute('autoplay'), - hasLoopAttr: a.hasAttribute('loop'), - paused: a.paused, - volume: a.volume, - muted: a.muted, - readyState: a.readyState, - outerHTML: a.outerHTML // Full element structure - }; + return a.hasAttribute('autoplay') && + a.hasAttribute('loop') && + a.autoplay === true && + a.loop === true; }); - - return { - holdMusicFound: true, - holdMusicElements, - totalHoldMusicElements: holdMusicElements.length - }; }); - if (!holdMusicInfo.holdMusicFound) { - throw new Error(`❌ No hold music audio elements found with autoplay and loop attributes. Total audio elements: ${holdMusicInfo.totalAudioElements}. All audio sources: ${JSON.stringify(holdMusicInfo.allAudioElements, null, 2)}`); - } - - // Verify at least one hold music element exists - expect(holdMusicInfo.totalHoldMusicElements).toBeGreaterThan(0); - - // Find the element that matches the pattern: - const targetElement = holdMusicInfo.holdMusicElements && - holdMusicInfo.holdMusicElements.find(audio => - audio.hasAutoplayAttr && - audio.hasLoopAttr && - audio.autoplay === true && - audio.loop === true - ); - - if (!targetElement) { - throw new Error(`❌ Hold music element with correct autoplay and loop attributes not found. Available elements: ${JSON.stringify(holdMusicInfo.holdMusicElements, null, 2)}`); + if (!holdMusicExists) { + throw new Error('❌ No hold music audio elements found with autoplay and loop attributes'); } - // Verify the element matches the expected pattern: - expect(targetElement.autoplay).toBe(true); - expect(targetElement.loop).toBe(true); - expect(targetElement.hasAutoplayAttr).toBe(true); - expect(targetElement.hasLoopAttr).toBe(true); - } catch (error) { throw new Error(`❌ Hold music element verification failed: ${error.message}`); } } -/** - * Executes the exact console command: document.querySelector("#remote-audio") - * This verifies that the remote audio element exists in the DOM - * @param page - The agent's main page (browser receiving audio) - * @returns Promise - * @throws Error if remote audio element is not found - */ -export async function executeRemoteAudioQuery(page: Page): Promise { - try { - // Execute the exact console command - const elementExists = await page.evaluate(() => { - const element = document.querySelector("#remote-audio"); - - if (!element) { - return null; - } - - // Return basic element info to verify it exists - return { - tagName: element.tagName, - id: element.id, - className: element.className, - nodeType: element.nodeType, - exists: true - }; - }); - - if (!elementExists) { - throw new Error('❌ document.querySelector("#remote-audio") returned null - element not found in DOM'); - } - - // Verify basic properties - expect(elementExists.exists).toBe(true); - expect(elementExists.tagName).toBe('AUDIO'); - expect(elementExists.id).toBe('remote-audio'); - expect(elementExists.nodeType).toBe(1); // ELEMENT_NODE - - } catch (error) { - throw new Error(`❌ Remote audio element query failed: ${error.message}`); - } -} - /** * Ends a task by clicking the end call button and waiting for it to be visible. * This function can be used for any task type (call, chat, email) as they all use the same end button. diff --git a/playwright/basic-task-controls-test.spec.ts b/playwright/basic-task-controls-test.spec.ts index b0b46504d..bc7c063f0 100644 --- a/playwright/basic-task-controls-test.spec.ts +++ b/playwright/basic-task-controls-test.spec.ts @@ -17,11 +17,14 @@ import { acceptExtensionCall, endCallTask } from './Utils/incomingTaskUtils'; -import { verifyTaskControls, holdCallToggle, recordCallToggle, setupConsoleLogging, clearCapturedLogs, verifyHoldLogs, verifyRecordingLogs, verifyEndLogs, verifyHoldTimer, verifyRemoteAudioTracks, verifyHoldMusicElement, executeRemoteAudioQuery, endTask } from './Utils/taskControlUtils'; +import { verifyTaskControls, holdCallToggle, recordCallToggle, setupConsoleLogging, clearCapturedLogs, verifyHoldLogs, verifyRecordingLogs, verifyEndLogs, verifyHoldTimer, verifyRemoteAudioTracks, verifyHoldMusicElement, endTask, verifyHoldButtonIcon, verifyRecordButtonIcon } from './Utils/taskControlUtils'; import { submitWrapup } from './Utils/wrapupUtils'; import { pageSetup } from './Utils/helperUtils'; import { USER_STATES, LOGIN_MODE, TASK_TYPES, WRAPUP_REASONS } from './constants'; +// Extract test functions for cleaner syntax +const { describe, beforeAll, afterAll, beforeEach } = test; + let page: Page; let context: BrowserContext; let callerPage: Page; @@ -29,12 +32,12 @@ let chatPage: Page; let context2: BrowserContext; const maxRetries = 3; -test.describe('Basic Task Controls Tests', () => { - test.beforeEach(() => { +describe('Basic Task Controls Tests', () => { + beforeEach(() => { clearCapturedLogs(); }); - test.beforeAll(async ({ browser }) => { + beforeAll(async ({ browser }) => { context = await browser.newContext(); context2 = await browser.newContext(); page = await context.newPage(); @@ -61,7 +64,7 @@ test.describe('Basic Task Controls Tests', () => { ]); }); - test.afterAll(async () => { + afterAll(async () => { if(await getCurrentState(page) === USER_STATES.ENGAGED) { // If still engaged, end the call to clean up await endTask(page); @@ -105,9 +108,7 @@ test.describe('Basic Task Controls Tests', () => { await verifyCurrentState(page, USER_STATES.ENGAGED); try { - // First execute the basic console query to verify element exists - await executeRemoteAudioQuery(page); - + // Then verify the audio tracks with the exact structure you provided await verifyRemoteAudioTracks(page); @@ -124,19 +125,25 @@ test.describe('Basic Task Controls Tests', () => { // Clear logs first to ensure clean state clearCapturedLogs(); + // Verify initial hold button icon (should show pause icon when call is active) + await verifyHoldButtonIcon(page, { expectedIsHeld: false }); + // Put call on hold from agent side await holdCallToggle(page); await page.waitForTimeout(3000); // Allow time for hold to take effect // Verify hold callback logs - verifyHoldLogs(true); + verifyHoldLogs({ expectedIsHeld: true }); + + // Verify hold button icon changed to play icon (when call is on hold) + await verifyHoldButtonIcon(page, { expectedIsHeld: true }); // Verify hold music element is present on the CALLER page (where hold music plays) // The hold music plays on the caller's side when agent puts call on hold await verifyHoldMusicElement(callerPage); // Verify hold timer is visible and functioning - await verifyHoldTimer(page, true); + await verifyHoldTimer(page, { shouldBeVisible: true }); clearCapturedLogs(); // Clear logs for next verification @@ -145,10 +152,12 @@ test.describe('Basic Task Controls Tests', () => { await page.waitForTimeout(2000); // Verify resume callback logs - verifyHoldLogs(false); - verifyHoldTimer(page, false); + verifyHoldLogs({ expectedIsHeld: false }); - clearCapturedLogs(); // Clear logs for next verification + // Verify hold button icon changed back to pause icon (when call is active) + await verifyHoldButtonIcon(page, { expectedIsHeld: false }); + + verifyHoldTimer(page, { shouldBeVisible: false }); } catch (error) { throw new Error(`Hold/Resume functionality with callbacks, timer, and hold music verification failed: ${error.message}`); @@ -160,12 +169,19 @@ test.describe('Basic Task Controls Tests', () => { await verifyCurrentState(page, USER_STATES.ENGAGED); try { + // Verify initial record button icon (should show pause icon when recording is active) + await verifyRecordButtonIcon(page, { expectedIsRecording: true }); + // Pause the call recording await recordCallToggle(page); await page.waitForTimeout(2000); // Verify pause recording callback logs - verifyRecordingLogs(false); + verifyRecordingLogs({ expectedIsRecording: false }); + + // Verify record button icon changed to record icon (when recording is paused) + await verifyRecordButtonIcon(page, { expectedIsRecording: false }); + clearCapturedLogs(); // Clear logs for next verification // Resume the call recording @@ -173,8 +189,10 @@ test.describe('Basic Task Controls Tests', () => { await page.waitForTimeout(2000); // Verify resume recording callback logs - verifyRecordingLogs(true); - clearCapturedLogs(); // Clear logs for next verification + verifyRecordingLogs({ expectedIsRecording: true }); + + // Verify record button icon changed back to pause icon (when recording is active) + await verifyRecordButtonIcon(page, { expectedIsRecording: true }); } catch (error) { throw new Error(`Recording pause/resume functionality verification failed: ${error.message}`); @@ -192,7 +210,6 @@ test.describe('Basic Task Controls Tests', () => { // Verify onEnd callback logs verifyEndLogs(); - clearCapturedLogs(); // Clear logs for next verification // Submit wrapup await submitWrapup(page, WRAPUP_REASONS.RESOLVED); @@ -230,7 +247,6 @@ test.describe('Basic Task Controls Tests', () => { // Verify onEnd callback logs verifyEndLogs(); - clearCapturedLogs(); // Clear logs for next verification // Submit wrapup await submitWrapup(page, WRAPUP_REASONS.RESOLVED); @@ -267,7 +283,6 @@ test.describe('Basic Task Controls Tests', () => { // Verify onEnd callback logs verifyEndLogs(); - clearCapturedLogs(); // Clear logs for next verification // Submit wrapup await submitWrapup(page, WRAPUP_REASONS.RESOLVED); @@ -278,7 +293,7 @@ test.describe('Basic Task Controls Tests', () => { }); }); -test.describe('Multi-Login Task Controls Tests', () => { +describe('Multi-Login Task Controls Tests', () => { let session1Page: Page; let session2Page: Page; let session1Context: BrowserContext; @@ -288,11 +303,11 @@ test.describe('Multi-Login Task Controls Tests', () => { let extensionPage: Page; let extensionContext: BrowserContext; - test.beforeEach(() => { + beforeEach(() => { clearCapturedLogs(); }); - test.beforeAll(async ({ browser }) => { + beforeAll(async ({ browser }) => { // Create separate browser contexts for multi-session testing session1Context = await browser.newContext(); session2Context = await browser.newContext(); @@ -341,7 +356,7 @@ test.describe('Multi-Login Task Controls Tests', () => { ]); }); - test.afterAll(async () => { + afterAll(async () => { // If still engaged, end the call to clean up if (await getCurrentState(session1Page) === USER_STATES.ENGAGED) { await endTask(session1Page); @@ -401,34 +416,70 @@ test.describe('Multi-Login Task Controls Tests', () => { setupConsoleLogging(session1Page); setupConsoleLogging(session2Page); + // Verify initial hold button icons on both sessions (should show pause icon when call is active) + await Promise.all([ + verifyHoldButtonIcon(session1Page, { expectedIsHeld: false }), + verifyHoldButtonIcon(session2Page, { expectedIsHeld: false }), + ]); + // Put call on hold from session 1 (AGENT1) await holdCallToggle(session1Page); await session1Page.waitForTimeout(3000); + // Verify hold button icons changed to play icon on both sessions (when call is on hold) + await Promise.all([ + verifyHoldButtonIcon(session1Page, { expectedIsHeld: true }), + verifyHoldButtonIcon(session2Page, { expectedIsHeld: true }), + ]); + // Verify hold timer is visible on both AGENT1 sessions await Promise.all([ - verifyHoldTimer(session1Page, true), - verifyHoldTimer(session2Page, true), + verifyHoldTimer(session1Page, { shouldBeVisible: true }), + verifyHoldTimer(session2Page, { shouldBeVisible: true }), ]); // Resume call from session 2 (AGENT1) await holdCallToggle(session2Page); await session2Page.waitForTimeout(3000); + // Verify hold button icons changed back to pause icon on both sessions (when call is active) + await Promise.all([ + verifyHoldButtonIcon(session1Page, { expectedIsHeld: false }), + verifyHoldButtonIcon(session2Page, { expectedIsHeld: false }), + ]); + // Verify hold timer disappears on both AGENT1 sessions await Promise.all([ - verifyHoldTimer(session1Page, false), - verifyHoldTimer(session2Page, false), + verifyHoldTimer(session1Page, { shouldBeVisible: false }), + verifyHoldTimer(session2Page, { shouldBeVisible: false }), + ]); + + // Verify initial record button icons on both sessions (should show pause icon when recording is active) + await Promise.all([ + verifyRecordButtonIcon(session1Page, { expectedIsRecording: true }), + verifyRecordButtonIcon(session2Page, { expectedIsRecording: true }), ]); // Pause recording from session 1 (AGENT1) await recordCallToggle(session1Page); await session1Page.waitForTimeout(2000); + // Verify record button icons changed to record icon on both sessions (when recording is paused) + await Promise.all([ + verifyRecordButtonIcon(session1Page, { expectedIsRecording: false }), + verifyRecordButtonIcon(session2Page, { expectedIsRecording: false }), + ]); + // Resume recording from session 2 (AGENT1) await recordCallToggle(session2Page); await session2Page.waitForTimeout(2000); + // Verify record button icons changed back to pause icon on both sessions (when recording is active) + await Promise.all([ + verifyRecordButtonIcon(session1Page, { expectedIsRecording: true }), + verifyRecordButtonIcon(session2Page, { expectedIsRecording: true }), + ]); + // End call from extension page await endCallTask(extensionPage); await session1Page.waitForTimeout(2000); From f98841df248cd297cffd7a9c9b9e3a2728ee10e3 Mon Sep 17 00:00:00 2001 From: Ritesh Singh Date: Wed, 16 Jul 2025 10:49:49 +0530 Subject: [PATCH 15/18] fix(cc-widgets): update after all --- playwright/tasklist-test.spec.ts | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/playwright/tasklist-test.spec.ts b/playwright/tasklist-test.spec.ts index 6f4f4af24..ba108811a 100644 --- a/playwright/tasklist-test.spec.ts +++ b/playwright/tasklist-test.spec.ts @@ -333,26 +333,7 @@ test.describe('Task List Tests for different types of Task', () => { }) test.afterAll(async () => { - if (context) { - await context.close(); - context = null; - } - if (context2) { - await context2.close(); - context2 = null; - } - if (page) { - await page.close(); - page = null; - } - if (callerpage) { - await callerpage.close(); - callerpage = null; - } - if (chatPage) { - await chatPage.close(); - chatPage = null; - } - + await context.close(); + await context2.close(); }) }) From 1f55c1d08b57c54b62f29b398d39adf406b71ab8 Mon Sep 17 00:00:00 2001 From: Ritesh Singh Date: Wed, 16 Jul 2025 13:56:22 +0530 Subject: [PATCH 16/18] fix(cc-widgets): fix variable types --- playwright/tasklist-test.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/playwright/tasklist-test.spec.ts b/playwright/tasklist-test.spec.ts index ba108811a..831b762b1 100644 --- a/playwright/tasklist-test.spec.ts +++ b/playwright/tasklist-test.spec.ts @@ -7,11 +7,11 @@ import { submitWrapup } from './Utils/wrapupUtils'; import { pageSetup, handleStrayTasks, waitForState } from './Utils/helperUtils'; -let page: Page | null = null; -let context: BrowserContext | null = null; -let callerpage: Page | null = null; -let context2: BrowserContext | null = null; -let chatPage: Page | null = null; +let page: Page; +let context: BrowserContext +let callerpage: Page +let context2: BrowserContext; +let chatPage: Page; let capturedLogs: string[] = []; const labelToMediaType: Record = { From 2cfe2a0726074bef0fbada17c8a73e000e559a15 Mon Sep 17 00:00:00 2001 From: adhmenon Date: Wed, 30 Jul 2025 10:54:22 +0530 Subject: [PATCH 17/18] fix(widgets): resolved-playwright-merge-conflicts --- playwright/tasklist-test.spec.ts | 575 +++++++++++++++---------------- 1 file changed, 285 insertions(+), 290 deletions(-) diff --git a/playwright/tasklist-test.spec.ts b/playwright/tasklist-test.spec.ts index 831b762b1..3bbb2a5a4 100644 --- a/playwright/tasklist-test.spec.ts +++ b/playwright/tasklist-test.spec.ts @@ -1,26 +1,33 @@ -import { test, Page, expect, BrowserContext } from '@playwright/test'; -import { changeUserState, getCurrentState } from './Utils/userStateUtils'; -import { createCallTask, createChatTask, loginExtension, acceptExtensionCall, createEmailTask, endExtensionCall, submitRonaPopup, acceptIncomingTask } from './Utils/incomingTaskUtils'; -import { TASK_TYPES, USER_STATES, LOGIN_MODE, THEME_COLORS, WRAPUP_REASONS, RONA_OPTIONS } from './constants'; -import { verifyTaskControls } from './Utils/taskControlUtils'; -import { submitWrapup } from './Utils/wrapupUtils'; -import { pageSetup, handleStrayTasks, waitForState } from './Utils/helperUtils'; - +import {test, Page, expect, BrowserContext} from '@playwright/test'; +import {changeUserState, getCurrentState} from './Utils/userStateUtils'; +import { + createCallTask, + createChatTask, + loginExtension, + acceptExtensionCall, + createEmailTask, + endExtensionCall, + submitRonaPopup, + acceptIncomingTask, +} from './Utils/incomingTaskUtils'; +import {TASK_TYPES, USER_STATES, LOGIN_MODE, THEME_COLORS, WRAPUP_REASONS, RONA_OPTIONS} from './constants'; +import {verifyTaskControls} from './Utils/taskControlUtils'; +import {submitWrapup} from './Utils/wrapupUtils'; +import {pageSetup, handleStrayTasks, waitForState} from './Utils/helperUtils'; let page: Page; -let context: BrowserContext -let callerpage: Page +let context: BrowserContext; +let callerpage: Page; let context2: BrowserContext; let chatPage: Page; let capturedLogs: string[] = []; const labelToMediaType: Record = { - Call: 'telephony', - Email: 'email', - Chat: 'chat', + Call: 'telephony', + Email: 'email', + Chat: 'chat', }; - /** * Reads the handle time of ith task in the task list. * @param page Playwright Page object @@ -29,16 +36,20 @@ const labelToMediaType: Record = { */ async function getCurrentHandleTime(page: Page, index: number = 0): Promise { - const full = await page.getByTestId('task:handle-time').nth(index).textContent(); + // find the task list item and read its interactionId + const taskListItem = page.getByTestId('task-list').getByRole('listitem').nth(index); + const interactionId = await taskListItem.getAttribute('id'); + + // read the dynamic handle-time test-id + const full = await page.getByTestId(`${interactionId}-handle-time`).textContent(); - // 2. Pull out the MM:SS via regex - const match = full?.match(/(\d{2}:\d{2})/); - const timer = match ? match[1] : null; + // 2. Pull out the MM:SS via regex + const match = full?.match(/(\d{2}:\d{2})/); + const timer = match ? match[1] : null; - // 3. (Optional) convert to total seconds - const [m, s] = timer!.split(':').map(Number); - const totalSeconds = m * 60 + s; - return totalSeconds; + // 3. (Optional) convert to total seconds + const [m, s] = timer!.split(':').map(Number); + return m * 60 + s; } /** @@ -50,290 +61,274 @@ async function getCurrentHandleTime(page: Page, index: number = 0): Promise { - const timeoutMs = 60000, pollInterval = 2000 - const start = Date.now(); - const type = testId.split('-').pop(); - while (Date.now() - start < timeoutMs) { - const taskDiv = page.getByTestId(testId).first(); - const isVisible = await taskDiv.isVisible().catch(() => false); - if (isVisible) { - const acceptButton = taskDiv.getByTestId('task:accept-button').first(); - await expect(acceptButton).toBeVisible({ timeout: 5000 }); - await acceptButton.click({ timeout: 3000 }); - - return; - } - await page.waitForTimeout(pollInterval); + const timeoutMs = 60000, + pollInterval = 2000; + const start = Date.now(); + const type = testId.split('-').pop(); + while (Date.now() - start < timeoutMs) { + const taskDiv = page.getByTestId(testId).first(); + const isVisible = await taskDiv.isVisible().catch(() => false); + if (isVisible) { + const acceptButton = taskDiv.getByTestId('task:accept-button').first(); + await expect(acceptButton).toBeVisible({timeout: 5000}); + await acceptButton.click({timeout: 3000}); + + return; } - throw new Error(`No incoming task found for ${testId} after ${timeoutMs / 1000} seconds`); + await page.waitForTimeout(pollInterval); + } + throw new Error(`No incoming task found for ${testId} after ${timeoutMs / 1000} seconds`); } - async function getTaskType(): Promise { - const callTimer = page.getByTestId('cc-cad:call-timer').first(); - const fullText = await callTimer.textContent(); - const mediaLabel = fullText!.split(' - ')[0].trim(); - return mediaLabel; + const callTimer = page.getByTestId('cc-cad:call-timer').first(); + const fullText = await callTimer.textContent(); + const mediaLabel = fullText!.split(' - ')[0].trim(); + return mediaLabel; } - function setupConsoleLogging(page: Page): () => void { - capturedLogs.length = 0; - - const consoleHandler = (msg) => { - const logText = msg.text(); - if ( - logText.startsWith('onTaskSelected invoked for task with title :') && - logText.includes(', and mediaType :') - ) { - capturedLogs.push(logText); - } - }; - - page.on('console', consoleHandler); - return () => page.off('console', consoleHandler); -} + capturedLogs.length = 0; + const consoleHandler = (msg) => { + const logText = msg.text(); + if (logText.startsWith('onTaskSelected invoked for task with title :') && logText.includes(', and mediaType :')) { + capturedLogs.push(logText); + } + }; + page.on('console', consoleHandler); + return () => page.off('console', consoleHandler); +} function escapeForRegExp(str?: string): string { - if (!str) { - return ''; - } - return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + if (!str) { + return ''; + } + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } async function waitForConsoleLogs( - logs: string[], - title: string, - mediaType: string, - timeoutMs = 15000, - intervalMs = 500 + logs: string[], + title: string, + mediaType: string, + timeoutMs = 15000, + intervalMs = 500 ): Promise { - const escTitle = escapeForRegExp(title!); - const escMedia = escapeForRegExp(mediaType!); - const pattern = new RegExp( - '^onTaskSelected invoked for task with title : ' + - escTitle + - ', and mediaType : ' + - escMedia + - '$' - ); - - const start = Date.now(); - while (Date.now() - start < timeoutMs) { - if (logs.some(log => pattern.test(log))) return; - await new Promise(r => setTimeout(r, intervalMs)); - } - - throw new Error( - `Timed out waiting for console log matching "${pattern.source}"` - ); + const escTitle = escapeForRegExp(title!); + const escMedia = escapeForRegExp(mediaType!); + const pattern = new RegExp( + '^onTaskSelected invoked for task with title : ' + escTitle + ', and mediaType : ' + escMedia + '$' + ); + + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + if (logs.some((log) => pattern.test(log))) return; + await new Promise((r) => setTimeout(r, intervalMs)); + } + + throw new Error(`Timed out waiting for console log matching "${pattern.source}"`); } +test.describe('Task List Tests for different types of Task', () => { + test.beforeEach(() => { + capturedLogs.length = 0; + }); + + test.beforeAll(async ({browser}) => { + context = await browser.newContext(); + context2 = await browser.newContext(); + page = await context.newPage(); + callerpage = await context2.newPage(); + chatPage = await context.newPage(); + setupConsoleLogging(page); + + await Promise.all([ + (async () => { + await loginExtension(callerpage, process.env.PW_AGENT2_USERNAME, process.env.PW_PASSWORD); + })(), + (async () => { + await pageSetup(page, LOGIN_MODE.DESKTOP, 'AGENT1'); + })(), + ]); + }); + + test('Verify Task List for incoming Call', async () => { + await createCallTask(callerpage); + await changeUserState(page, USER_STATES.AVAILABLE); + let incomingTaskDiv = page.getByTestId('samples:incoming-task-telephony').first(); + await incomingTaskDiv.waitFor({state: 'visible', timeout: 40000}); + await page.waitForTimeout(1000); + const taskListItem = page.getByTestId('task-list').getByRole('listitem').first(); + expect(taskListItem).toBeVisible(); + const taskListAcceptButton = taskListItem.getByTestId('task:accept-button').first(); + const taskListDeclineButton = taskListItem.getByTestId('task:decline-button').first(); + const title = await incomingTaskDiv.getByTestId('task:title').first().textContent(); + expect(await taskListItem.getByTestId('task:title').textContent()).toBe(title); + await expect(incomingTaskDiv.getByTestId('task:accept-button')).toBeVisible(); + await expect(incomingTaskDiv.getByTestId('task:decline-button')).toBeVisible(); + await expect(taskListAcceptButton).toBeVisible(); + await expect(taskListDeclineButton).toBeVisible(); + await taskListAcceptButton.click(); + await page.waitForTimeout(1000); + await expect(taskListAcceptButton).not.toBeVisible(); + await expect(taskListDeclineButton).not.toBeVisible(); + await page.waitForTimeout(5000); + try { + await verifyTaskControls(page, TASK_TYPES.CALL); + } catch (error) { + throw new Error(`Call control buttons verification failed: ${error.message}`); + } + await waitForState(page, USER_STATES.ENGAGED); + await taskListItem.click(); + await waitForConsoleLogs(capturedLogs, title, 'telephony'); + + // now use interactionId for dynamic assertions + const interactionId = await taskListItem.getAttribute('id'); + await expect(taskListItem.locator('[icon-name="handset-filled"]')).toBeVisible(); + await expect(page.getByTestId(`${interactionId}-state`)).toHaveText('Connected'); + await expect(page.getByTestId(`${interactionId}-handle-time`)).toBeVisible(); + await page.getByTestId('call-control:end-call').first().waitFor({state: 'visible', timeout: 5000}); + await page.getByTestId('call-control:end-call').first().click(); + await page.waitForTimeout(500); + await submitWrapup(page, WRAPUP_REASONS.SALE); + await waitForState(page, USER_STATES.AVAILABLE); + }); + + test('Verify Task List for incoming Chat Task', async () => { + await createChatTask(chatPage); + await changeUserState(page, USER_STATES.AVAILABLE); + const incomingTaskDiv = page.getByTestId('samples:incoming-task-chat').first(); + await incomingTaskDiv.waitFor({state: 'visible', timeout: 60000}); + await page.waitForTimeout(1000); + const taskListItem = page.getByTestId('task-list').getByRole('listitem').first(); + expect(taskListItem).toBeVisible(); + const taskListAcceptButton = taskListItem.getByTestId('task:accept-button').first(); + const taskListDeclineButton = taskListItem.getByTestId('task:decline-button').first(); + const title = await incomingTaskDiv.getByTestId('task:title').textContent(); + expect(await taskListItem.getByTestId('task:title').textContent()).toBe(title); + await expect(incomingTaskDiv.getByTestId('task:accept-button')).toBeVisible(); + await expect(incomingTaskDiv.getByTestId('task:decline-button')).not.toBeVisible(); + await expect(taskListAcceptButton).toBeVisible(); + await expect(taskListDeclineButton).not.toBeVisible(); + await taskListAcceptButton.click(); + await page.waitForTimeout(1000); + await waitForState(page, USER_STATES.ENGAGED); + const prevtimer = await getCurrentHandleTime(page); + await page.waitForTimeout(5000); + const currentTimer = await getCurrentHandleTime(page); + expect(currentTimer).toBeGreaterThan(prevtimer); + expect(Math.abs(currentTimer - prevtimer + 1)).toBeGreaterThanOrEqual(5); + try { + await verifyTaskControls(page, TASK_TYPES.CHAT); + } catch (error) { + throw new Error(`Call control buttons verification failed: ${error.message}`); + } + await waitForConsoleLogs(capturedLogs, title, 'chat'); + await expect(taskListAcceptButton).not.toBeVisible(); + await expect(taskListDeclineButton).not.toBeVisible(); + expect(await taskListItem.getByTestId('task:title').textContent()).toBe(title); + await expect(taskListItem.locator('[icon-name="chat-filled"]')).toBeVisible(); + await expect(taskListItem.getByTestId('task:item-state')).toHaveText('Connected'); + await expect(taskListItem.getByTestId('task:handle-time')).toBeVisible(); + await page.getByTestId('call-control:end-call').first().waitFor({state: 'visible', timeout: 5000}); + await page.getByTestId('call-control:end-call').first().click(); + await page.waitForTimeout(2000); + await submitWrapup(page, WRAPUP_REASONS.SALE); + await waitForState(page, USER_STATES.AVAILABLE); + }); + + test('Verify Task List for incoming Email Task', async () => { + await createEmailTask(); + await changeUserState(page, USER_STATES.AVAILABLE); + const incomingTaskDiv = page.getByTestId('samples:incoming-task-email').first(); + await incomingTaskDiv.waitFor({state: 'visible', timeout: 60000}); + await page.waitForTimeout(1000); + const taskListItem = page.getByTestId('task-list').getByRole('listitem').first(); + expect(taskListItem).toBeVisible(); + const taskListAcceptButton = taskListItem.getByTestId('task:accept-button').first(); + const taskListDeclineButton = taskListItem.getByTestId('task:decline-button').first(); + const title = await incomingTaskDiv.getByTestId('task:title').textContent(); + expect(await taskListItem.getByTestId('task:title').textContent()).toBe(title); + await expect(incomingTaskDiv.getByTestId('task:accept-button')).toBeVisible(); + await expect(incomingTaskDiv.getByTestId('task:decline-button')).not.toBeVisible(); + await expect(taskListAcceptButton).toBeVisible(); + await expect(taskListDeclineButton).not.toBeVisible(); + await taskListAcceptButton.click(); + await page.waitForTimeout(1000); + const prevtimer = await getCurrentHandleTime(page); + await page.waitForTimeout(5000); + + const currentTimer = await getCurrentHandleTime(page); + expect(currentTimer).toBeGreaterThan(prevtimer); + expect(Math.abs(currentTimer - prevtimer + 1)).toBeGreaterThanOrEqual(5); + + try { + await verifyTaskControls(page, TASK_TYPES.EMAIL); + } catch (error) { + throw new Error(`Call control buttons verification failed: ${error.message}`); + } + + await expect(taskListAcceptButton).not.toBeVisible(); + await expect(taskListDeclineButton).not.toBeVisible(); + await waitForState(page, USER_STATES.ENGAGED); + await waitForConsoleLogs(capturedLogs, title, 'email'); + expect(await taskListItem.getByTestId('task:title').textContent()).toBe(title); + await expect(taskListItem.locator('[icon-name="email-filled"]')).toBeVisible(); + await expect(taskListItem.getByTestId('task:item-state')).toHaveText('Connected'); + await expect(taskListItem.getByTestId('task:handle-time')).toBeVisible(); + await page.getByTestId('call-control:end-call').first().waitFor({state: 'visible', timeout: 5000}); + await page.getByTestId('call-control:end-call').first().click(); + await page.waitForTimeout(2000); + await submitWrapup(page, WRAPUP_REASONS.SALE); + await waitForState(page, USER_STATES.AVAILABLE); + }); + + test('Task List Test with Multiple Taks', async () => { + await changeUserState(page, USER_STATES.MEETING); + await waitForState(page, USER_STATES.MEETING); + await Promise.all([createCallTask(callerpage), createChatTask(chatPage), createEmailTask()]); + + await changeUserState(page, USER_STATES.AVAILABLE); + + await Promise.all([ + waitForAndAcceptSpecificTask(page, 'samples:incoming-task-telephony'), + waitForAndAcceptSpecificTask(page, 'samples:incoming-task-chat'), + waitForAndAcceptSpecificTask(page, 'samples:incoming-task-email'), + ]); + await page.waitForTimeout(3000); + + for (let i = 0; i < 3; i++) { + const taskListItem = page.getByTestId('task-list').getByRole('listitem').nth(i); + + await taskListItem.waitFor({state: 'visible', timeout: 5000}); + expect(taskListItem).toBeVisible(); + await taskListItem.click(); + const prevtimer = await getCurrentHandleTime(page, i); + await page.waitForTimeout(5000); + const currentTimer = await getCurrentHandleTime(page, i); + expect(currentTimer).toBeGreaterThan(prevtimer); + expect(Math.abs(currentTimer - prevtimer + 1)).toBeGreaterThanOrEqual(5); + const inferredType = await getTaskType(); + try { + await verifyTaskControls(page, inferredType); + } catch (error) { + throw new Error(`Call control buttons verification failed: ${error.message}`); + } + + await waitForConsoleLogs( + capturedLogs, + await taskListItem.getByTestId('task:title').textContent()!, + labelToMediaType[inferredType] + ); + capturedLogs.length = 0; + } + await handleStrayTasks(page); + }); -test.describe('Task List Tests for different types of Task', () => { - test.beforeEach(() => { - capturedLogs.length = 0; - }) - - test.beforeAll(async ({ browser }) => { - context = await browser.newContext(); - context2 = await browser.newContext(); - page = await context.newPage(); - callerpage = await context2.newPage(); - chatPage = await context.newPage(); - setupConsoleLogging(page); - - await Promise.all([ - (async () => { - await loginExtension(callerpage, process.env.PW_AGENT2_USERNAME, process.env.PW_PASSWORD); - })(), - (async () => { - await pageSetup(page, LOGIN_MODE.DESKTOP, 'AGENT1'); - })(), - ]) - }) - - - test('Verify Task List for incoming Call', async () => { - await createCallTask(callerpage); - await changeUserState(page, USER_STATES.AVAILABLE); - let incomingTaskDiv = page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTaskDiv.waitFor({ state: 'visible', timeout: 40000 }); - await page.waitForTimeout(1000); - const taskListItem = page.getByTestId('task-list').getByRole('listitem').first(); - expect(taskListItem).toBeVisible(); - const taskListAcceptButton = taskListItem.getByTestId('task:accept-button').first(); - const taskListDeclineButton = taskListItem.getByTestId('task:decline-button').first(); - const title = await incomingTaskDiv.getByTestId('task:title').first().textContent(); - expect(await taskListItem.getByTestId('task:title').textContent()).toBe(title); - await expect(incomingTaskDiv.getByTestId('task:accept-button')).toBeVisible(); - await expect(incomingTaskDiv.getByTestId('task:decline-button')).toBeVisible(); - await expect(taskListAcceptButton).toBeVisible(); - await expect(taskListDeclineButton).toBeVisible(); - await taskListAcceptButton.click(); - await page.waitForTimeout(1000); - await expect(taskListAcceptButton).not.toBeVisible(); - await expect(taskListDeclineButton).not.toBeVisible(); - await page.waitForTimeout(5000); - try { - await verifyTaskControls(page, TASK_TYPES.CALL); - } catch (error) { - throw new Error(`Call control buttons verification failed: ${error.message}`); - } - await waitForState(page, USER_STATES.ENGAGED); - await taskListItem.click(); - await waitForConsoleLogs(capturedLogs, title, 'telephony'); - expect(await taskListItem.getByTestId('task:title').textContent()).toBe(title); - await expect(taskListItem.locator('[icon-name="handset-filled"]')).toBeVisible(); - await expect(taskListItem.getByTestId('task:item-state')).toHaveText('Connected'); - await expect(taskListItem.getByTestId('task:handle-time')).toBeVisible(); - await page.getByTestId('call-control:end-call').first().waitFor({ state: 'visible', timeout: 5000 }); - await page.getByTestId('call-control:end-call').first().click(); - await page.waitForTimeout(500); - await submitWrapup(page, WRAPUP_REASONS.SALE); - await waitForState(page, USER_STATES.AVAILABLE); - }) - - test('Verify Task List for incoming Chat Task', async () => { - await createChatTask(chatPage); - await changeUserState(page, USER_STATES.AVAILABLE); - const incomingTaskDiv = page.getByTestId('samples:incoming-task-chat').first(); - await incomingTaskDiv.waitFor({ state: 'visible', timeout: 60000 }); - await page.waitForTimeout(1000); - const taskListItem = page.getByTestId('task-list').getByRole('listitem').first(); - expect(taskListItem).toBeVisible(); - const taskListAcceptButton = taskListItem.getByTestId('task:accept-button').first(); - const taskListDeclineButton = taskListItem.getByTestId('task:decline-button').first(); - const title = await incomingTaskDiv.getByTestId('task:title').textContent(); - expect(await taskListItem.getByTestId('task:title').textContent()).toBe(title); - await expect(incomingTaskDiv.getByTestId('task:accept-button')).toBeVisible(); - await expect(incomingTaskDiv.getByTestId('task:decline-button')).not.toBeVisible(); - await expect(taskListAcceptButton).toBeVisible(); - await expect(taskListDeclineButton).not.toBeVisible(); - await taskListAcceptButton.click(); - await page.waitForTimeout(1000); - await waitForState(page, USER_STATES.ENGAGED); - const prevtimer = await getCurrentHandleTime(page); - await page.waitForTimeout(5000); - const currentTimer = await getCurrentHandleTime(page); - expect(currentTimer).toBeGreaterThan(prevtimer); - expect(Math.abs(currentTimer - prevtimer + 1)).toBeGreaterThanOrEqual(5); - try { - await verifyTaskControls(page, TASK_TYPES.CHAT); - } catch (error) { - throw new Error(`Call control buttons verification failed: ${error.message}`); - } - await waitForConsoleLogs(capturedLogs, title, 'chat'); - await expect(taskListAcceptButton).not.toBeVisible(); - await expect(taskListDeclineButton).not.toBeVisible(); - expect(await taskListItem.getByTestId('task:title').textContent()).toBe(title); - await expect(taskListItem.locator('[icon-name="chat-filled"]')).toBeVisible(); - await expect(taskListItem.getByTestId('task:item-state')).toHaveText('Connected'); - await expect(taskListItem.getByTestId('task:handle-time')).toBeVisible(); - await page.getByTestId('call-control:end-call').first().waitFor({ state: 'visible', timeout: 5000 }); - await page.getByTestId('call-control:end-call').first().click(); - await page.waitForTimeout(2000); - await submitWrapup(page, WRAPUP_REASONS.SALE); - await waitForState(page, USER_STATES.AVAILABLE); - }) - - test('Verify Task List for incoming Email Task', async () => { - await createEmailTask(); - await changeUserState(page, USER_STATES.AVAILABLE); - const incomingTaskDiv = page.getByTestId('samples:incoming-task-email').first(); - await incomingTaskDiv.waitFor({ state: 'visible', timeout: 60000 }); - await page.waitForTimeout(1000); - const taskListItem = page.getByTestId('task-list').getByRole('listitem').first(); - expect(taskListItem).toBeVisible(); - const taskListAcceptButton = taskListItem.getByTestId('task:accept-button').first(); - const taskListDeclineButton = taskListItem.getByTestId('task:decline-button').first(); - const title = await incomingTaskDiv.getByTestId('task:title').textContent(); - expect(await taskListItem.getByTestId('task:title').textContent()).toBe(title); - await expect(incomingTaskDiv.getByTestId('task:accept-button')).toBeVisible(); - await expect(incomingTaskDiv.getByTestId('task:decline-button')).not.toBeVisible(); - await expect(taskListAcceptButton).toBeVisible(); - await expect(taskListDeclineButton).not.toBeVisible(); - await taskListAcceptButton.click(); - await page.waitForTimeout(1000); - const prevtimer = await getCurrentHandleTime(page); - await page.waitForTimeout(5000); - - const currentTimer = await getCurrentHandleTime(page); - expect(currentTimer).toBeGreaterThan(prevtimer); - expect(Math.abs(currentTimer - prevtimer + 1)).toBeGreaterThanOrEqual(5); - - try { - await verifyTaskControls(page, TASK_TYPES.EMAIL); - } catch (error) { - throw new Error(`Call control buttons verification failed: ${error.message}`); - } - - await expect(taskListAcceptButton).not.toBeVisible(); - await expect(taskListDeclineButton).not.toBeVisible(); - await waitForState(page, USER_STATES.ENGAGED); - await waitForConsoleLogs(capturedLogs, title, 'email'); - expect(await taskListItem.getByTestId('task:title').textContent()).toBe(title); - await expect(taskListItem.locator('[icon-name="email-filled"]')).toBeVisible(); - await expect(taskListItem.getByTestId('task:item-state')).toHaveText('Connected'); - await expect(taskListItem.getByTestId('task:handle-time')).toBeVisible(); - await page.getByTestId('call-control:end-call').first().waitFor({ state: 'visible', timeout: 5000 }); - await page.getByTestId('call-control:end-call').first().click(); - await page.waitForTimeout(2000); - await submitWrapup(page, WRAPUP_REASONS.SALE); - await waitForState(page, USER_STATES.AVAILABLE); - }); - - test('Task List Test with Multiple Taks', async () => { - await changeUserState(page, USER_STATES.MEETING); - await waitForState(page, USER_STATES.MEETING); - await Promise.all([ - createCallTask(callerpage), - createChatTask(chatPage), - createEmailTask() - ]) - - await changeUserState(page, USER_STATES.AVAILABLE); - - await Promise.all([ - waitForAndAcceptSpecificTask(page, 'samples:incoming-task-telephony'), - waitForAndAcceptSpecificTask(page, 'samples:incoming-task-chat'), - waitForAndAcceptSpecificTask(page, 'samples:incoming-task-email'), - ]); - await page.waitForTimeout(3000); - - for (let i = 0; i < 3; i++) { - const taskListItem = page - .getByTestId('task-list') - .getByRole('listitem') - .nth(i); - - await taskListItem.waitFor({ state: 'visible', timeout: 5000 }); - expect(taskListItem).toBeVisible(); - await taskListItem.click(); - const prevtimer = await getCurrentHandleTime(page, i); - await page.waitForTimeout(5000); - const currentTimer = await getCurrentHandleTime(page, i); - expect(currentTimer).toBeGreaterThan(prevtimer); - expect(Math.abs(currentTimer - prevtimer + 1)).toBeGreaterThanOrEqual(5); - const inferredType = await getTaskType(); - try { - await verifyTaskControls(page, inferredType); - } catch (error) { - throw new Error(`Call control buttons verification failed: ${error.message}`); - } - - await waitForConsoleLogs(capturedLogs, await taskListItem.getByTestId('task:title').textContent()!, labelToMediaType[inferredType]); - capturedLogs.length = 0; - } - - await handleStrayTasks(page); - }) - - test.afterAll(async () => { - await context.close(); - await context2.close(); - }) -}) + test.afterAll(async () => { + await context.close(); + await context2.close(); + }); +}); From 3c46e1c3e79f45faf33429c4e4bdd5716c413540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkula?= Date: Wed, 13 Aug 2025 16:58:39 +0530 Subject: [PATCH 18/18] fix(task-list-uts): update-snapshot --- .../__snapshots__/call-control-cad.snapshot.tsx.snap | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/contact-center/cc-components/tests/components/task/CallControlCAD/__snapshots__/call-control-cad.snapshot.tsx.snap b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/__snapshots__/call-control-cad.snapshot.tsx.snap index 1a564b737..c924a5abb 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControlCAD/__snapshots__/call-control-cad.snapshot.tsx.snap +++ b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/__snapshots__/call-control-cad.snapshot.tsx.snap @@ -28,6 +28,7 @@ exports[`CallControlCADComponent Snapshots should handle edge cases and control > Voice - @@ -213,6 +214,7 @@ exports[`CallControlCADComponent Snapshots should handle edge cases and control > Voice - @@ -362,6 +364,7 @@ exports[`CallControlCADComponent Snapshots should handle edge cases and control > Voice - @@ -639,6 +642,7 @@ exports[`CallControlCADComponent Snapshots should render basic call states and m > Chat - @@ -916,6 +920,7 @@ exports[`CallControlCADComponent Snapshots should render basic call states and m > Voice - @@ -1203,6 +1208,7 @@ exports[`CallControlCADComponent Snapshots should render basic call states and m > Voice - @@ -1480,6 +1486,7 @@ exports[`CallControlCADComponent Snapshots should render basic call states and m > Social - @@ -1758,6 +1765,7 @@ exports[`CallControlCADComponent Snapshots should render consultation and wrapup > Voice - @@ -1943,6 +1951,7 @@ exports[`CallControlCADComponent Snapshots should render consultation and wrapup > Voice - @@ -2048,6 +2057,7 @@ exports[`CallControlCADComponent Snapshots should render consultation and wrapup > Voice - @@ -2379,6 +2389,7 @@ exports[`CallControlCADComponent Snapshots should render consultation and wrapup > Voice -