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}) => {