diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx index b8afefe27..5781cd8c5 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx @@ -159,6 +159,7 @@ function CallControlComponent(props: CallControlComponentProps) { className: 'call-control-button', disabled: false, isVisible: controlVisibility.holdResume, + dataTestId: 'call-control:hold-toggle', }, { id: 'consult', @@ -168,6 +169,7 @@ function CallControlComponent(props: CallControlComponentProps) { disabled: false, menuType: 'Consult', isVisible: controlVisibility.consult, + dataTestId: 'call-control:consult', }, { id: 'transfer', @@ -177,6 +179,7 @@ function CallControlComponent(props: CallControlComponentProps) { disabled: false, menuType: 'Transfer', isVisible: controlVisibility.transfer, + dataTestId: 'call-control:transfer', }, { id: 'record', @@ -186,6 +189,7 @@ function CallControlComponent(props: CallControlComponentProps) { className: 'call-control-button', disabled: false, isVisible: controlVisibility.pauseResumeRecording, + dataTestId: 'call-control:recording-toggle', }, { id: 'end', @@ -195,6 +199,7 @@ function CallControlComponent(props: CallControlComponentProps) { className: 'call-control-button-cancel', disabled: isHeld, isVisible: controlVisibility.end, + dataTestId: 'call-control:end-call', }, ]; @@ -254,7 +259,7 @@ function CallControlComponent(props: CallControlComponentProps) { className={button.className} aria-label={button.tooltip} disabled={button.disabled || (consultInitiated && isTelephony)} - data-testid="ButtonCircle" + data-testid={button.dataTestId} onPress={() => handlePopoverOpen(button.menuType as CallControlMenuType)} > @@ -295,7 +300,7 @@ function CallControlComponent(props: CallControlComponentProps) { button.className + (button.disabled || (consultInitiated && isTelephony) ? ` ${button.className}-disabled` : '') } - data-testid={button.id === 'end' ? 'call-control:end-call' : button.id} + data-testid={button.dataTestId} onPress={button.onClick} disabled={button.disabled || (consultInitiated && isTelephony)} aria-label={button.tooltip} diff --git a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx index fdacfd384..32675cc92 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx @@ -159,7 +159,7 @@ const CallControlCADComponent: React.FC = (props) =>
{renderCustomerName()}
- + {currentMediaType.labelName} -
diff --git a/packages/contact-center/cc-components/src/components/task/Task/index.tsx b/packages/contact-center/cc-components/src/components/task/Task/index.tsx index 7d9ed1014..d77df64ed 100644 --- a/packages/contact-center/cc-components/src/components/task/Task/index.tsx +++ b/packages/contact-center/cc-components/src/components/task/Task/index.tsx @@ -73,6 +73,7 @@ const Task: React.FC = ({ type={selected ? 'body-large-bold' : 'body-large-medium'} className={getTitleClassName()} id={isNonVoiceMedia ? tooltipTriggerId : undefined} + data-testid="task:title" > {title} @@ -121,7 +122,7 @@ const Task: React.FC = ({
{renderTitle()} {state && !isIncomingTask && ( - + {capitalizeFirstWord(state)} )} @@ -135,9 +136,9 @@ const Task: React.FC = ({ {/* Handle Time should render if it's an incoming call without ronaTimeout OR if it's not an incoming call */} {(isIncomingTask && !ronaTimeout) || !isIncomingTask ? startTimeStamp && ( - + Handle Time: {' '} - + ) : null} diff --git a/playwright/Utils/helperUtils.ts b/playwright/Utils/helperUtils.ts index 918c36d98..bad820643 100644 --- a/playwright/Utils/helperUtils.ts +++ b/playwright/Utils/helperUtils.ts @@ -1,6 +1,6 @@ import { Page } from '@playwright/test'; import { getCurrentState, changeUserState } from './userStateUtils'; -import { WRAPUP_REASONS, USER_STATES, RONA_OPTIONS, LOGIN_MODE, LoginMode, ThemeColor, userState, WrapupReason } from 'playwright/constants'; +import { WRAPUP_REASONS, USER_STATES, RONA_OPTIONS, LOGIN_MODE, LoginMode, ThemeColor, userState, WrapupReason } from '../constants'; import { submitWrapup } from './wrapupUtils'; import { acceptExtensionCall, submitRonaPopup } from './incomingTaskUtils'; import { loginViaAccessToken, disableMultiLogin, enableMultiLogin, initialiseWidgets, enableAllWidgets, } from './initUtils'; @@ -317,6 +317,9 @@ export const handleStrayTasks = async (page: Page, extensionPage: Page | null = const acceptButtonVisible = await acceptButton.isVisible().catch(() => false); const isExtensionCall = await (await task.innerText()).includes('Ringing...'); if (isExtensionCall) { + if (!extensionPage) { + throw new Error('Extension page is not available for handling extension call'); + } const extensionCallVisible = await extensionPage.locator('[data-test="right-action-button"]').waitFor({ state: 'visible', timeout: 40000 }).then(() => true).catch(() => false); if (extensionCallVisible) { await acceptExtensionCall(extensionPage); diff --git a/playwright/Utils/taskControlUtils.ts b/playwright/Utils/taskControlUtils.ts new file mode 100644 index 000000000..cd7608a4a --- /dev/null +++ b/playwright/Utils/taskControlUtils.ts @@ -0,0 +1,420 @@ +import { Page, expect } from '@playwright/test'; +import { TASK_TYPES } from '../constants'; + +/** + * Utility functions for task controls testing. + * Verifies visibility of task control buttons for different task types. + * + * @packageDocumentation + */ + +/** + * Verifies that all call task control buttons are visible and accessible. + * Checks for hold, recording, transfer, consult, and end buttons. + * @param page - The agent's main page + * @returns Promise + */ +export async function callTaskControlCheck(page: Page): Promise { + // Verify call control container is visible + await expect(page.getByTestId('call-control-container').nth(0)).toBeVisible({ timeout: 30000 }); + + // Verify hold/resume toggle button is visible + await expect(page.getByTestId('call-control:hold-toggle').nth(0)).toBeVisible(); + + // Verify recording toggle button is visible + await expect(page.getByTestId('call-control:recording-toggle').nth(0)).toBeVisible(); + + // Verify transfer button is visible + await expect(page.getByTestId('call-control:transfer').nth(0)).toBeVisible(); + + // Verify consult button is visible + await expect(page.getByTestId('call-control:consult').nth(0)).toBeVisible(); + + // Verify end call button is visible + await expect(page.getByTestId('call-control:end-call').nth(0)).toBeVisible(); +} + +/** + * Verifies that chat task control buttons are visible and accessible. + * Checks for transfer and end buttons only. + * @param page - The agent's main page + * @returns Promise + */ +export async function chatTaskControlCheck(page: Page): Promise { + // Verify chat control container or equivalent is visible + await expect(page.getByTestId('call-control-container').nth(0)).toBeVisible({ timeout: 30000 }); + + // Verify transfer button is visible + await expect(page.getByTestId('call-control:transfer').nth(0)).toBeVisible(); + + // Verify end button is visible (for chat tasks) + await expect(page.getByTestId('call-control:end-call').nth(0)).toBeVisible(); +} + +/** + * Verifies that email task control buttons are visible and accessible. + * Checks for transfer and end buttons only. + * @param page - The agent's main page + * @returns Promise + */ +export async function emailTaskControlCheck(page: Page): Promise { + // Verify email control container or equivalent is visible + await expect(page.getByTestId('call-control-container').nth(0)).toBeVisible({ timeout: 30000 }); + + // Verify transfer button is visible + await expect(page.getByTestId('call-control:transfer').nth(0)).toBeVisible(); + + // Verify end button is visible (for email tasks) + await expect(page.getByTestId('call-control:end-call').nth(0)).toBeVisible(); +} + +/** + * Verifies task control buttons based on the task type. + * @param page - The agent's main page + * @param taskType - The type of the task (e.g., TASK_TYPES.CALL, TASK_TYPES.CHAT) + * @returns Promise + */ +export async function verifyTaskControls(page: Page, taskType: string): Promise { + switch (taskType) { + case TASK_TYPES.CALL: + await callTaskControlCheck(page); + break; + case TASK_TYPES.CHAT: + await chatTaskControlCheck(page); + break; + case TASK_TYPES.EMAIL: + await emailTaskControlCheck(page); + break; + default: + throw new Error(`Task control check not implemented for task type: ${taskType}`); + } +} + +/** + * Toggles the hold state of a call by clicking the hold/resume button. + * This function will put the call on hold if it's currently active, or resume it if it's on hold. + * @param page - The agent's main page + * @returns Promise + */ +export async function holdCallToggle(page: Page): Promise { + // Wait for hold toggle button to be visible and clickable + const holdButton = page.getByTestId('call-control:hold-toggle').nth(0); + await expect(holdButton).toBeVisible({ timeout: 10000 }); + + // Click the hold toggle button + await holdButton.click(); +} + +/** + * Toggles the recording state of a call by clicking the recording pause/resume button. + * This function will pause recording if it's currently active, or resume it if it's paused. + * @param page - The agent's main page + * @returns Promise + */ +export async function recordCallToggle(page: Page): Promise { + // Wait for recording toggle button to be visible and clickable + const recordButton = page.getByTestId('call-control:recording-toggle').nth(0); + await expect(recordButton).toBeVisible({ timeout: 10000 }); + + // Click the recording toggle button + await recordButton.click(); +} + +/** + * Verifies the hold timer visibility and content based on expected state. + * @param page - The agent's main page + * @param options - Configuration object + * @param options.shouldBeVisible - Whether the timer should be visible (true) or hidden (false) + * @param options.verifyContent - Whether to verify timer content (default: true when visible) + * @returns Promise + */ +export async function verifyHoldTimer(page: Page, { shouldBeVisible, verifyContent = shouldBeVisible }: { shouldBeVisible: boolean; verifyContent?: boolean }): Promise { + const holdTimerContainer = page.locator('.on-hold-chip-text'); + + if (shouldBeVisible) { + await expect(holdTimerContainer).toBeVisible({ timeout: 10000 }); + + if (verifyContent) { + // Verify "On hold" text is present + await expect(holdTimerContainer).toContainText('On hold'); + + // Verify timer format (should contain time like 00:XX) + await expect(holdTimerContainer).toContainText(/\d{2}:\d{2}/); + } + } else { + await expect(holdTimerContainer).toBeHidden({ timeout: 10000 }); + } +} + +/** + * Verifies the icon of the hold toggle button based on current hold state. + * - When call is NOT on hold: expects 'pause-bold' icon (to put call on hold) + * - When call IS on hold: expects 'play-bold' icon (to resume call) + * @param page - The agent's main page + * @param options - Configuration object + * @param options.expectedIsHeld - Expected hold state (true if call is on hold, false if active) + * @returns Promise + * @throws Error if icon verification fails + */ +export async function verifyHoldButtonIcon(page: Page, { expectedIsHeld }: { expectedIsHeld: boolean }): Promise { + const holdButton = page.getByTestId('call-control:hold-toggle').nth(0); + await expect(holdButton).toBeVisible({ timeout: 10000 }); + + // Get the icon element within the hold button + const iconElement = holdButton.locator('mdc-icon').nth(0); + await expect(iconElement).toBeVisible({ timeout: 5000 }); + + // Verify the correct icon based on hold state + const expectedIcon = expectedIsHeld ? 'play-bold' : 'pause-bold'; + const actualIcon = await iconElement.getAttribute('name'); + + if (actualIcon !== expectedIcon) { + throw new Error(`Hold button icon mismatch. Expected: '${expectedIcon}' (isHeld: ${expectedIsHeld}), but found: '${actualIcon}'`); + } +} + +/** + * Verifies the icon of the record toggle button based on current recording state. + * - When recording is ACTIVE: expects 'record-paused-bold' icon (to pause recording) + * - When recording is PAUSED: expects 'record-bold' icon (to resume recording) + * @param page - The agent's main page + * @param options - Configuration object + * @param options.expectedIsRecording - Expected recording state (true if recording, false if paused) + * @returns Promise + * @throws Error if icon verification fails + */ +export async function verifyRecordButtonIcon(page: Page, { expectedIsRecording }: { expectedIsRecording: boolean }): Promise { + const recordButton = page.getByTestId('call-control:recording-toggle').nth(0); + await expect(recordButton).toBeVisible({ timeout: 10000 }); + + // Get the icon element within the record button + const iconElement = recordButton.locator('mdc-icon').nth(0); + await expect(iconElement).toBeVisible({ timeout: 5000 }); + + // Verify the correct icon based on recording state + const expectedIcon = expectedIsRecording ? 'record-paused-bold' : 'record-bold'; + const actualIcon = await iconElement.getAttribute('name'); + + if (actualIcon !== expectedIcon) { + throw new Error(`Record button icon mismatch. Expected: '${expectedIcon}' (isRecording: ${expectedIsRecording}), but found: '${actualIcon}'`); + } +} + +// Global variable to store captured logs +let capturedLogs: string[] = []; + +/** + * Sets up console logging to capture callback logs for task controls. + * Captures onHoldResume, onRecordingToggle, onEnd callbacks and SDK success messages. + * @param page - The agent's main page + * @returns Function to remove the console handler + */ +export function setupConsoleLogging(page: Page): () => void { + capturedLogs.length = 0; + + const consoleHandler = (msg) => { + const logText = msg.text(); + if (logText.includes('onHoldResume invoked') || + logText.includes('onRecordingToggle invoked') || + logText.includes('onEnd invoked') || + logText.includes('WXCC_SDK_TASK_HOLD_SUCCESS') || + logText.includes('WXCC_SDK_TASK_RESUME_SUCCESS') || + logText.includes('WXCC_SDK_TASK_PAUSE_RECORDING_SUCCESS') || + logText.includes('WXCC_SDK_TASK_RESUME_RECORDING_SUCCESS')) { + capturedLogs.push(logText); + } + }; + + page.on('console', consoleHandler); + return () => page.off('console', consoleHandler); +} + +/** + * Clears the captured logs array. + * Should be called before each test or verification to ensure clean state. + */ +export function clearCapturedLogs(): void { + capturedLogs.length = 0; +} + +/** + * Verifies that hold/resume callback logs are present and contain expected values. + * @param options - Configuration object + * @param options.expectedIsHeld - Expected hold state (true for hold, false for resume) + * @throws Error if verification fails with detailed error message + */ +export function verifyHoldLogs({ expectedIsHeld }: { expectedIsHeld: boolean }): void { + const holdResumeLogs = capturedLogs.filter(log => log.includes('onHoldResume invoked')); + const statusLogs = capturedLogs.filter(log => + log.includes(expectedIsHeld ? 'WXCC_SDK_TASK_HOLD_SUCCESS' : 'WXCC_SDK_TASK_RESUME_SUCCESS') + ); + + if (holdResumeLogs.length === 0) { + throw new Error(`No 'onHoldResume invoked' logs found. Expected logs for isHeld: ${expectedIsHeld}. Captured logs: ${JSON.stringify(capturedLogs)}`); + } + + if (statusLogs.length === 0) { + const expectedStatus = expectedIsHeld ? 'WXCC_SDK_TASK_HOLD_SUCCESS' : 'WXCC_SDK_TASK_RESUME_SUCCESS'; + throw new Error(`No '${expectedStatus}' logs found. Captured logs: ${JSON.stringify(capturedLogs)}`); + } + + const lastHoldLog = holdResumeLogs[holdResumeLogs.length - 1]; + if (!lastHoldLog.includes(`isHeld: ${expectedIsHeld}`)) { + throw new Error(`Expected 'isHeld: ${expectedIsHeld}' in log but found: ${lastHoldLog}`); + } +} + +/** + * Verifies that recording pause/resume callback logs are present and contain expected values. + * @param options - Configuration object + * @param options.expectedIsRecording - Expected recording state (true for recording, false for paused) + * @throws Error if verification fails with detailed error message + */ +export function verifyRecordingLogs({ expectedIsRecording }: { expectedIsRecording: boolean }): void { + const recordingLogs = capturedLogs.filter(log => log.includes('onRecordingToggle invoked')); + const statusLogs = capturedLogs.filter(log => + log.includes(expectedIsRecording ? 'WXCC_SDK_TASK_RESUME_RECORDING_SUCCESS' : 'WXCC_SDK_TASK_PAUSE_RECORDING_SUCCESS') + ); + + if (recordingLogs.length === 0) { + throw new Error(`No 'onRecordingToggle invoked' logs found. Expected logs for isRecording: ${expectedIsRecording}. Captured logs: ${JSON.stringify(capturedLogs)}`); + } + + if (statusLogs.length === 0) { + const expectedStatus = expectedIsRecording ? 'WXCC_SDK_TASK_RESUME_RECORDING_SUCCESS' : 'WXCC_SDK_TASK_PAUSE_RECORDING_SUCCESS'; + throw new Error(`No '${expectedStatus}' logs found. Captured logs: ${JSON.stringify(capturedLogs)}`); + } + + const lastRecordingLog = recordingLogs[recordingLogs.length - 1]; + if (!lastRecordingLog.includes(`isRecording: ${expectedIsRecording}`)) { + throw new Error(`Expected 'isRecording: ${expectedIsRecording}' in log but found: ${lastRecordingLog}`); + } +} + +/** + * Verifies that onEnd callback logs are present when tasks are ended. + * @throws Error if verification fails with detailed error message + */ +export function verifyEndLogs(): void { + const endLogs = capturedLogs.filter(log => log.includes('onEnd invoked')); + + if (endLogs.length === 0) { + throw new Error(`No 'onEnd invoked' logs found. Captured logs: ${JSON.stringify(capturedLogs)}`); + } +} + +/** + * Verifies audio transfer from caller to browser by executing the exact console command. + * Executes: document.querySelector("#remote-audio").srcObject.getAudioTracks() + * Verifies that exactly 1 audio MediaStreamTrack is present with GUID label and proper properties + * @param page - The agent's main page (browser receiving audio) + * @returns Promise + * @throws Error if remote audio tracks verification fails + */ +export async function verifyRemoteAudioTracks(page: Page): Promise { + try { + // Execute the exact console command for audio tracks + const consoleResult = await page.evaluate(() => { + // This is the exact command from your console + const audioElem = document.querySelector("#remote-audio") as HTMLAudioElement; + + if (!audioElem) { + return []; + } + + if (!audioElem.srcObject) { + return []; + } + + const mediaStream = audioElem.srcObject as MediaStream; + const audioTracks = mediaStream.getAudioTracks(); + + // Convert MediaStreamTrack objects to serializable format (like console shows) + const result = audioTracks.map((track, index) => { + return { + index, + kind: track.kind, + id: track.id, + label: track.label, + enabled: track.enabled, + muted: track.muted, + readyState: track.readyState, + onended: track.onended, + onmute: track.onmute, + onunmute: track.onunmute + }; + }); + + return result; + }); + + // Verify we got exactly 1 audio track (no more, no less) + expect(consoleResult.length).toBe(1); + + // Get the single audio track (since we verified there's exactly 1) + const audioTrack = consoleResult[0]; + + // Verify it's an audio track + if (audioTrack.kind !== 'audio') { + throw new Error(`❌ Expected audio track but found ${audioTrack.kind} track. Track details: { kind: "${audioTrack.kind}", label: "${audioTrack.label}", id: "${audioTrack.id}" }`); + } + + // Verify essential track properties for audio transfer + expect(audioTrack.kind).toBe('audio'); + expect(audioTrack.enabled).toBe(true); + expect(audioTrack.muted).toBe(false); + expect(audioTrack.readyState).toBe('live'); + + } catch (error) { + throw new Error(`❌ Audio transfer verification failed: ${error.message}`); + } +} + +/** + * Verifies the presence of hold music audio element with autoplay and loop attributes. + * Looks for: + * This is checked on the caller page when call is put on hold + * @param page - The caller's page (where hold music should be playing) + * @returns Promise + * @throws Error if hold music element verification fails + */ +export async function verifyHoldMusicElement(page: Page): Promise { + try { + const holdMusicExists = await page.evaluate(() => { + // Look for audio elements with both autoplay and loop attributes + const audioElements = document.querySelectorAll('audio[autoplay][loop]'); + + if (audioElements.length === 0) { + 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; + }); + }); + + if (!holdMusicExists) { + throw new Error('❌ No hold music audio elements found with autoplay and loop attributes'); + } + + } catch (error) { + throw new Error(`❌ Hold music element verification failed: ${error.message}`); + } +} + +/** + * Ends a task by clicking the end call button and waiting for it to be visible. + * This function can be used for any task type (call, chat, email) as they all use the same end button. + * @param page - The agent's main page + * @returns Promise + */ +export async function endTask(page: Page): Promise { + const endButton = page.getByTestId('call-control:end-call').nth(0); + await endButton.waitFor({ state: 'visible', timeout: 30000 }); + await endButton.click(); +} \ No newline at end of file diff --git a/playwright/basic-task-controls-test.spec.ts b/playwright/basic-task-controls-test.spec.ts new file mode 100644 index 000000000..bc7c063f0 --- /dev/null +++ b/playwright/basic-task-controls-test.spec.ts @@ -0,0 +1,501 @@ +import { test, expect, Page, BrowserContext } from '@playwright/test'; +import { + enableAllWidgets, + enableMultiLogin, + initialiseWidgets, + loginViaAccessToken, +} from './Utils/initUtils'; +import { stationLogout, telephonyLogin } from './Utils/stationLoginUtils'; +import { changeUserState, getCurrentState, verifyCurrentState } from './Utils/userStateUtils'; +import { + createCallTask, + createChatTask, + createEmailTask, + acceptIncomingTask, + loginExtension, + endChatTask, + acceptExtensionCall, + endCallTask +} from './Utils/incomingTaskUtils'; +import { verifyTaskControls, holdCallToggle, recordCallToggle, setupConsoleLogging, clearCapturedLogs, verifyHoldLogs, verifyRecordingLogs, verifyEndLogs, verifyHoldTimer, verifyRemoteAudioTracks, verifyHoldMusicElement, endTask, verifyHoldButtonIcon, verifyRecordButtonIcon } from './Utils/taskControlUtils'; +import { submitWrapup } from './Utils/wrapupUtils'; +import { pageSetup } from './Utils/helperUtils'; +import { USER_STATES, LOGIN_MODE, TASK_TYPES, WRAPUP_REASONS } from './constants'; + +// Extract test functions for cleaner syntax +const { describe, beforeAll, afterAll, beforeEach } = test; + +let page: Page; +let context: BrowserContext; +let callerPage: Page; +let chatPage: Page; +let context2: BrowserContext; +const maxRetries = 3; + +describe('Basic Task Controls Tests', () => { + beforeEach(() => { + clearCapturedLogs(); + }); + + beforeAll(async ({ browser }) => { + context = await browser.newContext(); + context2 = await browser.newContext(); + page = await context.newPage(); + chatPage = await context.newPage(); + callerPage = await context2.newPage(); + + await Promise.all([ + (async () => { + for (let i = 0; i < maxRetries; i++) { + try { + await loginExtension(callerPage, process.env.PW_AGENT2_USERNAME ?? '', process.env.PW_PASSWORD ?? ''); + break; + } catch (error) { + if (i == maxRetries - 1) { + throw new Error(`Failed to login extension after ${maxRetries} attempts: ${error}`); + } + } + } + })(), + (async () => { + await pageSetup(page, LOGIN_MODE.DESKTOP, 'AGENT1'); + setupConsoleLogging(page); + })(), + ]); + }); + + afterAll(async () => { + if(await getCurrentState(page) === USER_STATES.ENGAGED) { + // If still engaged, end the call to clean up + await endTask(page); + await page.waitForTimeout(3000); + await submitWrapup(page, WRAPUP_REASONS.RESOLVED); + await page.waitForTimeout(2000); + } + await stationLogout(page); + await context.close(); + await context2.close(); + }); + + test('Call task - create call and verify all control buttons are visible', async () => { + // Create call task + await createCallTask(callerPage); + await changeUserState(page, USER_STATES.AVAILABLE); + + // Wait for incoming call notification + const incomingTaskDiv = page.getByTestId('samples:incoming-task-telephony').first(); + await incomingTaskDiv.waitFor({ state: 'visible', timeout: 120000 }); + await page.waitForTimeout(3000); + + // Accept the incoming call + await acceptIncomingTask(page, TASK_TYPES.CALL); + await page.waitForTimeout(5000); + + // Verify agent state changed to engaged + await verifyCurrentState(page, USER_STATES.ENGAGED); + + // Use utility to check all call control buttons are visible + try { + await verifyTaskControls(page, TASK_TYPES.CALL); + } catch (error) { + throw new Error(`Call control buttons verification failed: ${error.message}`); + } + }); + + + test('Call task - verify remote audio tracks from caller to browser', async () => { + // Verify we're still in an engaged call from previous test + await verifyCurrentState(page, USER_STATES.ENGAGED); + + try { + + // Then verify the audio tracks with the exact structure you provided + await verifyRemoteAudioTracks(page); + + } catch (error) { + throw new Error(`Remote audio tracks verification failed: ${error.message}`); + } + }); + + test('Call task - verify hold and resume functionality with callbacks, timer, and hold music', async () => { + // Verify we're still in an engaged call from previous test + await verifyCurrentState(page, USER_STATES.ENGAGED); + + try { + // Clear logs first to ensure clean state + clearCapturedLogs(); + + // Verify initial hold button icon (should show pause icon when call is active) + await verifyHoldButtonIcon(page, { expectedIsHeld: false }); + + // Put call on hold from agent side + await holdCallToggle(page); + await page.waitForTimeout(3000); // Allow time for hold to take effect + + // Verify hold callback logs + verifyHoldLogs({ expectedIsHeld: true }); + + // Verify hold button icon changed to play icon (when call is on hold) + await verifyHoldButtonIcon(page, { expectedIsHeld: true }); + + // Verify hold music element is present on the CALLER page (where hold music plays) + // The hold music plays on the caller's side when agent puts call on hold + await verifyHoldMusicElement(callerPage); + + // Verify hold timer is visible and functioning + await verifyHoldTimer(page, { shouldBeVisible: true }); + + clearCapturedLogs(); // Clear logs for next verification + + // Resume call from hold + await holdCallToggle(page); + await page.waitForTimeout(2000); + + // Verify resume callback logs + verifyHoldLogs({ expectedIsHeld: false }); + + // Verify hold button icon changed back to pause icon (when call is active) + await verifyHoldButtonIcon(page, { expectedIsHeld: false }); + + verifyHoldTimer(page, { shouldBeVisible: false }); + + } catch (error) { + throw new Error(`Hold/Resume functionality with callbacks, timer, and hold music verification failed: ${error.message}`); + } + }); + + test('Call task - verify recording pause and resume functionality with callbacks', async () => { + // Verify we're still in an engaged call from previous tests + await verifyCurrentState(page, USER_STATES.ENGAGED); + + try { + // Verify initial record button icon (should show pause icon when recording is active) + await verifyRecordButtonIcon(page, { expectedIsRecording: true }); + + // Pause the call recording + await recordCallToggle(page); + await page.waitForTimeout(2000); + + // Verify pause recording callback logs + verifyRecordingLogs({ expectedIsRecording: false }); + + // Verify record button icon changed to record icon (when recording is paused) + await verifyRecordButtonIcon(page, { expectedIsRecording: false }); + + clearCapturedLogs(); // Clear logs for next verification + + // Resume the call recording + await recordCallToggle(page); + await page.waitForTimeout(2000); + + // Verify resume recording callback logs + verifyRecordingLogs({ expectedIsRecording: true }); + + // Verify record button icon changed back to pause icon (when recording is active) + await verifyRecordButtonIcon(page, { expectedIsRecording: true }); + + } catch (error) { + throw new Error(`Recording pause/resume functionality verification failed: ${error.message}`); + } + }); + + test('Call task - end call and complete wrapup', async () => { + // Verify we're still in an engaged call from previous tests + await verifyCurrentState(page, USER_STATES.ENGAGED); + + try { + // End the call by clicking the end button + await endTask(page); + await page.waitForTimeout(3000); + + // Verify onEnd callback logs + verifyEndLogs(); + + // Submit wrapup + await submitWrapup(page, WRAPUP_REASONS.RESOLVED); + await page.waitForTimeout(2000); + + } catch (error) { + throw new Error(`Call task end and wrapup failed: ${error.message}`); + } + }); + + test('Chat task - verify transfer and end buttons are visible, end chat, and wrap up', async () => { + // Create chat task + await createChatTask(chatPage); + await changeUserState(page, USER_STATES.AVAILABLE); + + // Wait for incoming chat notification + const incomingTaskDiv = page.getByTestId('samples:incoming-task-chat').first(); + await incomingTaskDiv.waitFor({ state: 'visible', timeout: 120000 }); + await page.waitForTimeout(3000); + + // Accept the incoming chat + await acceptIncomingTask(page, TASK_TYPES.CHAT); + await page.waitForTimeout(5000); + + // Verify agent state changed to engaged + await verifyCurrentState(page, USER_STATES.ENGAGED); + + try { + // Use utility to check chat control buttons are visible + await verifyTaskControls(page, TASK_TYPES.CHAT); + + // End the chat by clicking the end button + await endTask(page); + await page.waitForTimeout(3000); + + // Verify onEnd callback logs + verifyEndLogs(); + + // Submit wrapup + await submitWrapup(page, WRAPUP_REASONS.RESOLVED); + await page.waitForTimeout(2000); + } catch (error) { + throw new Error(`Chat task control test failed: ${error.message}`); + } + }); + + test('Email task - verify transfer and end buttons are visible, end email, and wrap up', async () => { + // Create email task + await createEmailTask(); + await changeUserState(page, USER_STATES.AVAILABLE); + + // Wait for incoming email notification (emails may take longer) + const incomingTaskDiv = page.getByTestId('samples:incoming-task-email').first(); + await incomingTaskDiv.waitFor({ state: 'visible', timeout: 180000 }); // 3 minutes for email + await page.waitForTimeout(3000); + + // Accept the incoming email + await acceptIncomingTask(page, TASK_TYPES.EMAIL); + await page.waitForTimeout(5000); + + // Verify agent state changed to engaged + await verifyCurrentState(page, USER_STATES.ENGAGED); + + try { + // Use utility to check email control buttons are visible + await verifyTaskControls(page, TASK_TYPES.EMAIL); + + // End the email by clicking the end button + await endTask(page); + await page.waitForTimeout(3000); + + // Verify onEnd callback logs + verifyEndLogs(); + + // Submit wrapup + await submitWrapup(page, WRAPUP_REASONS.RESOLVED); + await page.waitForTimeout(2000); + } catch (error) { + throw new Error(`Email task control test failed: ${error.message}`); + } + }); +}); + +describe('Multi-Login Task Controls Tests', () => { + let session1Page: Page; + let session2Page: Page; + let session1Context: BrowserContext; + let session2Context: BrowserContext; + let callerPageMulti: Page; + let callerContextMulti: BrowserContext; + let extensionPage: Page; + let extensionContext: BrowserContext; + + beforeEach(() => { + clearCapturedLogs(); + }); + + beforeAll(async ({ browser }) => { + // Create separate browser contexts for multi-session testing + session1Context = await browser.newContext(); + session2Context = await browser.newContext(); + callerContextMulti = await browser.newContext(); + extensionContext = await browser.newContext(); + + session1Page = await session1Context.newPage(); + session2Page = await session2Context.newPage(); + callerPageMulti = await callerContextMulti.newPage(); + extensionPage = await extensionContext.newPage(); + + await Promise.all([ + (async () => { + for (let i = 0; i < maxRetries; i++) { + try { + await loginExtension(callerPageMulti, process.env.PW_AGENT2_USERNAME ?? '', process.env.PW_PASSWORD ?? ''); + break; + } catch (error) { + if (i == maxRetries - 1) { + throw new Error(`Failed to login extension for multi-session test after ${maxRetries} attempts: ${error}`); + } + } + } + })(), + (async () => { + await pageSetup(session1Page, LOGIN_MODE.EXTENSION, 'AGENT1', extensionPage); + setupConsoleLogging(session1Page); + })(), + (async () => { + await pageSetup(session2Page, LOGIN_MODE.EXTENSION, 'AGENT1', extensionPage); + setupConsoleLogging(session2Page); + })(), + (async () => { + + for (let i = 0; i < maxRetries; i++) { + try { + await loginExtension(extensionPage, process.env.PW_AGENT1_USERNAME ?? '', process.env.PW_PASSWORD ?? ''); + break; + } catch (error) { + if (i == maxRetries - 1) { + throw new Error(`Failed to login extension after ${maxRetries} attempts: ${error}`); + } + } + } + })(), + ]); + }); + + afterAll(async () => { + // If still engaged, end the call to clean up + if (await getCurrentState(session1Page) === USER_STATES.ENGAGED) { + await endTask(session1Page); + await session1Page.waitForTimeout(5000); + await submitWrapup(session1Page, WRAPUP_REASONS.RESOLVED); + await session1Page.waitForTimeout(2000); + } + await Promise.all([ + stationLogout(session1Page), + stationLogout(session2Page), + ]); + await session1Context.close(); + await session2Context.close(); + await callerContextMulti.close(); + await extensionContext.close(); + }); + + test('Multi-login call controls - verify controls are synchronized', async () => { + // Set both AGENT1 sessions to available state + await Promise.all([ + changeUserState(session1Page, USER_STATES.AVAILABLE), + ]); + await session1Page.waitForTimeout(2000); + + // Caller page creates call to extension page + await createCallTask(callerPageMulti); + + // Wait for incoming call notification on both AGENT1 sessions + const incomingTaskSession1 = session1Page.getByTestId('samples:incoming-task-telephony').first(); + const incomingTaskSession2 = session2Page.getByTestId('samples:incoming-task-telephony').first(); + + await Promise.all([ + incomingTaskSession1.waitFor({ state: 'visible', timeout: 40000 }), + incomingTaskSession2.waitFor({ state: 'visible', timeout: 40000 }), + ]); + + // Wait for extension caller to be visible and accept the call + await extensionPage.locator('[data-test="generic-person-item-base"]').waitFor({ state: 'visible', timeout: 20000 }); + await session1Page.waitForTimeout(3000); + await acceptExtensionCall(extensionPage); + await session1Page.waitForTimeout(2000); + + // Verify both AGENT1 sessions show engaged state + await Promise.all([ + verifyCurrentState(session1Page, USER_STATES.ENGAGED), + verifyCurrentState(session2Page, USER_STATES.ENGAGED), + ]); + + try { + // Verify call control buttons are visible on both AGENT1 sessions + await Promise.all([ + verifyTaskControls(session1Page, TASK_TYPES.CALL), + verifyTaskControls(session2Page, TASK_TYPES.CALL), + ]); + + // Setup console logging for both AGENT1 sessions + setupConsoleLogging(session1Page); + setupConsoleLogging(session2Page); + + // Verify initial hold button icons on both sessions (should show pause icon when call is active) + await Promise.all([ + verifyHoldButtonIcon(session1Page, { expectedIsHeld: false }), + verifyHoldButtonIcon(session2Page, { expectedIsHeld: false }), + ]); + + // Put call on hold from session 1 (AGENT1) + await holdCallToggle(session1Page); + await session1Page.waitForTimeout(3000); + + // Verify hold button icons changed to play icon on both sessions (when call is on hold) + await Promise.all([ + verifyHoldButtonIcon(session1Page, { expectedIsHeld: true }), + verifyHoldButtonIcon(session2Page, { expectedIsHeld: true }), + ]); + + // Verify hold timer is visible on both AGENT1 sessions + await Promise.all([ + verifyHoldTimer(session1Page, { shouldBeVisible: true }), + verifyHoldTimer(session2Page, { shouldBeVisible: true }), + ]); + + // Resume call from session 2 (AGENT1) + await holdCallToggle(session2Page); + await session2Page.waitForTimeout(3000); + + // Verify hold button icons changed back to pause icon on both sessions (when call is active) + await Promise.all([ + verifyHoldButtonIcon(session1Page, { expectedIsHeld: false }), + verifyHoldButtonIcon(session2Page, { expectedIsHeld: false }), + ]); + + // Verify hold timer disappears on both AGENT1 sessions + await Promise.all([ + verifyHoldTimer(session1Page, { shouldBeVisible: false }), + verifyHoldTimer(session2Page, { shouldBeVisible: false }), + ]); + + // Verify initial record button icons on both sessions (should show pause icon when recording is active) + await Promise.all([ + verifyRecordButtonIcon(session1Page, { expectedIsRecording: true }), + verifyRecordButtonIcon(session2Page, { expectedIsRecording: true }), + ]); + + // Pause recording from session 1 (AGENT1) + await recordCallToggle(session1Page); + await session1Page.waitForTimeout(2000); + + // Verify record button icons changed to record icon on both sessions (when recording is paused) + await Promise.all([ + verifyRecordButtonIcon(session1Page, { expectedIsRecording: false }), + verifyRecordButtonIcon(session2Page, { expectedIsRecording: false }), + ]); + + // Resume recording from session 2 (AGENT1) + await recordCallToggle(session2Page); + await session2Page.waitForTimeout(2000); + + // Verify record button icons changed back to pause icon on both sessions (when recording is active) + await Promise.all([ + verifyRecordButtonIcon(session1Page, { expectedIsRecording: true }), + verifyRecordButtonIcon(session2Page, { expectedIsRecording: true }), + ]); + + // End call from extension page + await endCallTask(extensionPage); + await session1Page.waitForTimeout(2000); + + // Submit wrapup from session 1 (AGENT1) + await submitWrapup(session1Page, WRAPUP_REASONS.RESOLVED); + await session1Page.waitForTimeout(2000); + + // Verify both AGENT1 sessions return to available state + await Promise.all([ + verifyCurrentState(session1Page, USER_STATES.AVAILABLE), + verifyCurrentState(session2Page, USER_STATES.AVAILABLE), + ]); + + } catch (error) { + throw new Error(`Multi-session call controls synchronization failed: ${error.message}`); + } + }); +}); \ No newline at end of file diff --git a/playwright/tasklist-test.spec.ts b/playwright/tasklist-test.spec.ts new file mode 100644 index 000000000..831b762b1 --- /dev/null +++ b/playwright/tasklist-test.spec.ts @@ -0,0 +1,339 @@ +import { test, Page, expect, BrowserContext } from '@playwright/test'; +import { changeUserState, getCurrentState } from './Utils/userStateUtils'; +import { createCallTask, createChatTask, loginExtension, acceptExtensionCall, createEmailTask, endExtensionCall, submitRonaPopup, acceptIncomingTask } from './Utils/incomingTaskUtils'; +import { TASK_TYPES, USER_STATES, LOGIN_MODE, THEME_COLORS, WRAPUP_REASONS, RONA_OPTIONS } from './constants'; +import { verifyTaskControls } from './Utils/taskControlUtils'; +import { submitWrapup } from './Utils/wrapupUtils'; +import { pageSetup, handleStrayTasks, waitForState } from './Utils/helperUtils'; + + +let page: Page; +let context: BrowserContext +let callerpage: Page +let context2: BrowserContext; +let chatPage: Page; +let capturedLogs: string[] = []; + +const labelToMediaType: Record = { + Call: 'telephony', + Email: 'email', + Chat: 'chat', +}; + + +/** + * Reads the handle time of ith task in the task list. + * @param page Playwright Page object + * @param index Index of the task in the task list (default is 0 for the first task) + * @returns returns the handle time in seconds + */ + +async function getCurrentHandleTime(page: Page, index: number = 0): Promise { + const full = await page.getByTestId('task:handle-time').nth(index).textContent(); + + // 2. Pull out the MM:SS via regex + const match = full?.match(/(\d{2}:\d{2})/); + const timer = match ? match[1] : null; + + // 3. (Optional) convert to total seconds + const [m, s] = timer!.split(':').map(Number); + const totalSeconds = m * 60 + s; + return totalSeconds; +} + +/** + * wait for and accept a specific task in the task list. + * @param page : Playwright Page object + * @param testId : testId of the task to accept + * @param extensionMode : extension mode flag + * @param extensionPage : extension page if in extension mode + */ + +async function waitForAndAcceptSpecificTask(page: Page, testId: string): Promise { + const timeoutMs = 60000, pollInterval = 2000 + const start = Date.now(); + const type = testId.split('-').pop(); + while (Date.now() - start < timeoutMs) { + const taskDiv = page.getByTestId(testId).first(); + const isVisible = await taskDiv.isVisible().catch(() => false); + if (isVisible) { + const acceptButton = taskDiv.getByTestId('task:accept-button').first(); + await expect(acceptButton).toBeVisible({ timeout: 5000 }); + await acceptButton.click({ timeout: 3000 }); + + return; + } + await page.waitForTimeout(pollInterval); + } + throw new Error(`No incoming task found for ${testId} after ${timeoutMs / 1000} seconds`); +} + + +async function getTaskType(): Promise { + const callTimer = page.getByTestId('cc-cad:call-timer').first(); + const fullText = await callTimer.textContent(); + const mediaLabel = fullText!.split(' - ')[0].trim(); + return mediaLabel; +} + + +function setupConsoleLogging(page: Page): () => void { + capturedLogs.length = 0; + + const consoleHandler = (msg) => { + const logText = msg.text(); + if ( + logText.startsWith('onTaskSelected invoked for task with title :') && + logText.includes(', and mediaType :') + ) { + capturedLogs.push(logText); + } + }; + + page.on('console', consoleHandler); + return () => page.off('console', consoleHandler); +} + + + +function escapeForRegExp(str?: string): string { + if (!str) { + return ''; + } + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +async function waitForConsoleLogs( + logs: string[], + title: string, + mediaType: string, + timeoutMs = 15000, + intervalMs = 500 +): Promise { + const escTitle = escapeForRegExp(title!); + const escMedia = escapeForRegExp(mediaType!); + const pattern = new RegExp( + '^onTaskSelected invoked for task with title : ' + + escTitle + + ', and mediaType : ' + + escMedia + + '$' + ); + + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + if (logs.some(log => pattern.test(log))) return; + await new Promise(r => setTimeout(r, intervalMs)); + } + + throw new Error( + `Timed out waiting for console log matching "${pattern.source}"` + ); +} + + + +test.describe('Task List Tests for different types of Task', () => { + test.beforeEach(() => { + capturedLogs.length = 0; + }) + + test.beforeAll(async ({ browser }) => { + context = await browser.newContext(); + context2 = await browser.newContext(); + page = await context.newPage(); + callerpage = await context2.newPage(); + chatPage = await context.newPage(); + setupConsoleLogging(page); + + await Promise.all([ + (async () => { + await loginExtension(callerpage, process.env.PW_AGENT2_USERNAME, process.env.PW_PASSWORD); + })(), + (async () => { + await pageSetup(page, LOGIN_MODE.DESKTOP, 'AGENT1'); + })(), + ]) + }) + + + test('Verify Task List for incoming Call', async () => { + await createCallTask(callerpage); + await changeUserState(page, USER_STATES.AVAILABLE); + let incomingTaskDiv = page.getByTestId('samples:incoming-task-telephony').first(); + await incomingTaskDiv.waitFor({ state: 'visible', timeout: 40000 }); + await page.waitForTimeout(1000); + const taskListItem = page.getByTestId('task-list').getByRole('listitem').first(); + expect(taskListItem).toBeVisible(); + const taskListAcceptButton = taskListItem.getByTestId('task:accept-button').first(); + const taskListDeclineButton = taskListItem.getByTestId('task:decline-button').first(); + const title = await incomingTaskDiv.getByTestId('task:title').first().textContent(); + expect(await taskListItem.getByTestId('task:title').textContent()).toBe(title); + await expect(incomingTaskDiv.getByTestId('task:accept-button')).toBeVisible(); + await expect(incomingTaskDiv.getByTestId('task:decline-button')).toBeVisible(); + await expect(taskListAcceptButton).toBeVisible(); + await expect(taskListDeclineButton).toBeVisible(); + await taskListAcceptButton.click(); + await page.waitForTimeout(1000); + await expect(taskListAcceptButton).not.toBeVisible(); + await expect(taskListDeclineButton).not.toBeVisible(); + await page.waitForTimeout(5000); + try { + await verifyTaskControls(page, TASK_TYPES.CALL); + } catch (error) { + throw new Error(`Call control buttons verification failed: ${error.message}`); + } + await waitForState(page, USER_STATES.ENGAGED); + await taskListItem.click(); + await waitForConsoleLogs(capturedLogs, title, 'telephony'); + expect(await taskListItem.getByTestId('task:title').textContent()).toBe(title); + await expect(taskListItem.locator('[icon-name="handset-filled"]')).toBeVisible(); + await expect(taskListItem.getByTestId('task:item-state')).toHaveText('Connected'); + await expect(taskListItem.getByTestId('task:handle-time')).toBeVisible(); + await page.getByTestId('call-control:end-call').first().waitFor({ state: 'visible', timeout: 5000 }); + await page.getByTestId('call-control:end-call').first().click(); + await page.waitForTimeout(500); + await submitWrapup(page, WRAPUP_REASONS.SALE); + await waitForState(page, USER_STATES.AVAILABLE); + }) + + test('Verify Task List for incoming Chat Task', async () => { + await createChatTask(chatPage); + await changeUserState(page, USER_STATES.AVAILABLE); + const incomingTaskDiv = page.getByTestId('samples:incoming-task-chat').first(); + await incomingTaskDiv.waitFor({ state: 'visible', timeout: 60000 }); + await page.waitForTimeout(1000); + const taskListItem = page.getByTestId('task-list').getByRole('listitem').first(); + expect(taskListItem).toBeVisible(); + const taskListAcceptButton = taskListItem.getByTestId('task:accept-button').first(); + const taskListDeclineButton = taskListItem.getByTestId('task:decline-button').first(); + const title = await incomingTaskDiv.getByTestId('task:title').textContent(); + expect(await taskListItem.getByTestId('task:title').textContent()).toBe(title); + await expect(incomingTaskDiv.getByTestId('task:accept-button')).toBeVisible(); + await expect(incomingTaskDiv.getByTestId('task:decline-button')).not.toBeVisible(); + await expect(taskListAcceptButton).toBeVisible(); + await expect(taskListDeclineButton).not.toBeVisible(); + await taskListAcceptButton.click(); + await page.waitForTimeout(1000); + await waitForState(page, USER_STATES.ENGAGED); + const prevtimer = await getCurrentHandleTime(page); + await page.waitForTimeout(5000); + const currentTimer = await getCurrentHandleTime(page); + expect(currentTimer).toBeGreaterThan(prevtimer); + expect(Math.abs(currentTimer - prevtimer + 1)).toBeGreaterThanOrEqual(5); + try { + await verifyTaskControls(page, TASK_TYPES.CHAT); + } catch (error) { + throw new Error(`Call control buttons verification failed: ${error.message}`); + } + await waitForConsoleLogs(capturedLogs, title, 'chat'); + await expect(taskListAcceptButton).not.toBeVisible(); + await expect(taskListDeclineButton).not.toBeVisible(); + expect(await taskListItem.getByTestId('task:title').textContent()).toBe(title); + await expect(taskListItem.locator('[icon-name="chat-filled"]')).toBeVisible(); + await expect(taskListItem.getByTestId('task:item-state')).toHaveText('Connected'); + await expect(taskListItem.getByTestId('task:handle-time')).toBeVisible(); + await page.getByTestId('call-control:end-call').first().waitFor({ state: 'visible', timeout: 5000 }); + await page.getByTestId('call-control:end-call').first().click(); + await page.waitForTimeout(2000); + await submitWrapup(page, WRAPUP_REASONS.SALE); + await waitForState(page, USER_STATES.AVAILABLE); + }) + + test('Verify Task List for incoming Email Task', async () => { + await createEmailTask(); + await changeUserState(page, USER_STATES.AVAILABLE); + const incomingTaskDiv = page.getByTestId('samples:incoming-task-email').first(); + await incomingTaskDiv.waitFor({ state: 'visible', timeout: 60000 }); + await page.waitForTimeout(1000); + const taskListItem = page.getByTestId('task-list').getByRole('listitem').first(); + expect(taskListItem).toBeVisible(); + const taskListAcceptButton = taskListItem.getByTestId('task:accept-button').first(); + const taskListDeclineButton = taskListItem.getByTestId('task:decline-button').first(); + const title = await incomingTaskDiv.getByTestId('task:title').textContent(); + expect(await taskListItem.getByTestId('task:title').textContent()).toBe(title); + await expect(incomingTaskDiv.getByTestId('task:accept-button')).toBeVisible(); + await expect(incomingTaskDiv.getByTestId('task:decline-button')).not.toBeVisible(); + await expect(taskListAcceptButton).toBeVisible(); + await expect(taskListDeclineButton).not.toBeVisible(); + await taskListAcceptButton.click(); + await page.waitForTimeout(1000); + const prevtimer = await getCurrentHandleTime(page); + await page.waitForTimeout(5000); + + const currentTimer = await getCurrentHandleTime(page); + expect(currentTimer).toBeGreaterThan(prevtimer); + expect(Math.abs(currentTimer - prevtimer + 1)).toBeGreaterThanOrEqual(5); + + try { + await verifyTaskControls(page, TASK_TYPES.EMAIL); + } catch (error) { + throw new Error(`Call control buttons verification failed: ${error.message}`); + } + + await expect(taskListAcceptButton).not.toBeVisible(); + await expect(taskListDeclineButton).not.toBeVisible(); + await waitForState(page, USER_STATES.ENGAGED); + await waitForConsoleLogs(capturedLogs, title, 'email'); + expect(await taskListItem.getByTestId('task:title').textContent()).toBe(title); + await expect(taskListItem.locator('[icon-name="email-filled"]')).toBeVisible(); + await expect(taskListItem.getByTestId('task:item-state')).toHaveText('Connected'); + await expect(taskListItem.getByTestId('task:handle-time')).toBeVisible(); + await page.getByTestId('call-control:end-call').first().waitFor({ state: 'visible', timeout: 5000 }); + await page.getByTestId('call-control:end-call').first().click(); + await page.waitForTimeout(2000); + await submitWrapup(page, WRAPUP_REASONS.SALE); + await waitForState(page, USER_STATES.AVAILABLE); + }); + + test('Task List Test with Multiple Taks', async () => { + await changeUserState(page, USER_STATES.MEETING); + await waitForState(page, USER_STATES.MEETING); + await Promise.all([ + createCallTask(callerpage), + createChatTask(chatPage), + createEmailTask() + ]) + + await changeUserState(page, USER_STATES.AVAILABLE); + + await Promise.all([ + waitForAndAcceptSpecificTask(page, 'samples:incoming-task-telephony'), + waitForAndAcceptSpecificTask(page, 'samples:incoming-task-chat'), + waitForAndAcceptSpecificTask(page, 'samples:incoming-task-email'), + ]); + await page.waitForTimeout(3000); + + for (let i = 0; i < 3; i++) { + const taskListItem = page + .getByTestId('task-list') + .getByRole('listitem') + .nth(i); + + await taskListItem.waitFor({ state: 'visible', timeout: 5000 }); + expect(taskListItem).toBeVisible(); + await taskListItem.click(); + const prevtimer = await getCurrentHandleTime(page, i); + await page.waitForTimeout(5000); + const currentTimer = await getCurrentHandleTime(page, i); + expect(currentTimer).toBeGreaterThan(prevtimer); + expect(Math.abs(currentTimer - prevtimer + 1)).toBeGreaterThanOrEqual(5); + const inferredType = await getTaskType(); + try { + await verifyTaskControls(page, inferredType); + } catch (error) { + throw new Error(`Call control buttons verification failed: ${error.message}`); + } + + await waitForConsoleLogs(capturedLogs, await taskListItem.getByTestId('task:title').textContent()!, labelToMediaType[inferredType]); + capturedLogs.length = 0; + } + + await handleStrayTasks(page); + }) + + test.afterAll(async () => { + await context.close(); + await context2.close(); + }) +}) diff --git a/widgets-samples/cc/samples-cc-react-app/src/App.tsx b/widgets-samples/cc/samples-cc-react-app/src/App.tsx index 5d0ea298d..d493d8e38 100644 --- a/widgets-samples/cc/samples-cc-react-app/src/App.tsx +++ b/widgets-samples/cc/samples-cc-react-app/src/App.tsx @@ -165,6 +165,7 @@ const onTaskDeclined = (task,reason) => { const onTaskSelected = ({task, isClicked}) => { console.log('onTaskSelected invoked for task:', task, 'isClicked:', isClicked); + console.log(`onTaskSelected invoked for task with title : ${task?.data?.interaction?.callAssociatedDetails?.ani}, and mediaType : ${task?.data?.mediaType}`); }; const onHoldResume = ({isHeld, task}) => {