Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,81 @@ sequenceDiagram
end
```

### Post-Dial Flow by Login Mode

After `cc.startOutdial()` succeeds, the platform establishes a first-leg call to the agent before dialing the customer (second leg). How the first leg connects depends on the agent's login mode.

#### Desktop Mode -- Customer Rings Directly

In Desktop mode, the agent is auto-connected. The customer's phone rings immediately, and once the customer answers, the agent reaches ENGAGED state.

```mermaid
sequenceDiagram
participant A as Agent
participant W as Widget / Store
participant P as CC Platform
participant C as Customer

A->>W: Click dial button
W->>P: cc.startOutdial(destination, origin)
P-->>W: TaskResponse
P->>C: Customer's phone rings (second leg)
C->>P: Customer answers
P->>W: Agent auto-connects → ENGAGED
```

The agent never needs to accept the incoming task. The Accept button is visible but disabled during the brief popup.

#### Extension Mode -- Manual Answer Required

The first-leg call rings on the agent's Webex Calling extension. The agent must answer it before the platform dials the customer.

```mermaid
sequenceDiagram
participant A as Agent
participant W as Widget / Store
participant E as Webex Calling Extension
participant P as CC Platform
participant C as Customer

A->>W: Click dial button
W->>P: cc.startOutdial(destination, origin)
P-->>W: TaskResponse
P->>E: First-leg rings on extension
Note over E: Answer button becomes enabled
A->>E: Answer call on extension
P->>C: Customer's phone rings (second leg)
C->>P: Customer answers
P->>W: Agent state → ENGAGED
```

The agent must answer the extension call before the customer is dialed.

#### Dial Number (DN) Mode -- Manual Answer Required

The first-leg call rings on the agent's DN phone. The agent must answer it before the platform dials the customer.

```mermaid
sequenceDiagram
participant A as Agent
participant W as Widget / Store
participant D as Agent DN Phone
participant P as CC Platform
participant C as Customer

A->>W: Click dial button
W->>P: cc.startOutdial(destination, origin)
P-->>W: TaskResponse
P->>D: First-leg rings on DN phone
Note over D: Answer button becomes enabled
A->>D: Answer call on DN phone
P->>C: Customer's phone rings (second leg)
C->>P: Customer answers
P->>W: Agent state → ENGAGED
```

The agent must answer the DN phone call before the customer is dialed.

### Number Validation

```mermaid
Expand Down
50 changes: 50 additions & 0 deletions playwright/Utils/outdialUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {Page, expect} from '@playwright/test';
import {ACCEPT_TASK_TIMEOUT, AWAIT_TIMEOUT, UI_SETTLE_TIMEOUT} from '../constants';

/**
* Enters a phone number into the outdial number input field.
* Prerequisite: Agent must be logged in and outdial-call-container must be visible.
* @param page Playwright Page object (agent widget page)
* @param number Phone number to dial (e.g., +14698041796)
*/
export async function enterOutdialNumber(page: Page, number: string): Promise<void> {
await page.bringToFront();
await expect(page.getByTestId('outdial-call-container')).toBeVisible({timeout: AWAIT_TIMEOUT});
const input = page.getByTestId('outdial-number-input').locator('input');
await input.fill(number, {timeout: AWAIT_TIMEOUT});
}

/**
* Clicks the outdial call button to initiate the outbound call.
* Prerequisite: A valid number must be entered in the outdial input.
* @param page Playwright Page object (agent widget page)
*/
export async function clickOutdialButton(page: Page): Promise<void> {
await page.bringToFront();
const dialButton = page.getByTestId('outdial-call-button');
await expect(dialButton).toBeEnabled({timeout: AWAIT_TIMEOUT});
await dialButton.click({timeout: AWAIT_TIMEOUT});
}

