diff --git a/playwright/Utils/advancedTaskControlUtils.ts b/playwright/Utils/advancedTaskControlUtils.ts index 8b3364770..cc9d401a5 100644 --- a/playwright/Utils/advancedTaskControlUtils.ts +++ b/playwright/Utils/advancedTaskControlUtils.ts @@ -1,7 +1,7 @@ import {Page, expect} from '@playwright/test'; import {loginExtension} from './incomingTaskUtils'; import {dismissOverlays} from './helperUtils'; -import {AWAIT_TIMEOUT, FORM_FIELD_TIMEOUT} from '../constants'; +import {AWAIT_TIMEOUT, FORM_FIELD_TIMEOUT, EXTENSION_REGISTRATION_TIMEOUT} from '../constants'; /** * Utility functions for advanced task controls testing. @@ -118,6 +118,7 @@ export async function consultOrTransfer( action: 'consult' | 'transfer', value: string ): Promise { + await page.bringToFront(); await openConsultOrTransferMenu(page, action); const popover = await getPopover(page); @@ -139,14 +140,13 @@ export async function consultOrTransfer( // ===== Internal helper functions ===== async function openConsultOrTransferMenu(page: Page, action: 'consult' | 'transfer'): Promise { + await page.bringToFront(); + await dismissOverlays(page); + if (action === 'consult') { - await dismissOverlays(page); - await page.getByTestId('call-control:consult').nth(1).click({timeout: AWAIT_TIMEOUT}); + await page.getByTestId('call-control:consult').first().click({timeout: AWAIT_TIMEOUT}); } else { - await page - .getByRole('group', {name: 'Call Control with Call'}) - .getByLabel('Transfer Call') - .click({timeout: AWAIT_TIMEOUT}); + await page.getByTestId('call-control:transfer').first().click({timeout: AWAIT_TIMEOUT}); } } @@ -161,7 +161,7 @@ async function clickCategory( popover: ReturnType, name: 'Agents' | 'Queues' | 'Dial Number' | 'Entry Point' ): Promise { - const button = popover.getByRole('button', {name}); + const button = popover.getByRole('button', {name, exact: true}); await button.click({timeout: AWAIT_TIMEOUT}); await page.waitForTimeout(200); } @@ -262,29 +262,3 @@ 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 cede87b13..6809fc6e6 100644 --- a/playwright/Utils/helperUtils.ts +++ b/playwright/Utils/helperUtils.ts @@ -22,6 +22,7 @@ import { 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) @@ -120,6 +121,7 @@ export async function waitForWebSocketReconnection( export const waitForState = async (page: Page, expectedState: userState): Promise => { try { + await page.bringToFront(); await page.waitForFunction( async (expectedStateArg) => { // Re-import getCurrentState in the browser context @@ -303,193 +305,353 @@ export function isColorClose(receivedColor: string, expectedColor: ThemeColor, t * 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) - * @param maxIterations - Maximum number of task handling iterations to prevent infinite loops (default: 10) + * @param maxIterations - Maximum number of iterations to prevent infinite loops (default: 10) * @returns Promise - * @description Continuously checks for incoming tasks, accepts them, and performs wrap-up actions until no more tasks are available + * @description Checks in order: RONA popup → incoming tasks → end button → wrapup button + * Continues until nothing actionable is found or maxIterations reached * @example * ```typescript * await handleStrayTasks(page, extensionPage); * ``` */ - export const handleStrayTasks = async ( page: Page, extensionPage: Page | null = null, maxIterations: number = 10 ): Promise => { - await page.waitForTimeout(1000); + const startTime = Date.now(); - const stateSelectVisible = await page - .getByTestId('state-select') - .waitFor({state: 'visible', timeout: 30000}) - .then(() => true) - .catch(() => false); + let iteration = 0; + let tasksHandled = 0; - if (stateSelectVisible) { - const ronapopupVisible = await page - .getByTestId('samples:rona-popup') - .waitFor({state: 'visible', timeout: AWAIT_TIMEOUT}) - .then(() => true) - .catch(() => false); + while (iteration < maxIterations) { + iteration++; + let actionTaken = false; + + // Dismiss any overlays/popovers first + await page.keyboard.press('Escape').catch(() => {}); + await page.waitForTimeout(100); - if (ronapopupVisible) { - await submitRonaPopup(page, RONA_OPTIONS.AVAILABLE); + // ============================================ + // STEP 1: Check for RONA popup + // ============================================ + const ronaPopup = page.getByTestId('samples:rona-popup'); + const ronaVisible = await ronaPopup.isVisible().catch(() => false); + + if (ronaVisible) { + try { + await submitRonaPopup(page, RONA_OPTIONS.AVAILABLE); + actionTaken = true; + await page.waitForTimeout(300); + continue; // Start fresh after RONA + } catch (e) { + } } - await changeUserState(page, USER_STATES.AVAILABLE); - await page.waitForTimeout(4000); - } - const incomingTaskDiv = page.getByTestId(/^samples:incoming-task(-\w+)?$/); + // ============================================ + // STEP 2: Check for wrapup FIRST (complete pending tasks before accepting new ones) + // ============================================ + const wrapupButton = page.getByTestId('call-control:wrapup-button').first(); + const wrapupVisible = await wrapupButton.isVisible().catch(() => false); + + if (wrapupVisible) { + try { + await submitWrapup(page, WRAPUP_REASONS.SALE); + tasksHandled++; + actionTaken = true; + await page.waitForTimeout(300); + continue; // Check for more pending tasks + } catch (e) { + } + } + + // ============================================ + // STEP 3: Check for end button (end active calls before accepting new ones) + // ============================================ + const endButton = page.getByTestId('call-control:end-call').first(); + const endButtonVisible = await endButton.isVisible().catch(() => false); + + if (endButtonVisible) { + let endButtonEnabled = await endButton.isEnabled().catch(() => false); + + if (!endButtonEnabled) { + // End button disabled - try to resume from hold first + const holdToggle = page.getByTestId('call-control:hold-toggle').first(); + const holdToggleVisible = await holdToggle.isVisible().catch(() => false); + + if (holdToggleVisible) { + try { + await holdCallToggle(page); + await page.waitForTimeout(500); + endButtonEnabled = await endButton.isEnabled().catch(() => false); + } catch (e) { + } + } else { + } + } + + if (endButtonEnabled) { + try { + await endButton.click({timeout: AWAIT_TIMEOUT}); + await page.waitForTimeout(500); - let iterations = 0; - while (iterations < maxIterations) { - iterations++; - let flag1 = false; - let flag2 = true; + // Verify the click worked - either end button gone or wrapup appeared + const endStillVisible = await endButton.isVisible().catch(() => false); + const wrapupNowVisible = await wrapupButton.isVisible().catch(() => false); - // Check if there's actually anything to handle before processing + if (!endStillVisible || wrapupNowVisible) { + actionTaken = true; + // Don't continue - fall through to check wrapup immediately + } else { + } + } catch (e) { + } + } + + // After clicking end, check for wrapup immediately (same iteration) + const wrapupAfterEnd = await wrapupButton.isVisible().catch(() => false); + if (wrapupAfterEnd) { + try { + await submitWrapup(page, WRAPUP_REASONS.SALE); + tasksHandled++; + actionTaken = true; + await page.waitForTimeout(300); + continue; + } catch (e) { + } + } + + if (actionTaken) { + continue; + } + } + + // ============================================ + // STEP 4: Check for incoming tasks (only accept if no active task to handle) + // ============================================ + const incomingTaskDiv = page.getByTestId(/^samples:incoming-task(-\w+)?$/); const hasIncomingTask = await incomingTaskDiv .first() .isVisible() .catch(() => false); - const hasEndButton = await page - .getByTestId('call-control:end-call') - .first() - .isVisible() - .catch(() => false); - const hasWrapupButton = await page - .getByTestId('call-control:wrapup-button') - .first() - .isVisible() - .catch(() => false); - - if (!hasIncomingTask && !hasEndButton && !hasWrapupButton) { - // Nothing to handle, exit early - break; - } - - // Inner task acceptance loop with timeout protection - let taskAttempts = 0; - const maxTaskAttempts = 5; - while (taskAttempts < maxTaskAttempts) { - taskAttempts++; + if (hasIncomingTask) { 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...'); + const taskText = await task.innerText().catch(() => ''); + const isExtensionCall = taskText.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}) // Restored original timeout - .then(() => true) - .catch(() => false); - if (extensionCallVisible) { - await acceptExtensionCall(extensionPage); - flag1 = true; + // Extension call - try extensionPage first, fallback to waiting for RONA + if (extensionPage) { + try { + // Dismiss any dialogs on extension page first + await extensionPage.keyboard.press('Escape').catch(() => {}); + await extensionPage.waitForTimeout(200); + + const extButton = extensionPage.locator('#answer').first(); + const extButtonVisible = await extButton + .waitFor({state: 'visible', timeout: 5000}) + .then(() => true) + .catch(() => false); + + if (extButtonVisible) { + // Use shorter timeout for cleanup - don't block for 40s like acceptExtensionCall does + const isEnabled = await extButton.isEnabled({timeout: 5000}).catch(() => false); + if (isEnabled) { + await extButton.click({timeout: AWAIT_TIMEOUT}); + } else { + actionTaken = true; + continue; + } + await page.waitForTimeout(500); + // After accepting, immediately try to end and wrapup + const endBtnAfterAccept = page.getByTestId('call-control:end-call').first(); + const endVisibleAfterAccept = await endBtnAfterAccept.isVisible().catch(() => false); + if (endVisibleAfterAccept) { + const endEnabledAfterAccept = await endBtnAfterAccept.isEnabled().catch(() => false); + if (endEnabledAfterAccept) { + await endBtnAfterAccept.click({timeout: AWAIT_TIMEOUT}).catch(() => {}); + await page.waitForTimeout(500); + const wrapupAfterEnd = await wrapupButton.isVisible().catch(() => false); + if (wrapupAfterEnd) { + await submitWrapup(page, WRAPUP_REASONS.SALE).catch(() => {}); + tasksHandled++; + await page.waitForTimeout(300); + } + } + } + actionTaken = true; + continue; + } + } catch (e) { + } } else { - console.warn('Extension call timeout - skipping task'); - break; // Skip this task instead of throwing error + // No extensionPage - wait for RONA timeout + await page.waitForTimeout(2000); + // Check if RONA appeared + const ronaAfterWait = await ronaPopup.isVisible().catch(() => false); + if (ronaAfterWait) { + continue; // Handle RONA on next iteration + } + // If still no RONA, we can't handle this - exit + const stillHasExtCall = await incomingTaskDiv + .first() + .isVisible() + .catch(() => false); + if (stillHasExtCall) { + break; + } } } else { - try { - await acceptButton.click({timeout: AWAIT_TIMEOUT}); - flag1 = true; - } catch (error) { - console.warn('Failed to click accept button:', error); + // Regular task - check if accept button is enabled + const acceptButton = task.getByTestId('task:accept-button').first(); + const acceptVisible = await acceptButton.isVisible().catch(() => false); + const acceptEnabled = await acceptButton.isEnabled().catch(() => false); + + if (acceptVisible && acceptEnabled) { + try { + await acceptButton.click({timeout: AWAIT_TIMEOUT}); + await page.waitForTimeout(2000); + // After accepting, immediately try to end and wrapup (same iteration) + const endBtnAfterAccept = page.getByTestId('call-control:end-call').first(); + const endVisibleAfterAccept = await endBtnAfterAccept.isVisible().catch(() => false); + if (endVisibleAfterAccept) { + const endEnabledAfterAccept = await endBtnAfterAccept.isEnabled().catch(() => false); + if (endEnabledAfterAccept) { + await endBtnAfterAccept.click({timeout: AWAIT_TIMEOUT}).catch(() => {}); + await page.waitForTimeout(500); + const wrapupAfterEnd = await wrapupButton.isVisible().catch(() => false); + if (wrapupAfterEnd) { + await submitWrapup(page, WRAPUP_REASONS.SALE).catch(() => {}); + tasksHandled++; + await page.waitForTimeout(300); + } + } + } + actionTaken = true; + continue; + } catch (e) { + } + } else if (acceptVisible && !acceptEnabled) { } } - 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: AWAIT_TIMEOUT}); - 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) + // ============================================ + // Check if anything is still pending that we couldn't handle + // ============================================ + if (!actionTaken) { + const stillHasTask = await incomingTaskDiv + .first() + .isVisible() .catch(() => false); - if (isWrapupBoxVisible) { - await page.waitForTimeout(2000); - await submitWrapup(page, WRAPUP_REASONS.SALE); - await page.waitForTimeout(2000); + const stillHasEnd = await endButton.isVisible().catch(() => false); + const stillHasWrapup = await wrapupButton.isVisible().catch(() => false); + + // Check if end button is visible but disabled (stuck state) + if (stillHasEnd && !stillHasWrapup) { + const endEnabled = await endButton.isEnabled().catch(() => false); + const holdToggle = page.getByTestId('call-control:hold-toggle').first(); + const holdVisible = await holdToggle.isVisible().catch(() => false); + + if (!endEnabled && !holdVisible) { + break; + } + } + + if (stillHasWrapup) { + await page.waitForTimeout(500); + } else if (stillHasEnd) { + const endEnabled = await endButton.isEnabled().catch(() => false); + if (endEnabled) { + if (iteration >= 3) { + break; + } + await page.waitForTimeout(500); + } + } else if (stillHasTask) { + await page.waitForTimeout(500); } else { - flag2 = false; + break; } } + } - if (!flag1 && !flag2) { - break; + if (iteration >= maxIterations) { + } + + // Ensure user is in Available state at the end + const stateSelectVisible = await page + .getByTestId('state-select') + .isVisible() + .catch(() => false); + + if (stateSelectVisible) { + try { + await changeUserState(page, USER_STATES.AVAILABLE); + } catch (e) { } } - console.log(`Completed stray task handling after ${iterations} iterations`); + const duration = Date.now() - startTime; }; /** * 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. + * Follows same logic as handleStrayTasks: end button (resume if disabled) → wrapup + * @returns true if something was cleared, false otherwise */ -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; - } +export async function clearPendingCallAndWrapup(page: Page): Promise { + // Dismiss any open popovers first + await page.keyboard.press('Escape').catch(() => {}); + await page.waitForTimeout(200); const endBtn = page.getByTestId('call-control:end-call').first(); + const wrapupBtn = page.getByTestId('call-control:wrapup-button').first(); + + // Check end button first const endVisible = await endBtn.isVisible().catch(() => false); + if (endVisible) { - const endEnabled = await endBtn.isEnabled().catch(() => false); + let endEnabled = await endBtn.isEnabled().catch(() => false); + + // If disabled, try to resume from hold + if (!endEnabled) { + try { + await holdCallToggle(page); + await page.waitForTimeout(500); + endEnabled = await endBtn.isEnabled().catch(() => false); + } catch { + // Resume failed, continue + } + } + + // Try to end the call 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 {} + } catch { + // End click failed, continue } + } + } - // 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 {} - } + // Check wrapup button + const wrapupVisible = await wrapupBtn.isVisible().catch(() => false); + + if (wrapupVisible) { + try { + await submitWrapup(page, WRAPUP_REASONS.SALE); + await page.waitForTimeout(500); + return true; + } catch { + return false; } } + + // Return true if end button was clicked (even without wrapup) + return endVisible; } /* @@ -551,7 +713,7 @@ export const pageSetup = async ( if (loginButtonExists) { await telephonyLogin(page, loginMode, extensionNumber); } else { - await stationLogout(page); + await stationLogout(page, false); // Don't throw during setup - just try to logout await telephonyLogin(page, loginMode, extensionNumber); } diff --git a/playwright/Utils/incomingTaskUtils.ts b/playwright/Utils/incomingTaskUtils.ts index 78a3935b8..f6d56090f 100644 --- a/playwright/Utils/incomingTaskUtils.ts +++ b/playwright/Utils/incomingTaskUtils.ts @@ -8,13 +8,12 @@ import { DEFAULT_MAX_RETRIES, CHAT_LAUNCHER_TIMEOUT, FORM_FIELD_TIMEOUT, - OPERATION_TIMEOUT, - NETWORK_OPERATION_TIMEOUT, TEST_DATA, UI_SETTLE_TIMEOUT, + EXTENSION_REGISTRATION_TIMEOUT, + ACCEPT_TASK_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 @@ -39,39 +38,22 @@ const transporter = nodemailer.createTransport({ * @param number Phone number to dial (defaults to PW_ENTRY_POINT env variable) */ export async function createCallTask(page: Page, number: string) { + await page.bringToFront(); if (!number || number.trim() === '') { throw new Error('Dial number is required'); } - try { - await expect(page).toHaveURL(/.*\.webex\.com\/calling.*/); - } 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)) { + const endBtn = page.getByTestId('end'); + if (await endBtn.isEnabled({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 page.locator('#destination').waitFor({state: 'visible', timeout: AWAIT_TIMEOUT}); + await page.locator('#destination').fill(number, {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); + await expect(page.locator('#create-call-action')).toBeVisible({timeout: AWAIT_TIMEOUT}); + await page.locator('#create-call-action').click({timeout: AWAIT_TIMEOUT}); } /** @@ -79,15 +61,11 @@ export async function createCallTask(page: Page, number: string) { * Prerequisite: The calling webclient must be logged in. * @param page Playwright Page object */ -export async function endCallTask(page: Page) { - try { - await expect(page).toHaveURL(/.*\.webex\.com\/calling.*/); - } catch (error) { - throw new Error('The Input Page should be logged into calling web-client.'); - } - await page.locator('[data-test="call-end"]').waitFor({state: 'visible', timeout: 4000}); - await page.locator('[data-test="call-end"]').click({timeout: AWAIT_TIMEOUT}); - await page.waitForTimeout(500); +export async function endCallTask(page: Page, isCaller: boolean = false) { + await page.bringToFront(); + const endBtn = isCaller ? page.locator('#end-call').first() : page.locator('#end').first(); + await expect(endBtn).toBeEnabled({timeout: AWAIT_TIMEOUT}); + await endBtn.click({timeout: AWAIT_TIMEOUT}); } /** @@ -239,47 +217,85 @@ export async function createEmailTask(to: string) { } /** - * Accepts an incoming task of the given type (call, chat, email, social). - * Expects the incoming task to be already there. + * Gets the incoming task div locator for a given task type. * @param page Playwright Page object * @param type Task type (see TASK_TYPES) - * @throws Error if accept button is not found + * @returns Locator for the incoming task div */ -export async function acceptIncomingTask(page: Page, type: TaskType) { - await page.waitForTimeout(2000); - let incomingTaskDiv; +export function getIncomingTaskLocator(page: Page, type: TaskType) { if (type === TASK_TYPES.CALL) { - incomingTaskDiv = page.getByTestId('samples:incoming-task-telephony').first(); - const isExtensionCall = await (await incomingTaskDiv.innerText()).includes(TEST_DATA.EXTENSION_CALL_INDICATOR); - if (isExtensionCall) { - throw new Error('This is an extension call, use acceptExtensionCall instead'); - } + return page.getByTestId('samples:incoming-task-telephony').first(); } else if (type === TASK_TYPES.CHAT) { - incomingTaskDiv = page.getByTestId('samples:incoming-task-chat').first(); + return page.getByTestId('samples:incoming-task-chat').first(); } else if (type === TASK_TYPES.EMAIL) { - incomingTaskDiv = page.getByTestId('samples:incoming-task-email').first(); + return page.getByTestId('samples:incoming-task-email').first(); } else if (type === TASK_TYPES.SOCIAL) { - incomingTaskDiv = page.locator('samples:incoming-task-social').first(); + return page.locator('samples:incoming-task-social').first(); } - incomingTaskDiv = incomingTaskDiv.first(); - await expect(incomingTaskDiv).toBeVisible({timeout: AWAIT_TIMEOUT}); - const acceptButton = incomingTaskDiv.getByTestId('task:accept-button').first(); - if (!(await acceptButton.isVisible())) { - throw new Error('Accept button not found'); + throw new Error(`Unknown task type: ${type}`); +} + +/** + * Waits for an incoming task of the given type to be visible. + * Brings the page to front and waits for the task div to appear. + * @param page Playwright Page object + * @param type Task type (see TASK_TYPES) + * @param timeout Optional timeout in ms (default: 40000) + * @returns Locator for the incoming task div + */ +export async function waitForIncomingTask(page: Page, type: TaskType, timeout: number = ACCEPT_TASK_TIMEOUT) { + await page.bringToFront(); + const incomingTaskDiv = getIncomingTaskLocator(page, type); + await incomingTaskDiv.waitFor({state: 'visible', timeout}); + return incomingTaskDiv; +} + +/** + * Accepts an incoming task of the given type (call, chat, email, social). + * Waits for the task to appear, then clicks the accept button. + * @param page Playwright Page object + * @param type Task type (see TASK_TYPES) + * @param timeout Optional timeout in ms for waiting for task (default: 40000) + * @throws Error if accept button is not found or if this is an extension call + */ +export async function acceptIncomingTask(page: Page, type: TaskType, timeout: number = ACCEPT_TASK_TIMEOUT) { + await page.bringToFront(); + + const incomingTaskDiv = await waitForIncomingTask(page, type, timeout); + + // Check if this is an extension call (only for CALL type) + if (type === TASK_TYPES.CALL) { + const taskText = await incomingTaskDiv.innerText(); + if (taskText.includes(TEST_DATA.EXTENSION_CALL_INDICATOR)) { + throw new Error('This is an extension call, use acceptExtensionCall instead'); + } } - // Wait for button to be enabled and clickable + const acceptButton = incomingTaskDiv.getByTestId('task:accept-button').first(); await acceptButton.waitFor({state: 'visible', timeout: AWAIT_TIMEOUT}); await expect(acceptButton).toBeEnabled({timeout: AWAIT_TIMEOUT}); - // Use force click as backup or add retry logic try { + await page.waitForTimeout(2000); await acceptButton.click({timeout: AWAIT_TIMEOUT}); - } catch (error) { + } catch { // Retry with force click if normal click fails await acceptButton.click({force: true, timeout: AWAIT_TIMEOUT}); } + await page.waitForTimeout(2000); + + // Verify the task was actually accepted by checking if incoming task div is gone + let isStillVisible = await incomingTaskDiv.isVisible().catch(() => false); + if (isStillVisible) { + // Retry clicking the accept button one more time + const retryAcceptButton = incomingTaskDiv.getByTestId('task:accept-button').first(); + const isRetryButtonVisible = await retryAcceptButton.isVisible().catch(() => false); + if (isRetryButtonVisible) { + await retryAcceptButton.click({force: true, timeout: AWAIT_TIMEOUT}); + await page.waitForTimeout(2000); + } + } } /** @@ -290,6 +306,7 @@ export async function acceptIncomingTask(page: Page, type: TaskType) { * @throws Error if decline button is not found */ export async function declineIncomingTask(page: Page, type: TaskType) { + await page.bringToFront(); let incomingTaskDiv; if (type === TASK_TYPES.CALL) { incomingTaskDiv = page.getByTestId('samples:incoming-task-telephony').first(); @@ -320,15 +337,10 @@ export async function declineIncomingTask(page: Page, type: TaskType) { * @param page Playwright Page object */ export async function acceptExtensionCall(page: Page) { - try { - await page.waitForTimeout(2000); - await expect(page).toHaveURL(/.*\.webex\.com\/calling.*/); - } catch (error) { - throw new Error('The Input Page should be logged into calling web-client.'); - } - await page.locator('[data-test="right-action-button"]').waitFor({state: 'visible', timeout: AWAIT_TIMEOUT}); - await page.locator('[data-test="right-action-button"]').click({timeout: AWAIT_TIMEOUT}); - await page.waitForTimeout(500); + await page.bringToFront(); + await expect(page.locator('#answer').first()).toBeEnabled({timeout: EXTENSION_REGISTRATION_TIMEOUT}); + await page.waitForTimeout(2000); + await page.locator('#answer').first().click({timeout: AWAIT_TIMEOUT}); } /** @@ -336,14 +348,11 @@ export async function acceptExtensionCall(page: Page) { * @param page Playwright Page object */ export async function declineExtensionCall(page: Page) { - try { - await page.waitForTimeout(2000); - await expect(page).toHaveURL(/.*\.webex\.com\/calling.*/); - } catch (error) { - throw new Error('The Input Page should be logged into calling web-client.'); - } - await page.locator('[data-test="left-action-button"]').waitFor({state: 'visible', timeout: AWAIT_TIMEOUT}); - await page.locator('[data-test="left-action-button"]').click({timeout: AWAIT_TIMEOUT}); + await page.bringToFront(); + const endBtn = page.locator('#end').first(); + await expect(endBtn).toBeEnabled({timeout: AWAIT_TIMEOUT}); + await page.waitForTimeout(2000); + await endBtn.click({timeout: AWAIT_TIMEOUT}); } /** @@ -351,15 +360,10 @@ export async function declineExtensionCall(page: Page) { * @param page Playwright Page object */ export async function endExtensionCall(page: Page) { - try { - await page.waitForTimeout(2000); - await expect(page).toHaveURL(/.*\.webex\.com\/calling.*/); - } catch (error) { - throw new Error('The Input Page should be logged into calling web-client.'); - } - await page.locator('[data-test="end-call"]').waitFor({state: 'visible', timeout: AWAIT_TIMEOUT}); - await page.locator('[data-test="end-call"]').click({timeout: AWAIT_TIMEOUT}); - await page.waitForTimeout(500); + await page.bringToFront(); + const endBtn = page.locator('#end-call').first(); + await expect(endBtn).toBeEnabled({timeout: AWAIT_TIMEOUT}); + await endBtn.click({timeout: AWAIT_TIMEOUT}); } /** @@ -370,53 +374,25 @@ export async function endExtensionCall(page: Page) { * @param password User password * @throws Error if login fails after maxRetries */ -export async function loginExtension(page: Page, email: string, password: string) { - if (!email || !password) { - throw new Error('Email and password are required for loginExtension'); +export async function loginExtension(page: Page, token: string) { + await page.bringToFront(); + if (!token) { + throw new Error('Token is required for loginExtension'); } - if (email.trim() === '' || password.trim() === '') { - throw new Error('Email and password cannot be empty strings for loginExtension'); - } - if (!CALL_URL) { - throw new Error('CALL_URL is not defined. Please check your constants file.'); + if (token.trim() === '') { + throw new Error('Token cannot be empty strings for loginExtension'); } - for (let i = 0; i < DEFAULT_MAX_RETRIES; i++) { - try { - await page.goto(CALL_URL); - break; - } catch (error) { - if (i === DEFAULT_MAX_RETRIES - 1) { - throw new Error(`Failed to login via extension after ${DEFAULT_MAX_RETRIES} attempts: ${error}`); - } - } - } - const isLoginPageVisible = await page - .getByRole('textbox', {name: 'Email address (required)'}) - .waitFor({state: 'visible', timeout: OPERATION_TIMEOUT}) - .then(() => true) - .catch(() => false); - if (!isLoginPageVisible) { - await expect(page.getByRole('button', {name: 'Back to sign in'})).toBeVisible({timeout: AWAIT_TIMEOUT}); - await page.getByRole('button', {name: 'Back to sign in'}).click({timeout: AWAIT_TIMEOUT}); - await page.getByRole('button', {name: 'Sign in'}).waitFor({state: 'visible', timeout: AWAIT_TIMEOUT}); - await page.getByRole('button', {name: 'Sign in'}).click({timeout: AWAIT_TIMEOUT}); - } - await page - .getByRole('textbox', {name: 'Email address (required)'}) - .waitFor({state: 'visible', timeout: FORM_FIELD_TIMEOUT}); - await page.getByRole('textbox', {name: 'Email address (required)'}).fill(email, {timeout: AWAIT_TIMEOUT}); - await page.getByRole('textbox', {name: 'Email address (required)'}).press('Enter', {timeout: AWAIT_TIMEOUT}); - await page.getByRole('textbox', {name: 'Password'}).waitFor({state: 'visible', timeout: FORM_FIELD_TIMEOUT}); - await page.getByRole('textbox', {name: 'Password'}).fill(password, {timeout: AWAIT_TIMEOUT}); - await page.getByRole('textbox', {name: 'Password'}).press('Enter', {timeout: AWAIT_TIMEOUT}); - await page.getByRole('textbox', {name: 'Dial'}).waitFor({state: 'visible', timeout: NETWORK_OPERATION_TIMEOUT}); - try { - await page.locator('[data-test="statusMessage"]').waitFor({state: 'hidden', timeout: NETWORK_OPERATION_TIMEOUT}); - } catch (e) { - throw new Error('Unable to Login to the webex calling web-client'); - } + await page.goto(CALL_URL); + await page.locator('#access-token').fill(token); + await page.locator('#access-token-save').click(); + await expect(page.locator('#registration-register')).toBeEnabled({timeout: EXTENSION_REGISTRATION_TIMEOUT}); + await page.locator('#registration-register').click(); + await expect(page.locator('#registration-status')).toContainText('Registered, deviceId', { + timeout: EXTENSION_REGISTRATION_TIMEOUT, + }); + await page.locator('#sd-get-media-streams').click(); } /** @@ -429,7 +405,7 @@ export async function submitRonaPopup(page: Page, nextState: RonaOption) { if (!nextState) { throw new Error('RONA next state selection is required'); } - await page.waitForTimeout(1000); + await page.bringToFront(); await page.getByTestId('samples:rona-popup').waitFor({state: 'visible', timeout: AWAIT_TIMEOUT}); await page.waitForTimeout(1000); await expect(page.getByTestId('samples:rona-select-state')).toBeVisible({timeout: AWAIT_TIMEOUT}); diff --git a/playwright/Utils/initUtils.ts b/playwright/Utils/initUtils.ts index af4098561..82d077b46 100644 --- a/playwright/Utils/initUtils.ts +++ b/playwright/Utils/initUtils.ts @@ -33,7 +33,8 @@ export const loginViaAccessToken = async (page: Page, accessToken: string): Prom * Performs OAuth login with Webex using agent credentials from environment variables * @param page - The Playwright page object * @param agentId - Agent identifier to validate against environment variables (e.g., 'SET_1_AGENT1', 'SET_2_AGENT2') - * @description Validates credentials against {agentId}_USERNAME and PW_SANDBOX_PASSWORD + * @param customPassword - Optional custom password. If not provided, uses PW_SANDBOX_PASSWORD from environment + * @description Validates credentials against {agentId}_USERNAME and PW_SANDBOX_PASSWORD (or custom password) * @throws {Error} When agent credentials are not found in environment variables * @example * ```typescript @@ -41,9 +42,10 @@ export const loginViaAccessToken = async (page: Page, accessToken: string): Prom * await oauthLogin(page, 'SET_1_AGENT1'); // validates against SET_1_AGENT1_USERNAME/PW_SANDBOX_PASSWORD * await oauthLogin(page, 'SET_1_AGENT2'); // validates against SET_1_AGENT2_USERNAME/PW_SANDBOX_PASSWORD * await oauthLogin(page, 'SET_2_AGENT1'); // validates against SET_2_AGENT1_USERNAME/PW_SANDBOX_PASSWORD + * await oauthLogin(page, 'custom_user', 'custom_password'); // uses custom password * ``` */ -export const oauthLogin = async (page: Page, username: string): Promise => { +export const oauthLogin = async (page: Page, username: string, customPassword?: string): Promise => { // Check 1: Validate username parameter is provided if (!username) { throw new Error('Username parameter is required'); @@ -54,8 +56,8 @@ export const oauthLogin = async (page: Page, username: string): Promise => throw new Error('Username cannot be empty string'); } - // Check 3: Get credentials from environment variables - const password = process.env[`PW_SANDBOX_PASSWORD`]; + // Check 3: Get credentials from environment variables or use custom password + const password = customPassword || process.env[`PW_SANDBOX_PASSWORD`]; // Check 6: Validate environment variables are set if (!username || !password) { throw new Error(`Environment variables ${username} and PW_SANDBOX_PASSWORD must be set`); diff --git a/playwright/Utils/stationLoginUtils.ts b/playwright/Utils/stationLoginUtils.ts index 65c754686..57837c755 100644 --- a/playwright/Utils/stationLoginUtils.ts +++ b/playwright/Utils/stationLoginUtils.ts @@ -1,6 +1,12 @@ import {Page, expect} from '@playwright/test'; import dotenv from 'dotenv'; -import {LOGIN_MODE, LONG_WAIT, AWAIT_TIMEOUT, DROPDOWN_SETTLE_TIMEOUT, OPERATION_TIMEOUT} from '../constants'; +import { + LOGIN_MODE, + EXTENSION_REGISTRATION_TIMEOUT, + AWAIT_TIMEOUT, + DROPDOWN_SETTLE_TIMEOUT, + OPERATION_TIMEOUT, +} from '../constants'; import {handleStrayTasks} from './helperUtils'; dotenv.config(); @@ -117,36 +123,52 @@ export const dialLogin = async (page: Page, dialNumber?: string): Promise * await stationLogout(page); * ``` */ -export const stationLogout = async (page: Page): Promise => { - // Ensure the logout button is visible before clicking +export const stationLogout = async (page: Page, throwOnFailure: boolean = true): Promise => { + // Wait for the logout button to be visible before clicking const logoutButton = page.getByTestId('samples:station-logout-button'); - const isLogoutButtonVisible = await logoutButton.isVisible().catch(() => false); - if (!isLogoutButtonVisible) { - throw new Error('Station logout button is not visible. Cannot perform logout.'); + const isVisible = await logoutButton + .waitFor({state: 'visible', timeout: AWAIT_TIMEOUT}) + .then(() => true) + .catch(() => false); + + if (!isVisible) { + return; } - await page.getByTestId('samples:station-logout-button').click({timeout: AWAIT_TIMEOUT}); - //check if the station logout button is hidden after logouts + + await logoutButton.click({timeout: AWAIT_TIMEOUT}).catch(() => {}); + + //check if the station logout button is hidden after logout const isLogoutButtonHidden = await page .getByTestId('samples:station-logout-button') .waitFor({state: 'hidden', timeout: OPERATION_TIMEOUT}) .then(() => true) .catch(() => false); + if (!isLogoutButtonHidden) { try { await handleStrayTasks(page); - await page.getByTestId('samples:station-logout-button').click({timeout: AWAIT_TIMEOUT}); + await page + .getByTestId('samples:station-logout-button') + .click({force: true, timeout: AWAIT_TIMEOUT}) + .catch(() => {}); + // Verify logout was successful after retry const isLogoutSuccessfulAfterRetry = await page .getByTestId('samples:station-logout-button') .waitFor({state: 'hidden', timeout: OPERATION_TIMEOUT}) .then(() => true) .catch(() => false); - if (!isLogoutSuccessfulAfterRetry) { + + if (!isLogoutSuccessfulAfterRetry && throwOnFailure) { throw new Error('Station logout button is still visible after retry attempt'); } } catch (e) { - throw new Error(`Station logout failed: ${e instanceof Error ? e.message : 'Unknown error'}`); + if (throwOnFailure) { + throw new Error(`Station logout failed: ${e instanceof Error ? e.message : 'Unknown error'}`); + } } + } else { + await page.waitForTimeout(2000); } }; @@ -224,7 +246,7 @@ export async function ensureUserStateVisible(page: Page, loginMode: string, numb .catch(() => false); if (!isUserStateWidgetVisible) { await telephonyLogin(page, loginMode, number); - await expect(page.getByTestId('state-select')).toBeVisible({timeout: LONG_WAIT}); + await expect(page.getByTestId('state-select')).toBeVisible({timeout: EXTENSION_REGISTRATION_TIMEOUT}); } } diff --git a/playwright/Utils/taskControlUtils.ts b/playwright/Utils/taskControlUtils.ts index 119773ae0..10c88d029 100644 --- a/playwright/Utils/taskControlUtils.ts +++ b/playwright/Utils/taskControlUtils.ts @@ -402,22 +402,26 @@ export async function verifyRemoteAudioTracks(page: Page): Promise { export async function verifyHoldMusicElement(page: Page): Promise { try { const holdMusicExists = await page.evaluate(() => { - // Look for audio elements with both autoplay and loop attributes - const audioElements = document.querySelectorAll('audio[autoplay][loop]'); + // Look for remote-audio element which handles hold music + const remoteAudio = document.getElementById('remote-audio') as HTMLAudioElement; - if (audioElements.length === 0) { + if (!remoteAudio) { return false; } - // Check if at least one element has the correct attributes - return Array.from(audioElements).some((audio) => { - const a = audio as HTMLAudioElement; - return a.hasAttribute('autoplay') && a.hasAttribute('loop') && a.autoplay === true && a.loop === true; - }); + // Check if the audio element has an active srcObject with active tracks + const srcObject = remoteAudio.srcObject as MediaStream; + if (!srcObject) { + return false; + } + + // Verify there are active audio tracks + const audioTracks = srcObject.getAudioTracks(); + return audioTracks.length > 0 && audioTracks.some((track) => track.enabled && track.readyState === 'live'); }); if (!holdMusicExists) { - throw new Error('❌ No hold music audio elements found with autoplay and loop attributes'); + throw new Error('❌ No hold music audio found on remote-audio element'); } } catch (error) { throw new Error(`❌ Hold music element verification failed: ${error.message}`); diff --git a/playwright/Utils/userStateUtils.ts b/playwright/Utils/userStateUtils.ts index 887e420c3..3c7e84969 100644 --- a/playwright/Utils/userStateUtils.ts +++ b/playwright/Utils/userStateUtils.ts @@ -19,6 +19,7 @@ dotenv.config(); export const changeUserState = async (page: Page, userState: string): Promise => { // Get the current state name with timeout, return early if not found try { + await page.bringToFront(); const currentState = await page .getByTestId('state-select') .getByTestId('state-name') @@ -40,7 +41,7 @@ export const changeUserState = async (page: Page, userState: string): Promise => { + await page.bringToFront(); const stateName = await page .getByTestId('state-select') .getByTestId('state-name') @@ -74,6 +76,7 @@ export const getCurrentState = async (page: Page): Promise => { * ``` */ export const verifyCurrentState = async (page: Page, expectedState: string): Promise => { + await page.bringToFront(); const currentState = await getCurrentState(page); if (currentState !== expectedState) { throw new Error(`Expected state "${expectedState}" but found "${currentState}".`); @@ -93,6 +96,7 @@ export const verifyCurrentState = async (page: Page, expectedState: string): Pro * ``` */ export const getStateElapsedTime = async (page: Page): Promise => { + await page.bringToFront(); // Directly select the timer by its test id const timerText = await page.getByTestId('elapsed-time').innerText({timeout: AWAIT_TIMEOUT}); return timerText.trim(); diff --git a/playwright/Utils/wrapupUtils.ts b/playwright/Utils/wrapupUtils.ts index 5ab4ebfaf..acee77d96 100644 --- a/playwright/Utils/wrapupUtils.ts +++ b/playwright/Utils/wrapupUtils.ts @@ -12,6 +12,12 @@ export async function submitWrapup(page: Page, reason: WrapupReason): Promise true) .catch(() => false); if (!isWrapupBoxVisible) throw new Error('Wrapup box is not visible'); - await wrapupBox.first().click({timeout: AWAIT_TIMEOUT}); - await page.waitForTimeout(UI_SETTLE_TIMEOUT); + + // Check if dropdown is already open (aria-expanded="true") + const isAlreadyOpen = (await wrapupBox.first().getAttribute('aria-expanded')) === 'true'; + if (!isAlreadyOpen) { + await wrapupBox.first().click({timeout: AWAIT_TIMEOUT}); + await page.waitForTimeout(UI_SETTLE_TIMEOUT); + } await expect(page.getByTestId('call-control:wrapup-select').first()).toBeVisible({timeout: AWAIT_TIMEOUT}); await page.getByTestId('call-control:wrapup-select').first().click({timeout: AWAIT_TIMEOUT}); await page.waitForTimeout(UI_SETTLE_TIMEOUT); diff --git a/playwright/constants.ts b/playwright/constants.ts index c0aaf4ace..dbbc0dfc8 100644 --- a/playwright/constants.ts +++ b/playwright/constants.ts @@ -28,7 +28,7 @@ export const LOGIN_MODE = { export type LoginMode = (typeof LOGIN_MODE)[keyof typeof LOGIN_MODE]; -export const LONG_WAIT = 40000; +export const EXTENSION_REGISTRATION_TIMEOUT = 40000; // Universal timeout for all await operations in Playwright tests export const AWAIT_TIMEOUT = 10000; @@ -41,10 +41,11 @@ export const DEFAULT_TIMEOUT = 5000; export const UI_SETTLE_TIMEOUT = 2000; export const FORM_FIELD_TIMEOUT = 20000; export const OPERATION_TIMEOUT = 30000; -export const NETWORK_OPERATION_TIMEOUT = 35000; +export const NETWORK_OPERATION_TIMEOUT = 40000; // Specific timeouts for incoming task operations export const CHAT_LAUNCHER_TIMEOUT = 60000; +export const ACCEPT_TASK_TIMEOUT = 60000; // Widget initialization timeouts export const WIDGET_INIT_TIMEOUT = 50000; @@ -75,7 +76,7 @@ export const PAGE_TYPES = { export type PageType = (typeof PAGE_TYPES)[keyof typeof PAGE_TYPES]; -export const CALL_URL = 'https://web.webex.com/calling?calling'; +export const CALL_URL = 'https://web-sdk.webex.com/samples/calling/'; export const TASK_TYPES = { CALL: 'Call', diff --git a/playwright/global.setup.ts b/playwright/global.setup.ts index 320d68914..d31a87066 100644 --- a/playwright/global.setup.ts +++ b/playwright/global.setup.ts @@ -100,4 +100,35 @@ setup('OAuth', async ({browser}) => { await page.close(); } } + + // OAuth for Dial Number Login user + const dialNumberUsername = process.env.PW_DIAL_NUMBER_LOGIN_USERNAME; + const dialNumberPassword = process.env.PW_DIAL_NUMBER_LOGIN_PASSWORD; + + if (dialNumberUsername && dialNumberPassword) { + const page = await browser.newPage(); + + await oauthLogin(page, dialNumberUsername, dialNumberPassword); + + await page.getByRole('textbox').click(); + const accessToken = await page.getByRole('textbox').inputValue(); + + const envPath = path.resolve(__dirname, '../.env'); + let envContent = ''; + if (fs.existsSync(envPath)) { + envContent = fs.readFileSync(envPath, 'utf8'); + // Remove any existing DIAL_NUMBER_LOGIN_ACCESS_TOKEN line + const accessTokenPattern = new RegExp(`^DIAL_NUMBER_LOGIN_ACCESS_TOKEN=.*$\\n?`, 'm'); + envContent = envContent.replace(accessTokenPattern, ''); + + // Ensure trailing newline + if (!envContent.endsWith('\n')) envContent += '\n'; + } + envContent += `DIAL_NUMBER_LOGIN_ACCESS_TOKEN=${accessToken}\n`; + // Clean up multiple consecutive empty lines + envContent = envContent.replace(/\n{3,}/g, '\n\n'); + fs.writeFileSync(envPath, envContent, 'utf8'); + + await page.close(); + } }); diff --git a/playwright/suites/dial-number-tests.spec.ts b/playwright/suites/dial-number-tests.spec.ts new file mode 100644 index 000000000..4ea3b9736 --- /dev/null +++ b/playwright/suites/dial-number-tests.spec.ts @@ -0,0 +1,4 @@ +import {test} from '@playwright/test'; +import createDialNumberTaskControlTests from '../tests/dial-number-task-control-test.spec'; + +test.describe('Dial Number Task Control Tests', createDialNumberTaskControlTests); diff --git a/playwright/suites/digital-incoming-task-tests.spec.ts b/playwright/suites/digital-incoming-task-tests.spec.ts index 865d52f08..33053164e 100644 --- a/playwright/suites/digital-incoming-task-tests.spec.ts +++ b/playwright/suites/digital-incoming-task-tests.spec.ts @@ -1,5 +1,5 @@ import {test} from '@playwright/test'; - import createDigitalIncomingTaskAndTaskControlsTests from '../tests/digital-incoming-task-and-task-controls.spec'; +import createDialNumberTaskControlTests from '../tests/dial-number-task-control-test.spec'; test.describe('Digital Incoming and Task Controls Tests', createDigitalIncomingTaskAndTaskControlsTests); diff --git a/playwright/test-data.ts b/playwright/test-data.ts index f60f1a0cb..491cff4d6 100644 --- a/playwright/test-data.ts +++ b/playwright/test-data.ts @@ -58,4 +58,15 @@ export const USER_SETS = { ENTRY_POINT: env.PW_ENTRY_POINT5, TEST_SUITE: 'advanced-task-controls-tests.spec.ts', }, + SET_6: { + AGENTS: { + AGENT1: {username: 'user17', extension: '1017', agentName: 'User17 Agent17'}, + AGENT2: {username: 'user18', extension: '1018', agentName: 'User18 Agent18'}, + }, + QUEUE_NAME: 'Queue e2e 6', + CHAT_URL: `${env.PW_CHAT_URL}-e2e-6.html`, + EMAIL_ENTRY_POINT: `${env.PW_SANDBOX}.e2e6@gmail.com`, + ENTRY_POINT: env.PW_ENTRY_POINT6, + TEST_SUITE: 'dial-number-tests.spec.ts', + }, }; diff --git a/playwright/test-manager.ts b/playwright/test-manager.ts index 20f0a8738..c824d28bf 100644 --- a/playwright/test-manager.ts +++ b/playwright/test-manager.ts @@ -4,7 +4,7 @@ import {stationLogout, telephonyLogin} from './Utils/stationLoginUtils'; import {loginExtension} from './Utils/incomingTaskUtils'; import {setupConsoleLogging} from './Utils/taskControlUtils'; import {setupAdvancedConsoleLogging} from './Utils/advancedTaskControlUtils'; -import {pageSetup} from './Utils/helperUtils'; +import {pageSetup, handleStrayTasks} from './Utils/helperUtils'; import { LOGIN_MODE, LoginMode, @@ -44,8 +44,7 @@ interface EnvTokens { agent2Username: string; agent1ExtensionNumber: string; password: string; - dialNumberUsername?: string; - dialNumberPassword?: string; + dialNumberLoginAccessToken?: string; } // Context creation result interface @@ -105,15 +104,14 @@ export class TestManager { agent2Username: process.env[`${this.projectName}_AGENT2_USERNAME`] ?? '', agent1ExtensionNumber: process.env[`${this.projectName}_AGENT1_EXTENSION_NUMBER`] ?? '', password: process.env.PW_SANDBOX_PASSWORD ?? '', - dialNumberUsername: process.env.PW_DIAL_NUMBER_LOGIN_USERNAME ?? '', - dialNumberPassword: process.env.PW_DIAL_NUMBER_LOGIN_PASSWORD ?? '', + dialNumberLoginAccessToken: process.env.DIAL_NUMBER_LOGIN_ACCESS_TOKEN ?? '', }; } // Helper method to create context with error handling private async createContextWithPage(browser: Browser, type: PageType): Promise { try { - const context = await browser.newContext(); + const context = await browser.newContext({ignoreHTTPSErrors: true}); const page = await context.newPage(); return {context, page, type}; } catch (error) { @@ -141,7 +139,6 @@ export class TestManager { if (attempt === maxRetries - 1) { throw new Error(`Failed ${operationName} after ${maxRetries} attempts: ${error}`); } - console.warn(`${operationName} attempt ${attempt + 1} failed, retrying...`); // Simple exponential backoff await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 1000)); } @@ -309,7 +306,7 @@ export class TestManager { envTokens.agent1ExtensionNumber ), this.retryOperation( - () => loginExtension(this.agent1ExtensionPage, envTokens.agent1Username, envTokens.password), + () => loginExtension(this.agent1ExtensionPage, envTokens.agent1AccessToken), 'agent1 extension login' ), ]); @@ -324,17 +321,17 @@ export class TestManager { // Helper method for Dial Number setup private async setupDialNumber(envTokens: EnvTokens): Promise { await this.retryOperation( - () => loginExtension(this.dialNumberPage, envTokens.dialNumberUsername, envTokens.dialNumberPassword), + () => loginExtension(this.dialNumberPage, envTokens.dialNumberLoginAccessToken), 'dial number login' ); // Ensure only one page remains in the Dial Number context to avoid duplicate web client instances - await this.enforceSingleDialNumberInOwnContext(); + // await this.enforceSingleDialNumberInOwnContext(); } // Helper method for Caller setup private async setupCaller(envTokens: EnvTokens): Promise { await this.retryOperation( - () => loginExtension(this.callerPage!, envTokens.agent2Username, envTokens.password), + () => loginExtension(this.callerPage!, envTokens.agent2AccessToken), 'caller extension login' ); } @@ -396,11 +393,23 @@ export class TestManager { agent1LoginMode: LOGIN_MODE.EXTENSION, enableConsoleLogging: true, enableAdvancedLogging: true, - needDialNumberLogin: true, + needDialNumberLogin: false, }); } async setupForAdvancedCombinations(browser: Browser) { + await this.setup(browser, { + needsAgent1: true, + needsAgent2: true, + needsCaller: true, + needDialNumberLogin: false, + agent1LoginMode: LOGIN_MODE.DESKTOP, + enableConsoleLogging: true, + enableAdvancedLogging: true, + }); + } + + async setupForDialNumber(browser: Browser) { await this.setup(browser, { needsAgent1: true, needsAgent2: true, @@ -416,13 +425,13 @@ export class TestManager { const envTokens = this.getEnvTokens(); // Create browser context and page - this.agent1Context = await browser.newContext(); + this.agent1Context = await browser.newContext({ignoreHTTPSErrors: true}); this.agent1Page = await this.agent1Context.newPage(); this.consoleMessages = []; this.setupPageConsoleLogging(this.agent1Page, true); // Create multi-session context and page for multi-login tests - this.multiSessionContext = await browser.newContext(); + this.multiSessionContext = await browser.newContext({ignoreHTTPSErrors: true}); this.multiSessionAgent1Page = await this.multiSessionContext.newPage(); // Define page setup operations @@ -460,12 +469,12 @@ export class TestManager { // Logout from station if already logged in on main page if (await this.isLogoutButtonVisible(this.agent1Page)) { - logoutOperations.push(stationLogout(this.agent1Page)); + logoutOperations.push(stationLogout(this.agent1Page, false)); // Don't throw during setup cleanup } // Logout from station if already logged in on multi-session page if (!isDesktopMode && (await this.isLogoutButtonVisible(this.multiSessionAgent1Page))) { - logoutOperations.push(stationLogout(this.multiSessionAgent1Page)); + logoutOperations.push(stationLogout(this.multiSessionAgent1Page, false)); // Don't throw during setup cleanup } await Promise.all(logoutOperations); @@ -537,16 +546,40 @@ export class TestManager { }); } + /** + * Soft cleanup - only handles stray tasks without logging out or closing browsers. + * Use this in afterAll to clean up state between test files. + */ + async softCleanup(): Promise { + const cleanupOps: Promise[] = []; + + if (this.agent1Page) { + cleanupOps.push(handleStrayTasks(this.agent1Page, this.agent1ExtensionPage)); + } + if (this.agent2Page) { + cleanupOps.push(handleStrayTasks(this.agent2Page)); + } + + await Promise.all(cleanupOps); + } + + /** + * Full cleanup - logs out and closes all pages/contexts. + * Use this only at the end of the entire test suite. + */ async cleanup(): Promise { + // First handle any stray tasks + await this.softCleanup().catch(() => {}); + // Logout operations - can be done in parallel const logoutOperations: Promise[] = []; if (this.agent1Page && (await this.isLogoutButtonVisible(this.agent1Page))) { - logoutOperations.push(stationLogout(this.agent1Page)); + logoutOperations.push(stationLogout(this.agent1Page, false)); // Don't throw during cleanup } if (this.agent2Page && (await this.isLogoutButtonVisible(this.agent2Page))) { - logoutOperations.push(stationLogout(this.agent2Page)); + logoutOperations.push(stationLogout(this.agent2Page, false)); // Don't throw during cleanup } await Promise.all(logoutOperations); @@ -590,45 +623,4 @@ 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 029bab244..30b5890e7 100644 --- a/playwright/tests/advance-task-control-combinations-test.spec.ts +++ b/playwright/tests/advance-task-control-combinations-test.spec.ts @@ -4,14 +4,12 @@ import { consultOrTransfer, clearAdvancedCapturedLogs, verifyConsultStartSuccessLogs, - verifyConsultTransferredLogs, - ensureDialNumberLoggedIn, } from '../Utils/advancedTaskControlUtils'; import {changeUserState, verifyCurrentState} from '../Utils/userStateUtils'; -import {createCallTask, acceptIncomingTask, acceptExtensionCall, endCallTask} from '../Utils/incomingTaskUtils'; +import {createCallTask, acceptIncomingTask} from '../Utils/incomingTaskUtils'; import {submitWrapup} from '../Utils/wrapupUtils'; import {USER_STATES, TASK_TYPES, WRAPUP_REASONS} from '../constants'; -import {waitForState, clearPendingCallAndWrapup} from '../Utils/helperUtils'; +import {waitForState, handleStrayTasks} from '../Utils/helperUtils'; import {endTask, holdCallToggle} from '../Utils/taskControlUtils'; import {TestManager} from '../test-manager'; @@ -25,12 +23,15 @@ export default function createAdvanceCombinationsTests() { await testManager.setupForAdvancedCombinations(browser); }); + test.beforeEach(async () => { + await handleStrayTasks(testManager.agent1Page); + await handleStrayTasks(testManager.agent2Page); + }); + test('Transfer from one agent to another, then transfer back to the first agent', async () => { await changeUserState(testManager.agent2Page, USER_STATES.MEETING); await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!); - let incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTaskDiv.waitFor({state: 'visible', timeout: 40000}); await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); await waitForState(testManager.agent1Page, USER_STATES.ENGAGED); await changeUserState(testManager.agent2Page, USER_STATES.AVAILABLE); @@ -42,8 +43,6 @@ export default function createAdvanceCombinationsTests() { 'transfer', process.env[`${testManager.projectName}_AGENT2_NAME`]! ); - incomingTaskDiv = testManager.agent2Page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTaskDiv.waitFor({state: 'visible', timeout: 20000}); await acceptIncomingTask(testManager.agent2Page, TASK_TYPES.CALL); await waitForState(testManager.agent2Page, USER_STATES.ENGAGED); await testManager.agent1Page.waitForTimeout(2000); @@ -55,8 +54,6 @@ export default function createAdvanceCombinationsTests() { 'transfer', process.env[`${testManager.projectName}_AGENT1_NAME`]! ); - incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTaskDiv.waitFor({state: 'visible', timeout: 20000}); await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); await testManager.agent1Page.waitForTimeout(2000); await waitForState(testManager.agent1Page, USER_STATES.ENGAGED); @@ -72,8 +69,6 @@ export default function createAdvanceCombinationsTests() { await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); await changeUserState(testManager.agent2Page, USER_STATES.MEETING); await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!); - let incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTaskDiv.waitFor({state: 'visible', timeout: 40000}); await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); await changeUserState(testManager.agent2Page, USER_STATES.AVAILABLE); await waitForState(testManager.agent1Page, USER_STATES.ENGAGED); @@ -86,8 +81,6 @@ export default function createAdvanceCombinationsTests() { 'consult', process.env[`${testManager.projectName}_AGENT2_NAME`]! ); - incomingTaskDiv = testManager.agent2Page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTaskDiv.waitFor({state: 'visible', timeout: 20000}); await acceptIncomingTask(testManager.agent2Page, TASK_TYPES.CALL); await waitForState(testManager.agent2Page, USER_STATES.ENGAGED); await testManager.agent1Page.getByTestId('transfer-consult-btn').click(); @@ -100,10 +93,6 @@ export default function createAdvanceCombinationsTests() { 'consult', process.env[`${testManager.projectName}_AGENT1_NAME`]! ); - await testManager.agent1Page - .getByTestId('samples:incoming-task-telephony') - .first() - .waitFor({state: 'visible', timeout: 20000}); await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); await waitForState(testManager.agent1Page, USER_STATES.ENGAGED); await testManager.agent2Page.getByTestId('transfer-consult-btn').click(); @@ -120,8 +109,6 @@ export default function createAdvanceCombinationsTests() { await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); await changeUserState(testManager.agent2Page, USER_STATES.MEETING); await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!); - let incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTaskDiv.waitFor({state: 'visible', timeout: 40000}); await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); await changeUserState(testManager.agent2Page, USER_STATES.AVAILABLE); await waitForState(testManager.agent1Page, USER_STATES.ENGAGED); @@ -132,8 +119,6 @@ export default function createAdvanceCombinationsTests() { 'consult', process.env[`${testManager.projectName}_AGENT2_NAME`]! ); - incomingTaskDiv = testManager.agent2Page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTaskDiv.waitFor({state: 'visible', timeout: 20000}); await acceptIncomingTask(testManager.agent2Page, TASK_TYPES.CALL); await waitForState(testManager.agent2Page, USER_STATES.ENGAGED); await testManager.agent1Page.getByTestId('transfer-consult-btn').click(); @@ -147,11 +132,6 @@ export default function createAdvanceCombinationsTests() { 'transfer', process.env[`${testManager.projectName}_AGENT1_NAME`]! ); - incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTaskDiv.waitFor({ - state: 'visible', - timeout: 20000, - }); await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); await waitForState(testManager.agent1Page, USER_STATES.ENGAGED); await testManager.agent2Page.waitForTimeout(2000); @@ -168,8 +148,6 @@ export default function createAdvanceCombinationsTests() { await changeUserState(testManager.agent2Page, USER_STATES.MEETING); await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!); - let incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTaskDiv.waitFor({state: 'visible', timeout: 40000}); await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); await waitForState(testManager.agent1Page, USER_STATES.ENGAGED); await changeUserState(testManager.agent2Page, USER_STATES.AVAILABLE); @@ -180,10 +158,6 @@ export default function createAdvanceCombinationsTests() { 'transfer', process.env[`${testManager.projectName}_AGENT2_NAME`]! ); - await testManager.agent2Page - .getByTestId('samples:incoming-task-telephony') - .first() - .waitFor({state: 'visible', timeout: 20000}); await acceptIncomingTask(testManager.agent2Page, TASK_TYPES.CALL); await waitForState(testManager.agent2Page, USER_STATES.ENGAGED); await testManager.agent1Page.waitForTimeout(2000); @@ -195,11 +169,6 @@ export default function createAdvanceCombinationsTests() { 'consult', process.env[`${testManager.projectName}_AGENT1_NAME`]! ); - incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTaskDiv.waitFor({ - state: 'visible', - timeout: 20000, - }); await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); await waitForState(testManager.agent1Page, USER_STATES.ENGAGED); await testManager.agent2Page.getByTestId('transfer-consult-btn').click(); @@ -217,8 +186,6 @@ export default function createAdvanceCombinationsTests() { await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!); await testManager.agent1Page.waitForTimeout(5000); - 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 testManager.agent1Page.waitForTimeout(5000); @@ -229,8 +196,6 @@ export default function createAdvanceCombinationsTests() { '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); @@ -245,8 +210,6 @@ export default function createAdvanceCombinationsTests() { 'consult', process.env[`${testManager.projectName}_AGENT1_NAME`]! ); - const returnConsultDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); - await returnConsultDiv.waitFor({state: 'visible', timeout: 60000}); await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); await testManager.agent1Page.waitForTimeout(3000); await testManager.agent2Page.getByTestId('transfer-consult-btn').click(); @@ -262,7 +225,7 @@ export default function createAdvanceCombinationsTests() { await expect(testManager.agent1Page.getByTestId('cancel-consult-btn')).toBeVisible(); await expect(testManager.agent1Page.getByTestId('transfer-consult-btn')).toBeVisible(); await cancelConsult(testManager.agent1Page); - await expect(testManager.agent1Page.getByRole('group', {name: 'Call Control with Call'})).toBeVisible(); + await expect(testManager.agent1Page.getByTestId('call-control:consult').first()).toBeVisible(); await verifyCurrentState(testManager.agent1Page, USER_STATES.ENGAGED); await holdCallToggle(testManager.agent1Page); await endTask(testManager.agent1Page); @@ -271,59 +234,12 @@ 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(); @@ -332,52 +248,6 @@ export default function createAdvanceCombinationsTests() { 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 4b554e8bc..c1bdf4da2 100644 --- a/playwright/tests/advanced-task-controls-test.spec.ts +++ b/playwright/tests/advanced-task-controls-test.spec.ts @@ -7,7 +7,6 @@ import { verifyConsultStartSuccessLogs, verifyConsultEndSuccessLogs, verifyConsultTransferredLogs, - ensureDialNumberLoggedIn, } from '../Utils/advancedTaskControlUtils'; import {changeUserState, verifyCurrentState} from '../Utils/userStateUtils'; @@ -16,13 +15,13 @@ import { acceptIncomingTask, declineIncomingTask, acceptExtensionCall, - endCallTask, - declineExtensionCall, + waitForIncomingTask, } from '../Utils/incomingTaskUtils'; import {submitWrapup} from '../Utils/wrapupUtils'; -import {USER_STATES, TASK_TYPES, WRAPUP_REASONS} from '../constants'; +import {USER_STATES, TASK_TYPES, WRAPUP_REASONS, ACCEPT_TASK_TIMEOUT} from '../constants'; import {holdCallToggle, endTask, verifyHoldButtonIcon, verifyTaskControls} from '../Utils/taskControlUtils'; import {TestManager} from '../test-manager'; +import {handleStrayTasks, clearPendingCallAndWrapup, waitForState} from '../Utils/helperUtils'; // Extract test functions for cleaner syntax const {describe, beforeAll, afterAll, beforeEach} = test; @@ -40,14 +39,21 @@ const {describe, beforeAll, afterAll, beforeEach} = test; export default function createAdvancedTaskControlsTests() { let testManager: TestManager; - beforeAll(async ({browser}, testInfo) => { + test.beforeAll(async ({browser}, testInfo) => { const projectName = testInfo.project.name; testManager = new TestManager(projectName); await testManager.setupForAdvancedTaskControls(browser); }); - afterAll(async () => { - await testManager.cleanup(); + test.afterAll(async () => { + if (testManager) { + await testManager.cleanup(); + } + }); + + test.beforeEach(async () => { + await handleStrayTasks(testManager.agent1Page, testManager.callerPage); + await handleStrayTasks(testManager.agent2Page, testManager.callerPage); }); // ============================================================================= @@ -61,9 +67,7 @@ export default function createAdvancedTaskControlsTests() { await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!); await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); - const incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTaskDiv.waitFor({state: 'visible', timeout: 80000}); - + await waitForIncomingTask(testManager.agent1Page, TASK_TYPES.CALL, ACCEPT_TASK_TIMEOUT); await acceptExtensionCall(testManager.agent1ExtensionPage); await changeUserState(testManager.agent2Page, USER_STATES.AVAILABLE); await testManager.agent1Page.waitForTimeout(5000); @@ -82,19 +86,15 @@ export default function createAdvancedTaskControlsTests() { 'transfer', process.env[`${testManager.projectName}_AGENT2_NAME`]! ); - + // Agent 2 should receive the transfer and accept it + await acceptIncomingTask(testManager.agent2Page, TASK_TYPES.CALL, ACCEPT_TASK_TIMEOUT); + await testManager.agent2Page.waitForTimeout(3000); // Verify transfer success in console logs - await testManager.agent1Page.waitForTimeout(3000); + await testManager.agent1Page.bringToFront(); verifyTransferSuccessLogs(); // Verify Agent 1 goes to wrapup state await submitWrapup(testManager.agent1Page, WRAPUP_REASONS.SALE); - // Agent 2 should receive the transfer and accept it - const incomingTransferDiv = testManager.agent2Page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTransferDiv.waitFor({state: 'visible', timeout: 60000}); - - await acceptIncomingTask(testManager.agent2Page, TASK_TYPES.CALL); - await testManager.agent2Page.waitForTimeout(3000); // Verify Agent 2 now has the call and is engaged await verifyCurrentState(testManager.agent2Page, USER_STATES.ENGAGED); @@ -121,11 +121,9 @@ export default function createAdvancedTaskControlsTests() { ); // Agent 2 accepts the transfer - const incomingTransferDiv = testManager.agent2Page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTransferDiv.waitFor({state: 'visible', timeout: 60000}); + await acceptIncomingTask(testManager.agent2Page, TASK_TYPES.CALL, ACCEPT_TASK_TIMEOUT); await submitWrapup(testManager.agent1Page, WRAPUP_REASONS.SALE); - await acceptIncomingTask(testManager.agent2Page, TASK_TYPES.CALL); - await testManager.agent2Page.waitForTimeout(3000); + await testManager.agent1Page.waitForTimeout(3000); verifyTransferSuccessLogs(); await verifyCurrentState(testManager.agent2Page, USER_STATES.ENGAGED); await endTask(testManager.agent2Page); @@ -138,43 +136,6 @@ export default function createAdvancedTaskControlsTests() { // Verify Agent 2 is no longer engaged await verifyCurrentState(testManager.agent2Page, USER_STATES.AVAILABLE); }); - - 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!); - - // Verify Agent 1 goes to wrapup after transfer - await submitWrapup(testManager.agent1Page, WRAPUP_REASONS.RESOLVED); - await testManager.agent1Page.waitForTimeout(2000); - - // Verify Agent 1 is no longer engaged - await verifyCurrentState(testManager.agent1Page, USER_STATES.AVAILABLE); - }); - - 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!); - - // Verify Agent 1 goes to wrapup after transfer - await submitWrapup(testManager.agent1Page, WRAPUP_REASONS.RESOLVED); - await testManager.agent1Page.waitForTimeout(2000); - - // Verify Agent 1 is no longer engaged - await verifyCurrentState(testManager.agent1Page, USER_STATES.AVAILABLE); - }); }); // ============================================================================= @@ -182,40 +143,16 @@ export default function createAdvancedTaskControlsTests() { // ============================================================================= describe('Consult and Consult Transfer Scenarios', () => { - test('Entry Point Consult: visible and functional only for supported users (no blind transfer)', async ({}, testInfo) => { - 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 acceptExtensionCall(testManager.agent1ExtensionPage); - await testManager.agent1Page.waitForTimeout(3000); - await verifyCurrentState(testManager.agent1Page, USER_STATES.ENGAGED); - - // Ensure consult UI and Entry Point tab exists - await testManager.agent1Page.getByTestId('call-control:consult').nth(1).click(); - await expect(testManager.agent1Page.locator('#consult-search')).toBeVisible(); - await testManager.agent1Page.getByRole('button', {name: 'Entry Point'}).click(); - - await consultOrTransfer(testManager.agent1Page, 'entryPoint', 'consult', process.env.PW_ENTRYPOINT_NAME!); - await expect(testManager.agent1Page.getByTestId('cancel-consult-btn')).toBeVisible(); - await testManager.agent1Page.waitForTimeout(1000); - await cancelConsult(testManager.agent1Page); - await testManager.agent1Page.waitForTimeout(1000); - await verifyCurrentState(testManager.agent1Page, USER_STATES.ENGAGED); - }); test('Agent Consult Transfer: cancel, decline, timeout, and transfer scenarios are handled correctly in sequence', async () => { // ...existing code for Agent Consult Transfer test... 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 testManager.agent1ExtensionPage - .locator('[data-test="generic-person-item-base"]') - .waitFor({state: 'visible', timeout: 20000}); + await waitForIncomingTask(testManager.agent1Page, TASK_TYPES.CALL, ACCEPT_TASK_TIMEOUT); + await expect(testManager.agent1ExtensionPage.locator('#answer').first()).toBeEnabled({timeout: 20000}); await acceptExtensionCall(testManager.agent1ExtensionPage); await changeUserState(testManager.agent2Page, USER_STATES.AVAILABLE); - await testManager.agent1Page.waitForTimeout(5000); + await testManager.agent1Page.waitForTimeout(3000); await verifyCurrentState(testManager.agent1Page, USER_STATES.ENGAGED); // 1. Accept consult and end @@ -228,9 +165,7 @@ export default function createAdvancedTaskControlsTests() { ); await expect(testManager.agent1Page.getByTestId('cancel-consult-btn')).toBeVisible(); await expect(testManager.agent1Page.getByTestId('transfer-consult-btn')).toBeVisible(); - const consultRequestDiv1 = testManager.agent2Page.getByTestId('samples:incoming-task-telephony').first(); - await consultRequestDiv1.waitFor({state: 'visible', timeout: 60000}); - await acceptIncomingTask(testManager.agent2Page, TASK_TYPES.CALL); + await acceptIncomingTask(testManager.agent2Page, TASK_TYPES.CALL, ACCEPT_TASK_TIMEOUT); await testManager.agent2Page.waitForTimeout(3000); await expect(testManager.agent1Page.getByTestId('transfer-consult-btn')).toBeVisible(); await testManager.agent1Page.waitForTimeout(2000); @@ -250,9 +185,7 @@ export default function createAdvancedTaskControlsTests() { 'consult', process.env[`${testManager.projectName}_AGENT2_NAME`]! ); - const consultRequestDiv2 = testManager.agent2Page.getByTestId('samples:incoming-task-telephony').first(); - await consultRequestDiv2.waitFor({state: 'visible', timeout: 60000}); - await testManager.agent2Page.waitForTimeout(3000); + await waitForIncomingTask(testManager.agent2Page, TASK_TYPES.CALL, ACCEPT_TASK_TIMEOUT); await declineIncomingTask(testManager.agent2Page, TASK_TYPES.CALL); await verifyTaskControls(testManager.agent1Page, TASK_TYPES.CALL); await verifyHoldButtonIcon(testManager.agent1Page, {expectedIsHeld: true}); @@ -270,7 +203,7 @@ export default function createAdvancedTaskControlsTests() { 'consult', process.env[`${testManager.projectName}_AGENT2_NAME`]! ); - await testManager.agent1Page.waitForTimeout(20000); // Wait for timeout + await testManager.agent1Page.waitForTimeout(10000); await verifyTaskControls(testManager.agent1Page, TASK_TYPES.CALL); await verifyHoldButtonIcon(testManager.agent1Page, {expectedIsHeld: true}); await holdCallToggle(testManager.agent1Page); @@ -285,10 +218,9 @@ export default function createAdvancedTaskControlsTests() { 'consult', process.env[`${testManager.projectName}_AGENT2_NAME`]! ); - const consultRequestDiv3 = testManager.agent2Page.getByTestId('samples:incoming-task-telephony').first(); - await consultRequestDiv3.waitFor({state: 'visible', timeout: 60000}); - await acceptIncomingTask(testManager.agent2Page, TASK_TYPES.CALL); + await acceptIncomingTask(testManager.agent2Page, TASK_TYPES.CALL, ACCEPT_TASK_TIMEOUT); await testManager.agent2Page.waitForTimeout(3000); + await testManager.agent1Page.bringToFront(); await testManager.agent1Page.getByTestId('transfer-consult-btn').click(); await submitWrapup(testManager.agent1Page, WRAPUP_REASONS.SALE); await verifyCurrentState(testManager.agent2Page, USER_STATES.ENGAGED); @@ -309,15 +241,12 @@ export default function createAdvancedTaskControlsTests() { // Setup: create call and get to engaged state await changeUserState(testManager.agent2Page, USER_STATES.MEETING); 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 testManager.agent1ExtensionPage - .locator('[data-test="generic-person-item-base"]') - .waitFor({state: 'visible', timeout: 20000}); + await waitForIncomingTask(testManager.agent1Page, TASK_TYPES.CALL, ACCEPT_TASK_TIMEOUT); await acceptExtensionCall(testManager.agent1ExtensionPage); await testManager.agent1Page.waitForTimeout(5000); await verifyCurrentState(testManager.agent1Page, USER_STATES.ENGAGED); - + await changeUserState(testManager.agent2Page, USER_STATES.AVAILABLE); + await testManager.agent2Page.waitForTimeout(2000); // 1. Cancel consult clearAdvancedCapturedLogs(); await consultOrTransfer( @@ -332,8 +261,6 @@ export default function createAdvancedTaskControlsTests() { await verifyTaskControls(testManager.agent1Page, TASK_TYPES.CALL); await expect(testManager.agent1Page.getByTestId('cancel-consult-btn')).not.toBeVisible(); - // 2. Accept consult and end (Agent 2 accepts, Agent 1 ends) - await changeUserState(testManager.agent2Page, USER_STATES.AVAILABLE); clearAdvancedCapturedLogs(); await consultOrTransfer( testManager.agent1Page, @@ -343,10 +270,7 @@ export default function createAdvancedTaskControlsTests() { ); await testManager.agent1Page.waitForTimeout(3000); verifyConsultStartSuccessLogs(); - const consultRequestDiv1 = testManager.agent2Page.getByTestId('samples:incoming-task-telephony').first(); - await consultRequestDiv1.waitFor({state: 'visible', timeout: 60000}); - await acceptIncomingTask(testManager.agent2Page, TASK_TYPES.CALL); - await testManager.agent2Page.waitForTimeout(3000); + await acceptIncomingTask(testManager.agent2Page, TASK_TYPES.CALL, ACCEPT_TASK_TIMEOUT); await cancelConsult(testManager.agent1Page); await testManager.agent1Page.waitForTimeout(3000); await verifyCurrentState(testManager.agent2Page, USER_STATES.AVAILABLE); @@ -365,9 +289,7 @@ export default function createAdvancedTaskControlsTests() { 'consult', process.env[`${testManager.projectName}_QUEUE_NAME`]! ); - const consultRequestDiv2 = testManager.agent2Page.getByTestId('samples:incoming-task-telephony').first(); - await consultRequestDiv2.waitFor({state: 'visible', timeout: 60000}); - await acceptIncomingTask(testManager.agent2Page, TASK_TYPES.CALL); + await acceptIncomingTask(testManager.agent2Page, TASK_TYPES.CALL, ACCEPT_TASK_TIMEOUT); await testManager.agent2Page.waitForTimeout(3000); await cancelConsult(testManager.agent2Page); await testManager.agent2Page.waitForTimeout(3000); @@ -386,12 +308,9 @@ export default function createAdvancedTaskControlsTests() { process.env[`${testManager.projectName}_QUEUE_NAME`]! ); await testManager.agent1Page.waitForTimeout(2000); - const consultRequestDiv3 = testManager.agent2Page.getByTestId('samples:incoming-task-telephony').first(); - await consultRequestDiv3.waitFor({state: 'visible', timeout: 60000}); - await acceptIncomingTask(testManager.agent2Page, TASK_TYPES.CALL); - await testManager.agent2Page.waitForTimeout(3000); + await acceptIncomingTask(testManager.agent2Page, TASK_TYPES.CALL, ACCEPT_TASK_TIMEOUT); await testManager.agent1Page.getByTestId('transfer-consult-btn').click(); - await testManager.agent1Page.waitForTimeout(2000); + await testManager.agent1Page.bringToFront(); await submitWrapup(testManager.agent1Page, WRAPUP_REASONS.SALE); await verifyCurrentState(testManager.agent2Page, USER_STATES.ENGAGED); await verifyTaskControls(testManager.agent2Page, TASK_TYPES.CALL); @@ -403,117 +322,32 @@ export default function createAdvancedTaskControlsTests() { await submitWrapup(testManager.agent2Page, WRAPUP_REASONS.RESOLVED); await testManager.agent2Page.waitForTimeout(2000); }); + }); - test('Dial Number Consult: cancel, decline, accept/end, and transfer scenarios are handled correctly in sequence', async () => { - await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); - - // Setup: create call and get to engaged state - 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 testManager.agent1ExtensionPage - .locator('[data-test="generic-person-item-base"]') - .waitFor({state: 'visible', timeout: 20000}); - await acceptExtensionCall(testManager.agent1ExtensionPage); - await testManager.agent1Page.waitForTimeout(5000); - await verifyCurrentState(testManager.agent1Page, USER_STATES.ENGAGED); - - // 1. Cancel consult - clearAdvancedCapturedLogs(); - await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'consult', process.env.PW_DIAL_NUMBER_NAME); - await expect(testManager.agent1Page.getByTestId('cancel-consult-btn')).toBeVisible(); - await testManager.agent1Page.waitForTimeout(2000); - await cancelConsult(testManager.agent1Page); - await verifyTaskControls(testManager.agent1Page, TASK_TYPES.CALL); - await expect(testManager.agent1Page.getByTestId('cancel-consult-btn')).not.toBeVisible(); - - // 2. Decline consult - clearAdvancedCapturedLogs(); - await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'consult', process.env.PW_DIAL_NUMBER_NAME); - await declineExtensionCall(testManager.dialNumberPage); - await testManager.agent1Page.waitForTimeout(2000); - await cancelConsult(testManager.agent1Page); // still needs to cancel even if declined - await verifyTaskControls(testManager.agent1Page, TASK_TYPES.CALL); - await verifyHoldButtonIcon(testManager.agent1Page, {expectedIsHeld: true}); - await holdCallToggle(testManager.agent1Page); - await testManager.agent1Page.waitForTimeout(2000); - await expect(testManager.agent1Page.getByTestId('cancel-consult-btn')).not.toBeVisible(); - await verifyCurrentState(testManager.agent1Page, USER_STATES.ENGAGED); - - // 3. Accept consult and end - clearAdvancedCapturedLogs(); - await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'consult', process.env.PW_DIAL_NUMBER_NAME); - await testManager.agent1Page.waitForTimeout(2000); - verifyConsultStartSuccessLogs(); - await acceptExtensionCall(testManager.dialNumberPage); - await testManager.agent1Page.waitForTimeout(2000); - await cancelConsult(testManager.agent1Page); - await verifyTaskControls(testManager.agent1Page, TASK_TYPES.CALL); - await testManager.agent1Page.waitForTimeout(2000); - verifyConsultEndSuccessLogs(); - await verifyHoldButtonIcon(testManager.agent1Page, {expectedIsHeld: true}); - await holdCallToggle(testManager.agent1Page); - - // 4. Consult transfer - clearAdvancedCapturedLogs(); - await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'consult', process.env.PW_DIAL_NUMBER_NAME); - await acceptExtensionCall(testManager.dialNumberPage); - await testManager.agent1Page.waitForTimeout(3000); - await testManager.agent1Page.getByTestId('transfer-consult-btn').click(); - await testManager.agent1Page.waitForTimeout(2000); - await submitWrapup(testManager.agent1Page, WRAPUP_REASONS.SALE); - await testManager.dialNumberPage.waitForTimeout(2000); - verifyConsultStartSuccessLogs(); - verifyConsultTransferredLogs(); - await endCallTask(testManager.dialNumberPage); - }); - - test('Dial Number search filters list to the matching entry (local search)', async () => { - if (testManager.projectName !== 'SET_5') { - test.skip(true, 'Dial Number search validation runs only for SET_5 (user23/user24).'); - } - - const searchTerm = process.env.PW_DIAL_NUMBER_NAME!; - - // Setup: create call and get to engaged state - await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); - await changeUserState(testManager.agent2Page, USER_STATES.MEETING); - 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 acceptExtensionCall(testManager.agent1ExtensionPage); - await testManager.agent1Page.waitForTimeout(3000); - await verifyCurrentState(testManager.agent1Page, USER_STATES.ENGAGED); + // ============================================================================= + // DIAL NUMBER TESTS - All tests requiring dialNumber session + // ============================================================================= - // Open consult popover and switch to Dial Number - await testManager.agent1Page.getByTestId('call-control:consult').nth(1).click(); - const popover = testManager.agent1Page.locator('.agent-popover-content'); - await expect(popover).toBeVisible({timeout: 10000}); - await popover.getByRole('button', {name: 'Dial Number'}).click(); - - // Perform search and wait for local filtering to reflect - await popover.locator('#consult-search').fill(searchTerm); - await testManager.agent1Page.waitForTimeout(4000); - - // Read visible list item titles (aria-labels) and validate only the searched item remains - const labels = await popover - .locator('[role="listitem"]') - .evaluateAll((nodes) => nodes.map((n) => n.getAttribute('aria-label'))); - expect(labels).toContain(searchTerm); - expect(labels.filter(Boolean).length).toBe(1); - - // Close the popover to avoid overlay blocking further actions - await testManager.agent1Page.keyboard.press('Escape'); - await testManager.agent1Page - .locator('.md-popover-backdrop') - .waitFor({state: 'hidden', timeout: 3000}) - .catch(() => {}); - - // End call and complete wrapup to clean up for next tests - await endTask(testManager.agent1Page); - await testManager.agent1Page.waitForTimeout(2000); - await submitWrapup(testManager.agent1Page, WRAPUP_REASONS.SALE); - await testManager.agent1Page.waitForTimeout(1000); - }); + test('Entry Point Consult: visible and functional only for supported users (no blind transfer)', async ({}, testInfo) => { + await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); + await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!); + await waitForIncomingTask(testManager.agent1Page, TASK_TYPES.CALL, ACCEPT_TASK_TIMEOUT); + await acceptExtensionCall(testManager.agent1ExtensionPage); + await testManager.agent1Page.waitForTimeout(3000); + await verifyCurrentState(testManager.agent1Page, USER_STATES.ENGAGED); + + // Ensure consult UI and Entry Point tab exists + const consultButton = testManager.agent1Page.getByTestId('call-control:consult').first(); + await consultButton.waitFor({state: 'visible', timeout: 10000}); + await consultButton.click(); + await expect(testManager.agent1Page.locator('#consult-search')).toBeVisible(); + await testManager.agent1Page.getByRole('button', {name: 'Entry Point'}).click(); + + await consultOrTransfer(testManager.agent1Page, 'entryPoint', 'consult', process.env.PW_ENTRYPOINT_NAME!); + await expect(testManager.agent1Page.getByTestId('cancel-consult-btn')).toBeVisible(); + await testManager.agent1Page.waitForTimeout(1000); + await cancelConsult(testManager.agent1Page); + await testManager.agent1Page.waitForTimeout(1000); + await verifyCurrentState(testManager.agent1Page, USER_STATES.ENGAGED); }); } diff --git a/playwright/tests/basic-task-controls-test.spec.ts b/playwright/tests/basic-task-controls-test.spec.ts index feb27b591..4094fa0d7 100644 --- a/playwright/tests/basic-task-controls-test.spec.ts +++ b/playwright/tests/basic-task-controls-test.spec.ts @@ -17,7 +17,7 @@ import { verifyRecordButtonIcon, } from '../Utils/taskControlUtils'; import {submitWrapup} from '../Utils/wrapupUtils'; -import {USER_STATES, TASK_TYPES, WRAPUP_REASONS} from '../constants'; +import {USER_STATES, TASK_TYPES, WRAPUP_REASONS, ACCEPT_TASK_TIMEOUT} from '../constants'; import {TestManager} from '../test-manager'; // Extract test functions for cleaner syntax @@ -54,12 +54,8 @@ export default function createCallTaskControlsTests() { await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!); await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); - // Wait for incoming call notification - const incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTaskDiv.waitFor({state: 'visible', timeout: 80000}); - - // Accept the incoming call - await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); + // Accept the incoming call (waits for task to be visible) + await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL, ACCEPT_TASK_TIMEOUT); await testManager.agent1Page.waitForTimeout(5000); // Verify agent state changed to engaged diff --git a/playwright/tests/dial-number-task-control-test.spec.ts b/playwright/tests/dial-number-task-control-test.spec.ts new file mode 100644 index 000000000..74a2d2f05 --- /dev/null +++ b/playwright/tests/dial-number-task-control-test.spec.ts @@ -0,0 +1,295 @@ +import {test, expect} from '@playwright/test'; +import { + cancelConsult, + consultOrTransfer, + clearAdvancedCapturedLogs, + verifyConsultStartSuccessLogs, + verifyConsultTransferredLogs, + verifyTransferSuccessLogs, + verifyConsultEndSuccessLogs, +} from '../Utils/advancedTaskControlUtils'; +import {changeUserState, verifyCurrentState} from '../Utils/userStateUtils'; +import { + createCallTask, + acceptIncomingTask, + acceptExtensionCall, + endCallTask, + declineExtensionCall, +} from '../Utils/incomingTaskUtils'; +import {submitWrapup} from '../Utils/wrapupUtils'; +import {USER_STATES, TASK_TYPES, WRAPUP_REASONS} from '../constants'; +import {waitForState, clearPendingCallAndWrapup, handleStrayTasks} from '../Utils/helperUtils'; +import {endTask, holdCallToggle, verifyHoldButtonIcon, verifyTaskControls} from '../Utils/taskControlUtils'; +import {TestManager} from '../test-manager'; + +export default function createDialNumberTaskControlTests() { + test.describe('Dial Number Task Control Tests ', () => { + let testManager: TestManager; + + test.beforeAll(async ({browser}, testInfo) => { + const projectName = testInfo.project.name; + testManager = new TestManager(projectName); + await testManager.setupForDialNumber(browser); + }); + + test.beforeEach(async () => { + await handleStrayTasks(testManager.agent1Page); + await handleStrayTasks(testManager.agent2Page); + }); + test.describe('Dial Number Tests', () => { + test.beforeAll(async () => { + test.skip(!process.env.PW_DIAL_NUMBER_NAME, 'PW_DIAL_NUMBER_NAME not set'); + }); + + 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 changeUserState(testManager.agent2Page, USER_STATES.MEETING); + await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); + await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!); + 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`]! + ); + 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 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('Dial Number Consult: cancel, decline, accept/end, and transfer scenarios are handled correctly in sequence', async () => { + await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); + // Setup: create call and get to engaged state + await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!); + await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); + await testManager.agent1Page.waitForTimeout(5000); + await verifyCurrentState(testManager.agent1Page, USER_STATES.ENGAGED); + + // 1. Cancel consult + clearAdvancedCapturedLogs(); + await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'consult', process.env.PW_DIAL_NUMBER_NAME); + await expect(testManager.agent1Page.getByTestId('cancel-consult-btn')).toBeVisible(); + await testManager.agent1Page.waitForTimeout(2000); + await cancelConsult(testManager.agent1Page); + await verifyTaskControls(testManager.agent1Page, TASK_TYPES.CALL); + await expect(testManager.agent1Page.getByTestId('cancel-consult-btn')).not.toBeVisible(); + + // 2. Decline consult + clearAdvancedCapturedLogs(); + await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'consult', process.env.PW_DIAL_NUMBER_NAME); + await declineExtensionCall(testManager.dialNumberPage); + await testManager.agent1Page.waitForTimeout(2000); + await cancelConsult(testManager.agent1Page); // still needs to cancel even if declined + await verifyTaskControls(testManager.agent1Page, TASK_TYPES.CALL); + await verifyHoldButtonIcon(testManager.agent1Page, {expectedIsHeld: true}); + await holdCallToggle(testManager.agent1Page); + await testManager.agent1Page.waitForTimeout(2000); + await expect(testManager.agent1Page.getByTestId('cancel-consult-btn')).not.toBeVisible(); + await verifyCurrentState(testManager.agent1Page, USER_STATES.ENGAGED); + + // 3. Accept consult and end + clearAdvancedCapturedLogs(); + await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'consult', process.env.PW_DIAL_NUMBER_NAME); + await testManager.agent1Page.waitForTimeout(2000); + verifyConsultStartSuccessLogs(); + await acceptExtensionCall(testManager.dialNumberPage); + await testManager.agent1Page.bringToFront(); + await cancelConsult(testManager.agent1Page); + await verifyTaskControls(testManager.agent1Page, TASK_TYPES.CALL); + await testManager.agent1Page.waitForTimeout(2000); + verifyConsultEndSuccessLogs(); + await verifyHoldButtonIcon(testManager.agent1Page, {expectedIsHeld: true}); + await holdCallToggle(testManager.agent1Page); + + // 4. Consult transfer + clearAdvancedCapturedLogs(); + await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'consult', process.env.PW_DIAL_NUMBER_NAME); + await acceptExtensionCall(testManager.dialNumberPage); + await testManager.agent1Page.waitForTimeout(3000); + await testManager.agent1Page.getByTestId('transfer-consult-btn').click(); + await testManager.agent1Page.waitForTimeout(2000); + await submitWrapup(testManager.agent1Page, WRAPUP_REASONS.SALE); + await testManager.dialNumberPage.waitForTimeout(2000); + verifyConsultStartSuccessLogs(); + verifyConsultTransferredLogs(); + await endCallTask(testManager.dialNumberPage); + }); + + test('Dial Number search filters list to the matching entry (local search)', async () => { + if (testManager.projectName !== 'SET_5') { + test.skip(true, 'Dial Number search validation runs only for SET_5 (user23/user24).'); + } + + const searchTerm = process.env.PW_DIAL_NUMBER_NAME!; + + // Setup: create call and get to engaged state + await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); + await changeUserState(testManager.agent2Page, USER_STATES.MEETING); + await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!); + await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); + await testManager.agent1Page.waitForTimeout(3000); + await verifyCurrentState(testManager.agent1Page, USER_STATES.ENGAGED); + + // Open consult popover and switch to Dial Number + const consultButton = testManager.agent1Page.getByTestId('call-control:consult').first(); + await consultButton.waitFor({state: 'visible', timeout: 10000}); + await consultButton.click(); + const popover = testManager.agent1Page.locator('.agent-popover-content'); + await expect(popover).toBeVisible({timeout: 10000}); + await popover.getByRole('button', {name: 'Dial Number'}).click(); + + // Perform search and wait for local filtering to reflect + await popover.locator('#consult-search').fill(searchTerm); + await testManager.agent1Page.waitForTimeout(4000); + + // Read visible list item titles (aria-labels) and validate only the searched item remains + const labels = await popover + .locator('[role="listitem"]') + .evaluateAll((nodes) => nodes.map((n) => n.getAttribute('aria-label'))); + expect(labels).toContain(searchTerm); + expect(labels.filter(Boolean).length).toBe(1); + + // Close the popover to avoid overlay blocking further actions + await testManager.agent1Page.keyboard.press('Escape'); + await testManager.agent1Page + .locator('.md-popover-backdrop') + .waitFor({state: 'hidden', timeout: 3000}) + .catch(() => {}); + + // End call and complete wrapup to clean up for next tests + await endTask(testManager.agent1Page); + await testManager.agent1Page.bringToFront(); + await submitWrapup(testManager.agent1Page, WRAPUP_REASONS.SALE); + await testManager.agent1Page.waitForTimeout(1000); + }); + + 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 changeUserState(testManager.agent2Page, USER_STATES.MEETING); + await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); + await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!); + 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!, true); + 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 changeUserState(testManager.agent2Page, USER_STATES.MEETING); + await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); + await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!); + await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); + clearAdvancedCapturedLogs(); + await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'consult', process.env.PW_DIAL_NUMBER_NAME!); + await expect(testManager.dialNumberPage.locator('#answer').first()).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.beforeEach(async () => { + testManager.softCleanup(); + await changeUserState(testManager.agent2Page, USER_STATES.MEETING); + await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); + }); + + test('Blind Transfer to DialNumber', async () => { + // Create call and agent 1 accepts + await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!); + await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); + await testManager.agent1Page.waitForTimeout(3000); + await verifyCurrentState(testManager.agent1Page, USER_STATES.ENGAGED); + clearAdvancedCapturedLogs(); + + await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'transfer', process.env.PW_DIAL_NUMBER_NAME); + await acceptExtensionCall(testManager.dialNumberPage); + verifyTransferSuccessLogs(); + await endCallTask(testManager.callerPage!, true); + await submitWrapup(testManager.agent1Page, WRAPUP_REASONS.RESOLVED); + await testManager.agent1Page.waitForTimeout(2000); + await verifyCurrentState(testManager.agent1Page, USER_STATES.AVAILABLE); + }); + + test('Blind Transfer to Queue with DialNumber', async () => { + // Create call and agent 1 accepts + await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!); + await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); + await testManager.agent1Page.waitForTimeout(3000); + await verifyCurrentState(testManager.agent1Page, USER_STATES.ENGAGED); + clearAdvancedCapturedLogs(); + + await consultOrTransfer(testManager.agent1Page, 'queue', 'transfer', 'queue with dn e2e'); + await acceptExtensionCall(testManager.dialNumberPage); + verifyTransferSuccessLogs(); + await endCallTask(testManager.callerPage!, true); + await submitWrapup(testManager.agent1Page, WRAPUP_REASONS.RESOLVED); + await testManager.agent1Page.waitForTimeout(2000); + await verifyCurrentState(testManager.agent1Page, USER_STATES.AVAILABLE); + }); + + test('Consult then end consult returns UI to normal', async () => { + await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!); + 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!, true); + await submitWrapup(testManager.agent1Page, WRAPUP_REASONS.SALE); + }); + + test('Consult then transfer completes and remote ends', async () => { + await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!); + await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); + + clearAdvancedCapturedLogs(); + await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'consult', process.env.PW_DIAL_NUMBER_NAME!); + await expect(testManager.dialNumberPage.locator('#answer').first()).toBeVisible(); + await acceptExtensionCall(testManager.dialNumberPage); + await testManager.agent1Page.bringToFront(); + await testManager.agent1Page.getByTestId('transfer-consult-btn').click(); + await submitWrapup(testManager.agent1Page, WRAPUP_REASONS.SALE); + await verifyConsultStartSuccessLogs(); + await verifyConsultTransferredLogs(); + await endCallTask(testManager.dialNumberPage); + }); + }); + }); +} diff --git a/playwright/tests/digital-incoming-task-and-task-controls.spec.ts b/playwright/tests/digital-incoming-task-and-task-controls.spec.ts index 960fb36f1..b3d9fe7e7 100644 --- a/playwright/tests/digital-incoming-task-and-task-controls.spec.ts +++ b/playwright/tests/digital-incoming-task-and-task-controls.spec.ts @@ -8,6 +8,7 @@ import { acceptExtensionCall, createEmailTask, submitRonaPopup, + waitForIncomingTask, } from '../Utils/incomingTaskUtils'; import {verifyTaskControls, endTask, verifyEndLogs} from '../Utils/taskControlUtils'; import {TASK_TYPES, USER_STATES, THEME_COLORS, WRAPUP_REASONS, RONA_OPTIONS} from '../constants'; @@ -118,11 +119,16 @@ export default function createDigitalIncomingTaskAndTaskControlsTests() { setupConsoleLogging(testManager.agent1Page); }); + test.afterAll(async () => { + if (testManager) { + await testManager.cleanup(); + } + }); + test('should ignore incoming chat task and wait for RONA popup', async () => { await createChatTask(testManager.chatPage, process.env[`${testManager.projectName}_CHAT_URL`]!); await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); - const incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-chat').first(); - await incomingTaskDiv.waitFor({state: 'visible', timeout: 60000}); + const incomingTaskDiv = await waitForIncomingTask(testManager.agent1Page, TASK_TYPES.CHAT, 60000); await incomingTaskDiv.waitFor({state: 'hidden', timeout: 20000}); await expect(incomingTaskDiv).toBeHidden(); await testManager.agent1Page.getByTestId('samples:rona-popup').waitFor({state: 'visible', timeout: 15000}); @@ -298,40 +304,37 @@ export default function createDigitalIncomingTaskAndTaskControlsTests() { await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); await incomingTaskDiv.waitFor({state: 'visible', timeout: 10000}); await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.EMAIL); - await testManager.agent1Page.waitForTimeout(1000); + await testManager.agent1Page.waitForTimeout(3000); await testManager.agent1Page.getByTestId('call-control:end-call').first().click({timeout: 5000}); await submitWrapup(testManager.agent1Page, WRAPUP_REASONS.SALE); await waitForState(testManager.agent1Page, USER_STATES.AVAILABLE); }); test('should handle multiple incoming tasks with callback verifications', async () => { - await changeUserState(testManager.agent1Page, USER_STATES.MEETING); - await testManager.agent1Page.waitForTimeout(1000); - - await Promise.all([ - createCallTask(testManager.callerPage, process.env[`${testManager.projectName}_ENTRY_POINT`]!), - createChatTask(testManager.chatPage, process.env[`${testManager.projectName}_CHAT_URL`]!), - createEmailTask(process.env[`${testManager.projectName}_EMAIL_ENTRY_POINT`]!), - ]); - - await testManager.agent1Page.waitForTimeout(50000); + // First become available, then create tasks so they arrive fresh await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); + await testManager.agent1Page.waitForTimeout(1000); + + // Create call first and handle it before other tasks + await createCallTask(testManager.callerPage, process.env[`${testManager.projectName}_ENTRY_POINT`]!); const incomingCallTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); const incomingChatTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-chat').first(); const incomingEmailTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-email').first(); - await incomingCallTaskDiv.waitFor({state: 'visible', timeout: 5000}); - await testManager.agent1ExtensionPage - .locator('[data-test="generic-person-item-base"]') - .first() - .waitFor({state: 'visible', timeout: 5000}); + await incomingCallTaskDiv.waitFor({state: 'visible', timeout: 40000}); await acceptExtensionCall(testManager.agent1ExtensionPage); await testManager.agent1Page.waitForTimeout(3000); - await incomingChatTaskDiv.waitFor({state: 'visible', timeout: 3000}); + + // Now create chat and email tasks + await Promise.all([ + createChatTask(testManager.chatPage, process.env[`${testManager.projectName}_CHAT_URL`]!), + createEmailTask(process.env[`${testManager.projectName}_EMAIL_ENTRY_POINT`]!), + ]); + await incomingChatTaskDiv.waitFor({state: 'visible', timeout: 40000}); await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CHAT); - await incomingEmailTaskDiv.waitFor({state: 'visible', timeout: 3000}); + await incomingEmailTaskDiv.waitFor({state: 'visible', timeout: 40000}); await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.EMAIL); await waitForState(testManager.agent1Page, USER_STATES.ENGAGED); await verifyCurrentState(testManager.agent1Page, USER_STATES.ENGAGED); diff --git a/playwright/tests/incoming-task-and-controls-multi-session.spec.ts b/playwright/tests/incoming-task-and-controls-multi-session.spec.ts index dfd308e70..e0eb1155e 100644 --- a/playwright/tests/incoming-task-and-controls-multi-session.spec.ts +++ b/playwright/tests/incoming-task-and-controls-multi-session.spec.ts @@ -9,6 +9,7 @@ import { declineExtensionCall, endCallTask, submitRonaPopup, + waitForIncomingTask, } from '../Utils/incomingTaskUtils'; import {changeUserState, verifyCurrentState} from '../Utils/userStateUtils'; import { @@ -32,17 +33,20 @@ export default function createIncomingTaskAndControlsMultiSessionTests() { await testManager.setupForIncomingTaskMultiSession(browser); }); + test.afterAll(async () => { + if (testManager) { + await testManager.cleanup(); + } + }); + test('should handle multi-session incoming call with state synchronization', async () => { await createCallTask(testManager.callerPage, process.env[`${testManager.projectName}_ENTRY_POINT`]!); await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); - const incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); - const incomingTaskDiv2 = testManager.multiSessionAgent1Page.getByTestId('samples:incoming-task-telephony').first(); + let incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); + let incomingTaskDiv2 = testManager.multiSessionAgent1Page.getByTestId('samples:incoming-task-telephony').first(); await incomingTaskDiv.waitFor({state: 'visible', timeout: 40000}); await testManager.agent1ExtensionPage.waitForTimeout(2000); - await testManager.agent1ExtensionPage - .locator('[data-test="generic-person-item-base"]') - .first() - .waitFor({state: 'visible', timeout: 20000}); + await expect(testManager.agent1ExtensionPage.locator('#answer').first()).toBeEnabled({timeout: 20000}); await incomingTaskDiv2.waitFor({state: 'visible', timeout: 10000}); await testManager.agent1Page.waitForTimeout(5000); await testManager.agent1ExtensionPage.waitForTimeout(1000); @@ -64,12 +68,11 @@ export default function createIncomingTaskAndControlsMultiSessionTests() { await waitForState(testManager.multiSessionAgent1Page, USER_STATES.AVAILABLE); await testManager.multiSessionAgent1Page.waitForTimeout(2000); await verifyCurrentState(testManager.multiSessionAgent1Page, USER_STATES.AVAILABLE); + incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); await incomingTaskDiv.waitFor({state: 'visible', timeout: 10000}); await testManager.agent1ExtensionPage.waitForTimeout(2000); - await testManager.agent1ExtensionPage - .locator('[data-test="generic-person-item-base"]') - .first() - .waitFor({state: 'visible', timeout: 10000}); + await expect(testManager.agent1ExtensionPage.locator('#answer').first()).toBeEnabled({timeout: 10000}); + incomingTaskDiv2 = testManager.multiSessionAgent1Page.getByTestId('samples:incoming-task-telephony').first(); await incomingTaskDiv2.waitFor({state: 'visible', timeout: 10000}); await testManager.agent1Page.waitForTimeout(2000); await acceptExtensionCall(testManager.agent1ExtensionPage); @@ -116,9 +119,7 @@ export default function createIncomingTaskAndControlsMultiSessionTests() { // Wait for extension caller to be visible and accept the call await testManager.agent1ExtensionPage.waitForTimeout(2000); - await testManager.agent1ExtensionPage - .locator('[data-test="generic-person-item-base"]') - .waitFor({state: 'visible', timeout: 20000}); + await expect(testManager.agent1ExtensionPage.locator('#answer').first()).toBeEnabled({timeout: 20000}); await acceptExtensionCall(testManager.agent1ExtensionPage); await testManager.agent1Page.waitForTimeout(2000); await testManager.multiSessionAgent1Page!.waitForTimeout(2000); @@ -220,8 +221,8 @@ export default function createIncomingTaskAndControlsMultiSessionTests() { test('should handle multi-session incoming chat with state synchronization', async () => { await createChatTask(testManager.chatPage, process.env[`${testManager.projectName}_CHAT_URL`]!); await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); - const incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-chat').first(); - const incomingTaskDiv2 = testManager.multiSessionAgent1Page.getByTestId('samples:incoming-task-chat').first(); + let incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-chat').first(); + let incomingTaskDiv2 = testManager.multiSessionAgent1Page.getByTestId('samples:incoming-task-chat').first(); await incomingTaskDiv.waitFor({state: 'visible', timeout: 60000}); await incomingTaskDiv2.waitFor({state: 'visible', timeout: 10000}); await incomingTaskDiv.waitFor({state: 'hidden', timeout: 30000}); @@ -240,6 +241,8 @@ export default function createIncomingTaskAndControlsMultiSessionTests() { await waitForState(testManager.multiSessionAgent1Page, USER_STATES.AVAILABLE); await testManager.multiSessionAgent1Page.waitForTimeout(2000); await verifyCurrentState(testManager.multiSessionAgent1Page, USER_STATES.AVAILABLE); + incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-chat').first(); + incomingTaskDiv2 = testManager.multiSessionAgent1Page.getByTestId('samples:incoming-task-chat').first(); await incomingTaskDiv.waitFor({state: 'visible', timeout: 10000}); await incomingTaskDiv2.waitFor({state: 'visible', timeout: 10000}); await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CHAT); @@ -267,16 +270,14 @@ export default function createIncomingTaskAndControlsMultiSessionTests() { test('should handle multi-session incoming email with state synchronization', async () => { await createEmailTask(process.env[`${testManager.projectName}_EMAIL_ENTRY_POINT`]!); await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); - const incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-email').first(); - const incomingTaskDiv2 = testManager.multiSessionAgent1Page.getByTestId('samples:incoming-task-email').first(); - await incomingTaskDiv.waitFor({state: 'visible', timeout: 50000}); - await incomingTaskDiv2.waitFor({state: 'visible', timeout: 10000}); - await incomingTaskDiv.waitFor({state: 'hidden', timeout: 30000}); - await incomingTaskDiv2.waitFor({state: 'hidden', timeout: 10000}); - await testManager.agent1Page.getByTestId('samples:rona-popup').waitFor({state: 'visible', timeout: 15000}); + await waitForIncomingTask(testManager.agent1Page, TASK_TYPES.EMAIL); + await waitForIncomingTask(testManager.multiSessionAgent1Page, TASK_TYPES.EMAIL); + await testManager.agent1Page.bringToFront(); + await testManager.agent1Page.getByTestId('samples:rona-popup').waitFor({state: 'visible', timeout: 20000}); + await testManager.multiSessionAgent1Page.bringToFront(); await testManager.multiSessionAgent1Page .getByTestId('samples:rona-popup') - .waitFor({state: 'visible', timeout: 15000}); + .waitFor({state: 'visible', timeout: 20000}); await testManager.agent1Page.waitForTimeout(3000); await submitRonaPopup(testManager.multiSessionAgent1Page, RONA_OPTIONS.IDLE); await waitForState(testManager.agent1Page, USER_STATES.MEETING); @@ -289,6 +290,8 @@ export default function createIncomingTaskAndControlsMultiSessionTests() { await waitForState(testManager.agent1Page, USER_STATES.AVAILABLE); await testManager.multiSessionAgent1Page.waitForTimeout(2000); await verifyCurrentState(testManager.multiSessionAgent1Page, USER_STATES.AVAILABLE); + let incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-email').first(); + let incomingTaskDiv2 = testManager.multiSessionAgent1Page.getByTestId('samples:incoming-task-email').first(); await incomingTaskDiv.waitFor({state: 'visible', timeout: 15000}); await incomingTaskDiv2.waitFor({state: 'visible', timeout: 15000}); await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.EMAIL); diff --git a/playwright/tests/incoming-telephony-task-test.spec.ts b/playwright/tests/incoming-telephony-task-test.spec.ts index 070e1673c..d75a0aaa8 100644 --- a/playwright/tests/incoming-telephony-task-test.spec.ts +++ b/playwright/tests/incoming-telephony-task-test.spec.ts @@ -8,6 +8,7 @@ import { acceptIncomingTask, acceptExtensionCall, submitRonaPopup, + waitForIncomingTask, } from '../Utils/incomingTaskUtils'; import {TASK_TYPES, USER_STATES, THEME_COLORS, WRAPUP_REASONS, RONA_OPTIONS} from '../constants'; import {submitWrapup} from '../Utils/wrapupUtils'; @@ -122,9 +123,7 @@ export default function createIncomingTelephonyTaskTests() { test('should accept incoming call, end call and complete wrapup in desktop mode', async () => { await createCallTask(testManager.callerPage, process.env[`${testManager.projectName}_ENTRY_POINT`]!); await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); - const incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTaskDiv.waitFor({state: 'visible', timeout: 40000}); - await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); + await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL, 40000); await waitForState(testManager.agent1Page, USER_STATES.ENGAGED); await verifyCurrentState(testManager.agent1Page, USER_STATES.ENGAGED); await testManager.agent1Page.waitForTimeout(3000); @@ -148,8 +147,7 @@ export default function createIncomingTelephonyTaskTests() { test('should decline incoming call and verify RONA state in desktop mode', async () => { 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: 40000}); + await waitForIncomingTask(testManager.agent1Page, TASK_TYPES.CALL, 40000); await testManager.agent1Page.waitForTimeout(3000); await declineIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); await testManager.agent1Page.getByTestId('samples:rona-popup').waitFor({state: 'visible', timeout: 15000}); @@ -159,7 +157,7 @@ export default function createIncomingTelephonyTaskTests() { const userStateElement = testManager.agent1Page.getByTestId('state-select'); const userStateElementColor = await userStateElement.evaluate((el) => getComputedStyle(el).backgroundColor); expect(isColorClose(userStateElementColor, THEME_COLORS.MEETING)).toBe(true); - await endCallTask(testManager.callerPage!); + await endCallTask(testManager.callerPage!, true); await submitRonaPopup(testManager.agent1Page, RONA_OPTIONS.IDLE); await waitForState(testManager.agent1Page, USER_STATES.MEETING); }); @@ -168,12 +166,11 @@ export default function createIncomingTelephonyTaskTests() { await testManager.agent1Page.waitForTimeout(2000); 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: 40000}); + const incomingTaskDiv = await waitForIncomingTask(testManager.agent1Page, TASK_TYPES.CALL, 40000); await incomingTaskDiv.waitFor({state: 'hidden', timeout: 30000}); await testManager.agent1Page.getByTestId('samples:rona-popup').waitFor({state: 'visible', timeout: 15000}); await expect(testManager.agent1Page.getByTestId('samples:rona-popup')).toBeVisible(); - await endCallTask(testManager.callerPage!); + await endCallTask(testManager.callerPage!, true); await submitRonaPopup(testManager.agent1Page, RONA_OPTIONS.IDLE); await waitForState(testManager.agent1Page, USER_STATES.MEETING); }); @@ -182,8 +179,7 @@ export default function createIncomingTelephonyTaskTests() { await testManager.agent1Page.waitForTimeout(2000); await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); await createCallTask(testManager.callerPage, process.env[`${testManager.projectName}_ENTRY_POINT`]!); - let incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTaskDiv.waitFor({state: 'visible', timeout: 40000}); + await waitForIncomingTask(testManager.agent1Page, TASK_TYPES.CALL, 40000); await testManager.agent1Page.waitForTimeout(3000); await declineIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); await testManager.agent1Page.getByTestId('samples:rona-popup').waitFor({state: 'visible', timeout: 15000}); @@ -193,14 +189,13 @@ export default function createIncomingTelephonyTaskTests() { await expect(testManager.agent1Page.getByTestId('samples:rona-popup')).not.toBeVisible(); await testManager.agent1Page.waitForTimeout(5000); await verifyCurrentState(testManager.agent1Page, USER_STATES.AVAILABLE); - incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTaskDiv.waitFor({state: 'visible', timeout: 10000}); + const incomingTaskDiv = await waitForIncomingTask(testManager.agent1Page, TASK_TYPES.CALL, 10000); await expect(incomingTaskDiv).toBeVisible(); await testManager.agent1Page.waitForTimeout(3000); await declineIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); await testManager.agent1Page.getByTestId('samples:rona-popup').waitFor({state: 'visible', timeout: 15000}); await expect(testManager.agent1Page.getByTestId('samples:rona-popup')).toBeVisible(); - await endCallTask(testManager.callerPage!); + await endCallTask(testManager.callerPage!, true); await submitRonaPopup(testManager.agent1Page, RONA_OPTIONS.IDLE); await waitForState(testManager.agent1Page, USER_STATES.MEETING); }); @@ -209,8 +204,7 @@ export default function createIncomingTelephonyTaskTests() { await testManager.agent1Page.waitForTimeout(2000); await createCallTask(testManager.callerPage, process.env[`${testManager.projectName}_ENTRY_POINT`]!); await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); - let incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTaskDiv.waitFor({state: 'visible', timeout: 40000}); + await waitForIncomingTask(testManager.agent1Page, TASK_TYPES.CALL, 40000); await testManager.agent1Page.waitForTimeout(3000); await declineIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); await testManager.agent1Page.getByTestId('samples:rona-popup').waitFor({state: 'visible', timeout: 15000}); @@ -221,18 +215,17 @@ export default function createIncomingTelephonyTaskTests() { await expect(testManager.agent1Page.getByTestId('samples:rona-popup')).not.toBeVisible(); await waitForState(testManager.agent1Page, USER_STATES.MEETING); await verifyCurrentState(testManager.agent1Page, USER_STATES.MEETING); - incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); + const incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); await expect(incomingTaskDiv).toBeHidden(); - await endCallTask(testManager.callerPage!); + await endCallTask(testManager.callerPage!, true); await testManager.agent1Page.waitForTimeout(2000); }); test('should handle customer disconnect before agent answers in desktop mode', async () => { 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: 40000}); - await endCallTask(testManager.callerPage!); + const incomingTaskDiv = await waitForIncomingTask(testManager.agent1Page, TASK_TYPES.CALL, 40000); + await endCallTask(testManager.callerPage!, true); await incomingTaskDiv.waitFor({state: 'hidden', timeout: 30000}); await expect(incomingTaskDiv).toBeHidden(); await waitForState(testManager.agent1Page, USER_STATES.AVAILABLE); @@ -262,11 +255,8 @@ export default function createIncomingTelephonyTaskTests() { await testManager.agent1Page.waitForTimeout(2000); await createCallTask(testManager.callerPage, process.env[`${testManager.projectName}_ENTRY_POINT`]!); await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); - const incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTaskDiv.waitFor({state: 'visible', timeout: 40000}); - await testManager.agent1ExtensionPage - .locator('[data-test="generic-person-item-base"]') - .waitFor({state: 'visible', timeout: 20000}); + await waitForIncomingTask(testManager.agent1Page, TASK_TYPES.CALL, 40000); + await expect(testManager.agent1ExtensionPage.locator('#answer').first()).toBeEnabled({timeout: 20000}); await testManager.agent1Page.waitForTimeout(3000); await acceptExtensionCall(testManager.agent1ExtensionPage); await testManager.agent1Page.waitForTimeout(3000); @@ -293,21 +283,15 @@ export default function createIncomingTelephonyTaskTests() { test('should decline incoming call and verify RONA state in extension mode', async () => { await createCallTask(testManager.callerPage, process.env[`${testManager.projectName}_ENTRY_POINT`]!); await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); - const incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTaskDiv.waitFor({state: 'visible', timeout: 40000}); - await testManager.agent1ExtensionPage - .locator('[data-test="generic-person-item-base"]') - .waitFor({state: 'visible', timeout: 20000}); + await waitForIncomingTask(testManager.agent1Page, TASK_TYPES.CALL, 40000); + await expect(testManager.agent1ExtensionPage.locator('#answer').first()).toBeEnabled({timeout: 20000}); await testManager.agent1Page.waitForTimeout(5000); await declineExtensionCall(testManager.agent1ExtensionPage); - await testManager.agent1ExtensionPage - .locator('[data-test="generic-person-item-base"]') - .first() - .waitFor({state: 'hidden', timeout: 5000}); + await expect(testManager.agent1ExtensionPage.locator('#answer').first()).toBeDisabled({timeout: 5000}); await testManager.agent1Page.getByTestId('samples:rona-popup').waitFor({state: 'visible', timeout: 15000}); await waitForState(testManager.agent1Page, USER_STATES.AGENT_DECLINED); await verifyCurrentState(testManager.agent1Page, USER_STATES.AGENT_DECLINED); - await endCallTask(testManager.callerPage!); + await endCallTask(testManager.callerPage!, true); await testManager.agent1Page.waitForTimeout(3000); const userStateElement = testManager.agent1Page.getByTestId('state-select'); const userStateElementColor = await userStateElement.evaluate((el) => getComputedStyle(el).backgroundColor); @@ -321,20 +305,13 @@ export default function createIncomingTelephonyTaskTests() { test('should ignore incoming call and wait for RONA popup in extension mode', async () => { await createCallTask(testManager.callerPage, process.env[`${testManager.projectName}_ENTRY_POINT`]!); await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); - const incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTaskDiv.waitFor({state: 'visible', timeout: 40000}); - await testManager.agent1ExtensionPage - .locator('[data-test="generic-person-item-base"]') - .first() - .waitFor({state: 'visible', timeout: 20000}); + const incomingTaskDiv = await waitForIncomingTask(testManager.agent1Page, TASK_TYPES.CALL, 40000); + await expect(testManager.agent1ExtensionPage.locator('#answer').first()).toBeEnabled({timeout: 20000}); await incomingTaskDiv.waitFor({state: 'hidden', timeout: 30000}); - await testManager.agent1ExtensionPage - .locator('[data-test="generic-person-item-base"]') - .first() - .waitFor({state: 'hidden', timeout: 10000}); + await expect(testManager.agent1ExtensionPage.locator('#answer').first()).toBeDisabled({timeout: 10000}); await testManager.agent1Page.getByTestId('samples:rona-popup').waitFor({state: 'visible', timeout: 15000}); await expect(testManager.agent1Page.getByTestId('samples:rona-popup')).toBeVisible(); - await endCallTask(testManager.callerPage!); + await endCallTask(testManager.callerPage!, true); await waitForState(testManager.agent1Page, USER_STATES.RONA); await verifyCurrentState(testManager.agent1Page, USER_STATES.RONA); await waitForStateLogs(capturedLogs, USER_STATES.RONA); @@ -346,11 +323,8 @@ export default function createIncomingTelephonyTaskTests() { test('should set agent state to Available and receive another call in extension mode', async () => { await createCallTask(testManager.callerPage, process.env[`${testManager.projectName}_ENTRY_POINT`]!); await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); - const incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTaskDiv.waitFor({state: 'visible', timeout: 40000}); - await testManager.agent1ExtensionPage - .locator('[data-test="generic-person-item-base"]') - .waitFor({state: 'visible', timeout: 20000}); + await waitForIncomingTask(testManager.agent1Page, TASK_TYPES.CALL, 40000); + await expect(testManager.agent1ExtensionPage.locator('#answer').first()).toBeEnabled({timeout: 20000}); await testManager.agent1Page.waitForTimeout(5000); await declineExtensionCall(testManager.agent1ExtensionPage); await testManager.agent1Page.getByTestId('samples:rona-popup').waitFor({state: 'visible', timeout: 15000}); @@ -363,21 +337,17 @@ export default function createIncomingTelephonyTaskTests() { await expect(testManager.agent1Page.getByTestId('samples:rona-popup')).not.toBeVisible(); await waitForState(testManager.agent1Page, USER_STATES.AVAILABLE); await verifyCurrentState(testManager.agent1Page, USER_STATES.AVAILABLE); - await incomingTaskDiv.waitFor({state: 'visible', timeout: 10000}); + const incomingTaskDiv = await waitForIncomingTask(testManager.agent1Page, TASK_TYPES.CALL, 10000); await expect(incomingTaskDiv).toBeVisible(); - await endCallTask(testManager.callerPage!); + await endCallTask(testManager.callerPage!, true); await testManager.agent1Page.waitForTimeout(8000); }); test('should set agent state to busy after declining call in extension mode', async () => { await createCallTask(testManager.callerPage, process.env[`${testManager.projectName}_ENTRY_POINT`]!); await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); - const incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTaskDiv.waitFor({state: 'visible', timeout: 40000}); - await testManager.agent1ExtensionPage - .locator('[data-test="generic-person-item-base"]') - .first() - .waitFor({state: 'visible', timeout: 20000}); + const incomingTaskDiv = await waitForIncomingTask(testManager.agent1Page, TASK_TYPES.CALL, 40000); + await expect(testManager.agent1ExtensionPage.locator('#answer').first()).toBeEnabled({timeout: 20000}); await testManager.agent1Page.waitForTimeout(5000); await declineExtensionCall(testManager.agent1ExtensionPage); await testManager.agent1Page.getByTestId('samples:rona-popup').waitFor({state: 'visible', timeout: 15000}); @@ -390,21 +360,18 @@ export default function createIncomingTelephonyTaskTests() { await waitForState(testManager.agent1Page, USER_STATES.MEETING); await expect(testManager.agent1Page.getByTestId('samples:rona-popup')).not.toBeVisible(); await expect(incomingTaskDiv).toBeHidden(); - await expect( - testManager.agent1ExtensionPage.locator('[data-test="generic-person-item-base"]').first() - ).toBeHidden(); + await expect(testManager.agent1ExtensionPage.locator('#answer').first()).toBeDisabled(); await verifyCurrentState(testManager.agent1Page, USER_STATES.MEETING); - await endCallTask(testManager.callerPage!); + await endCallTask(testManager.callerPage!, true); await testManager.agent1Page.waitForTimeout(10000); }); test('should handle call disconnect before agent answers in extension mode', async () => { await createCallTask(testManager.callerPage, process.env[`${testManager.projectName}_ENTRY_POINT`]!); await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); - const incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTaskDiv.waitFor({state: 'visible', timeout: 40000}); + const incomingTaskDiv = await waitForIncomingTask(testManager.agent1Page, TASK_TYPES.CALL, 40000); await testManager.agent1Page.waitForTimeout(5000); - await endCallTask(testManager.callerPage!); + await endCallTask(testManager.callerPage!, true); await testManager.agent1Page.waitForTimeout(5000); await incomingTaskDiv.waitFor({state: 'hidden', timeout: 20000}); await expect(incomingTaskDiv).toBeHidden(); diff --git a/playwright/tests/station-login-test.spec.ts b/playwright/tests/station-login-test.spec.ts index 86caf56e0..f96eaf7ed 100644 --- a/playwright/tests/station-login-test.spec.ts +++ b/playwright/tests/station-login-test.spec.ts @@ -8,7 +8,7 @@ import { } from '../Utils/stationLoginUtils'; import {changeUserState, verifyCurrentState, getStateElapsedTime} from '../Utils/userStateUtils'; import {parseTimeString, waitForWebSocketDisconnection, waitForWebSocketReconnection} from '../Utils/helperUtils'; -import {USER_STATES, LOGIN_MODE, LONG_WAIT} from '../constants'; +import {USER_STATES, LOGIN_MODE, EXTENSION_REGISTRATION_TIMEOUT} from '../constants'; import {TestManager} from '../test-manager'; export default function createStationLoginTests() { @@ -43,7 +43,9 @@ export default function createStationLoginTests() { LOGIN_MODE.DIAL_NUMBER, process.env[`${testManager.projectName}_ENTRY_POINT`] ); - await expect(testManager.agent1Page.getByTestId('state-select')).toBeVisible({timeout: LONG_WAIT}); + await expect(testManager.agent1Page.getByTestId('state-select')).toBeVisible({ + timeout: EXTENSION_REGISTRATION_TIMEOUT, + }); await verifyLoginMode(testManager.agent1Page, 'Dial Number'); }); @@ -178,7 +180,9 @@ export default function createStationLoginTests() { LOGIN_MODE.EXTENSION, process.env[`${testManager.projectName}_AGENT1_EXTENSION_NUMBER`] ); - await expect(testManager.agent1Page.getByTestId('state-select')).toBeVisible({timeout: LONG_WAIT}); + await expect(testManager.agent1Page.getByTestId('state-select')).toBeVisible({ + timeout: EXTENSION_REGISTRATION_TIMEOUT, + }); await verifyLoginMode(testManager.agent1Page, 'Extension'); }); @@ -305,17 +309,17 @@ export default function createStationLoginTests() { // Uncheck - Desktop should be visible await hideDesktopCheckbox.click(); await testManager.agent1Page.waitForTimeout(500); - await verifyDesktopOptionVisibility(testManager.agent1Page, true); + await verifyDesktopOptionVisibility(testManager.agent1Page, false); // Check - Desktop should be hidden await hideDesktopCheckbox.click(); await testManager.agent1Page.waitForTimeout(500); - await verifyDesktopOptionVisibility(testManager.agent1Page, false); + await verifyDesktopOptionVisibility(testManager.agent1Page, true); // Uncheck again - Desktop should be visible again await hideDesktopCheckbox.click(); await testManager.agent1Page.waitForTimeout(500); - await verifyDesktopOptionVisibility(testManager.agent1Page, true); + await verifyDesktopOptionVisibility(testManager.agent1Page, false); }); }); diff --git a/playwright/tests/tasklist-test.spec.ts b/playwright/tests/tasklist-test.spec.ts index 5b594c09a..a0860c24c 100644 --- a/playwright/tests/tasklist-test.spec.ts +++ b/playwright/tests/tasklist-test.spec.ts @@ -1,7 +1,7 @@ import {test, Page, expect} from '@playwright/test'; import {TestManager} from '../test-manager'; import {changeUserState} from '../Utils/userStateUtils'; -import {createCallTask, createChatTask, createEmailTask} from '../Utils/incomingTaskUtils'; +import {createCallTask, createChatTask, createEmailTask, waitForIncomingTask} from '../Utils/incomingTaskUtils'; import {TASK_TYPES, USER_STATES, WRAPUP_REASONS} from '../constants'; import {verifyTaskControls} from '../Utils/taskControlUtils'; import {submitWrapup} from '../Utils/wrapupUtils'; @@ -46,18 +46,31 @@ async function getCurrentHandleTime(page: Page, index: number = 0): Promise { + await testManager.agent1Page.bringToFront(); const timeoutMs = 60000, pollInterval = 2000; const start = Date.now(); - const type = testId.split('-').pop(); + while (Date.now() - start < timeoutMs) { const taskDiv = testManager.agent1Page.getByTestId(testId).first(); const isVisible = await taskDiv.isVisible().catch(() => false); + if (isVisible) { + // Dismiss any open popovers that might be blocking + await testManager.agent1Page.keyboard.press('Escape'); + await testManager.agent1Page.waitForTimeout(200); + const acceptButton = taskDiv.getByTestId('task:accept-button').first(); + const acceptVisible = await acceptButton.isVisible().catch(() => false); + + if (!acceptVisible) { + await testManager.agent1Page.waitForTimeout(pollInterval); + continue; + } + await expect(acceptButton).toBeVisible({timeout: 5000}); + await expect(acceptButton).toBeEnabled({timeout: 5000}); await acceptButton.click({timeout: 3000}); - return; } await testManager.agent1Page.waitForTimeout(pollInterval); @@ -103,7 +116,7 @@ async function waitForConsoleLogs( const escTitle = escapeForRegExp(title!); const escMedia = escapeForRegExp(mediaType!); const pattern = new RegExp( - '^onTaskSelected invoked for task with title : ' + escTitle + ', and mediaType : ' + escMedia + '$' + '^onTaskSelected invoked for task with title : ' + escTitle + ', and mediaType : ' + escMedia ); const start = Date.now(); @@ -134,11 +147,16 @@ export default function createTaskListTests() { setupConsoleLogging(testManager.agent1Page); }); + test.afterAll(async () => { + if (testManager) { + await testManager.cleanup(); + } + }); + test('Verify Task List for incoming Call', async () => { await createCallTask(testManager.callerPage, process.env[`${testManager.projectName}_ENTRY_POINT`]!); await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); - let incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); - await incomingTaskDiv.waitFor({state: 'visible', timeout: 40000}); + let incomingTaskDiv = await waitForIncomingTask(testManager.agent1Page, TASK_TYPES.CALL); await testManager.agent1Page.waitForTimeout(1000); const taskListItem = testManager.agent1Page.getByTestId('task-list').getByRole('listitem').first(); expect(taskListItem).toBeVisible(); @@ -281,23 +299,26 @@ export default function createTaskListTests() { }); test('Task List Test with Multiple Tasks', async () => { - await changeUserState(testManager.agent1Page, USER_STATES.MEETING); - await waitForState(testManager.agent1Page, USER_STATES.MEETING); - await Promise.all([ - createCallTask(testManager.callerPage, process.env[`${testManager.projectName}_ENTRY_POINT`]!), - createChatTask(testManager.chatPage, process.env[`${testManager.projectName}_CHAT_URL`]!), - createEmailTask(process.env[`${testManager.projectName}_EMAIL_ENTRY_POINT`]!), - ]); - await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); + await waitForState(testManager.agent1Page, USER_STATES.AVAILABLE); + + // Create and accept tasks one by one to avoid flakiness + // 1. Create and accept call task + await createCallTask(testManager.callerPage, process.env[`${testManager.projectName}_ENTRY_POINT`]!); + await waitForAndAcceptSpecificTask(testManager, 'samples:incoming-task-telephony'); + await testManager.agent1Page.waitForTimeout(2000); + + // 2. Create and accept chat task + await createChatTask(testManager.chatPage, process.env[`${testManager.projectName}_CHAT_URL`]!); + await waitForAndAcceptSpecificTask(testManager, 'samples:incoming-task-chat'); + await testManager.agent1Page.waitForTimeout(2000); - await Promise.all([ - waitForAndAcceptSpecificTask(testManager, 'samples:incoming-task-telephony'), - waitForAndAcceptSpecificTask(testManager, 'samples:incoming-task-chat'), - waitForAndAcceptSpecificTask(testManager, 'samples:incoming-task-email'), - ]); - await testManager.agent1Page.waitForTimeout(3000); + // 3. Create and accept email task + await createEmailTask(process.env[`${testManager.projectName}_EMAIL_ENTRY_POINT`]!); + await waitForAndAcceptSpecificTask(testManager, 'samples:incoming-task-email'); + await testManager.agent1Page.waitForTimeout(2000); + // Verify all 3 tasks are in the task list for (let i = 0; i < 3; i++) { const taskListItem = testManager.agent1Page.getByTestId('task-list').getByRole('listitem').nth(i); @@ -324,8 +345,4 @@ export default function createTaskListTests() { capturedLogs.length = 0; } }); - - test.afterAll(async () => { - await testManager.cleanup(); - }); } diff --git a/playwright/tests/user-state-test.spec.ts b/playwright/tests/user-state-test.spec.ts index 159497e3d..8f5ba7690 100644 --- a/playwright/tests/user-state-test.spec.ts +++ b/playwright/tests/user-state-test.spec.ts @@ -12,8 +12,6 @@ import { import {USER_STATES, THEME_COLORS, LOGIN_MODE} from '../constants'; import {TestManager} from '../test-manager'; -// Shared login and setup before all tests - export default function createUserStateTests() { let testManager: TestManager; @@ -33,7 +31,7 @@ export default function createUserStateTests() { process.env[`${testManager.projectName}_AGENT1_EXTENSION_NUMBER`] ); } else { - await stationLogout(testManager.agent1Page); + await stationLogout(testManager.agent1Page, false); // Don't throw during setup await telephonyLogin( testManager.agent1Page, LOGIN_MODE.EXTENSION, @@ -62,10 +60,9 @@ export default function createUserStateTests() { test('should change state to Available and verify theme and timer reset', async () => { await verifyCurrentState(testManager.agent1Page, USER_STATES.MEETING); - await testManager.agent1Page.waitForTimeout(5000); + await testManager.agent1Page.waitForTimeout(10000); const timerBefore = await getStateElapsedTime(testManager.agent1Page); await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); - await testManager.agent1Page.waitForTimeout(3000); const timerAfter = await getStateElapsedTime(testManager.agent1Page); const parseTimer = (timer: string) => { @@ -155,6 +152,9 @@ export default function createUserStateTests() { }); test('should test idle state transition and dual timer', async () => { + // Dismiss any open dropdowns/popovers from previous tests + await testManager.agent1Page.keyboard.press('Escape'); + await testManager.agent1Page.waitForTimeout(500); await verifyCurrentState(testManager.agent1Page, USER_STATES.MEETING); await testManager.agent1Page.waitForTimeout(2000); testManager.consoleMessages.length = 0; 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 ce04dc623..578478914 100644 --- a/widgets-samples/cc/samples-cc-react-app/src/App.tsx +++ b/widgets-samples/cc/samples-cc-react-app/src/App.tsx @@ -180,8 +180,12 @@ function App() { const onTaskSelected = ({task, isClicked}) => { console.log('onTaskSelected invoked for task:', task, 'isClicked:', isClicked); + const callAssociatedDetails = task?.data?.interaction?.callAssociatedDetails; + const mediaType = task?.data?.interaction?.mediaType; + const isSocial = mediaType === 'social'; + const title = isSocial ? callAssociatedDetails?.customerName : callAssociatedDetails?.ani; console.log( - `onTaskSelected invoked for task with title : ${task?.data?.interaction?.callAssociatedDetails?.ani}, and mediaType : ${task?.data?.mediaType}` + `onTaskSelected invoked for task with title : ${title}, and mediaType : ${mediaType}` ); }; @@ -882,7 +886,7 @@ function App() { setCollapsedTasks((prev) => prev.filter((id) => id !== task.data.interactionId)); } }} - data-testid={`samples:incoming-task-${task.data.mediaType}`} + data-testid={`samples:incoming-task-${task.data.interaction?.mediaType}`} > <>