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 362b13573..250e04067 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
@@ -192,6 +192,7 @@ function CallControlComponent(props: CallControlComponentProps) {
className: 'call-control-button',
disabled: false,
isVisible: controlVisibility.holdResume,
+ dataTestId: 'call-control:hold-toggle',
},
{
id: 'consult',
@@ -201,6 +202,7 @@ function CallControlComponent(props: CallControlComponentProps) {
disabled: false,
menuType: 'Consult',
isVisible: controlVisibility.consult,
+ dataTestId: 'call-control:consult',
},
{
id: 'transfer',
@@ -210,6 +212,7 @@ function CallControlComponent(props: CallControlComponentProps) {
disabled: false,
menuType: 'Transfer',
isVisible: controlVisibility.transfer,
+ dataTestId: 'call-control:transfer',
},
{
id: 'record',
@@ -219,6 +222,7 @@ function CallControlComponent(props: CallControlComponentProps) {
className: 'call-control-button',
disabled: false,
isVisible: controlVisibility.pauseResumeRecording,
+ dataTestId: 'call-control:recording-toggle',
},
{
id: 'end',
@@ -228,6 +232,7 @@ function CallControlComponent(props: CallControlComponentProps) {
className: 'call-control-button-cancel',
disabled: isHeld,
isVisible: controlVisibility.end,
+ dataTestId: 'call-control:end-call',
},
];
@@ -287,7 +292,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)}
>
@@ -328,7 +333,7 @@ function CallControlComponent(props: CallControlComponentProps) {
button.className +
(button.disabled || (consultInitiated && isTelephony) ? ` ${button.className}-disabled` : '')
}
- data-testid={button.id === 'end' ? 'call-control:end-call' : button.id}
+ data-testid={button.dataTestId}
onPress={button.onClick}
disabled={button.disabled || (consultInitiated && isTelephony)}
aria-label={button.tooltip}
diff --git a/playwright.config.ts b/playwright.config.ts
index 7ebe6da5b..12a995b60 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -46,6 +46,8 @@ export default defineConfig({
},
{
name: 'Test: Chrome',
+ // Run all tests except advanced tests and transfer/consult tests on Chrome
+ testIgnore: [/advanced-task-controls-test\.spec\.ts/],
use: {
...devices['Desktop Chrome'],
launchOptions: {
@@ -61,14 +63,35 @@ export default defineConfig({
],
}
},
-
+ // dependencies: ['OAuth: Get Access Token'],
+ },
+ {
+ name: 'Test: Firefox - Advanced Tests',
+ // Run only advanced tests and transfer/consult tests on Firefox
+ testMatch: [/advanced-task-controls-test\.spec\.ts/],
+ use: {
+ ...devices['Desktop Firefox'],
+ launchOptions: {
+ // Firefox-specific preferences for fake media
+ firefoxUserPrefs: {
+ // Essential media preferences for testing
+ 'media.navigator.streams.fake': true,
+ 'media.navigator.permission.disabled': true,
+ 'media.getusermedia.insecure.enabled': true,
+ 'media.peerconnection.enabled': true,
+ 'permissions.default.microphone': 1,
+ 'permissions.default.camera': 1,
+
+ // Automation-friendly settings
+ 'media.autoplay.default': 0,
+ 'media.autoplay.blocking_policy': 0,
+ 'privacy.webrtc.legacyGlobalIndicator': false,
+ }
+ },
+ // Remove permissions - Firefox handles this differently
+ },
+ // dependencies: ['OAuth: Get Access Token'],
},
- // Once we have stability for playwright tests, we can enable the following browsers
- // {
- // name: 'Test: Firefox',
- // use: {...devices['Desktop Firefox']},
- // dependencies: ['chromium'],
- // },
// {
// name: 'Test: Webkit',
@@ -76,4 +99,4 @@ export default defineConfig({
// dependencies: ['firefox'],
// },
],
-});
+});
\ No newline at end of file
diff --git a/playwright/Utils/advancedTaskControlUtils.ts b/playwright/Utils/advancedTaskControlUtils.ts
new file mode 100644
index 000000000..b3e0681e1
--- /dev/null
+++ b/playwright/Utils/advancedTaskControlUtils.ts
@@ -0,0 +1,204 @@
+import { Page, expect } from '@playwright/test';
+import { WRAPUP_REASONS } from '../constants';
+
+/**
+ * Utility functions for advanced task controls testing.
+ * Provides functions for consult operations, transfer operations, and end consult actions.
+ * These utilities handle complex multi-agent scenarios and task state transitions.
+ *
+ * @packageDocumentation
+ */
+
+// Array to store captured console logs for verification
+let capturedAdvancedLogs: string[] = [];
+
+/**
+ * Sets up console logging to capture transfer and consult related callback logs.
+ * Captures transfer success, consult start/end success, and related SDK messages.
+ * @param page - The agent's main page
+ * @returns Function to remove the console handler
+ */
+export function setupAdvancedConsoleLogging(page: Page): () => void {
+ capturedAdvancedLogs.length = 0;
+
+ const consoleHandler = (msg) => {
+ const logText = msg.text();
+ if (logText.includes('WXCC_SDK_TASK_TRANSFER_SUCCESS') ||
+ logText.includes('WXCC_SDK_TASK_CONSULT_START_SUCCESS') ||
+ logText.includes('WXCC_SDK_TASK_CONSULT_END_SUCCESS') ||
+ logText.includes('AgentConsultTransferred') ||
+ logText.includes('onEnd invoked') ||
+ logText.includes('onTransfer invoked') ||
+ logText.includes('onConsult invoked')) {
+ capturedAdvancedLogs.push(logText);
+ }
+ };
+
+ page.on('console', consoleHandler);
+ return () => page.off('console', consoleHandler);
+}
+
+/**
+ * Clears the captured advanced logs array.
+ * Should be called before each test or verification to ensure clean state.
+ */
+export function clearAdvancedCapturedLogs(): void {
+ capturedAdvancedLogs.length = 0;
+}
+
+/**
+ * Verifies that transfer success logs are present.
+ * @throws Error if verification fails with detailed error message
+ */
+export function verifyTransferSuccessLogs(): void {
+ const transferLogs = capturedAdvancedLogs.filter(log => log.includes('WXCC_SDK_TASK_TRANSFER_SUCCESS'));
+
+ if (transferLogs.length === 0) {
+ throw new Error(`No 'WXCC_SDK_TASK_TRANSFER_SUCCESS' logs found. Captured logs: ${JSON.stringify(capturedAdvancedLogs)}`);
+ }
+}
+
+/**
+ * Verifies that consult start success logs are present.
+ * @throws Error if verification fails with detailed error message
+ */
+export function verifyConsultStartSuccessLogs(): void {
+ const consultStartLogs = capturedAdvancedLogs.filter(log => log.includes('WXCC_SDK_TASK_CONSULT_START_SUCCESS'));
+
+ if (consultStartLogs.length === 0) {
+ throw new Error(`No 'WXCC_SDK_TASK_CONSULT_START_SUCCESS' logs found. Captured logs: ${JSON.stringify(capturedAdvancedLogs)}`);
+ }
+}
+
+/**
+ * Verifies that consult end success logs are present.
+ * @throws Error if verification fails with detailed error message
+ */
+export function verifyConsultEndSuccessLogs(): void {
+ const consultEndLogs = capturedAdvancedLogs.filter(log => log.includes('WXCC_SDK_TASK_CONSULT_END_SUCCESS'));
+
+ if (consultEndLogs.length === 0) {
+ throw new Error(`No 'WXCC_SDK_TASK_CONSULT_END_SUCCESS' logs found. Captured logs: ${JSON.stringify(capturedAdvancedLogs)}`);
+ }
+}
+
+/**
+ * Verifies that agent consult transferred logs are present (when consult is converted to transfer).
+ * @throws Error if verification fails with detailed error message
+ */
+export function verifyConsultTransferredLogs(): void {
+ const consultTransferredLogs = capturedAdvancedLogs.filter(log => log.includes('AgentConsultTransferred'));
+
+ if (consultTransferredLogs.length === 0) {
+ throw new Error(`No 'AgentConsultTransferred' logs found. Captured logs: ${JSON.stringify(capturedAdvancedLogs)}`);
+ }
+}
+
+/**
+ * Utility function to get all captured logs for debugging purposes.
+ * @returns Array of all captured log messages
+ */
+export function getAllCapturedLogs(): string[] {
+ return [...capturedAdvancedLogs];
+}
+
+/**
+ * Initiates a consult with another agent via the agents tab.
+ * @param page - The agent's main page
+ * @param agentName - Name of the agent to consult with (e.g., 'User2 Agent2')
+ * @returns Promise
+ */
+export async function consultViaAgent(page: Page, agentName: string = 'User2 Agent2'): Promise {
+ // Click consult with another agent button
+ await page.getByTestId('call-control:consult').nth(1).click();
+ // Navigate to Agents tab
+ await page.getByRole('tab', { name: 'Agents' }).click();
+
+ //hover over the agent name - use exact match to avoid confusion with similar names
+ await page.getByRole('listitem', { name: agentName, exact: true }).hover();
+
+ // Select the specific agent
+ await page.getByRole('listitem', { name: agentName, exact: true }).getByRole('button').click();
+
+ // Wait a moment for the consult to be initiated
+ await page.waitForTimeout(2000);
+}
+
+/**
+ * Initiates a consult with a queue via the queues tab.
+ * @param page - The agent's main page
+ * @param queueName - Name of the queue to consult with (e.g., 'Customer Service Queue')
+ * @returns Promise
+ */
+export async function consultViaQueue(page: Page, queueName: string): Promise {
+ // Click consult with another agent button
+ await page.getByTestId('call-control:consult').nth(1).click();
+
+ // Navigate to Queues tab
+ await page.getByRole('tab', { name: 'Queues' }).click();
+
+ // Hover over the queue name - use exact match to avoid confusion with similar names
+ await page.getByRole('listitem', { name: queueName, exact: true }).hover();
+
+ // Select the specific queue
+ await page.getByRole('listitem', { name: queueName, exact: true }).getByRole('button').click();
+
+ // Wait a moment for the consult to be initiated
+ await page.waitForTimeout(2000);
+}
+
+/**
+ * Cancels an ongoing consult and resumes the original call.
+ * @param page - The agent's main page
+ * @returns Promise
+ */
+export async function cancelConsult(page: Page): Promise {
+ // Click cancel consult button
+ await page.getByTestId('cancel-consult-btn').click();
+ }
+
+/**
+ * Initiates a transfer via the agents tab (without prior consult).
+ * @param page - The agent's main page
+ * @param agentName - Name of the agent to transfer to (e.g., 'User2 Agent2')
+ * @returns Promise
+ */
+export async function transferViaAgent(page: Page, agentName: string = 'User2 Agent2'): Promise {
+ // Click transfer call button
+ await page.getByRole('group', { name: 'Call Control with Call' }).getByLabel('Transfer Call').click();
+
+ // Navigate to Agents tab
+ await page.getByRole('tab', { name: 'Agents' }).click();
+
+ // Hover over the agent name - use exact match to avoid confusion with similar names
+ await page.getByRole('listitem', { name: agentName, exact: true }).hover();
+
+ // Select the specific agent
+ await page.getByRole('listitem', { name: agentName, exact: true }).getByRole('button').click();
+
+ // Wait a moment for the transfer to be processed
+ await page.waitForTimeout(2000);
+}
+
+/**
+ * Initiates a transfer via the queues tab (without prior consult).
+ * @param page - The agent's main page
+ * @param queueName - Name of the queue to transfer to (e.g., 'Customer Service Queue')
+ * @returns Promise
+ */
+export async function transferViaQueue(page: Page, queueName: string): Promise {
+ // Click transfer call button
+ await page.getByRole('group', { name: 'Call Control with Call' }).getByLabel('Transfer Call').click();
+
+ // Navigate to Queues tab
+ await page.getByRole('tab', { name: 'Queues' }).click();
+
+ // Hover over the queue name - use exact match to avoid confusion with similar names
+ await page.getByRole('listitem', { name: queueName, exact: true }).hover();
+
+ // Select the specific queue
+ await page.getByRole('listitem', { name: queueName, exact: true }).getByRole('button').click();
+
+ // Wait a moment for the transfer to be processed
+ await page.waitForTimeout(2000);
+}
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/advanced-task-controls-test.spec.ts b/playwright/advanced-task-controls-test.spec.ts
new file mode 100644
index 000000000..e038472a5
--- /dev/null
+++ b/playwright/advanced-task-controls-test.spec.ts
@@ -0,0 +1,680 @@
+import { test, expect, Page, BrowserContext } from '@playwright/test';
+import {
+ consultViaAgent,
+ consultViaQueue,
+ transferViaAgent,
+ transferViaQueue,
+ cancelConsult,
+ setupAdvancedConsoleLogging,
+ clearAdvancedCapturedLogs,
+ verifyTransferSuccessLogs,
+ verifyConsultStartSuccessLogs,
+ verifyConsultEndSuccessLogs,
+ verifyConsultTransferredLogs,
+} from './Utils/advancedTaskControlUtils';
+
+import { stationLogout, telephonyLogin } from './Utils/stationLoginUtils';
+import { changeUserState, getCurrentState, verifyCurrentState } from './Utils/userStateUtils';
+import {
+ createCallTask,
+ acceptIncomingTask,
+ loginExtension,
+ declineIncomingTask
+} from './Utils/incomingTaskUtils';
+import { submitWrapup } from './Utils/wrapupUtils';
+import { USER_STATES, LOGIN_MODE, TASK_TYPES, WRAPUP_REASONS } from './constants';
+import { holdCallToggle, endTask, setupConsoleLogging, clearCapturedLogs, verifyHoldButtonIcon, verifyTaskControls } from './Utils/taskControlUtils';
+import { pageSetup } from './Utils/helperUtils';
+
+// Extract test functions for cleaner syntax
+const { describe, beforeAll, afterAll, beforeEach } = test;
+
+/**
+ * Transfer and Consult Tests
+ *
+ * Comprehensive test suite covering:
+ * - Blind Transfer Operations (Agent to Agent, Agent to Queue)
+ * - Consult Transfer Operations (with acceptance, decline, timeout scenarios)
+ * - Queue Consult Operations (multi-agent scenarios)
+ * - Multi-stage Consult Transfer Operations
+ */
+
+let agent1Page: Page;
+let agent2Page: Page;
+let callerPage: Page;
+let agent1Context: BrowserContext;
+let agent2Context: BrowserContext;
+let callerContext: BrowserContext;
+const maxRetries = 3;
+
+describe('Transfer and Consult Tests', () => {
+
+ // =============================================================================
+ // BLIND TRANSFER TESTS
+ // =============================================================================
+
+ describe('Blind Transfer Tests', () => {
+
+ beforeAll(async ({ browser }) => {
+ agent1Context = await browser.newContext();
+ agent2Context = await browser.newContext();
+ callerContext = await browser.newContext();
+
+ agent1Page = await agent1Context.newPage();
+ agent2Page = await agent2Context.newPage();
+ callerPage = await callerContext.newPage();
+
+ await Promise.all([
+ (async () => {
+ for (let i = 0; i < maxRetries; i++) {
+ try {
+ await loginExtension(callerPage, 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}`);
+ }
+ }
+ }
+ })(),
+ (async () => {
+ await pageSetup(agent1Page, LOGIN_MODE.DESKTOP, 'AGENT1');
+ // Setup console logging for callbacks
+ setupConsoleLogging(agent1Page);
+ setupAdvancedConsoleLogging(agent1Page);
+ })(),
+ (async () => {
+ await pageSetup(agent2Page, LOGIN_MODE.DESKTOP, 'AGENT2');
+ // Setup console logging for callbacks
+ setupConsoleLogging(agent2Page);
+ setupAdvancedConsoleLogging(agent2Page);
+ })(),
+ ]);
+ });
+
+ afterAll(async () => {
+ await Promise.all([
+ (async () => {
+ if(await getCurrentState(agent1Page) === USER_STATES.ENGAGED) {
+ await endTask(agent1Page);
+ await agent1Page.waitForTimeout(3000);
+ await submitWrapup(agent1Page, WRAPUP_REASONS.RESOLVED);
+ await agent1Page.waitForTimeout(2000);
+ }
+ await stationLogout(agent1Page);
+ })(),
+ (async () => {
+ if(await getCurrentState(agent2Page) === USER_STATES.ENGAGED) {
+ await endTask(agent2Page);
+ await agent2Page.waitForTimeout(3000);
+ await submitWrapup(agent2Page, WRAPUP_REASONS.RESOLVED);
+ await agent2Page.waitForTimeout(2000);
+ }
+ await stationLogout(agent2Page);
+ })(),
+ ]);
+ await agent1Context.close();
+ await agent2Context.close();
+ await callerContext.close();
+ });
+
+ beforeEach(async () => {
+ await changeUserState(agent2Page, USER_STATES.MEETING);
+ // Create call task and agent 1 accepts it
+ await createCallTask(callerPage);
+
+ const incomingTaskDiv = agent1Page.getByTestId('samples:incoming-task-telephony').first();
+ await incomingTaskDiv.waitFor({ state: 'visible', timeout: 120000 });
+ await agent1Page.waitForTimeout(3000);
+
+ await acceptIncomingTask(agent1Page, TASK_TYPES.CALL);
+ await changeUserState(agent2Page, USER_STATES.AVAILABLE);
+ await agent1Page.waitForTimeout(5000);
+
+ await verifyCurrentState(agent1Page, USER_STATES.ENGAGED);
+
+ // Clear console logs to track transfer events
+ clearAdvancedCapturedLogs();
+ });
+
+ test('Normal Call Blind Transferred by Agent to Another Agent', async () => {
+ // Agent 1 performs blind transfer to Agent 2
+ await transferViaAgent(agent1Page, 'User2 Agent2');
+
+ // Verify transfer success in console logs
+ await agent1Page.waitForTimeout(3000);
+ verifyTransferSuccessLogs();
+
+ // Verify Agent 1 goes to wrapup state
+ await submitWrapup(agent1Page, WRAPUP_REASONS.SALE);
+ // Agent 2 should receive the transfer and accept it
+ const incomingTransferDiv = agent2Page.getByTestId('samples:incoming-task-telephony').first();
+ await incomingTransferDiv.waitFor({ state: 'visible', timeout: 60000 });
+ await agent2Page.waitForTimeout(3000);
+
+ await acceptIncomingTask(agent2Page, TASK_TYPES.CALL);
+ await agent2Page.waitForTimeout(3000);
+
+ // Verify Agent 2 now has the call and is engaged
+ await verifyCurrentState(agent2Page, USER_STATES.ENGAGED);
+ await verifyTaskControls(agent2Page, TASK_TYPES.CALL);
+
+ // Verify transfer success was logged
+ await agent2Page.waitForTimeout(2000);
+ verifyTransferSuccessLogs();
+
+ // End the call and complete wrapup to clean up for next test
+ await endTask(agent2Page);
+ await agent2Page.waitForTimeout(3000);
+ await submitWrapup(agent2Page, WRAPUP_REASONS.RESOLVED);
+ await agent2Page.waitForTimeout(2000);
+ });
+
+ test('Normal Call Blind Transferred to Queue', async () => {
+ // First transfer from Agent 1 to Agent 2
+ await transferViaQueue(agent1Page, 'Queue-1');
+
+ // Agent 2 accepts the transfer
+ const incomingTransferDiv = agent2Page.getByTestId('samples:incoming-task-telephony').first();
+ await incomingTransferDiv.waitFor({ state: 'visible', timeout: 60000 });
+ await submitWrapup(agent1Page, WRAPUP_REASONS.SALE);
+ await agent2Page.waitForTimeout(3000);
+ await acceptIncomingTask(agent2Page, TASK_TYPES.CALL);
+ await agent2Page.waitForTimeout(3000);
+ verifyTransferSuccessLogs();
+ await verifyCurrentState(agent2Page, USER_STATES.ENGAGED);
+ await endTask(agent2Page);
+ await agent2Page.waitForTimeout(2000);
+
+ // Verify Agent 2 goes to wrapup after transfer
+ await submitWrapup(agent2Page, WRAPUP_REASONS.RESOLVED);
+ await agent2Page.waitForTimeout(2000);
+
+ // Verify Agent 2 is no longer engaged
+ await verifyCurrentState(agent2Page, USER_STATES.AVAILABLE);
+ });
+});
+
+// =============================================================================
+// CONSULT TRANSFER TESTS
+// =============================================================================
+
+describe('Consult Transfer Tests', () => {
+
+ beforeAll(async ({ browser }) => {
+ agent1Context = await browser.newContext();
+ agent2Context = await browser.newContext();
+ callerContext = await browser.newContext();
+
+ agent1Page = await agent1Context.newPage();
+ agent2Page = await agent2Context.newPage();
+ callerPage = await callerContext.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(agent1Page, LOGIN_MODE.DESKTOP, 'AGENT1');
+ // Setup console logging for callbacks
+ setupConsoleLogging(agent1Page);
+ setupAdvancedConsoleLogging(agent1Page);
+ })(),
+ (async () => {
+ await pageSetup(agent2Page, LOGIN_MODE.DESKTOP, 'AGENT2');
+ // Setup console logging for callbacks
+ setupConsoleLogging(agent2Page);
+ setupAdvancedConsoleLogging(agent2Page);
+ })(),
+ ]);
+ });
+
+ afterAll(async () => {
+ await Promise.all([
+ (async () => {
+ if(await getCurrentState(agent1Page) === USER_STATES.ENGAGED) {
+ await endTask(agent1Page);
+ await agent1Page.waitForTimeout(3000);
+ await submitWrapup(agent1Page, WRAPUP_REASONS.RESOLVED);
+ await agent1Page.waitForTimeout(2000);
+ }
+ await stationLogout(agent1Page);
+ })(),
+ (async () => {
+ if(await getCurrentState(agent2Page) === USER_STATES.ENGAGED) {
+ await endTask(agent2Page);
+ await agent2Page.waitForTimeout(3000);
+ await submitWrapup(agent2Page, WRAPUP_REASONS.RESOLVED);
+ await agent2Page.waitForTimeout(2000);
+ }
+ await stationLogout(agent2Page);
+ })(),
+ ]);
+ await agent1Context.close();
+ await agent2Context.close();
+ await callerContext.close();
+ });
+
+ beforeEach(async () => {
+ await changeUserState(agent2Page, USER_STATES.MEETING);
+ // Create call task and agent 1 accepts it
+ await createCallTask(callerPage);
+
+ const incomingTaskDiv = agent1Page.getByTestId('samples:incoming-task-telephony').first();
+ await incomingTaskDiv.waitFor({ state: 'visible', timeout: 120000 });
+ await agent1Page.waitForTimeout(3000);
+
+ await acceptIncomingTask(agent1Page, TASK_TYPES.CALL);
+ await changeUserState(agent2Page, USER_STATES.AVAILABLE);
+ await agent1Page.waitForTimeout(5000);
+
+ await verifyCurrentState(agent1Page, USER_STATES.ENGAGED);
+
+ // Clear console logs to track consult events
+ clearAdvancedCapturedLogs();
+ });
+
+ test('Normal Call Consulted via Agent and Accepted (A1 → A2)', async () => {
+
+ // Agent 1 initiates consult with Agent 2
+ await consultViaAgent(agent1Page, 'User2 Agent2');
+
+ // Verify consult UI elements are visible
+ await expect(agent1Page.getByTestId('cancel-consult-btn')).toBeVisible();
+ await expect(agent1Page.getByTestId('transfer-consult-btn')).toBeVisible();
+
+ // Agent 2 receives and accepts the consult
+ const consultRequestDiv = agent2Page.getByTestId('samples:incoming-task-telephony').first();
+ await consultRequestDiv.waitFor({ state: 'visible', timeout: 60000 });
+ await agent2Page.waitForTimeout(3000);
+
+ await acceptIncomingTask(agent2Page, TASK_TYPES.CALL);
+ await agent2Page.waitForTimeout(3000);
+
+ // Verify both agents are in consult state
+ await expect(agent1Page.getByTestId('transfer-consult-btn')).toBeVisible();
+
+ // Verify consult start success was logged
+ await agent1Page.waitForTimeout(2000);
+ verifyConsultStartSuccessLogs();
+
+ // End the consult and verify state
+ await cancelConsult(agent2Page);
+
+ // Verify consult end success was logged
+ await agent1Page.waitForTimeout(2000);
+ verifyConsultEndSuccessLogs();
+
+ // Verify call is on hold after consult ends
+ await verifyHoldButtonIcon(agent1Page, { expectedIsHeld: true });
+
+ await verifyCurrentState(agent2Page, USER_STATES.AVAILABLE);
+ await holdCallToggle(agent1Page);
+ // End the call and complete wrapup to clean up for next test
+ await endTask(agent1Page);
+ await agent1Page.waitForTimeout(3000);
+ await submitWrapup(agent1Page, WRAPUP_REASONS.RESOLVED);
+ await agent1Page.waitForTimeout(2000);
+ await verifyCurrentState(agent1Page, USER_STATES.AVAILABLE);
+ });
+
+ test('Normal Call Consulted via Agent and Declined (A1 → A2)', async () => {
+ // Agent 1 initiates another consult with Agent 2
+ await consultViaAgent(agent1Page, 'User2 Agent2');
+
+ // Agent 2 receives and declines the consult
+ const consultRequestDiv = agent2Page.getByTestId('samples:incoming-task-telephony').first();
+ await consultRequestDiv.waitFor({ state: 'visible', timeout: 60000 });
+ await agent2Page.waitForTimeout(3000);
+
+ await declineIncomingTask(agent2Page, TASK_TYPES.CALL);
+
+ // Verify Agent 1 returns to normal call state
+ await verifyTaskControls(agent1Page, TASK_TYPES.CALL);
+
+ // Verify call is on hold after consult decline
+ await verifyHoldButtonIcon(agent1Page, { expectedIsHeld: true });
+
+ await holdCallToggle(agent1Page);
+ await agent1Page.waitForTimeout(2000);
+ await expect(agent1Page.getByTestId('cancel-consult-btn')).not.toBeVisible();
+
+ // Agent 1 should still be engaged with customer call
+ await verifyCurrentState(agent1Page, USER_STATES.ENGAGED);
+
+ // End the call and complete wrapup to clean up for next test
+ await endTask(agent1Page);
+ await agent1Page.waitForTimeout(3000);
+ await submitWrapup(agent1Page, WRAPUP_REASONS.RESOLVED);
+ await agent1Page.waitForTimeout(2000);
+ });
+
+ test('Normal Call Consulted via Agent and Not Picked Up by Agent 2', async () => {
+ // Agent 1 initiates consult with Agent 2
+ await consultViaAgent(agent1Page, 'User2 Agent2');
+
+ // Wait for consult to timeout (Agent 2 doesn't respond)
+ // This should timeout after some time and return to normal state
+ await agent1Page.waitForTimeout(20000); // Wait for timeout
+
+ // Verify Agent 1 returns to call state (call should still be on hold)
+ await verifyTaskControls(agent1Page, TASK_TYPES.CALL);
+
+ // Verify call is on hold after consult timeout
+ await verifyHoldButtonIcon(agent1Page, { expectedIsHeld: true });
+
+ await holdCallToggle(agent1Page);
+ await agent1Page.waitForTimeout(2000);
+
+ // End the call and complete wrapup to clean up for next test
+ await endTask(agent1Page);
+ await agent1Page.waitForTimeout(3000);
+ await submitWrapup(agent1Page, WRAPUP_REASONS.RESOLVED);
+ await agent1Page.waitForTimeout(2000);
+ });
+
+ test('Consult Transfer - Normal Call to Agent 2', async () => {
+ await consultViaAgent(agent1Page, 'User2 Agent2');
+
+ // Agent 2 accepts the consult first
+ const consultRequestDiv = agent2Page.getByTestId('samples:incoming-task-telephony').first();
+ await consultRequestDiv.waitFor({ state: 'visible', timeout: 60000 });
+ await agent2Page.waitForTimeout(3000);
+
+ await acceptIncomingTask(agent2Page, TASK_TYPES.CALL);
+ await agent2Page.waitForTimeout(3000);
+ await agent1Page.getByTestId('transfer-consult-btn').click();
+
+ // Agent 1 completes the transfer and goes to wrapup
+ await submitWrapup(agent1Page, WRAPUP_REASONS.SALE);
+ // Verify Agent 2 has the transferred call
+ await verifyCurrentState(agent2Page, USER_STATES.ENGAGED);
+ await verifyTaskControls(agent2Page, TASK_TYPES.CALL);
+
+ // Verify consult start and transfer success were logged
+ await agent2Page.waitForTimeout(2000);
+ verifyConsultStartSuccessLogs();
+ verifyTransferSuccessLogs();
+
+ // End the call and complete wrapup to clean up for next test
+ await endTask(agent2Page);
+ await agent2Page.waitForTimeout(3000);
+ await submitWrapup(agent2Page, WRAPUP_REASONS.RESOLVED);
+ await agent2Page.waitForTimeout(2000);
+ });
+});
+
+// =============================================================================
+// QUEUE CONSULT TESTS
+// =============================================================================
+
+describe('Queue Consult Tests', () => {
+
+ beforeAll(async ({ browser }) => {
+ agent1Context = await browser.newContext();
+ agent2Context = await browser.newContext();
+ callerContext = await browser.newContext();
+
+ agent1Page = await agent1Context.newPage();
+ agent2Page = await agent2Context.newPage();
+ callerPage = await callerContext.newPage();
+
+ await Promise.all([
+ (async () => {
+ for (let i = 0; i < maxRetries; i++) {
+ try {
+ await loginExtension(callerPage, 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}`);
+ }
+ }
+ }
+ })(),
+ (async () => {
+ await pageSetup(agent1Page, LOGIN_MODE.DESKTOP, 'AGENT1');
+ await setupConsoleLogging(agent1Page);
+ await setupAdvancedConsoleLogging(agent1Page);
+ })(),
+ (async () => {
+ await pageSetup(agent2Page, LOGIN_MODE.DESKTOP, 'AGENT2');
+ await setupConsoleLogging(agent2Page);
+ await setupAdvancedConsoleLogging(agent2Page);
+ // Set Agent 2 to idle for some tests
+ await changeUserState(agent2Page, USER_STATES.AVAILABLE);
+ })(),
+ ]);
+ });
+
+ afterAll(async () => {
+ await Promise.all([
+ (async () => {
+ if(await getCurrentState(agent1Page) === USER_STATES.ENGAGED) {
+ await endTask(agent1Page);
+ await agent1Page.waitForTimeout(3000);
+ await submitWrapup(agent1Page, WRAPUP_REASONS.RESOLVED);
+ await agent1Page.waitForTimeout(2000);
+ }
+ await stationLogout(agent1Page);
+ })(),
+ (async () => {
+ if(await getCurrentState(agent2Page) === USER_STATES.ENGAGED) {
+ await endTask(agent2Page);
+ await agent2Page.waitForTimeout(3000);
+ await submitWrapup(agent2Page, WRAPUP_REASONS.RESOLVED);
+ await agent2Page.waitForTimeout(2000);
+ }
+ await stationLogout(agent2Page);
+ })(),
+ ]);
+ await agent1Context.close();
+ await agent2Context.close();
+ await callerContext.close();
+ });
+
+ test('Agent 1 Consults via Queue When Agent 2 is Idle, Then Cancels the Consultation', async () => {
+ await changeUserState(agent2Page, USER_STATES.MEETING);
+ // Create call task and agent 1 accepts it
+ await createCallTask(callerPage);
+
+ const incomingTaskDiv = agent1Page.getByTestId('samples:incoming-task-telephony').first();
+ await incomingTaskDiv.waitFor({ state: 'visible', timeout: 120000 });
+ await agent1Page.waitForTimeout(3000);
+
+ await acceptIncomingTask(agent1Page, TASK_TYPES.CALL);
+ await agent1Page.waitForTimeout(5000);
+
+ await verifyCurrentState(agent1Page, USER_STATES.ENGAGED);
+
+ // Clear logs before consult
+ clearAdvancedCapturedLogs();
+
+ // Agent 1 initiates queue consult
+ await consultViaQueue(agent1Page, 'Queue-1');
+
+ // Verify consult UI elements are visible
+ await expect(agent1Page.getByTestId('cancel-consult-btn')).toBeVisible();
+ await agent1Page.waitForTimeout(2000);
+
+ // Agent 1 cancels consult before Agent 2 responds
+ await cancelConsult(agent1Page);
+
+ // Verify customer call returns to regular connected state
+ await verifyTaskControls(agent1Page, TASK_TYPES.CALL);
+ await expect(agent1Page.getByTestId('cancel-consult-btn')).not.toBeVisible();
+
+
+ // End the call and complete wrapup to clean up for next test
+ await endTask(agent1Page);
+ await agent1Page.waitForTimeout(3000);
+ await submitWrapup(agent1Page, WRAPUP_REASONS.RESOLVED);
+ await agent1Page.waitForTimeout(2000);
+ });
+
+ test('Agent 1 Consults via Queue with Available Agent 2, Then Ends Consultation', async () => {
+ await changeUserState(agent2Page, USER_STATES.MEETING);
+ // Create new call for this test
+ await createCallTask(callerPage);
+
+ const incomingTaskDiv = agent1Page.getByTestId('samples:incoming-task-telephony').first();
+ await incomingTaskDiv.waitFor({ state: 'visible', timeout: 120000 });
+ await agent1Page.waitForTimeout(3000);
+
+ await acceptIncomingTask(agent1Page, TASK_TYPES.CALL);
+ await changeUserState(agent2Page, USER_STATES.AVAILABLE);
+ await agent1Page.waitForTimeout(5000);
+
+ await verifyCurrentState(agent1Page, USER_STATES.ENGAGED);
+
+ // Clear logs before consult
+ clearAdvancedCapturedLogs();
+
+ // Agent 1 initiates queue consult
+ await consultViaQueue(agent1Page, 'Queue-1');
+
+ // Verify consult start success was logged
+ await agent1Page.waitForTimeout(2000);
+ verifyConsultStartSuccessLogs();
+
+ // Agent 2 accepts the consult
+ const consultRequestDiv = agent2Page.getByTestId('samples:incoming-task-telephony').first();
+ await consultRequestDiv.waitFor({ state: 'visible', timeout: 60000 });
+ await agent2Page.waitForTimeout(3000);
+
+ await acceptIncomingTask(agent2Page, TASK_TYPES.CALL);
+ await agent2Page.waitForTimeout(3000);
+
+ // Agent 1 ends the consultation
+ await cancelConsult(agent1Page);
+ await agent1Page.waitForTimeout(3000);
+ await verifyCurrentState(agent2Page, USER_STATES.AVAILABLE);
+ // Verify call returns to Agent 1
+ await verifyTaskControls(agent1Page, TASK_TYPES.CALL);
+
+ // Verify consult end success was logged
+ await agent1Page.waitForTimeout(2000);
+ verifyConsultEndSuccessLogs();
+
+ // Verify call is on hold after consult ends
+ await verifyHoldButtonIcon(agent1Page, { expectedIsHeld: true });
+
+ await holdCallToggle(agent1Page);
+
+ // End the call and complete wrapup to clean up for next test
+ await endTask(agent1Page);
+ await agent1Page.waitForTimeout(3000);
+ await submitWrapup(agent1Page, WRAPUP_REASONS.RESOLVED);
+ await agent1Page.waitForTimeout(2000);
+ });
+
+ test('Agent 2 Ends the Consultation Initiated by Agent 1 via Queue', async () => {
+ await changeUserState(agent2Page, USER_STATES.MEETING);
+ // Create new call for this test
+ await createCallTask(callerPage);
+
+ const incomingTaskDiv = agent1Page.getByTestId('samples:incoming-task-telephony').first();
+ await incomingTaskDiv.waitFor({ state: 'visible', timeout: 120000 });
+ await agent1Page.waitForTimeout(3000);
+
+ await acceptIncomingTask(agent1Page, TASK_TYPES.CALL);
+ await changeUserState(agent2Page, USER_STATES.AVAILABLE);
+ await agent1Page.waitForTimeout(5000);
+
+ await verifyCurrentState(agent1Page, USER_STATES.ENGAGED);
+
+ // Clear logs before consult
+ clearAdvancedCapturedLogs();
+
+ // Agent 1 initiates queue consult
+ await consultViaQueue(agent1Page, 'Queue-1');
+
+ // Agent 2 accepts the consult
+ const consultRequestDiv = agent2Page.getByTestId('samples:incoming-task-telephony').first();
+ await consultRequestDiv.waitFor({ state: 'visible', timeout: 60000 });
+ await agent2Page.waitForTimeout(3000);
+
+ await acceptIncomingTask(agent2Page, TASK_TYPES.CALL);
+ await agent2Page.waitForTimeout(3000);
+
+ // Agent 2 ends the consultation from their side
+ await cancelConsult(agent2Page);
+ await agent2Page.waitForTimeout(3000);
+ await verifyCurrentState(agent2Page, USER_STATES.AVAILABLE);
+ // Customer call should return to Agent 1
+ await verifyTaskControls(agent1Page, TASK_TYPES.CALL);
+
+ // Verify call is on hold after consult ends
+ await verifyHoldButtonIcon(agent1Page, { expectedIsHeld: true });
+
+ await holdCallToggle(agent1Page);
+ // End the call and complete wrapup to clean up for next test
+ await endTask(agent1Page);
+ await agent1Page.waitForTimeout(3000);
+ await submitWrapup(agent1Page, WRAPUP_REASONS.RESOLVED);
+ await agent1Page.waitForTimeout(2000);
+ });
+
+ test('Agent 1 Consults via Queue with Agent 2, Then Transfers Call to Agent 2', async () => {
+ await changeUserState(agent2Page, USER_STATES.MEETING);
+ // Create new call for this test
+ await createCallTask(callerPage);
+
+ const incomingTaskDiv = agent1Page.getByTestId('samples:incoming-task-telephony').first();
+ await incomingTaskDiv.waitFor({ state: 'visible', timeout: 120000 });
+ await agent1Page.waitForTimeout(3000);
+
+ await acceptIncomingTask(agent1Page, TASK_TYPES.CALL);
+ await changeUserState(agent2Page, USER_STATES.AVAILABLE);
+ await agent1Page.waitForTimeout(5000);
+
+ await verifyCurrentState(agent1Page, USER_STATES.ENGAGED);
+
+ // Clear logs before consult
+ clearAdvancedCapturedLogs();
+
+ // Agent 1 initiates queue consult
+ await consultViaQueue(agent1Page, 'Queue-1');
+
+ // Agent 2 accepts the consultation
+ const consultRequestDiv = agent2Page.getByTestId('samples:incoming-task-telephony').first();
+ await consultRequestDiv.waitFor({ state: 'visible', timeout: 60000 });
+ await agent2Page.waitForTimeout(3000);
+
+ await acceptIncomingTask(agent2Page, TASK_TYPES.CALL);
+ await agent2Page.waitForTimeout(3000);
+
+ // Agent 1 transfers the call to Agent 2
+ await agent1Page.getByTestId('transfer-consult-btn').click();
+ await agent1Page.waitForTimeout(2000);
+
+ // Agent 1 enters wrap-up state
+ await submitWrapup(agent1Page, WRAPUP_REASONS.SALE);
+
+ // Verify ownership shifts to Agent 2
+ await verifyCurrentState(agent2Page, USER_STATES.ENGAGED);
+ await verifyTaskControls(agent2Page, TASK_TYPES.CALL);
+
+ // Verify consult start and transfer success were logged
+ await agent2Page.waitForTimeout(2000);
+ verifyConsultStartSuccessLogs();
+ verifyConsultTransferredLogs();
+
+ // End the call and complete wrapup to clean up for next test
+ await endTask(agent2Page);
+ await agent2Page.waitForTimeout(3000);
+ await submitWrapup(agent2Page, WRAPUP_REASONS.RESOLVED);
+ await agent2Page.waitForTimeout(2000);
+ });
+});
+
+});
\ 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