@webex/contact-center 3.12.0-task-refactor.2 → 3.12.0-task-refactor.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/services/config/Util.js +1 -0
  2. package/dist/services/config/Util.js.map +1 -1
  3. package/dist/services/config/types.js.map +1 -1
  4. package/dist/services/task/TaskManager.js +8 -2
  5. package/dist/services/task/TaskManager.js.map +1 -1
  6. package/dist/services/task/state-machine/TaskStateMachine.js +45 -8
  7. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  8. package/dist/services/task/state-machine/actions.js +11 -6
  9. package/dist/services/task/state-machine/actions.js.map +1 -1
  10. package/dist/services/task/state-machine/constants.js +20 -1
  11. package/dist/services/task/state-machine/constants.js.map +1 -1
  12. package/dist/services/task/state-machine/guards.js +27 -8
  13. package/dist/services/task/state-machine/guards.js.map +1 -1
  14. package/dist/services/task/state-machine/uiControlsComputer.js +38 -9
  15. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
  16. package/dist/services/task/types.js.map +1 -1
  17. package/dist/services/task/voice/Voice.js +7 -2
  18. package/dist/services/task/voice/Voice.js.map +1 -1
  19. package/dist/types/services/config/types.d.ts +4 -0
  20. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +44 -4
  21. package/dist/types/services/task/state-machine/constants.d.ts +13 -0
  22. package/dist/types/services/task/state-machine/guards.d.ts +6 -1
  23. package/dist/types/services/task/types.d.ts +2 -0
  24. package/dist/webex.js +1 -1
  25. package/package.json +1 -1
  26. package/src/services/config/Util.ts +1 -0
  27. package/src/services/config/types.ts +4 -0
  28. package/src/services/task/TaskManager.ts +8 -2
  29. package/src/services/task/state-machine/TaskStateMachine.ts +72 -9
  30. package/src/services/task/state-machine/actions.ts +19 -10
  31. package/src/services/task/state-machine/constants.ts +19 -0
  32. package/src/services/task/state-machine/guards.ts +34 -7
  33. package/src/services/task/state-machine/uiControlsComputer.ts +37 -11
  34. package/src/services/task/types.ts +2 -0
  35. package/src/services/task/voice/Voice.ts +12 -7
  36. package/test/unit/spec/services/config/index.ts +1 -0
  37. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +103 -0
  38. package/test/unit/spec/services/task/state-machine/guards.ts +103 -0
  39. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +189 -1
  40. package/umd/contact-center.min.js +2 -2
  41. package/umd/contact-center.min.js.map +1 -1
@@ -238,6 +238,109 @@ describe('State Machine Guards', () => {
238
238
  });
239
239
  });
240
240
 
