Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 27 additions & 0 deletions playwright/Utils/advancedTaskControlUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {Page, expect} from '@playwright/test';
import {loginExtension} from './incomingTaskUtils';
import {dismissOverlays} from './helperUtils';
import {AWAIT_TIMEOUT, FORM_FIELD_TIMEOUT} from '../constants';

Expand Down Expand Up @@ -261,3 +262,29 @@ export async function cancelConsult(page: Page): Promise<void> {
// Click cancel consult button
await page.getByTestId('cancel-consult-btn').click({timeout: AWAIT_TIMEOUT});
}

/**
* Ensures the Dial Number softphone (web.webex.com) page is logged in using env creds.
* Uses: PW_DIAL_NUMBER_LOGIN_USERNAME, PW_DIAL_NUMBER_LOGIN_PASSWORD
* Also dismisses any stale overlays/popovers (e.g., "Media failed") that might block interaction.
*/
export async function ensureDialNumberLoggedIn(page: Page): Promise<void> {
const currentUrl = page?.url?.() || '';
if (!/\.webex\.com\/calling/.test(currentUrl)) {
const user = process.env.PW_DIAL_NUMBER_LOGIN_USERNAME;
const pass = process.env.PW_DIAL_NUMBER_LOGIN_PASSWORD;
if (user && pass) {
await loginExtension(page, user, pass);
}
}

// Dismiss any stale overlays/popovers (e.g., "Media failed" from previous calls)
await dismissOverlays(page);

// Ensure the dial number page is in the foreground to avoid background throttling
await page.bringToFront();

// Wait for the incoming call to appear on the dial number page
// Use extended timeout to handle network routing delays and test interference
await page.locator('[data-test="right-action-button"]').waitFor({state: 'visible', timeout: AWAIT_TIMEOUT * 2.5});
}
52 changes: 52 additions & 0 deletions playwright/Utils/helperUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
AWAIT_TIMEOUT,
} from '../constants';
import {submitWrapup} from './wrapupUtils';
import {holdCallToggle} from './taskControlUtils';
import {acceptExtensionCall, submitRonaPopup} from './incomingTaskUtils';
import {
loginViaAccessToken,
Expand Down Expand Up @@ -440,6 +441,57 @@ export const handleStrayTasks = async (
console.log(`Completed stray task handling after ${iterations} iterations`);
};

/**
* Clears any pending call UI on the page by ending the call and/or submitting wrapup if visible.
* Does nothing if neither end-call nor wrapup controls are present.
*/
export async function clearPendingCallAndWrapup(page: Page): Promise<void> {
// If wrapup is available, submit it
const wrapupBtn = page.getByTestId('call-control:wrapup-button').first();
const wrapupVisible = await wrapupBtn.isVisible().catch(() => false);
if (wrapupVisible) {
await submitWrapup(page, WRAPUP_REASONS.SALE);
await page.waitForTimeout(500);
return;
}

const endBtn = page.getByTestId('call-control:end-call').first();
const endVisible = await endBtn.isVisible().catch(() => false);
if (endVisible) {
const endEnabled = await endBtn.isEnabled().catch(() => false);
if (endEnabled) {
try {
await endBtn.click({timeout: AWAIT_TIMEOUT});
await submitWrapup(page, WRAPUP_REASONS.SALE);
await page.waitForTimeout(500);
} catch {}
} else {
// If end button is disabled, try resuming from hold then end; otherwise, see if wrapup is available
try {
await holdCallToggle(page);
} catch {}
const endEnabledAfterResume = await endBtn.isEnabled().catch(() => false);
if (endEnabledAfterResume) {
try {
await endBtn.click({timeout: AWAIT_TIMEOUT});
await submitWrapup(page, WRAPUP_REASONS.SALE);
await page.waitForTimeout(500);
return;
} catch {}
}

// If resume path failed, see if wrapup became available instead; otherwise, skip silently
const wrapupNowVisible = await wrapupBtn.isVisible().catch(() => false);
if (wrapupNowVisible) {
try {
await submitWrapup(page, WRAPUP_REASONS.SALE);
await page.waitForTimeout(500);
} catch {}
}
}
}
}

/*
/ * Sets up the page for testing by logging in, enabling widgets, and handling user states, cleaning up stray tasks, submitting RONA popups
* @param page - Playwright Page object
Expand Down
30 changes: 23 additions & 7 deletions playwright/Utils/incomingTaskUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
UI_SETTLE_TIMEOUT,
} from '../constants';
import nodemailer from 'nodemailer';
import {dismissOverlays} from './helperUtils';

const transporter = nodemailer.createTransport({
service: 'gmail', // Make sure to use Secure Port for Gmail SMTP
Expand Down Expand Up @@ -46,15 +47,30 @@ export async function createCallTask(page: Page, number: string) {
} catch (error) {
throw new Error('The Input Page should be logged into calling web-client.');
}

// Ensure page is foregrounded and clean of overlays
await page.bringToFront();
await dismissOverlays(page);

const endBtn = page.locator('[data-test="call-end"]');
if (await endBtn.isVisible({timeout: 500}).catch(() => false)) {
await endBtn.click({timeout: AWAIT_TIMEOUT});
await page.waitForTimeout(500);
}

await page
.locator('[data-test="statusMessage"]')
.waitFor({state: 'hidden', timeout: NETWORK_OPERATION_TIMEOUT})
.catch(() => {});

await page.getByRole('textbox', {name: 'Dial'}).waitFor({state: 'visible', timeout: AWAIT_TIMEOUT});
await page.getByRole('textbox', {name: 'Dial'}).fill(number, {timeout: AWAIT_TIMEOUT});
await expect(
page.locator('[data-test="calling-ui-keypad-control"]').getByRole('button', {name: 'Call'})
).toBeVisible();
await page
.locator('[data-test="calling-ui-keypad-control"]')
.getByRole('button', {name: 'Call'})
.click({timeout: AWAIT_TIMEOUT});

const callButton = page.locator('[data-test="calling-ui-keypad-control"]').getByRole('button', {name: 'Call'});
await expect(callButton).toBeVisible({timeout: AWAIT_TIMEOUT});
// Ensure button is enabled before clicking
await callButton.waitFor({state: 'visible', timeout: AWAIT_TIMEOUT});
await callButton.click({timeout: AWAIT_TIMEOUT});
await page.waitForTimeout(2000);
}

Expand Down
47 changes: 47 additions & 0 deletions playwright/test-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
AWAIT_TIMEOUT,
PAGE_TYPES,
PageType,
CALL_URL,
} from './constants';

// Configuration interfaces for setup options
Expand Down Expand Up @@ -326,6 +327,8 @@ export class TestManager {
() => loginExtension(this.dialNumberPage, envTokens.dialNumberUsername, envTokens.dialNumberPassword),
'dial number login'
);
// Ensure only one page remains in the Dial Number context to avoid duplicate web client instances
await this.enforceSingleDialNumberInOwnContext();
}

// Helper method for Caller setup
Expand Down Expand Up @@ -402,6 +405,7 @@ export class TestManager {
needsAgent1: true,
needsAgent2: true,
needsCaller: true,
needDialNumberLogin: true,
agent1LoginMode: LOGIN_MODE.DESKTOP,
enableConsoleLogging: true,
enableAdvancedLogging: true,
Expand Down Expand Up @@ -558,6 +562,7 @@ export class TestManager {
this.callerPage,
this.agent1ExtensionPage,
this.chatPage,
this.dialNumberPage,
].filter(Boolean);

pagesToClose.forEach((page) => {
Expand All @@ -574,6 +579,7 @@ export class TestManager {
this.callerExtensionContext,
this.extensionContext,
this.chatContext,
this.dialNumberContext,
].filter(Boolean);

contextsToClose.forEach((context) => {
Expand All @@ -584,4 +590,45 @@ export class TestManager {

await Promise.all(cleanupOperations);
}

// Helper method to hard-reset the dial number login session
public async resetDialNumberSession(): Promise<void> {
if (!this.dialNumberPage || !this.dialNumberContext) {
return;
}
const envTokens = this.getEnvTokens();
try {
await this.dialNumberContext.clearCookies();
await this.dialNumberPage.evaluate(() => {
try {
localStorage.clear();
} catch {}
try {
sessionStorage.clear();
} catch {}
});
// Navigate fresh and login again
await this.dialNumberPage.goto(CALL_URL);
await loginExtension(this.dialNumberPage, envTokens.dialNumberUsername!, envTokens.dialNumberPassword!);
await this.enforceSingleDialNumberInOwnContext();
} catch (error) {
throw new Error(`Failed to reset dial number session: ${error}`);
}
}

// Ensures at most one page exists in the dedicated Dial Number context we manage.
// Closes any extra tabs/pages opened in that context to prevent multiple web.webex.com instances for the Dial Number user.
private async enforceSingleDialNumberInOwnContext(): Promise<void> {
if (!this.dialNumberContext) return;
try {
const pages = this.dialNumberContext.pages();
for (const p of pages) {
if (p !== this.dialNumberPage) {
try {
await p.close();
} catch {}
}
}
} catch {}
}
}
120 changes: 117 additions & 3 deletions playwright/tests/advance-task-control-combinations-test.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import {test, expect} from '@playwright/test';
import {cancelConsult, consultOrTransfer} from '../Utils/advancedTaskControlUtils';
import {
cancelConsult,
consultOrTransfer,
clearAdvancedCapturedLogs,
verifyConsultStartSuccessLogs,
verifyConsultTransferredLogs,
ensureDialNumberLoggedIn,
} from '../Utils/advancedTaskControlUtils';
import {changeUserState, verifyCurrentState} from '../Utils/userStateUtils';
import {createCallTask, acceptIncomingTask} from '../Utils/incomingTaskUtils';
import {createCallTask, acceptIncomingTask, acceptExtensionCall, endCallTask} from '../Utils/incomingTaskUtils';
import {submitWrapup} from '../Utils/wrapupUtils';
import {USER_STATES, TASK_TYPES, WRAPUP_REASONS} from '../constants';
import {waitForState} from '../Utils/helperUtils';
import {waitForState, clearPendingCallAndWrapup} from '../Utils/helperUtils';
import {endTask, holdCallToggle} from '../Utils/taskControlUtils';
import {TestManager} from '../test-manager';

Expand Down Expand Up @@ -264,6 +271,113 @@ export default function createAdvanceCombinationsTests() {
await testManager.agent1Page.waitForTimeout(2000);
});

test('Dial Number: consult then end consult returns UI to normal', async () => {
test.skip(!process.env.PW_DIAL_NUMBER_NAME, 'PW_DIAL_NUMBER_NAME not set');

await testManager.resetDialNumberSession();
await changeUserState(testManager.agent2Page, USER_STATES.MEETING);
await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE);
await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!);
const incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first();
await incomingTaskDiv.waitFor({state: 'visible', timeout: 80000});
await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL);
await waitForState(testManager.agent1Page, USER_STATES.ENGAGED);

clearAdvancedCapturedLogs();
await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'consult', process.env.PW_DIAL_NUMBER_NAME!);
await expect(testManager.agent1Page.getByTestId('cancel-consult-btn')).toBeVisible();
await cancelConsult(testManager.agent1Page);
await expect(testManager.agent1Page.getByTestId('cancel-consult-btn')).not.toBeVisible();
await endCallTask(testManager.callerPage!);
await submitWrapup(testManager.agent1Page, WRAPUP_REASONS.SALE);
});

test('Dial Number: consult then transfer completes and remote ends', async () => {
test.skip(!process.env.PW_DIAL_NUMBER_NAME, 'PW_DIAL_NUMBER_NAME not set');

await testManager.resetDialNumberSession();
await changeUserState(testManager.agent2Page, USER_STATES.MEETING);
await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE);
await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!);
const incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first();
await incomingTaskDiv.waitFor({state: 'visible', timeout: 80000});
await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL);

clearAdvancedCapturedLogs();
await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'consult', process.env.PW_DIAL_NUMBER_NAME!);
await ensureDialNumberLoggedIn(testManager.dialNumberPage);
await expect(testManager.dialNumberPage.locator('[data-test="right-action-button"]')).toBeVisible();
await acceptExtensionCall(testManager.dialNumberPage);
await testManager.agent1Page.getByTestId('transfer-consult-btn').click();
await submitWrapup(testManager.agent1Page, WRAPUP_REASONS.SALE);
await verifyConsultStartSuccessLogs();
await verifyConsultTransferredLogs();
await endCallTask(testManager.dialNumberPage);
});

test('Entry Point: consult then end consult returns UI to normal', async () => {
test.skip(!process.env.PW_ENTRYPOINT_NAME, 'PW_ENTRYPOINT_NAME not set');

await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE);
await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!);
const incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first();
await incomingTaskDiv.waitFor({state: 'visible', timeout: 80000});
await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL);

clearAdvancedCapturedLogs();
await consultOrTransfer(testManager.agent1Page, 'entryPoint', 'consult', process.env.PW_ENTRYPOINT_NAME!);
await expect(testManager.agent1Page.getByTestId('cancel-consult-btn')).toBeVisible();
await verifyConsultStartSuccessLogs();
await cancelConsult(testManager.agent1Page);
await testManager.agent1Page.waitForTimeout(1000);
});

// Two-hop to Dial Number (moved into top-level describe)
test('Two-hop: consult to Agent then consult-transfer to Dial Number', async () => {
test.skip(!process.env.PW_DIAL_NUMBER_NAME, 'PW_DIAL_NUMBER_NAME not set');

await clearPendingCallAndWrapup(testManager.agent1Page);
await clearPendingCallAndWrapup(testManager.agent2Page);
await testManager.resetDialNumberSession();
await changeUserState(testManager.agent2Page, USER_STATES.MEETING);
await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE);
await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!);
const incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first();
await incomingTaskDiv.waitFor({state: 'visible', timeout: 80000});
await acceptIncomingTask(testManager.agent1Page, TASK_TYPES.CALL);
await changeUserState(testManager.agent2Page, USER_STATES.AVAILABLE);
await waitForState(testManager.agent1Page, USER_STATES.ENGAGED);
await waitForState(testManager.agent2Page, USER_STATES.AVAILABLE);
await testManager.agent2Page.waitForTimeout(2000);
await testManager.agent1Page.waitForTimeout(2000);

clearAdvancedCapturedLogs();
await consultOrTransfer(
testManager.agent1Page,
'agent',
'consult',
process.env[`${testManager.projectName}_AGENT2_NAME`]!
);
const consultRequestDiv = testManager.agent2Page.getByTestId('samples:incoming-task-telephony').first();
await consultRequestDiv.waitFor({state: 'visible', timeout: 60000});
await acceptIncomingTask(testManager.agent2Page, TASK_TYPES.CALL);
await testManager.agent2Page.waitForTimeout(3000);
await verifyCurrentState(testManager.agent2Page, USER_STATES.ENGAGED);
await testManager.agent1Page.getByTestId('transfer-consult-btn').click();
await testManager.agent1Page.waitForTimeout(2000);
await submitWrapup(testManager.agent1Page, WRAPUP_REASONS.SALE);
await testManager.agent2Page.waitForTimeout(3000);
await verifyCurrentState(testManager.agent2Page, USER_STATES.ENGAGED);

await consultOrTransfer(testManager.agent2Page, 'dialNumber', 'consult', process.env.PW_DIAL_NUMBER_NAME!);
await ensureDialNumberLoggedIn(testManager.dialNumberPage);
await acceptExtensionCall(testManager.dialNumberPage);
await testManager.agent2Page.getByTestId('transfer-consult-btn').click();
await submitWrapup(testManager.agent2Page, WRAPUP_REASONS.SALE);
await verifyConsultTransferredLogs();
await endCallTask(testManager.dialNumberPage);
});

test.afterAll(async () => {
await testManager.cleanup();
});
Expand Down
Loading
Loading