Skip to content

Commit d101a6b

Browse files
committed
fix: refactor the code
1 parent 111d0d3 commit d101a6b

5 files changed

Lines changed: 510 additions & 453 deletions

File tree

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
import {expect, Page} from '@playwright/test';
2+
import {consultOrTransfer} from './advancedTaskControlUtils';
3+
import {acceptIncomingTask, createCallTask} from './incomingTaskUtils';
4+
import {handleStrayTasks, waitForState} from './helperUtils';
5+
import {endTask} from './taskControlUtils';
6+
import {changeUserState, verifyCurrentState} from './userStateUtils';
7+
import {submitWrapup} from './wrapupUtils';
8+
import {
9+
ACCEPT_TASK_TIMEOUT,
10+
AWAIT_TIMEOUT,
11+
CONFERENCE_ACTION_SETTLE_TIMEOUT,
12+
CONFERENCE_END_TASK_SETTLE_TIMEOUT,
13+
CONFERENCE_SWITCH_TOGGLE_TIMEOUT,
14+
TASK_TYPES,
15+
USER_STATES,
16+
WRAPUP_REASONS,
17+
WRAPUP_TIMEOUT,
18+
} from '../constants';
19+
20+
export type AgentId = 1 | 2 | 3 | 4;
21+
22+
export const CONFERENCE_AGENT_IDS: AgentId[] = [1, 2, 3, 4];
23+
24+
type GetAgentPage = (agentId: AgentId) => Page;
25+
type GetRequiredEnv = (suffix: string) => string;
26+
type GetAgentName = (agentId: AgentId) => string;
27+
28+
interface CleanupConferenceStateOptions {
29+
getAgentPage: GetAgentPage;
30+
callerPage?: Page;
31+
agentIds?: AgentId[];
32+
cleanupTaskTimeout?: number;
33+
}
34+
35+
interface StartConferenceCallOptions {
36+
getAgentPage: GetAgentPage;
37+
callerPage?: Page;
38+
getRequiredEnv: GetRequiredEnv;
39+
agentIds?: AgentId[];
40+
acceptTimeout?: number;
41+
waitForAvailableBeforeDial?: boolean;
42+
}
43+
44+
interface ConferenceConsultOptions {
45+
fromAgent: AgentId;
46+
toAgent: AgentId;
47+
getAgentPage: GetAgentPage;
48+
getAgentName: GetAgentName;
49+
acceptTimeout?: number;
50+
}
51+
52+
interface QueueConsultOptions {
53+
fromAgent: AgentId;
54+
toAgent: AgentId;
55+
getAgentPage: GetAgentPage;
56+
getRequiredEnv: GetRequiredEnv;
57+
acceptTimeout?: number;
58+
}
59+
60+
interface SingleAgentActionOptions {
61+
fromAgent: AgentId;
62+
getAgentPage: GetAgentPage;
63+
acceptTimeout?: number;
64+
}
65+
66+
export const getConferenceRequiredEnv = (projectName: string, suffix: string): string => {
67+
const value = process.env[`${projectName}_${suffix}`];
68+
if (!value) {
69+
throw new Error(`Missing env key: ${projectName}_${suffix}`);
70+
}
71+
return value;
72+
};
73+
74+
export const getConferenceAgentName = (projectName: string, agentId: AgentId): string =>
75+
getConferenceRequiredEnv(projectName, `AGENT${agentId}_NAME`);
76+
77+
export const cleanupConferencePageWithTimeout = async (
78+
page?: Page,
79+
auxiliaryPage?: Page,
80+
cleanupTaskTimeout: number = 60000
81+
) => {
82+
if (!page || page.isClosed()) {
83+
return;
84+
}
85+
86+
const validAuxiliaryPage = auxiliaryPage && !auxiliaryPage.isClosed() ? auxiliaryPage : undefined;
87+
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
88+
const timeoutPromise = new Promise<never>((_, reject) => {
89+
timeoutHandle = setTimeout(() => reject(new Error('conference cleanup timeout')), cleanupTaskTimeout);
90+
});
91+
92+
try {
93+
await Promise.race([handleStrayTasks(page, validAuxiliaryPage), timeoutPromise]);
94+
} catch {
95+
// Ignore cleanup errors/timeouts so hook teardown does not fail test execution.
96+
} finally {
97+
if (timeoutHandle) {
98+
clearTimeout(timeoutHandle);
99+
}
100+
}
101+
};
102+
103+
export const setConferenceBaselineAvailability = async (
104+
getAgentPage: GetAgentPage,
105+
availableAgents: AgentId[],
106+
agentIds: AgentId[] = CONFERENCE_AGENT_IDS
107+
) => {
108+
for (const agentId of agentIds) {
109+
const page = getAgentPage(agentId);
110+
const state = availableAgents.includes(agentId) ? USER_STATES.AVAILABLE : USER_STATES.MEETING;
111+
await changeUserState(page, state);
112+
}
113+
};
114+
115+
export const setConferenceAgentsAvailable = async (getAgentPage: GetAgentPage, agentIds: AgentId[]) => {
116+
for (const agentId of agentIds) {
117+
const page = getAgentPage(agentId);
118+
await changeUserState(page, USER_STATES.AVAILABLE);
119+
await verifyCurrentState(page, USER_STATES.AVAILABLE);
120+
}
121+
};
122+
123+
export const waitForConferenceControlReady = async (
124+
getAgentPage: GetAgentPage,
125+
agentId: AgentId,
126+
controlTestId: string,
127+
timeout: number = ACCEPT_TASK_TIMEOUT
128+
) => {
129+
const control = getAgentPage(agentId).getByTestId(controlTestId).first();
130+
await expect(control).toBeVisible({timeout});
131+
await expect(control).toBeEnabled({timeout});
132+
};
133+
134+
export const resetCallerPageForNextConferenceCall = async (callerPage?: Page) => {
135+
if (!callerPage || callerPage.isClosed()) return;
136+
const endBtn = callerPage.getByTestId('end');
137+
const isEnabled = await endBtn.isEnabled({timeout: 1000}).catch(() => false);
138+
if (isEnabled) {
139+
await endBtn.click({timeout: AWAIT_TIMEOUT});
140+
await callerPage.waitForTimeout(1000);
141+
}
142+
await callerPage.locator('#sd-get-media-streams').click({timeout: AWAIT_TIMEOUT});
143+
await callerPage.waitForTimeout(500);
144+
};
145+
146+
export const cleanupConferenceState = async ({
147+
getAgentPage,
148+
callerPage,
149+
agentIds = CONFERENCE_AGENT_IDS,
150+
cleanupTaskTimeout = 60000,
151+
}: CleanupConferenceStateOptions) => {
152+
await resetCallerPageForNextConferenceCall(callerPage);
153+
for (const agentId of agentIds) {
154+
await cleanupConferencePageWithTimeout(getAgentPage(agentId), callerPage, cleanupTaskTimeout);
155+
}
156+
await cleanupConferencePageWithTimeout(callerPage, undefined, cleanupTaskTimeout);
157+
};
158+
159+
export const startBaselineCallOnAgent1 = async ({
160+
getAgentPage,
161+
callerPage,
162+
getRequiredEnv,
163+
agentIds = CONFERENCE_AGENT_IDS,
164+
acceptTimeout = ACCEPT_TASK_TIMEOUT,
165+
waitForAvailableBeforeDial = true,
166+
}: StartConferenceCallOptions) => {
167+
await setConferenceBaselineAvailability(getAgentPage, [1], agentIds);
168+
if (waitForAvailableBeforeDial) {
169+
await waitForState(getAgentPage(1), USER_STATES.AVAILABLE);
170+
await verifyCurrentState(getAgentPage(1), USER_STATES.AVAILABLE);
171+
}
172+
173+
if (!callerPage || callerPage.isClosed()) {
174+
throw new Error('Caller page is not available for conference call setup');
175+
}
176+
177+
await createCallTask(callerPage, getRequiredEnv('ENTRY_POINT'));
178+
await acceptIncomingTask(getAgentPage(1), TASK_TYPES.CALL, acceptTimeout);
179+
await verifyCurrentState(getAgentPage(1), USER_STATES.ENGAGED);
180+
};
181+
182+
export const consultAgentAndAcceptCall = async ({
183+
fromAgent,
184+
toAgent,
185+
getAgentPage,
186+
getAgentName,
187+
acceptTimeout = ACCEPT_TASK_TIMEOUT,
188+
}: ConferenceConsultOptions) => {
189+
await setConferenceAgentsAvailable(getAgentPage, [toAgent]);
190+
await waitForConferenceControlReady(getAgentPage, fromAgent, 'call-control:consult', acceptTimeout);
191+
await consultOrTransfer(getAgentPage(fromAgent), 'agent', 'consult', getAgentName(toAgent));
192+
await acceptIncomingTask(getAgentPage(toAgent), TASK_TYPES.CALL, acceptTimeout);
193+
await verifyCurrentState(getAgentPage(toAgent), USER_STATES.ENGAGED);
194+
};
195+
196+
export const consultQueueAndAcceptCall = async ({
197+
fromAgent,
198+
toAgent,
199+
getAgentPage,
200+
getRequiredEnv,
201+
acceptTimeout = ACCEPT_TASK_TIMEOUT,
202+
}: QueueConsultOptions) => {
203+
await setConferenceAgentsAvailable(getAgentPage, [toAgent]);
204+
await waitForConferenceControlReady(getAgentPage, fromAgent, 'call-control:consult', acceptTimeout);
205+
await consultOrTransfer(getAgentPage(fromAgent), 'queue', 'consult', getRequiredEnv('QUEUE_NAME'));
206+
await acceptIncomingTask(getAgentPage(toAgent), TASK_TYPES.CALL, acceptTimeout);
207+
await verifyCurrentState(getAgentPage(toAgent), USER_STATES.ENGAGED);
208+
};
209+
210+
export const mergeConsultIntoConference = async ({
211+
fromAgent,
212+
getAgentPage,
213+
acceptTimeout = ACCEPT_TASK_TIMEOUT,
214+
}: SingleAgentActionOptions) => {
215+
const page = getAgentPage(fromAgent);
216+
const mergeButton = page.getByTestId('conference-consult-btn');
217+
await expect(mergeButton).toBeVisible({timeout: acceptTimeout});
218+
await mergeButton.click();
219+
await page.waitForTimeout(CONFERENCE_ACTION_SETTLE_TIMEOUT);
220+
};
221+
222+
export const transferConsultAndSubmitWrapup = async ({
223+
fromAgent,
224+
getAgentPage,
225+
acceptTimeout = ACCEPT_TASK_TIMEOUT,
226+
}: SingleAgentActionOptions) => {
227+
const page = getAgentPage(fromAgent);
228+
await expect(page.getByTestId('transfer-consult-btn')).toBeVisible({timeout: acceptTimeout});
229+
await page.getByTestId('transfer-consult-btn').click();
230+
await page.waitForTimeout(CONFERENCE_ACTION_SETTLE_TIMEOUT);
231+
await submitWrapup(page, WRAPUP_REASONS.SALE);
232+
};
233+
234+
export const toggleConferenceLegIfSwitchAvailable = async (getAgentPage: GetAgentPage, agentId: AgentId) => {
235+
const page = getAgentPage(agentId);
236+
const switchButton = page.getByTestId('switchToMainCall-consult-btn');
237+
const canToggle = await switchButton.isVisible().catch(() => false);
238+
if (!canToggle) {
239+
return false;
240+
}
241+
await switchButton.click();
242+
await page.waitForTimeout(CONFERENCE_SWITCH_TOGGLE_TIMEOUT);
243+
return true;
244+
};
245+
246+
export const exitConferenceParticipantOrEndTask = async (getAgentPage: GetAgentPage, agentId: AgentId) => {
247+
const page = getAgentPage(agentId);
248+
const exitButton = page.getByTestId('call-control:exit-conference').first();
249+
const canExit = await exitButton.isVisible().catch(() => false);
250+
251+
if (canExit) {
252+
await exitButton.click();
253+
await page.waitForTimeout(CONFERENCE_ACTION_SETTLE_TIMEOUT);
254+
const wrapupBox = page.getByTestId('call-control:wrapup-button').first();
255+
const needsWrapup = await wrapupBox
256+
.waitFor({state: 'visible', timeout: WRAPUP_TIMEOUT})
257+
.then(() => true)
258+
.catch(() => false);
259+
if (needsWrapup) {
260+
await submitWrapup(page, WRAPUP_REASONS.SALE);
261+
}
262+
return;
263+
}
264+
265+
await endTask(page);
266+
await page.waitForTimeout(CONFERENCE_END_TASK_SETTLE_TIMEOUT);
267+
await submitWrapup(page, WRAPUP_REASONS.SALE);
268+
};

playwright/ai-docs/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ When Playwright behavior changes:
128128
- `pageSetup` has a single bounded station logout/re-login recovery if `state-select` does not appear after telephony login.
129129
- Multi-incoming digital scenarios should create/accept chat/email sequentially to reduce avoidable RONA races.
130130
- `afterAll` cleanup in chained-call suites should guard state reads when setup never reached user-state visibility.
131-
- Conference suite `cleanSlate` should clean shared-call agents sequentially (not in parallel) to avoid leg ownership races across participants.
131+
- Conference suites should run conference-state cleanup sequentially across shared-call agents (not in parallel) to avoid leg ownership races.
132132

133133
---
134134

0 commit comments

Comments
 (0)