Skip to content

Commit 6aa1c90

Browse files
PrayagGPrsarikaShreyas281299
authored
feat(cc-widgets): UI Automation for Basic Task Controls (#497)
Co-authored-by: rsarika <95286093+rsarika@users.noreply.github.com> Co-authored-by: Shreyas Sharma <72344404+Shreyas281299@users.noreply.github.com>
1 parent d6682d5 commit 6aa1c90

7 files changed

Lines changed: 1847 additions & 11 deletions

File tree

packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ function CallControlComponent(props: CallControlComponentProps) {
192192
className: 'call-control-button',
193193
disabled: false,
194194
isVisible: controlVisibility.holdResume,
195+
dataTestId: 'call-control:hold-toggle',
195196
},
196197
{
197198
id: 'consult',
@@ -201,6 +202,7 @@ function CallControlComponent(props: CallControlComponentProps) {
201202
disabled: false,
202203
menuType: 'Consult',
203204
isVisible: controlVisibility.consult,
205+
dataTestId: 'call-control:consult',
204206
},
205207
{
206208
id: 'transfer',
@@ -210,6 +212,7 @@ function CallControlComponent(props: CallControlComponentProps) {
210212
disabled: false,
211213
menuType: 'Transfer',
212214
isVisible: controlVisibility.transfer,
215+
dataTestId: 'call-control:transfer',
213216
},
214217
{
215218
id: 'record',
@@ -219,6 +222,7 @@ function CallControlComponent(props: CallControlComponentProps) {
219222
className: 'call-control-button',
220223
disabled: false,
221224
isVisible: controlVisibility.pauseResumeRecording,
225+
dataTestId: 'call-control:recording-toggle',
222226
},
223227
{
224228
id: 'end',
@@ -228,6 +232,7 @@ function CallControlComponent(props: CallControlComponentProps) {
228232
className: 'call-control-button-cancel',
229233
disabled: isHeld,
230234
isVisible: controlVisibility.end,
235+
dataTestId: 'call-control:end-call',
231236
},
232237
];
233238

@@ -287,7 +292,7 @@ function CallControlComponent(props: CallControlComponentProps) {
287292
className={button.className}
288293
aria-label={button.tooltip}
289294
disabled={button.disabled || (consultInitiated && isTelephony)}
290-
data-testid="ButtonCircle"
295+
data-testid={button.dataTestId}
291296
onPress={() => handlePopoverOpen(button.menuType as CallControlMenuType)}
292297
>
293298
<Icon className={button.className + '-icon'} name={button.icon} />
@@ -328,7 +333,7 @@ function CallControlComponent(props: CallControlComponentProps) {
328333
button.className +
329334
(button.disabled || (consultInitiated && isTelephony) ? ` ${button.className}-disabled` : '')
330335
}
331-
data-testid={button.id === 'end' ? 'call-control:end-call' : button.id}
336+
data-testid={button.dataTestId}
332337
onPress={button.onClick}
333338
disabled={button.disabled || (consultInitiated && isTelephony)}
334339
aria-label={button.tooltip}

playwright.config.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export default defineConfig({
4646
},
4747
{
4848
name: 'Test: Chrome',
49+
// Run all tests except advanced tests and transfer/consult tests on Chrome
50+
testIgnore: [/advanced-task-controls-test\.spec\.ts/],
4951
use: {
5052
...devices['Desktop Chrome'],
5153
launchOptions: {
@@ -61,19 +63,40 @@ export default defineConfig({
6163
],
6264
}
6365
},
64-
66+
// dependencies: ['OAuth: Get Access Token'],
67+
},
68+
{
69+
name: 'Test: Firefox - Advanced Tests',
70+
// Run only advanced tests and transfer/consult tests on Firefox
71+
testMatch: [/advanced-task-controls-test\.spec\.ts/],
72+
use: {
73+
...devices['Desktop Firefox'],
74+
launchOptions: {
75+
// Firefox-specific preferences for fake media
76+
firefoxUserPrefs: {
77+
// Essential media preferences for testing
78+
'media.navigator.streams.fake': true,
79+
'media.navigator.permission.disabled': true,
80+
'media.getusermedia.insecure.enabled': true,
81+
'media.peerconnection.enabled': true,
82+
'permissions.default.microphone': 1,
83+
'permissions.default.camera': 1,
84+
85+
// Automation-friendly settings
86+
'media.autoplay.default': 0,
87+
'media.autoplay.blocking_policy': 0,
88+
'privacy.webrtc.legacyGlobalIndicator': false,
89+
}
90+
},
91+
// Remove permissions - Firefox handles this differently
92+
},
93+
// dependencies: ['OAuth: Get Access Token'],
6594
},
66-
// Once we have stability for playwright tests, we can enable the following browsers
67-
// {
68-
// name: 'Test: Firefox',
69-
// use: {...devices['Desktop Firefox']},
70-
// dependencies: ['chromium'],
71-
// },
7295

7396
// {
7497
// name: 'Test: Webkit',
7598
// use: {...devices['Desktop Safari']},
7699
// dependencies: ['firefox'],
77100
// },
78101
],
79-
});
102+
});
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { Page, expect } from '@playwright/test';
2+
import { WRAPUP_REASONS } from '../constants';
3+
4+
/**
5+
* Utility functions for advanced task controls testing.
6+
* Provides functions for consult operations, transfer operations, and end consult actions.
7+
* These utilities handle complex multi-agent scenarios and task state transitions.
8+
*
9+
* @packageDocumentation
10+
*/
11+
12+
// Array to store captured console logs for verification
13+
let capturedAdvancedLogs: string[] = [];
14+
15+
/**
16+
* Sets up console logging to capture transfer and consult related callback logs.
17+
* Captures transfer success, consult start/end success, and related SDK messages.
18+
* @param page - The agent's main page
19+
* @returns Function to remove the console handler
20+
*/
21+
export function setupAdvancedConsoleLogging(page: Page): () => void {
22+
capturedAdvancedLogs.length = 0;
23+
24+
const consoleHandler = (msg) => {
25+
const logText = msg.text();
26+
if (logText.includes('WXCC_SDK_TASK_TRANSFER_SUCCESS') ||
27+
logText.includes('WXCC_SDK_TASK_CONSULT_START_SUCCESS') ||
28+
logText.includes('WXCC_SDK_TASK_CONSULT_END_SUCCESS') ||
29+
logText.includes('AgentConsultTransferred') ||
30+
logText.includes('onEnd invoked') ||
31+
logText.includes('onTransfer invoked') ||
32+
logText.includes('onConsult invoked')) {
33+
capturedAdvancedLogs.push(logText);
34+
}
35+
};
36+
37+
page.on('console', consoleHandler);
38+
return () => page.off('console', consoleHandler);
39+
}
40+
41+
/**
42+
* Clears the captured advanced logs array.
43+
* Should be called before each test or verification to ensure clean state.
44+
*/
45+
export function clearAdvancedCapturedLogs(): void {
46+
capturedAdvancedLogs.length = 0;
47+
}
48+
49+
/**
50+
* Verifies that transfer success logs are present.
51+
* @throws Error if verification fails with detailed error message
52+
*/
53+
export function verifyTransferSuccessLogs(): void {
54+
const transferLogs = capturedAdvancedLogs.filter(log => log.includes('WXCC_SDK_TASK_TRANSFER_SUCCESS'));
55+
56+
if (transferLogs.length === 0) {
57+
throw new Error(`No 'WXCC_SDK_TASK_TRANSFER_SUCCESS' logs found. Captured logs: ${JSON.stringify(capturedAdvancedLogs)}`);
58+
}
59+
}
60+
61+
/**
62+
* Verifies that consult start success logs are present.
63+
* @throws Error if verification fails with detailed error message
64+
*/
65+
export function verifyConsultStartSuccessLogs(): void {
66+
const consultStartLogs = capturedAdvancedLogs.filter(log => log.includes('WXCC_SDK_TASK_CONSULT_START_SUCCESS'));
67+
68+
if (consultStartLogs.length === 0) {
69+
throw new Error(`No 'WXCC_SDK_TASK_CONSULT_START_SUCCESS' logs found. Captured logs: ${JSON.stringify(capturedAdvancedLogs)}`);
70+
}
71+
}
72+
73+
/**
74+
* Verifies that consult end success logs are present.
75+
* @throws Error if verification fails with detailed error message
76+
*/
77+
export function verifyConsultEndSuccessLogs(): void {
78+
const consultEndLogs = capturedAdvancedLogs.filter(log => log.includes('WXCC_SDK_TASK_CONSULT_END_SUCCESS'));
79+
80+
if (consultEndLogs.length === 0) {
81+
throw new Error(`No 'WXCC_SDK_TASK_CONSULT_END_SUCCESS' logs found. Captured logs: ${JSON.stringify(capturedAdvancedLogs)}`);
82+
}
83+
}
84+
85+
/**
86+
* Verifies that agent consult transferred logs are present (when consult is converted to transfer).
87+
* @throws Error if verification fails with detailed error message
88+
*/
89+
export function verifyConsultTransferredLogs(): void {
90+
const consultTransferredLogs = capturedAdvancedLogs.filter(log => log.includes('AgentConsultTransferred'));
91+
92+
if (consultTransferredLogs.length === 0) {
93+
throw new Error(`No 'AgentConsultTransferred' logs found. Captured logs: ${JSON.stringify(capturedAdvancedLogs)}`);
94+
}
95+
}
96+
97+
/**
98+
* Utility function to get all captured logs for debugging purposes.
99+
* @returns Array of all captured log messages
100+
*/
101+
export function getAllCapturedLogs(): string[] {
102+
return [...capturedAdvancedLogs];
103+
}
104+
105+
/**
106+
* Initiates a consult with another agent via the agents tab.
107+
* @param page - The agent's main page
108+
* @param agentName - Name of the agent to consult with (e.g., 'User2 Agent2')
109+
* @returns Promise<void>
110+
*/
111+
export async function consultViaAgent(page: Page, agentName: string = 'User2 Agent2'): Promise<void> {
112+
// Click consult with another agent button
113+
await page.getByTestId('call-control:consult').nth(1).click();
114+
// Navigate to Agents tab
115+
await page.getByRole('tab', { name: 'Agents' }).click();
116+
117+
//hover over the agent name - use exact match to avoid confusion with similar names
118+
await page.getByRole('listitem', { name: agentName, exact: true }).hover();
119+
120+
// Select the specific agent
121+
await page.getByRole('listitem', { name: agentName, exact: true }).getByRole('button').click();
122+
123+
// Wait a moment for the consult to be initiated
124+
await page.waitForTimeout(2000);
125+
}
126+
127+
/**
128+
* Initiates a consult with a queue via the queues tab.
129+
* @param page - The agent's main page
130+
* @param queueName - Name of the queue to consult with (e.g., 'Customer Service Queue')
131+
* @returns Promise<void>
132+
*/
133+
export async function consultViaQueue(page: Page, queueName: string): Promise<void> {
134+
// Click consult with another agent button
135+
await page.getByTestId('call-control:consult').nth(1).click();
136+
137+
// Navigate to Queues tab
138+
await page.getByRole('tab', { name: 'Queues' }).click();
139+
140+
// Hover over the queue name - use exact match to avoid confusion with similar names
141+
await page.getByRole('listitem', { name: queueName, exact: true }).hover();
142+
143+
// Select the specific queue
144+
await page.getByRole('listitem', { name: queueName, exact: true }).getByRole('button').click();
145+
146+
// Wait a moment for the consult to be initiated
147+
await page.waitForTimeout(2000);
148+
}
149+
150+
/**
151+
* Cancels an ongoing consult and resumes the original call.
152+
* @param page - The agent's main page
153+
* @returns Promise<void>
154+
*/
155+
export async function cancelConsult(page: Page): Promise<void> {
156+
// Click cancel consult button
157+
await page.getByTestId('cancel-consult-btn').click();
158+
}
159+
160+
/**
161+
* Initiates a transfer via the agents tab (without prior consult).
162+
* @param page - The agent's main page
163+
* @param agentName - Name of the agent to transfer to (e.g., 'User2 Agent2')
164+
* @returns Promise<void>
165+
*/
166+
export async function transferViaAgent(page: Page, agentName: string = 'User2 Agent2'): Promise<void> {
167+
// Click transfer call button
168+
await page.getByRole('group', { name: 'Call Control with Call' }).getByLabel('Transfer Call').click();
169+
170+
// Navigate to Agents tab
171+
await page.getByRole('tab', { name: 'Agents' }).click();
172+
173+
// Hover over the agent name - use exact match to avoid confusion with similar names
174+
await page.getByRole('listitem', { name: agentName, exact: true }).hover();
175+
176+
// Select the specific agent
177+
await page.getByRole('listitem', { name: agentName, exact: true }).getByRole('button').click();
178+
179+
// Wait a moment for the transfer to be processed
180+
await page.waitForTimeout(2000);
181+
}
182+
183+
/**
184+
* Initiates a transfer via the queues tab (without prior consult).
185+
* @param page - The agent's main page
186+
* @param queueName - Name of the queue to transfer to (e.g., 'Customer Service Queue')
187+
* @returns Promise<void>
188+
*/
189+
export async function transferViaQueue(page: Page, queueName: string): Promise<void> {
190+
// Click transfer call button
191+
await page.getByRole('group', { name: 'Call Control with Call' }).getByLabel('Transfer Call').click();
192+
193+
// Navigate to Queues tab
194+
await page.getByRole('tab', { name: 'Queues' }).click();
195+
196+
// Hover over the queue name - use exact match to avoid confusion with similar names
197+
await page.getByRole('listitem', { name: queueName, exact: true }).hover();
198+
199+
// Select the specific queue
200+
await page.getByRole('listitem', { name: queueName, exact: true }).getByRole('button').click();
201+
202+
// Wait a moment for the transfer to be processed
203+
await page.waitForTimeout(2000);
204+
}

playwright/Utils/helperUtils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Page } from '@playwright/test';
22
import { getCurrentState, changeUserState } from './userStateUtils';
3-
import { WRAPUP_REASONS, USER_STATES, RONA_OPTIONS, LOGIN_MODE, LoginMode, ThemeColor, userState, WrapupReason } from 'playwright/constants';
3+
import { WRAPUP_REASONS, USER_STATES, RONA_OPTIONS, LOGIN_MODE, LoginMode, ThemeColor, userState, WrapupReason } from '../constants';
44
import { submitWrapup } from './wrapupUtils';
55
import { acceptExtensionCall, submitRonaPopup } from './incomingTaskUtils';
66
import { loginViaAccessToken, disableMultiLogin, enableMultiLogin, initialiseWidgets, enableAllWidgets, } from './initUtils';
@@ -317,6 +317,9 @@ export const handleStrayTasks = async (page: Page, extensionPage: Page | null =
317317
const acceptButtonVisible = await acceptButton.isVisible().catch(() => false);
318318
const isExtensionCall = await (await task.innerText()).includes('Ringing...');
319319
if (isExtensionCall) {
320+
if (!extensionPage) {
321+
throw new Error('Extension page is not available for handling extension call');
322+
}
320323
const extensionCallVisible = await extensionPage.locator('[data-test="right-action-button"]').waitFor({ state: 'visible', timeout: 40000 }).then(() => true).catch(() => false);
321324
if (extensionCallVisible) {
322325
await acceptExtensionCall(extensionPage);

0 commit comments

Comments
 (0)