241
+ describe('Hydration Guards - isInteractionConsulting', () => {
242
+ it('returns true when interaction state is consulting', () => {
243
+ const ctx = createContext();
244
+ const taskData = createTaskData({
245
+ interaction: {
246
+ ...createTaskData().interaction,
247
+ state: 'consulting',
248
+ },
249
+ });
250
+ expect(
251
+ guards.isInteractionConsulting(createParams(ctx, createEventWithTaskData(taskData)))
252
+ ).toBe(true);
253
+ });
254
+
255
+ it('returns true for EP_DN consulted agent (state=connected, CPD relationshipType=consult)', () => {
256
+ const ctx = createContext();
257
+ const taskData = createTaskData({
258
+ interaction: {
259
+ ...createTaskData().interaction,
260
+ state: 'connected',
261
+ callProcessingDetails: {
262
+ ...createTaskData().interaction!.callProcessingDetails,
263
+ relationshipType: 'consult',
264
+ },
265
+ },
266
+ });
267
+ expect(
268
+ guards.isInteractionConsulting(createParams(ctx, createEventWithTaskData(taskData)))
269
+ ).toBe(true);
270
+ });
271
+
272
+ it('returns true when post_call with active consult (consultState=consulting + consult media)', () => {
273
+ const ctx = createContext();
274
+ const taskData = createTaskData({
275
+ interaction: {
276
+ ...createTaskData().interaction,
277
+ state: 'post_call',
278
+ participants: {
279
+ 'agent-123': {
280
+ ...createParticipant('agent-123', 'Agent'),
281
+ consultState: 'consulting',
282
+ },
283
+ },
284
+ media: {
285
+ [INTERACTION_ID]: {
286
+ mediaResourceId: INTERACTION_ID,
287
+ mediaType: 'telephony',
288
+ mediaMgr: 'media-mgr',
289
+ participants: ['agent-123'],
290
+ mType: 'mainCall',
291
+ isHold: false,
292
+ holdTimestamp: null,
293
+ },
294
+ 'consult-media': {
295
+ mediaResourceId: 'consult-media',
296
+ mediaType: 'telephony',
297
+ mediaMgr: 'media-mgr',
298
+ participants: ['agent-123', 'agent-2'],
299
+ mType: 'consult',
300
+ isHold: false,
301
+ holdTimestamp: null,
302
+ },
303
+ },
304
+ },
305
+ });
306
+ expect(
307
+ guards.isInteractionConsulting(createParams(ctx, createEventWithTaskData(taskData)))
308
+ ).toBe(true);
309
+ });
310
+
311
+ it('returns false for post_call without consult media', () => {
312
+ const ctx = createContext();
313
+ const taskData = createTaskData({
314
+ interaction: {
315
+ ...createTaskData().interaction,
316
+ state: 'post_call',
317
+ participants: {
318
+ 'agent-123': {
319
+ ...createParticipant('agent-123', 'Agent'),
320
+ consultState: undefined,
321
+ },
322
+ },
323
+ },
324
+ });
325
+ expect(
326
+ guards.isInteractionConsulting(createParams(ctx, createEventWithTaskData(taskData)))
327
+ ).toBe(false);
328
+ });
329
+
330
+ it('returns false for plain connected state without consult CPD', () => {
331
+ const ctx = createContext();
332
+ const taskData = createTaskData({
333
+ interaction: {
334
+ ...createTaskData().interaction,
335
+ state: 'connected',
336
+ },
337
+ });
338
+ expect(
339
+ guards.isInteractionConsulting(createParams(ctx, createEventWithTaskData(taskData)))
340
+ ).toBe(false);
341
+ });
342
+ });
343
+
241
344
  describe('Server State Guards', () => {
242
345
  it('isPrimaryMediaOnHold returns true when media isHold is true', () => {
243
346
  const ctx = createContext();
@@ -27,7 +27,7 @@ function createConsultTaskData() {
27
27
  currentState: 'consulting',
28
28
  isConsulted: true,
29
29
  },
30
- 'customer-1': {id: 'customer-1', pType: 'CUSTOMER', hasLeft: false},
30
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
31
31
  } as any,
32
32
  media: {
33
33
  'interaction-1': {
@@ -145,3 +145,191 @@ describe('uiControlsComputer consult initiator controls', () => {
145
145
  expect(uiControls.consult).toEqual(getDefaultUIControls().consult);
146
146
  });
147
147
  });
148
+
149
+ describe('uiControlsComputer conference controls', () => {
150
+ function createConferenceTaskData(participantCount: number) {
151
+ const participants: Record<string, any> = {
152
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasJoined: true, hasLeft: false},
153
+ 'agent-1': {
154
+ id: 'agent-1',
155
+ pType: 'Agent',
156
+ hasJoined: true,
157
+ hasLeft: false,
158
+ consultState: 'conferencing',
159
+ },
160
+ };
161
+ const mainCallParticipants = ['customer-1', 'agent-1'];
162
+
163
+ if (participantCount > 1) {
164
+ participants['agent-2'] = {
165
+ id: 'agent-2',
166
+ pType: 'Agent',
167
+ hasJoined: true,
168
+ hasLeft: false,
169
+ consultState: 'conferencing',
170
+ };
171
+ mainCallParticipants.push('agent-2');
172
+ }
173
+
174
+ return createTaskData({
175
+ agentId: 'agent-1',
176
+ mediaResourceId: 'interaction-1',
177
+ consultMediaResourceId: '' as any,
178
+ interaction: {
179
+ interactionId: 'interaction-1',
180
+ mainInteractionId: 'interaction-1',
181
+ state: 'conference',
182
+ participants,
183
+ media: {
184
+ 'interaction-1': {
185
+ mediaResourceId: 'interaction-1',
186
+ mType: 'mainCall',
187
+ isHold: false,
188
+ participants: mainCallParticipants,
189
+ },
190
+ },
191
+ } as any,
192
+ });
193
+ }
194
+
195
+ function createConferenceContext(participantCount: number, overrides: Partial<TaskContext> = {}): TaskContext {
196
+ return {
197
+ taskData: createConferenceTaskData(participantCount),
198
+ consultInitiator: true,
199
+ exitingConference: false,
200
+ consultFromConference: false,
201
+ transferConferenceRequested: false,
202
+ consultDestinationType: null,
203
+ consultDestinationAgentId: null,
204
+ consultDestinationAgentJoined: false,
205
+ consultCallHeld: false,
206
+ recordingControlsAvailable: true,
207
+ recordingInProgress: true,
208
+ uiControlConfig: {
209
+ isEndTaskEnabled: true,
210
+ isEndConsultEnabled: true,
211
+ channelType: TASK_CHANNEL_TYPE.VOICE,
212
+ isRecordingEnabled: true,
213
+ agentId: 'agent-1',
214
+ },
215
+ uiControls: getDefaultUIControls(),
216
+ ...overrides,
217
+ };
218
+ }
219
+
220
+ it('pending conference (participantCount <= 1): transfer/recording enabled, consult disabled, exitConference hidden', () => {
221
+ const context = createConferenceContext(1);
222
+
223
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, context.taskData);
224
+
225
+ expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: true});
226
+ expect(uiControls.main.recording).toEqual({isVisible: true, isEnabled: true});
227
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: false});
228
+ expect(uiControls.main.exitConference).toEqual({isVisible: false, isEnabled: false});
229
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
230
+ });
231
+
232
+ it('real conference (participantCount > 1): transfer/recording hidden, consult enabled, exitConference visible', () => {
233
+ const context = createConferenceContext(2);
234
+
235
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, context.taskData);
236
+
237
+ expect(uiControls.main.transfer).toEqual({isVisible: false, isEnabled: false});
238
+ expect(uiControls.main.recording).toEqual({isVisible: false, isEnabled: false});
239
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: true});
240
+ expect(uiControls.main.exitConference).toEqual({isVisible: true, isEnabled: true});
241
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
242
+ });
243
+
244
+ it('hold button shows visible but disabled in conference', () => {
245
+ const context = createConferenceContext(2);
246
+
247
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, context.taskData);
248
+
249
+ expect(uiControls.main.hold).toEqual({isVisible: true, isEnabled: false});
250
+ });
251
+ });
252
+
253
+ describe('uiControlsComputer post-call consult controls (customer left)', () => {
254
+ function createPostCallConsultTaskData() {
255
+ return createTaskData({
256
+ agentId: 'agent-1',
257
+ mediaResourceId: 'interaction-1',
258
+ consultMediaResourceId: 'consult-media',
259
+ consultingAgentId: 'agent-1',
260
+ destAgentId: 'agent-2',
261
+ interaction: {
262
+ interactionId: 'interaction-1',
263
+ mainInteractionId: 'interaction-1',
264
+ state: 'post_call',
265
+ isTerminated: false,
266
+ participants: {
267
+ 'agent-1': {
268
+ id: 'agent-1',
269
+ pType: 'Agent',
270
+ hasJoined: true,
271
+ hasLeft: false,
272
+ consultState: 'consulting',
273
+ },
274
+ 'agent-2': {
275
+ id: 'agent-2',
276
+ pType: 'Agent',
277
+ hasLeft: false,
278
+ consultState: 'consulting',
279
+ isConsulted: true,
280
+ },
281
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasJoined: true, hasLeft: true},
282
+ } as any,
283
+ media: {
284
+ 'interaction-1': {
285
+ mediaResourceId: 'interaction-1',
286
+ mType: 'mainCall',
287
+ isHold: false,
288
+ participants: ['agent-1'],
289
+ },
290
+ 'consult-media': {
291
+ mediaResourceId: 'consult-media',
292
+ mType: 'consult',
293
+ isHold: false,
294
+ participants: ['agent-1', 'agent-2'],
295
+ },
296
+ } as any,
297
+ } as any,
298
+ });
299
+ }
300
+
301
+ it('consult controls are visible but disabled when customer has left', () => {
302
+ const taskData = createPostCallConsultTaskData();
303
+ const context: TaskContext = {
304
+ taskData,
305
+ consultInitiator: true,
306
+ exitingConference: false,
307
+ consultFromConference: false,
308
+ transferConferenceRequested: false,
309
+ consultDestinationType: null,
310
+ consultDestinationAgentId: null,
311
+ consultDestinationAgentJoined: true,
312
+ consultCallHeld: false,
313
+ recordingControlsAvailable: true,
314
+ recordingInProgress: true,
315
+ uiControlConfig: {
316
+ isEndTaskEnabled: true,
317
+ isEndConsultEnabled: true,
318
+ channelType: TASK_CHANNEL_TYPE.VOICE,
319
+ isRecordingEnabled: true,
320
+ agentId: 'agent-1',
321
+ },
322
+ uiControls: getDefaultUIControls(),
323
+ };
324
+
325
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, context.taskData);
326
+
327
+ // Consult leg: transfer/conference/merge/switch should be visible but disabled
328
+ expect(uiControls.consult.transfer).toEqual({isVisible: true, isEnabled: false});
329
+ expect(uiControls.consult.conference).toEqual({isVisible: true, isEnabled: false});
330
+ expect(uiControls.consult.switch).toEqual({isVisible: true, isEnabled: false});
331
+
332
+ // Main leg end should be visible but disabled
333
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
334
+ });
335
+ });