/**
* Accepts an incoming call on the customer's Webex Calling web client.
* Used for outdial scenarios where the customer receives the outbound call.
* @param customerPage Playwright Page object (customer's Webex Calling web client)
*/
export async function acceptCustomerCall(customerPage: Page): Promise<void> {
await customerPage.bringToFront();
await expect(customerPage.locator('#answer').first()).toBeEnabled({timeout: ACCEPT_TASK_TIMEOUT});
await customerPage.waitForTimeout(UI_SETTLE_TIMEOUT);
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Medium Severity - Hard-coded wait in utility function

This waitForTimeout(UI_SETTLE_TIMEOUT) is a hard-coded delay that could be flaky. If this is for UI animation/settling, consider using a more deterministic condition.

Suggestion: Replace with waitForLoadState or wait for a specific UI condition:

await customerPage.waitForLoadState('networkidle');
// or
await expect(someElement).toBeStable();

Copy link
Contributor Author

Choose a reason for hiding this comment

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

UI_SETTLE_TIMEOUT is consistent with codebase convention — waitForTimeout is used throughout the test suite for UI settling between actions.

await customerPage.locator('#answer').first().click({timeout: AWAIT_TIMEOUT});
}

/**
* Ends the call on the customer's Webex Calling web client.
* @param customerPage Playwright Page object (customer's Webex Calling web client)
*/
export async function endCustomerCall(customerPage: Page): Promise<void> {
await customerPage.bringToFront();
const endBtn = customerPage.locator('#end-call').first();
await expect(endBtn).toBeEnabled({timeout: AWAIT_TIMEOUT});
await endBtn.click({timeout: AWAIT_TIMEOUT});
}
13 changes: 10 additions & 3 deletions playwright/ai-docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ playwright/
│ ├── basic-task-controls-test.spec.ts
│ ├── advanced-task-controls-test.spec.ts
│ ├── advance-task-control-combinations-test.spec.ts
│ ├── outdial-call-test.spec.ts
│ ├── dial-number-task-control-test.spec.ts
│ ├── tasklist-test.spec.ts
│ ├── multiparty-conference-set-7-test.spec.ts
Expand All @@ -66,6 +67,7 @@ playwright/
│ ├── userStateUtils.ts
│ ├── taskControlUtils.ts
│ ├── advancedTaskControlUtils.ts
│ ├── outdialUtils.ts
│ └── wrapupUtils.ts
├── test-manager.ts
├── test-data.ts
Expand All @@ -86,7 +88,7 @@ Keep this section aligned to real repository contents.
| ------- | -------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `SET_1` | `digital-incoming-task-tests.spec.ts` | `digital-incoming-task-and-task-controls.spec.ts`, `dial-number-task-control-test.spec.ts` |
| `SET_2` | `task-list-multi-session-tests.spec.ts` | `incoming-task-and-controls-multi-session.spec.ts`, `tasklist-test.spec.ts` |
| `SET_3` | `station-login-user-state-tests.spec.ts` | `station-login-test.spec.ts`, `user-state-test.spec.ts`, `incoming-telephony-task-test.spec.ts` |
| `SET_3` | `station-login-user-state-tests.spec.ts` | `station-login-test.spec.ts`, `user-state-test.spec.ts`, `incoming-telephony-task-test.spec.ts`, `outdial-call-test.spec.ts` |
| `SET_4` | `basic-advanced-task-controls-tests.spec.ts` | `basic-task-controls-test.spec.ts`, `advance-task-control-combinations-test.spec.ts` |
| `SET_5` | `advanced-task-controls-tests.spec.ts` | `advanced-task-controls-test.spec.ts` |
| `SET_6` | `dial-number-tests.spec.ts` | `dial-number-task-control-test.spec.ts` |
Expand Down Expand Up @@ -150,7 +152,8 @@ These flags are part of baseline runtime behavior and should be preserved unless
- `[SET_7, SET_8]`
- `[SET_9]`
3. Optionally fetches dial-number OAuth token
4. Performs one final `.env` upsert in the same OAuth setup run
4. Optionally fetches customer outdial OAuth token (for outdial E2E tests)
5. Performs one final `.env` upsert in the same OAuth setup run

Test files:

Expand Down Expand Up @@ -232,6 +235,9 @@ When enabled by setup config/method, these page properties are created and avail
| `setupForIncomingTaskExtension()` | Calls `setup()` for extension incoming-task flow |
| `setupForIncomingTaskMultiSession()` | Calls `setup()` for multi-session incoming-task flow |
| `setupForStationLogin()` | Custom path (does not call `setup()`), purpose-built station-login + multi-login bootstrap. Station-login page initialization runs sequentially (main then multi-session) to reduce init contention. |
| `setupForOutdialDesktop()` | Calls `setup()` with desktop agent1 + outdial customer login |
| `setupForOutdialExtension()` | Calls `setup()` with extension agent1 + outdial customer login |
| `setupForOutdialDN()` | Calls `setup()` with dial-number agent1 + dial-number phone login + outdial customer login |
| `setupForMultipartyConference()` | Sets up 4 agents + caller for conference tests (agent1–4 pages + callerPage) |
| `setupMultiSessionPage()` | Targeted helper to initialize only multi-session page when needed |

