Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ function CallControlComponent(props: CallControlComponentProps) {
className: 'call-control-button',
disabled: false,
isVisible: controlVisibility.holdResume,
dataTestId: 'call-control:hold-toggle',
},
{
id: 'consult',
Expand All @@ -201,6 +202,7 @@ function CallControlComponent(props: CallControlComponentProps) {
disabled: false,
menuType: 'Consult',
isVisible: controlVisibility.consult,
dataTestId: 'call-control:consult',
},
{
id: 'transfer',
Expand All @@ -210,6 +212,7 @@ function CallControlComponent(props: CallControlComponentProps) {
disabled: false,
menuType: 'Transfer',
isVisible: controlVisibility.transfer,
dataTestId: 'call-control:transfer',
},
{
id: 'record',
Expand All @@ -219,6 +222,7 @@ function CallControlComponent(props: CallControlComponentProps) {
className: 'call-control-button',
disabled: false,
isVisible: controlVisibility.pauseResumeRecording,
dataTestId: 'call-control:recording-toggle',
},
{
id: 'end',
Expand All @@ -228,6 +232,7 @@ function CallControlComponent(props: CallControlComponentProps) {
className: 'call-control-button-cancel',
disabled: isHeld,
isVisible: controlVisibility.end,
dataTestId: 'call-control:end-call',
},
];

Expand Down Expand Up @@ -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)}
>
<Icon className={button.className + '-icon'} name={button.icon} />
Expand Down Expand Up @@ -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}
Expand Down
39 changes: 31 additions & 8 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why we are not running this in chromium?

use: {
...devices['Desktop Chrome'],
launchOptions: {
Expand All @@ -61,19 +63,40 @@ 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',
// use: {...devices['Desktop Safari']},
// dependencies: ['firefox'],
// },
],
});
});
204 changes: 204 additions & 0 deletions playwright/Utils/advancedTaskControlUtils.ts
Original file line number Diff line number Diff line change
@@ -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[] {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should remove this if not used

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

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<void>
*/
export async function consultViaAgent(page: Page, agentName: string = 'User2 Agent2'): Promise<void> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

combine 4 methods as one and differentiate using param, param should be a json

// 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<void>
*/
export async function consultViaQueue(page: Page, queueName: string): Promise<void> {
// 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<void>
*/
export async function cancelConsult(page: Page): Promise<void> {
// 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<void>
*/
export async function transferViaAgent(page: Page, agentName: string = 'User2 Agent2'): Promise<void> {
// 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<void>
*/
export async function transferViaQueue(page: Page, queueName: string): Promise<void> {
// 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);
}
5 changes: 4 additions & 1 deletion playwright/Utils/helperUtils.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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');
}
Comment on lines +320 to +322

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we rather check if the UI goes rightly to RONA if the extension page is not open?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be done as a separate test? If so, I think it would be more suitable to add it to incomingTask tests.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rsarika - Here' we are failing the test. However, shouldn't it be a test that simply ensures it goes to RONA if the extension isn't open?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes. we should do this

const extensionCallVisible = await extensionPage.locator('[data-test="right-action-button"]').waitFor({ state: 'visible', timeout: 40000 }).then(() => true).catch(() => false);
if (extensionCallVisible) {
await acceptExtensionCall(extensionPage);
Expand Down
Loading
Loading