Skip to content

Commit 0627663

Browse files
committed
test: update the unit for contact center
1 parent a382577 commit 0627663

File tree

13 files changed

+894
-968
lines changed

13 files changed

+894
-968
lines changed

packages/@webex/contact-center/src/services/task/TaskManager.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ type WebSocketMessage = {
2626
keepalive?: 'true' | 'false' | boolean;
2727
data: WebSocketPayload;
2828
};
29+
30+
const CC_EVENT_SET = new Set<CC_EVENTS>(Object.values(CC_EVENTS) as CC_EVENTS[]);
31+
32+
const isCcEvent = (value: string): value is CC_EVENTS => CC_EVENT_SET.has(value as CC_EVENTS);
2933
/** @internal */
3034
export default class TaskManager extends EventEmitter {
3135
private call: ICall;
@@ -196,7 +200,7 @@ export default class TaskManager extends EventEmitter {
196200
* @param payload - The event payload
197201
* @param task - The task instance
198202
*/
199-
private sendEventToStateMachine(
203+
private static sendEventToStateMachine(
200204
ccEvent: CC_EVENTS,
201205
payload: WebSocketPayload,
202206
task?: ITask
@@ -232,16 +236,17 @@ export default class TaskManager extends EventEmitter {
232236
}
233237
// Re-emit the task events to the task object
234238
let task: ITask;
235-
if (payload.data?.type) {
236-
if (Object.values(CC_TASK_EVENTS).includes(payload.data.type)) {
237-
task = this.taskCollection[payload.data.interactionId];
238-
}
239-
LoggerProxy.info(`Handling task event ${payload.data?.type}`, {
239+
const eventType = payload.data?.type;
240+
241+
if (eventType && isCcEvent(eventType)) {
242+
task = this.taskCollection[payload.data.interactionId];
243+
244+
LoggerProxy.info(`Handling task event ${eventType}`, {
240245
module: TASK_MANAGER_FILE,
241246
method: METHODS.REGISTER_TASK_LISTENERS,
242247
interactionId: payload.data?.interactionId,
243248
});
244-
switch (payload.data.type) {
249+
switch (eventType) {
245250
case CC_EVENTS.AGENT_CONTACT:
246251
if (!task) {
247252
// Re-create task if it does not exist
@@ -452,10 +457,13 @@ export default class TaskManager extends EventEmitter {
452457
// Send all events to state machine after processing
453458
// Task may have been created in AGENT_CONTACT or AGENT_CONTACT_RESERVED cases
454459
if (task) {
455-
task.emit(payload.data.type, payload.data);
460+
// Only emit task-specific events to the task object
461+
if (Object.values(CC_TASK_EVENTS).includes(eventType as any)) {
462+
task.emit(eventType as any, payload.data);
463+
}
456464

457465
// Send event to state machine for all events
458-
this.sendEventToStateMachine(payload.data.type as CC_EVENTS, payload.data, task);
466+
TaskManager.sendEventToStateMachine(eventType, payload.data, task);
459467
}
460468
}
461469
});

packages/@webex/contact-center/src/services/task/state-machine/TaskStateMachine.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,26 @@
55
* It orchestrates state transitions, guards, and actions for task lifecycle management.
66
*/
77

8-
import {createMachine} from 'xstate';
9-
import {TaskState, TaskEvent, UIControlConfig} from './types';
8+
import {setup} from 'xstate';
9+
import {TaskState, TaskEvent, TaskContext, TaskEventPayload, UIControlConfig} from './types';
1010
import {actions, createInitialContext} from './actions';
1111

12+
type TaskActionConfigMap = {[K in keyof typeof actions]: undefined};
13+
14+
const taskStateMachineSetup = setup<
15+
TaskContext,
16+
TaskEventPayload,
17+
Record<string, never>,
18+
Record<string, never>,
19+
TaskActionConfigMap
20+
>({
21+
types: {
22+
context: {} as TaskContext,
23+
events: {} as TaskEventPayload,
24+
},
25+
actors: {},
26+
});
27+
1228
/**
1329
* Get task state machine configuration with UI control config
1430
* Defines all states, transitions, guards, and actions for task management
@@ -256,9 +272,9 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
256272
* @returns StateMachine instance for task management
257273
*/
258274
export function createTaskStateMachine(uiControlConfig: UIControlConfig) {
259-
return createMachine(getTaskStateMachineConfig(uiControlConfig), {
260-
actions,
261-
});
275+
return taskStateMachineSetup
276+
.createMachine(getTaskStateMachineConfig(uiControlConfig))
277+
.provide({actions});
262278
}
263279

264280
export type TaskStateMachine = ReturnType<typeof createTaskStateMachine>;

packages/@webex/contact-center/src/services/task/state-machine/actions.ts

Lines changed: 75 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,21 @@
1212
*/
1313

1414
import {assign} from 'xstate';
15+
import type {ActionFunctionMap, EventObject} from 'xstate';
1516
import {TaskContext, TaskEventPayload, TaskEvent, UIControlConfig, TaskState} from './types';
1617
import {TaskData} from '../types';
1718
import {computeUIControls, getDefaultUIControls} from './uiControlsComputer';
1819

20+
type TaskActionsMap = ActionFunctionMap<
21+
TaskContext,
22+
TaskEventPayload,
23+
never,
24+
{type: string; params: undefined},
25+
never,
26+
never,
27+
EventObject
28+
>;
29+
1930
type RecordingStateUpdate = Partial<
2031
Pick<TaskContext, 'recordingControlsAvailable' | 'recordingInProgress'>
2132
>;
@@ -28,7 +39,11 @@ const deriveRecordingState = (taskData?: TaskData | null): RecordingStateUpdate
2839
}
2940

3041
const update: RecordingStateUpdate = {};
31-
const {recordingStarted, recordInProgress} = callProcessingDetails;
42+
const {recordingStarted, recordInProgress, isPaused} = callProcessingDetails as {
43+
recordingStarted?: boolean;
44+
recordInProgress?: boolean;
45+
isPaused?: boolean;
46+
};
3247

3348
if (recordingStarted !== undefined) {
3449
update.recordingControlsAvailable = recordingStarted;
@@ -51,6 +66,11 @@ const deriveRecordingState = (taskData?: TaskData | null): RecordingStateUpdate
5166
update.recordingInProgress = true;
5267
}
5368

69+
if (isPaused !== undefined) {
70+
update.recordingControlsAvailable = true;
71+
update.recordingInProgress = !isPaused;
72+
}
73+
5474
return update;
5575
};
5676

@@ -64,10 +84,16 @@ const deriveRecordingState = (taskData?: TaskData | null): RecordingStateUpdate
6484
* single source of truth, while derived values (like recording flags)
6585
* are recalculated here via deriveRecordingState.
6686
*/
67-
const deriveTaskDataUpdates = (_context: TaskContext, taskData: TaskData) => ({
68-
taskData,
69-
...deriveRecordingState(taskData),
70-
});
87+
const deriveTaskDataUpdates = (_context: TaskContext, taskData: TaskData | undefined) =>
88+
taskData
89+
? {
90+
taskData,
91+
...deriveRecordingState(taskData),
92+
}
93+
: {};
94+
95+
const getTaskDataFromEvent = (event?: TaskEventPayload): TaskData | undefined =>
96+
event && typeof event === 'object' ? (event as any).taskData : undefined;
7197

7298
/**
7399
* Create initial context for a new task.
@@ -116,7 +142,7 @@ export function createInitialContext(
116142
* @returns Assign action that updates UI controls
117143
*/
118144
export function updateUIControls(currentState: TaskState) {
119-
return assign((context: TaskContext) => ({
145+
return assign(({context}: {context: TaskContext}) => ({
120146
uiControls: computeUIControls(currentState, context),
121147
}));
122148
}
@@ -125,25 +151,19 @@ export function updateUIControls(currentState: TaskState) {
125151
* Action implementations
126152
* These return XState assign actions that update the context
127153
*/
128-
export const actions = {
154+
export const actions: TaskActionsMap = {
129155
/**
130156
* Initialize task with offer data
131157
*/
132-
initializeTask: assign((context: TaskContext, event: TaskEventPayload) => {
133-
// Guard not needed in this action because the state machine only references
134-
// initializeTask from OFFER/OFFER_CONSULT transitions, both of which carry taskData.
135-
const {taskData} = event as Extract<TaskEventPayload, {taskData: TaskData}>;
136-
137-
return deriveTaskDataUpdates(context, taskData);
158+
initializeTask: assign(({context, event}: {context: TaskContext; event: TaskEventPayload}) => {
159+
return deriveTaskDataUpdates(context, getTaskDataFromEvent(event));
138160
}),
139161

140162
/**
141163
* Update task data from ASSIGN event
142164
*/
143-
updateTaskData: assign((context: TaskContext, event: TaskEventPayload) => {
144-
const {taskData} = event as Extract<TaskEventPayload, {taskData: TaskData}>;
145-
146-
return deriveTaskDataUpdates(context, taskData);
165+
updateTaskData: assign(({context, event}: {context: TaskContext; event: TaskEventPayload}) => {
166+
return deriveTaskDataUpdates(context, getTaskDataFromEvent(event));
147167
}),
148168

149169
/**
@@ -156,35 +176,42 @@ export const actions = {
156176
/**
157177
* Set consult destination details
158178
*/
159-
setConsultDestination: assign((context: TaskContext, event: TaskEventPayload) => {
160-
const consultEvent = event as Extract<
161-
TaskEventPayload,
162-
{type: TaskEvent.CONSULT; destination: string}
163-
>;
179+
setConsultDestination: assign(({event}: {event: TaskEventPayload}) => {
180+
if (!event || event.type !== TaskEvent.CONSULT || !('destination' in event)) {
181+
return {};
182+
}
164183

165184
return {
166-
consultDestination: consultEvent.destination,
185+
consultDestination: (event as {destination: string}).destination,
167186
};
168187
}),
169188

170189
/**
171190
* Mark that consult destination agent has joined
172191
*/
173-
setConsultAgentJoined: assign((context: TaskContext, event: TaskEventPayload) => {
174-
const consultingActive = event as Extract<
175-
TaskEventPayload,
176-
{type: TaskEvent.CONSULTING_ACTIVE; consultDestinationAgentJoined: boolean}
177-
>;
192+
setConsultAgentJoined: assign(({event}: {event: TaskEventPayload}) => {
193+
if (
194+
!event ||
195+
event.type !== TaskEvent.CONSULTING_ACTIVE ||
196+
!('consultDestinationAgentJoined' in event)
197+
) {
198+
return {};
199+
}
178200

179201
return {
180-
consultDestinationAgentJoined: consultingActive.consultDestinationAgentJoined,
202+
consultDestinationAgentJoined: (event as {consultDestinationAgentJoined: boolean})
203+
.consultDestinationAgentJoined,
181204
};
182205
}),
183206

184207
/**
185208
* Set recording state
186209
*/
187-
setRecordingState: assign((context: TaskContext, event: TaskEventPayload) => {
210+
setRecordingState: assign(({event}: {event: TaskEventPayload}) => {
211+
if (!event || !('type' in event)) {
212+
return {};
213+
}
214+
188215
if (event.type === TaskEvent.PAUSE_RECORDING) {
189216
return {
190217
recordingControlsAvailable: true,
@@ -212,13 +239,23 @@ export const actions = {
212239
/**
213240
* Track hold state updates (currently no-op placeholder)
214241
*/
215-
setHoldState: assign((context: TaskContext, event: TaskEventPayload) => {
216-
const holdEvent = event as Extract<
217-
TaskEventPayload,
218-
| {type: TaskEvent.HOLD_SUCCESS; mediaResourceId: string}
219-
| {type: TaskEvent.UNHOLD_SUCCESS; mediaResourceId: string}
220-
>;
221-
const mediaResourceId = holdEvent.mediaResourceId;
242+
setHoldState: assign(({context, event}: {context: TaskContext; event: TaskEventPayload}) => {
243+
if (
244+
!event ||
245+
(event.type !== TaskEvent.HOLD_SUCCESS && event.type !== TaskEvent.UNHOLD_SUCCESS)
246+
) {
247+
return {};
248+
}
249+
250+
const mediaResourceId =
251+
'mediaResourceId' in event
252+
? (event as {mediaResourceId?: string}).mediaResourceId
253+
: undefined;
254+
255+
if (!mediaResourceId) {
256+
return {};
257+
}
258+
222259
const interaction = context.taskData?.interaction;
223260
const mediaEntry = interaction?.media?.[mediaResourceId];
224261

@@ -230,7 +267,7 @@ export const actions = {
230267
...interaction.media,
231268
[mediaResourceId]: {
232269
...mediaEntry,
233-
isHold: holdEvent.type === TaskEvent.HOLD_SUCCESS,
270+
isHold: event.type === TaskEvent.HOLD_SUCCESS,
234271
},
235272
};
236273

0 commit comments

Comments
 (0)