Skip to content
Open
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
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});
}
12 changes: 9 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 @@ -89,7 +91,7 @@ Keep this section aligned to real repository contents.
| `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_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` |
| `SET_6` | `dial-number-tests.spec.ts` | `dial-number-task-control-test.spec.ts`, `outdial-call-test.spec.ts` |
| `SET_7` | `multiparty-conference-set-7-tests.spec.ts` | `multiparty-conference-set-7-test.spec.ts` |
| `SET_8` | `multiparty-conference-set-8-tests.spec.ts` | `multiparty-conference-set-8-test.spec.ts` |
| `SET_9` | `multiparty-conference-set-9-tests.spec.ts` | `multiparty-conference-set-9-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,8 @@ 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 |
| `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 +268,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 +438,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_
2 changes: 2 additions & 0 deletions playwright/suites/dial-number-tests.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {test} from '@playwright/test';
import createDialNumberTaskControlTests from '../tests/dial-number-task-control-test.spec';
import createOutdialCallTests from '../tests/outdial-call-test.spec';

test.describe('Dial Number Task Control Tests', createDialNumberTaskControlTests);
test.describe('Outdial Call Tests', createOutdialCallTests);

Choose a reason for hiding this comment

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

P1 Badge Split outdial tests from dial-number suite state

Adding createOutdialCallTests to SET_6 makes these tests run after createDialNumberTaskControlTests, but that test factory never performs a full testManager.cleanup() teardown, so the first agent session remains active in Desktop mode before outdial setup starts. In a full --project=SET_6 run, setupForOutdialDesktop then reuses the same ${projectName}_AGENT1_ACCESS_TOKEN for a second station login while the prior session is still alive, which can block the new login and fail the outdial suite before test steps begin.

Useful? React with 👍 / 👎.

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 - Test isolation concern

The outdial tests now share SET_6 with dial-number task control tests. If the dial-number tests don't fully clean up their agent sessions in afterAll, the subsequent outdial setup may conflict with stale session state.

Impact: Potential test conflicts and failures.

Suggestion: Verify that dial-number-task-control-test.spec.ts has proper afterAll cleanup, or consider running outdial tests in a separate suite/set for better isolation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not seeing issues with shared SET_6 currently. Both suites have afterAll cleanup that handles session teardown.

2 changes: 1 addition & 1 deletion playwright/suites/station-login-user-state-tests.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
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';
Expand Down
32 changes: 32 additions & 0 deletions playwright/test-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,38 @@ 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);
}

private async setupOutdialCustomer(browser: Browser): Promise<void> {
const envTokens = this.getEnvTokens();
const customerToken = envTokens.dialNumberLoginAccessToken;
if (!customerToken) {
throw new Error('Environment variable DIAL_NUMBER_LOGIN_ACCESS_TOKEN is missing or empty');
}
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