diff --git a/playwright/Utils/advancedTaskControlUtils.ts b/playwright/Utils/advancedTaskControlUtils.ts index 69bb206ef..8b3364770 100644 --- a/playwright/Utils/advancedTaskControlUtils.ts +++ b/playwright/Utils/advancedTaskControlUtils.ts @@ -1,4 +1,5 @@ import {Page, expect} from '@playwright/test'; +import {loginExtension} from './incomingTaskUtils'; import {dismissOverlays} from './helperUtils'; import {AWAIT_TIMEOUT, FORM_FIELD_TIMEOUT} from '../constants'; @@ -261,3 +262,29 @@ export async function cancelConsult(page: Page): Promise { // Click cancel consult button await page.getByTestId('cancel-consult-btn').click({timeout: AWAIT_TIMEOUT}); } + +/** + * Ensures the Dial Number softphone (web.webex.com) page is logged in using env creds. + * Uses: PW_DIAL_NUMBER_LOGIN_USERNAME, PW_DIAL_NUMBER_LOGIN_PASSWORD + * Also dismisses any stale overlays/popovers (e.g., "Media failed") that might block interaction. + */ +export async function ensureDialNumberLoggedIn(page: Page): Promise { + const currentUrl = page?.url?.() || ''; + if (!/\.webex\.com\/calling/.test(currentUrl)) { + const user = process.env.PW_DIAL_NUMBER_LOGIN_USERNAME; + const pass = process.env.PW_DIAL_NUMBER_LOGIN_PASSWORD; + if (user && pass) { + await loginExtension(page, user, pass); + } + } + + // Dismiss any stale overlays/popovers (e.g., "Media failed" from previous calls) + await dismissOverlays(page); + + // Ensure the dial number page is in the foreground to avoid background throttling + await page.bringToFront(); + + // Wait for the incoming call to appear on the dial number page + // Use extended timeout to handle network routing delays and test interference + await page.locator('[data-test="right-action-button"]').waitFor({state: 'visible', timeout: AWAIT_TIMEOUT * 2.5}); +} diff --git a/playwright/Utils/helperUtils.ts b/playwright/Utils/helperUtils.ts index 047fe3462..cede87b13 100644 --- a/playwright/Utils/helperUtils.ts +++ b/playwright/Utils/helperUtils.ts @@ -12,6 +12,7 @@ import { AWAIT_TIMEOUT, } from '../constants'; import {submitWrapup} from './wrapupUtils'; +import {holdCallToggle} from './taskControlUtils'; import {acceptExtensionCall, submitRonaPopup} from './incomingTaskUtils'; import { loginViaAccessToken, @@ -440,6 +441,57 @@ export const handleStrayTasks = async ( console.log(`Completed stray task handling after ${iterations} iterations`); }; +/** + * Clears any pending call UI on the page by ending the call and/or submitting wrapup if visible. + * Does nothing if neither end-call nor wrapup controls are present. + */ +export async function clearPendingCallAndWrapup(page: Page): Promise { + // If wrapup is available, submit it + const wrapupBtn = page.getByTestId('call-control:wrapup-button').first(); + const wrapupVisible = await wrapupBtn.isVisible().catch(() => false); + if (wrapupVisible) { + await submitWrapup(page, WRAPUP_REASONS.SALE); + await page.waitForTimeout(500); + return; + } + + const endBtn = page.getByTestId('call-control:end-call').first(); + const endVisible = await endBtn.isVisible().catch(() => false); + if (endVisible) { + const endEnabled = await endBtn.isEnabled().catch(() => false); + if (endEnabled) { + try { + await endBtn.click({timeout: AWAIT_TIMEOUT}); + await submitWrapup(page, WRAPUP_REASONS.SALE); + await page.waitForTimeout(500); + } catch {} + } else { + // If end button is disabled, try resuming from hold then end; otherwise, see if wrapup is available + try { + await holdCallToggle(page); + } catch {} + const endEnabledAfterResume = await endBtn.isEnabled().catch(() => false); + if (endEnabledAfterResume) { + try { + await endBtn.click({timeout: AWAIT_TIMEOUT}); + await submitWrapup(page, WRAPUP_REASONS.SALE); + await page.waitForTimeout(500); + return; + } catch {} + } + + // If resume path failed, see if wrapup became available instead; otherwise, skip silently + const wrapupNowVisible = await wrapupBtn.isVisible().catch(() => false); + if (wrapupNowVisible) { + try { + await submitWrapup(page, WRAPUP_REASONS.SALE); + await page.waitForTimeout(500); + } catch {} + } + } + } +} + /* / * 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 diff --git a/playwright/Utils/incomingTaskUtils.ts b/playwright/Utils/incomingTaskUtils.ts index 764d7122e..78a3935b8 100644 --- a/playwright/Utils/incomingTaskUtils.ts +++ b/playwright/Utils/incomingTaskUtils.ts @@ -14,6 +14,7 @@ import { UI_SETTLE_TIMEOUT, } from '../constants'; import nodemailer from 'nodemailer'; +import {dismissOverlays} from './helperUtils'; const transporter = nodemailer.createTransport({ service: 'gmail', // Make sure to use Secure Port for Gmail SMTP @@ -46,15 +47,30 @@ export async function createCallTask(page: Page, number: string) { } catch (error) { throw new Error('The Input Page should be logged into calling web-client.'); } + + // Ensure page is foregrounded and clean of overlays + await page.bringToFront(); + await dismissOverlays(page); + + const endBtn = page.locator('[data-test="call-end"]'); + if (await endBtn.isVisible({timeout: 500}).catch(() => false)) { + await endBtn.click({timeout: AWAIT_TIMEOUT}); + await page.waitForTimeout(500); + } + + await page + .locator('[data-test="statusMessage"]') + .waitFor({state: 'hidden', timeout: NETWORK_OPERATION_TIMEOUT}) + .catch(() => {}); + await page.getByRole('textbox', {name: 'Dial'}).waitFor({state: 'visible', timeout: AWAIT_TIMEOUT}); await page.getByRole('textbox', {name: 'Dial'}).fill(number, {timeout: AWAIT_TIMEOUT}); - await expect( - page.locator('[data-test="calling-ui-keypad-control"]').getByRole('button', {name: 'Call'}) - ).toBeVisible(); - await page - .locator('[data-test="calling-ui-keypad-control"]') - .getByRole('button', {name: 'Call'}) - .click({timeout: AWAIT_TIMEOUT}); + + const callButton = page.locator('[data-test="calling-ui-keypad-control"]').getByRole('button', {name: 'Call'}); + await expect(callButton).toBeVisible({timeout: AWAIT_TIMEOUT}); + // Ensure button is enabled before clicking + await callButton.waitFor({state: 'visible', timeout: AWAIT_TIMEOUT}); + await callButton.click({timeout: AWAIT_TIMEOUT}); await page.waitForTimeout(2000); } diff --git a/playwright/test-manager.ts b/playwright/test-manager.ts index af309f7d8..20f0a8738 100644 --- a/playwright/test-manager.ts +++ b/playwright/test-manager.ts @@ -14,6 +14,7 @@ import { AWAIT_TIMEOUT, PAGE_TYPES, PageType, + CALL_URL, } from './constants'; // Configuration interfaces for setup options @@ -326,6 +327,8 @@ export class TestManager { () => loginExtension(this.dialNumberPage, envTokens.dialNumberUsername, envTokens.dialNumberPassword), 'dial number login' ); + // Ensure only one page remains in the Dial Number context to avoid duplicate web client instances + await this.enforceSingleDialNumberInOwnContext(); } // Helper method for Caller setup @@ -402,6 +405,7 @@ export class TestManager { needsAgent1: true, needsAgent2: true, needsCaller: true, + needDialNumberLogin: true, agent1LoginMode: LOGIN_MODE.DESKTOP, enableConsoleLogging: true, enableAdvancedLogging: true, @@ -558,6 +562,7 @@ export class TestManager { this.callerPage, this.agent1ExtensionPage, this.chatPage, + this.dialNumberPage, ].filter(Boolean); pagesToClose.forEach((page) => { @@ -574,6 +579,7 @@ export class TestManager { this.callerExtensionContext, this.extensionContext, this.chatContext, + this.dialNumberContext, ].filter(Boolean); contextsToClose.forEach((context) => { @@ -584,4 +590,45 @@ export class TestManager { await Promise.all(cleanupOperations); } + + // Helper method to hard-reset the dial number login session + public async resetDialNumberSession(): Promise { + if (!this.dialNumberPage || !this.dialNumberContext) { + return; + } + const envTokens = this.getEnvTokens(); + try { + await this.dialNumberContext.clearCookies(); + await this.dialNumberPage.evaluate(() => { + try { + localStorage.clear(); + } catch {} + try { + sessionStorage.clear(); + } catch {} + }); + // Navigate fresh and login again + await this.dialNumberPage.goto(CALL_URL); + await loginExtension(this.dialNumberPage, envTokens.dialNumberUsername!, envTokens.dialNumberPassword!); + await this.enforceSingleDialNumberInOwnContext(); + } catch (error) { + throw new Error(`Failed to reset dial number session: ${error}`); + } + } + + // Ensures at most one page exists in the dedicated Dial Number context we manage. + // Closes any extra tabs/pages opened in that context to prevent multiple web.webex.com instances for the Dial Number user. + private async enforceSingleDialNumberInOwnContext(): Promise { + if (!this.dialNumberContext) return; + try { + const pages = this.dialNumberContext.pages(); + for (const p of pages) { + if (p !== this.dialNumberPage) { + try { + await p.close(); + } catch {} + } + } + } catch {} + } } diff --git a/playwright/tests/advance-task-control-combinations-test.spec.ts b/playwright/tests/advance-task-control-combinations-test.spec.ts index a0f7084b5..029bab244 100644 --- a/playwright/tests/advance-task-control-combinations-test.spec.ts +++ b/playwright/tests/advance-task-control-combinations-test.spec.ts @@ -1,10 +1,17 @@ import {test, expect} from '@playwright/test'; -import {cancelConsult, consultOrTransfer} from '../Utils/advancedTaskControlUtils'; +import { + cancelConsult, + consultOrTransfer, + clearAdvancedCapturedLogs, + verifyConsultStartSuccessLogs, + verifyConsultTransferredLogs, + ensureDialNumberLoggedIn, +} from '../Utils/advancedTaskControlUtils'; import {changeUserState, verifyCurrentState} from '../Utils/userStateUtils'; -import {createCallTask, acceptIncomingTask} from '../Utils/incomingTaskUtils'; +import {createCallTask, acceptIncomingTask, acceptExtensionCall, endCallTask} from '../Utils/incomingTaskUtils'; import {submitWrapup} from '../Utils/wrapupUtils'; import {USER_STATES, TASK_TYPES, WRAPUP_REASONS} from '../constants'; -import {waitForState} from '../Utils/helperUtils'; +import {waitForState, clearPendingCallAndWrapup} from '../Utils/helperUtils'; import {endTask, holdCallToggle} from '../Utils/taskControlUtils'; import {TestManager} from '../test-manager'; @@ -264,6 +271,113 @@ export default function createAdvanceCombinationsTests() { await testManager.agent1Page.waitForTimeout(2000); }); + test('Dial Number: consult then end consult returns UI to normal', async () => { + test.skip(!process.env.PW_DIAL_NUMBER_NAME, 'PW_DIAL_NUMBER_NAME not set'); + + await testManager.resetDialNumberSession(); + await changeUserState(testManager.agent2Page, USER_STATES.MEETING); + await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); + await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!); + const incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); + await incomingTaskDiv.waitFor({state: 'visible', timeout: 80000}); + await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); + await waitForState(testManager.agent1Page, USER_STATES.ENGAGED); + + clearAdvancedCapturedLogs(); + await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'consult', process.env.PW_DIAL_NUMBER_NAME!); + await expect(testManager.agent1Page.getByTestId('cancel-consult-btn')).toBeVisible(); + await cancelConsult(testManager.agent1Page); + await expect(testManager.agent1Page.getByTestId('cancel-consult-btn')).not.toBeVisible(); + await endCallTask(testManager.callerPage!); + await submitWrapup(testManager.agent1Page, WRAPUP_REASONS.SALE); + }); + + test('Dial Number: consult then transfer completes and remote ends', async () => { + test.skip(!process.env.PW_DIAL_NUMBER_NAME, 'PW_DIAL_NUMBER_NAME not set'); + + await testManager.resetDialNumberSession(); + await changeUserState(testManager.agent2Page, USER_STATES.MEETING); + await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); + await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!); + const incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); + await incomingTaskDiv.waitFor({state: 'visible', timeout: 80000}); + await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); + + clearAdvancedCapturedLogs(); + await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'consult', process.env.PW_DIAL_NUMBER_NAME!); + await ensureDialNumberLoggedIn(testManager.dialNumberPage); + await expect(testManager.dialNumberPage.locator('[data-test="right-action-button"]')).toBeVisible(); + await acceptExtensionCall(testManager.dialNumberPage); + await testManager.agent1Page.getByTestId('transfer-consult-btn').click(); + await submitWrapup(testManager.agent1Page, WRAPUP_REASONS.SALE); + await verifyConsultStartSuccessLogs(); + await verifyConsultTransferredLogs(); + await endCallTask(testManager.dialNumberPage); + }); + + test('Entry Point: consult then end consult returns UI to normal', async () => { + test.skip(!process.env.PW_ENTRYPOINT_NAME, 'PW_ENTRYPOINT_NAME not set'); + + await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); + await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!); + const incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); + await incomingTaskDiv.waitFor({state: 'visible', timeout: 80000}); + await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); + + clearAdvancedCapturedLogs(); + await consultOrTransfer(testManager.agent1Page, 'entryPoint', 'consult', process.env.PW_ENTRYPOINT_NAME!); + await expect(testManager.agent1Page.getByTestId('cancel-consult-btn')).toBeVisible(); + await verifyConsultStartSuccessLogs(); + await cancelConsult(testManager.agent1Page); + await testManager.agent1Page.waitForTimeout(1000); + }); + + // Two-hop to Dial Number (moved into top-level describe) + test('Two-hop: consult to Agent then consult-transfer to Dial Number', async () => { + test.skip(!process.env.PW_DIAL_NUMBER_NAME, 'PW_DIAL_NUMBER_NAME not set'); + + await clearPendingCallAndWrapup(testManager.agent1Page); + await clearPendingCallAndWrapup(testManager.agent2Page); + await testManager.resetDialNumberSession(); + await changeUserState(testManager.agent2Page, USER_STATES.MEETING); + await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); + await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!); + const incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); + await incomingTaskDiv.waitFor({state: 'visible', timeout: 80000}); + await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); + await changeUserState(testManager.agent2Page, USER_STATES.AVAILABLE); + await waitForState(testManager.agent1Page, USER_STATES.ENGAGED); + await waitForState(testManager.agent2Page, USER_STATES.AVAILABLE); + await testManager.agent2Page.waitForTimeout(2000); + await testManager.agent1Page.waitForTimeout(2000); + + clearAdvancedCapturedLogs(); + await consultOrTransfer( + testManager.agent1Page, + 'agent', + 'consult', + process.env[`${testManager.projectName}_AGENT2_NAME`]! + ); + const consultRequestDiv = testManager.agent2Page.getByTestId('samples:incoming-task-telephony').first(); + await consultRequestDiv.waitFor({state: 'visible', timeout: 60000}); + await acceptIncomingTask(testManager.agent2Page, TASK_TYPES.CALL); + await testManager.agent2Page.waitForTimeout(3000); + await verifyCurrentState(testManager.agent2Page, USER_STATES.ENGAGED); + await testManager.agent1Page.getByTestId('transfer-consult-btn').click(); + await testManager.agent1Page.waitForTimeout(2000); + await submitWrapup(testManager.agent1Page, WRAPUP_REASONS.SALE); + await testManager.agent2Page.waitForTimeout(3000); + await verifyCurrentState(testManager.agent2Page, USER_STATES.ENGAGED); + + await consultOrTransfer(testManager.agent2Page, 'dialNumber', 'consult', process.env.PW_DIAL_NUMBER_NAME!); + await ensureDialNumberLoggedIn(testManager.dialNumberPage); + await acceptExtensionCall(testManager.dialNumberPage); + await testManager.agent2Page.getByTestId('transfer-consult-btn').click(); + await submitWrapup(testManager.agent2Page, WRAPUP_REASONS.SALE); + await verifyConsultTransferredLogs(); + await endCallTask(testManager.dialNumberPage); + }); + test.afterAll(async () => { await testManager.cleanup(); }); diff --git a/playwright/tests/advanced-task-controls-test.spec.ts b/playwright/tests/advanced-task-controls-test.spec.ts index e43d4909d..4b554e8bc 100644 --- a/playwright/tests/advanced-task-controls-test.spec.ts +++ b/playwright/tests/advanced-task-controls-test.spec.ts @@ -7,6 +7,7 @@ import { verifyConsultStartSuccessLogs, verifyConsultEndSuccessLogs, verifyConsultTransferredLogs, + ensureDialNumberLoggedIn, } from '../Utils/advancedTaskControlUtils'; import {changeUserState, verifyCurrentState} from '../Utils/userStateUtils'; @@ -139,9 +140,11 @@ export default function createAdvancedTaskControlsTests() { }); test('Call Blind Transferred to DialNumber', async () => { + await testManager.resetDialNumberSession(); await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'transfer', process.env.PW_DIAL_NUMBER_NAME); //DialNumber accepts the transfer + await ensureDialNumberLoggedIn(testManager.dialNumberPage); await acceptExtensionCall(testManager.dialNumberPage); verifyTransferSuccessLogs(); await endCallTask(testManager.callerPage!); @@ -155,10 +158,12 @@ export default function createAdvancedTaskControlsTests() { }); test('Call Blind Transferred to Queue with DialNumber', async () => { + await testManager.resetDialNumberSession(); // First transfer from Agent 1 to Agent 2 await consultOrTransfer(testManager.agent1Page, 'queue', 'transfer', 'queue with dn'); //DialNumber accepts the transfer + await ensureDialNumberLoggedIn(testManager.dialNumberPage); await acceptExtensionCall(testManager.dialNumberPage); verifyTransferSuccessLogs(); await endCallTask(testManager.callerPage!);