Expand Down Expand Up @@ -263,6 +269,7 @@ When enabled by setup config/method, these page properties are created and avail
| `taskControlUtils.ts` | `holdCallToggle`, `recordCallToggle`, `isCallHeld`, `endTask`, `verifyHoldTimer`, `verifyHoldButtonIcon`, `verifyRecordButtonIcon`, `setupConsoleLogging`, `verifyHoldLogs`, `verifyRecordingLogs`, `verifyEndLogs`, `verifyRemoteAudioTracks` | Basic call control actions + callback/event log assertions. `endTask` now stays generic and assumes the caller has already restored the page to a normal endable state. |
| `advancedTaskControlUtils.ts` | `consultOrTransfer`, `cancelConsult`, `waitForPrimaryCallAfterConsult`, `setupAdvancedConsoleLogging`, `verifyTransferSuccessLogs`, `verifyConsultStartSuccessLogs`, `verifyConsultEndSuccessLogs`, `verifyConsultTransferredLogs`, `ACTIVE_CONSULT_CONTROL_TEST_IDS` | Consult/transfer operations + advanced callback/event log assertions. Includes consult-state polling and post-consult primary-call restoration before generic end-task operations. |
| `incomingTaskUtils.ts` | `createCallTask`, `createChatTask`, `createEmailTask`, `waitForIncomingTask`, `acceptIncomingTask`, `declineIncomingTask`, `acceptExtensionCall`, `loginExtension`, `submitRonaPopup` | Incoming task creation/acceptance/decline and extension helpers |
| `outdialUtils.ts` | `enterOutdialNumber`, `clickOutdialButton`, `acceptCustomerCall`, `endCustomerCall` | Outdial call helpers for entering number, clicking dial, accepting/ending customer-side calls |
| `wrapupUtils.ts` | `submitWrapup` | Wrapup submission |
| `helperUtils.ts` | `handleStrayTasks`, `pageSetup`, `waitForState`, `waitForStateLogs`, `waitForWebSocketDisconnection`, `waitForWebSocketReconnection`, `clearPendingCallAndWrapup`, `dismissOverlays` | Shared setup/cleanup/state polling/network-watch helpers. `waitForState` polls visible state text (`state-name`) to align with `verifyCurrentState`. `pageSetup` includes one bounded station logout/re-login recovery if `state-select` is still missing after login. `handleStrayTasks` handles exit-conference, dual call control groups (iterates all end-call buttons to find enabled one), cancel-consult with switch-leg fallback. |
| `conferenceUtils.ts` | `cleanupConferenceState`, `startBaselineCallOnAgent1`, `consultAgentAndAcceptCall`, `consultQueueAndAcceptCall`, `mergeConsultIntoConference`, `transferConsultAndSubmitWrapup`, `toggleConferenceLegIfSwitchAvailable`, `exitConferenceParticipantAndWrapup`, `endConferenceTaskAndWrapup` | Shared conference helpers used by Set 7, Set 8, and Set 9 to keep call setup/cleanup and consult-transfer flows consistent and reusable. Conference callers now choose explicit exit-vs-end behavior instead of using one mixed helper. |
Expand Down Expand Up @@ -432,4 +439,4 @@ After a call ends, the Make Call button on the caller page may stay disabled. Cl

---

_Last Updated: 2026-03-09_
_Last Updated: 2026-03-11_
22 changes: 22 additions & 0 deletions playwright/global.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,21 @@ const buildDialNumberTask = (): OAuthTask | null => {
return null;
};

const buildCustomerOutdialTask = (): OAuthTask | null => {
const username = process.env.PW_DIAL_NUMBER_LOGIN_USERNAME1;
const password = process.env.PW_DIAL_NUMBER_LOGIN_PASSWORD1;

if (username && password) {
return {
envKey: 'CUSTOMER_OUTDIAL_ACCESS_TOKEN',
username,
password,
};
}

return null;
};

const fetchOAuthAccessToken = async (browser: Browser, username: string, password?: string): Promise<string> => {
const context = await browser.newContext({ignoreHTTPSErrors: true});
const page = await context.newPage();
Expand Down Expand Up @@ -170,6 +185,13 @@ setup('OAuth', async ({browser}) => {
tokenUpdates[dialNumberTask.envKey] = dialNumberToken;
}

// Fetch customer outdial token (if configured)
const customerOutdialTask = buildCustomerOutdialTask();
if (customerOutdialTask) {
const customerToken = await fetchOAuthAccessToken(browser, customerOutdialTask.username, customerOutdialTask.password);
tokenUpdates[customerOutdialTask.envKey] = customerToken;
}

const allUpdates = {...userSetUpdates, ...tokenUpdates};

// Write everything at once
Expand Down
4 changes: 3 additions & 1 deletion playwright/suites/station-login-user-state-tests.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {test} from '@playwright/test';
import { test } from '@playwright/test';
import createStationLoginTests from '../tests/station-login-test.spec';
import createUserStateTests from '../tests/user-state-test.spec';
import createIncomingTelephonyTaskTests from '../tests/incoming-telephony-task-test.spec';
import createOutdialCallTests from '../tests/outdial-call-test.spec';

test.describe('Station Login Tests', createStationLoginTests);
test.describe('User State Tests', createUserStateTests);
test.describe('Incoming Telephony Task Tests', createIncomingTelephonyTaskTests);
test.describe('Outdial Call Tests', createOutdialCallTests);
40 changes: 40 additions & 0 deletions playwright/test-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,9 @@ export class TestManager {
'agent1 extension login'
),
]);
} else if (config.agent1LoginMode === LOGIN_MODE.DIAL_NUMBER) {
const dialNumber = process.env.PW_DIAL_NUMBER_PSTN ?? '';
await pageSetup(this.agent1Page, LOGIN_MODE.DIAL_NUMBER, envTokens.agent1AccessToken, null, dialNumber);
}
}

Expand Down Expand Up @@ -520,6 +523,43 @@ export class TestManager {
});
}

async setupForOutdialDesktop(browser: Browser): Promise<void> {
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Medium Severity - No cleanup on setup failure

These methods call this.setup() followed by this.setupOutdialCustomer(). If setupOutdialCustomer fails, the agent setup from this.setup() is already done but won't be cleaned up since the test will fail in beforeAll.

Impact: Resource leaks and potential conflicts in subsequent test runs.

Suggestion: Consider wrapping in try/catch with cleanup, or document that cleanup is the caller's responsibility on setup failure.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No other test in the codebase uses try/catch in beforeAll — the convention is to let failures propagate to the test runner. Added the if (testManager) guard in afterAll to match the pattern used across all other test files (14 instances).

await this.setup(browser, {
needsAgent1: true,
agent1LoginMode: LOGIN_MODE.DESKTOP,
});
await this.setupOutdialCustomer(browser);
}

async setupForOutdialExtension(browser: Browser): Promise<void> {
await this.setup(browser, {
needsAgent1: true,
needsExtension: true,
agent1LoginMode: LOGIN_MODE.EXTENSION,
});
await this.setupOutdialCustomer(browser);
}

async setupForOutdialDN(browser: Browser): Promise<void> {
await this.setup(browser, {
needsAgent1: true,
agent1LoginMode: LOGIN_MODE.DIAL_NUMBER,
needDialNumberLogin: true,
});
await this.setupOutdialCustomer(browser);
}

private async setupOutdialCustomer(browser: Browser): Promise<void> {
const customerToken = process.env.CUSTOMER_OUTDIAL_ACCESS_TOKEN ?? '';
const result = await this.createContextWithPage(browser, PAGE_TYPES.CALLER);
this.callerExtensionContext = result.context;
this.callerPage = result.page;
await this.retryOperation(
() => loginExtension(this.callerPage, customerToken),
'outdial customer login'
);
}

async setupForMultipartyConference(browser: Browser) {
await this.setup(browser, {
needsAgent1: true,
Expand Down
Loading
Loading