@webex/contact-center 3.12.0-task-refactor.3 → 3.12.0-task-refactor.5
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.
- package/dist/services/task/TaskManager.js +9 -2
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/services/task/state-machine/TaskStateMachine.js +51 -9
- package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
- package/dist/services/task/state-machine/actions.js +11 -6
- package/dist/services/task/state-machine/actions.js.map +1 -1
- package/dist/services/task/state-machine/constants.js +20 -1
- package/dist/services/task/state-machine/constants.js.map +1 -1
- package/dist/services/task/state-machine/guards.js +27 -8
- package/dist/services/task/state-machine/guards.js.map +1 -1
- package/dist/services/task/state-machine/uiControlsComputer.js +45 -12
- package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
- package/dist/services/task/types.js.map +1 -1
- package/dist/services/task/voice/Voice.js +16 -5
- package/dist/services/task/voice/Voice.js.map +1 -1
- package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +52 -4
- package/dist/types/services/task/state-machine/constants.d.ts +13 -0
- package/dist/types/services/task/state-machine/guards.d.ts +6 -1
- package/dist/types/services/task/types.d.ts +2 -0
- package/dist/types/services/task/voice/Voice.d.ts +18 -17
- package/dist/webex.js +1 -1
- package/package.json +1 -1
- package/src/services/task/TaskManager.ts +9 -3
- package/src/services/task/state-machine/TaskStateMachine.ts +79 -10
- package/src/services/task/state-machine/actions.ts +19 -10
- package/src/services/task/state-machine/constants.ts +19 -0
- package/src/services/task/state-machine/guards.ts +34 -7
- package/src/services/task/state-machine/uiControlsComputer.ts +45 -17
- package/src/services/task/types.ts +2 -0
- package/src/services/task/voice/Voice.ts +20 -11
- package/test/unit/spec/services/task/TaskManager.ts +26 -0
- package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +164 -0
- package/test/unit/spec/services/task/state-machine/guards.ts +103 -0
- package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +237 -1
- package/test/unit/spec/services/task/voice/Voice.ts +24 -0
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
|
@@ -444,6 +444,109 @@ describe('Task state machine', () => {
|
|
|
444
444
|
});
|
|
445
445
|
});
|
|
446
446
|
|
|
447
|
+
describe('CONF_INITIATING state event handlers', () => {
|
|
448
|
+
it('transitions to CONFERENCING on CONFERENCE_START', () => {
|
|
449
|
+
const service = startMachine();
|
|
450
|
+
const taskData = createTaskData({consultingAgentId: 'agent-1'});
|
|
451
|
+
|
|
452
|
+
service.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
453
|
+
service.send({type: TaskEvent.ASSIGN, taskData});
|
|
454
|
+
service.send({
|
|
455
|
+
type: TaskEvent.CONSULT,
|
|
456
|
+
destination: 'agent-42',
|
|
457
|
+
destinationType: 'agent',
|
|
458
|
+
});
|
|
459
|
+
service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
|
|
460
|
+
service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
|
|
461
|
+
expect(service.getSnapshot().value).toBe(TaskState.CONF_INITIATING);
|
|
462
|
+
|
|
463
|
+
service.send({type: TaskEvent.CONFERENCE_START, taskData});
|
|
464
|
+
expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('transitions to WRAPPING_UP on CONSULT_END with isTerminated during CONF_INITIATING', () => {
|
|
468
|
+
const service = startMachine();
|
|
469
|
+
const taskData = createTaskData({consultingAgentId: 'agent-1'});
|
|
470
|
+
|
|
471
|
+
service.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
472
|
+
service.send({type: TaskEvent.ASSIGN, taskData});
|
|
473
|
+
service.send({
|
|
474
|
+
type: TaskEvent.CONSULT,
|
|
475
|
+
destination: 'agent-42',
|
|
476
|
+
destinationType: 'agent',
|
|
477
|
+
});
|
|
478
|
+
service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
|
|
479
|
+
service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
|
|
480
|
+
expect(service.getSnapshot().value).toBe(TaskState.CONF_INITIATING);
|
|
481
|
+
|
|
482
|
+
const terminatedTaskData = createTaskData({
|
|
483
|
+
consultingAgentId: 'agent-1',
|
|
484
|
+
interaction: {
|
|
485
|
+
isTerminated: true,
|
|
486
|
+
owner: 'agent-1',
|
|
487
|
+
} as any,
|
|
488
|
+
});
|
|
489
|
+
service.send({type: TaskEvent.CONSULT_END, taskData: terminatedTaskData});
|
|
490
|
+
expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('transitions to CONNECTED on CONSULT_END without isTerminated during CONF_INITIATING', () => {
|
|
494
|
+
const service = startMachine();
|
|
495
|
+
const taskData = createTaskData({consultingAgentId: 'agent-1'});
|
|
496
|
+
|
|
497
|
+
service.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
498
|
+
service.send({type: TaskEvent.ASSIGN, taskData});
|
|
499
|
+
service.send({
|
|
500
|
+
type: TaskEvent.CONSULT,
|
|
501
|
+
destination: 'agent-42',
|
|
502
|
+
destinationType: 'agent',
|
|
503
|
+
});
|
|
504
|
+
service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
|
|
505
|
+
service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
|
|
506
|
+
expect(service.getSnapshot().value).toBe(TaskState.CONF_INITIATING);
|
|
507
|
+
|
|
508
|
+
service.send({type: TaskEvent.CONSULT_END, taskData});
|
|
509
|
+
expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
describe('CONFERENCING state CONSULT_END with terminated interaction', () => {
|
|
515
|
+
it('transitions to WRAPPING_UP when CONSULT_END arrives with isTerminated in CONFERENCING', () => {
|
|
516
|
+
const service = startMachine();
|
|
517
|
+
const taskData = createTaskData({
|
|
518
|
+
consultingAgentId: 'agent-1',
|
|
519
|
+
interaction: {
|
|
520
|
+
owner: 'agent-1',
|
|
521
|
+
state: 'conference',
|
|
522
|
+
} as any,
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
service.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
526
|
+
service.send({type: TaskEvent.ASSIGN, taskData});
|
|
527
|
+
service.send({
|
|
528
|
+
type: TaskEvent.CONSULT,
|
|
529
|
+
destination: 'agent-42',
|
|
530
|
+
destinationType: 'agent',
|
|
531
|
+
});
|
|
532
|
+
service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
|
|
533
|
+
service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
|
|
534
|
+
service.send({type: TaskEvent.CONFERENCE_START, taskData});
|
|
535
|
+
expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
|
|
536
|
+
|
|
537
|
+
const terminatedTaskData = createTaskData({
|
|
538
|
+
consultingAgentId: 'agent-1',
|
|
539
|
+
interaction: {
|
|
540
|
+
isTerminated: true,
|
|
541
|
+
owner: 'agent-1',
|
|
542
|
+
state: 'conference',
|
|
543
|
+
} as any,
|
|
544
|
+
});
|
|
545
|
+
service.send({type: TaskEvent.CONSULT_END, taskData: terminatedTaskData});
|
|
546
|
+
expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
|
|
447
550
|
describe('OFFERED state event handlers', () => {
|
|
448
551
|
it('transitions to TERMINATED when customer disconnects before agent answers', () => {
|
|
449
552
|
const service = startMachine();
|
|
@@ -470,4 +573,65 @@ describe('Task state machine', () => {
|
|
|
470
573
|
expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
|
|
471
574
|
});
|
|
472
575
|
});
|
|
576
|
+
|
|
577
|
+
describe('OUTBOUND_FAILED handling', () => {
|
|
578
|
+
it('transitions from IDLE to TERMINATED on OUTBOUND_FAILED (race condition)', () => {
|
|
579
|
+
const service = startMachine();
|
|
580
|
+
expect(service.getSnapshot().value).toBe(TaskState.IDLE);
|
|
581
|
+
|
|
582
|
+
const taskData = createTaskData({
|
|
583
|
+
interaction: {
|
|
584
|
+
outboundType: 'OUTDIAL',
|
|
585
|
+
isTerminated: true,
|
|
586
|
+
} as any,
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
service.send({type: TaskEvent.OUTBOUND_FAILED, taskData, reason: 'CUSTOMER_BUSY'});
|
|
590
|
+
expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it('transitions from OFFERED to TERMINATED on OUTBOUND_FAILED without wrapup', () => {
|
|
594
|
+
const service = startMachine();
|
|
595
|
+
const offerTaskData = createTaskData({
|
|
596
|
+
interaction: {
|
|
597
|
+
outboundType: 'OUTDIAL',
|
|
598
|
+
} as any,
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
service.send({type: TaskEvent.TASK_INCOMING, taskData: offerTaskData});
|
|
602
|
+
expect(service.getSnapshot().value).toBe(TaskState.OFFERED);
|
|
603
|
+
|
|
604
|
+
const failedTaskData = createTaskData({
|
|
605
|
+
interaction: {
|
|
606
|
+
outboundType: 'OUTDIAL',
|
|
607
|
+
isTerminated: true,
|
|
608
|
+
} as any,
|
|
609
|
+
});
|
|
610
|
+
service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'CUSTOMER_BUSY'});
|
|
611
|
+
expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it('transitions from OFFERED to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
|
|
615
|
+
const service = startMachine();
|
|
616
|
+
const offerTaskData = createTaskData({
|
|
617
|
+
interaction: {
|
|
618
|
+
outboundType: 'OUTDIAL',
|
|
619
|
+
} as any,
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
service.send({type: TaskEvent.TASK_INCOMING, taskData: offerTaskData});
|
|
623
|
+
expect(service.getSnapshot().value).toBe(TaskState.OFFERED);
|
|
624
|
+
|
|
625
|
+
const failedTaskData = createTaskData({
|
|
626
|
+
agentId: 'agent-1',
|
|
627
|
+
agentsPendingWrapUp: ['agent-1'],
|
|
628
|
+
interaction: {
|
|
629
|
+
outboundType: 'OUTDIAL',
|
|
630
|
+
isTerminated: true,
|
|
631
|
+
} as any,
|
|
632
|
+
});
|
|
633
|
+
service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'CUSTOMER_BUSY'});
|
|
634
|
+
expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
|
|
635
|
+
});
|
|
636
|
+
});
|
|
473
637
|
});
|
|
@@ -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: '
|
|
30
|
+
'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
|
|
31
31
|
} as any,
|
|
32
32
|
media: {
|
|
33
33
|
'interaction-1': {
|
|
@@ -145,3 +145,239 @@ describe('uiControlsComputer consult initiator controls', () => {
|
|
|
145
145
|
expect(uiControls.consult).toEqual(getDefaultUIControls().consult);
|
|
146
146
|
});
|
|
147
147
|
});
|
|
148
|
+
|
|
149
|
+
describe('uiControlsComputer outdial accept/decline controls', () => {
|
|
150
|
+
function createOutdialContext(voiceVariant: 'webrtc' | 'pstn' = 'webrtc'): TaskContext {
|
|
151
|
+
const taskData = createTaskData({
|
|
152
|
+
interaction: {
|
|
153
|
+
outboundType: 'OUTDIAL',
|
|
154
|
+
state: 'new',
|
|
155
|
+
isTerminated: false,
|
|
156
|
+
} as any,
|
|
157
|
+
});
|
|
158
|
+
return {
|
|
159
|
+
taskData,
|
|
160
|
+
consultInitiator: false,
|
|
161
|
+
exitingConference: false,
|
|
162
|
+
consultFromConference: false,
|
|
163
|
+
transferConferenceRequested: false,
|
|
164
|
+
consultDestinationType: null,
|
|
165
|
+
consultDestinationAgentId: null,
|
|
166
|
+
consultDestinationAgentJoined: false,
|
|
167
|
+
consultCallHeld: false,
|
|
168
|
+
recordingControlsAvailable: false,
|
|
169
|
+
recordingInProgress: false,
|
|
170
|
+
uiControlConfig: {
|
|
171
|
+
isEndTaskEnabled: true,
|
|
172
|
+
isEndConsultEnabled: true,
|
|
173
|
+
channelType: TASK_CHANNEL_TYPE.VOICE,
|
|
174
|
+
isRecordingEnabled: false,
|
|
175
|
+
agentId: 'agent-1',
|
|
176
|
+
voiceVariant,
|
|
177
|
+
},
|
|
178
|
+
uiControls: getDefaultUIControls(),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
it('accept is visible but disabled for WebRTC outdial in OFFERED state', () => {
|
|
183
|
+
const context = createOutdialContext('webrtc');
|
|
184
|
+
const uiControls = computeUIControls(TaskState.OFFERED, context, context.taskData);
|
|
185
|
+
|
|
186
|
+
expect(uiControls.main.accept).toEqual({isVisible: true, isEnabled: false});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('decline is visible but disabled for WebRTC outdial in OFFERED state', () => {
|
|
190
|
+
const context = createOutdialContext('webrtc');
|
|
191
|
+
const uiControls = computeUIControls(TaskState.OFFERED, context, context.taskData);
|
|
192
|
+
|
|
193
|
+
expect(uiControls.main.decline).toEqual({isVisible: true, isEnabled: false});
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('uiControlsComputer conference controls', () => {
|
|
198
|
+
function createConferenceTaskData(participantCount: number) {
|
|
199
|
+
const participants: Record<string, any> = {
|
|
200
|
+
'customer-1': {id: 'customer-1', pType: 'Customer', hasJoined: true, hasLeft: false},
|
|
201
|
+
'agent-1': {
|
|
202
|
+
id: 'agent-1',
|
|
203
|
+
pType: 'Agent',
|
|
204
|
+
hasJoined: true,
|
|
205
|
+
hasLeft: false,
|
|
206
|
+
consultState: 'conferencing',
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
const mainCallParticipants = ['customer-1', 'agent-1'];
|
|
210
|
+
|
|
211
|
+
if (participantCount > 1) {
|
|
212
|
+
participants['agent-2'] = {
|
|
213
|
+
id: 'agent-2',
|
|
214
|
+
pType: 'Agent',
|
|
215
|
+
hasJoined: true,
|
|
216
|
+
hasLeft: false,
|
|
217
|
+
consultState: 'conferencing',
|
|
218
|
+
};
|
|
219
|
+
mainCallParticipants.push('agent-2');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return createTaskData({
|
|
223
|
+
agentId: 'agent-1',
|
|
224
|
+
mediaResourceId: 'interaction-1',
|
|
225
|
+
consultMediaResourceId: '' as any,
|
|
226
|
+
interaction: {
|
|
227
|
+
interactionId: 'interaction-1',
|
|
228
|
+
mainInteractionId: 'interaction-1',
|
|
229
|
+
state: 'conference',
|
|
230
|
+
participants,
|
|
231
|
+
media: {
|
|
232
|
+
'interaction-1': {
|
|
233
|
+
mediaResourceId: 'interaction-1',
|
|
234
|
+
mType: 'mainCall',
|
|
235
|
+
isHold: false,
|
|
236
|
+
participants: mainCallParticipants,
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
} as any,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function createConferenceContext(participantCount: number, overrides: Partial<TaskContext> = {}): TaskContext {
|
|
244
|
+
return {
|
|
245
|
+
taskData: createConferenceTaskData(participantCount),
|
|
246
|
+
consultInitiator: true,
|
|
247
|
+
exitingConference: false,
|
|
248
|
+
consultFromConference: false,
|
|
249
|
+
transferConferenceRequested: false,
|
|
250
|
+
consultDestinationType: null,
|
|
251
|
+
consultDestinationAgentId: null,
|
|
252
|
+
consultDestinationAgentJoined: false,
|
|
253
|
+
consultCallHeld: false,
|
|
254
|
+
recordingControlsAvailable: true,
|
|
255
|
+
recordingInProgress: true,
|
|
256
|
+
uiControlConfig: {
|
|
257
|
+
isEndTaskEnabled: true,
|
|
258
|
+
isEndConsultEnabled: true,
|
|
259
|
+
channelType: TASK_CHANNEL_TYPE.VOICE,
|
|
260
|
+
isRecordingEnabled: true,
|
|
261
|
+
agentId: 'agent-1',
|
|
262
|
+
},
|
|
263
|
+
uiControls: getDefaultUIControls(),
|
|
264
|
+
...overrides,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
it('pending conference (participantCount <= 1): transfer/recording enabled, consult disabled, exitConference hidden', () => {
|
|
269
|
+
const context = createConferenceContext(1);
|
|
270
|
+
|
|
271
|
+
const uiControls = computeUIControls(TaskState.CONFERENCING, context, context.taskData);
|
|
272
|
+
|
|
273
|
+
expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: true});
|
|
274
|
+
expect(uiControls.main.recording).toEqual({isVisible: true, isEnabled: true});
|
|
275
|
+
expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: false});
|
|
276
|
+
expect(uiControls.main.exitConference).toEqual({isVisible: false, isEnabled: false});
|
|
277
|
+
expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('real conference (participantCount > 1): transfer/recording hidden, consult enabled, exitConference visible', () => {
|
|
281
|
+
const context = createConferenceContext(2);
|
|
282
|
+
|
|
283
|
+
const uiControls = computeUIControls(TaskState.CONFERENCING, context, context.taskData);
|
|
284
|
+
|
|
285
|
+
expect(uiControls.main.transfer).toEqual({isVisible: false, isEnabled: false});
|
|
286
|
+
expect(uiControls.main.recording).toEqual({isVisible: false, isEnabled: false});
|
|
287
|
+
expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: true});
|
|
288
|
+
expect(uiControls.main.exitConference).toEqual({isVisible: true, isEnabled: true});
|
|
289
|
+
expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('hold button shows visible but disabled in conference', () => {
|
|
293
|
+
const context = createConferenceContext(2);
|
|
294
|
+
|
|
295
|
+
const uiControls = computeUIControls(TaskState.CONFERENCING, context, context.taskData);
|
|
296
|
+
|
|
297
|
+
expect(uiControls.main.hold).toEqual({isVisible: true, isEnabled: false});
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe('uiControlsComputer post-call consult controls (customer left)', () => {
|
|
302
|
+
function createPostCallConsultTaskData() {
|
|
303
|
+
return createTaskData({
|
|
304
|
+
agentId: 'agent-1',
|
|
305
|
+
mediaResourceId: 'interaction-1',
|
|
306
|
+
consultMediaResourceId: 'consult-media',
|
|
307
|
+
consultingAgentId: 'agent-1',
|
|
308
|
+
destAgentId: 'agent-2',
|
|
309
|
+
interaction: {
|
|
310
|
+
interactionId: 'interaction-1',
|
|
311
|
+
mainInteractionId: 'interaction-1',
|
|
312
|
+
state: 'post_call',
|
|
313
|
+
isTerminated: false,
|
|
314
|
+
participants: {
|
|
315
|
+
'agent-1': {
|
|
316
|
+
id: 'agent-1',
|
|
317
|
+
pType: 'Agent',
|
|
318
|
+
hasJoined: true,
|
|
319
|
+
hasLeft: false,
|
|
320
|
+
consultState: 'consulting',
|
|
321
|
+
},
|
|
322
|
+
'agent-2': {
|
|
323
|
+
id: 'agent-2',
|
|
324
|
+
pType: 'Agent',
|
|
325
|
+
hasLeft: false,
|
|
326
|
+
consultState: 'consulting',
|
|
327
|
+
isConsulted: true,
|
|
328
|
+
},
|
|
329
|
+
'customer-1': {id: 'customer-1', pType: 'Customer', hasJoined: true, hasLeft: true},
|
|
330
|
+
} as any,
|
|
331
|
+
media: {
|
|
332
|
+
'interaction-1': {
|
|
333
|
+
mediaResourceId: 'interaction-1',
|
|
334
|
+
mType: 'mainCall',
|
|
335
|
+
isHold: false,
|
|
336
|
+
participants: ['agent-1'],
|
|
337
|
+
},
|
|
338
|
+
'consult-media': {
|
|
339
|
+
mediaResourceId: 'consult-media',
|
|
340
|
+
mType: 'consult',
|
|
341
|
+
isHold: false,
|
|
342
|
+
participants: ['agent-1', 'agent-2'],
|
|
343
|
+
},
|
|
344
|
+
} as any,
|
|
345
|
+
} as any,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
it('consult controls are visible but disabled when customer has left', () => {
|
|
350
|
+
const taskData = createPostCallConsultTaskData();
|
|
351
|
+
const context: TaskContext = {
|
|
352
|
+
taskData,
|
|
353
|
+
consultInitiator: true,
|
|
354
|
+
exitingConference: false,
|
|
355
|
+
consultFromConference: false,
|
|
356
|
+
transferConferenceRequested: false,
|
|
357
|
+
consultDestinationType: null,
|
|
358
|
+
consultDestinationAgentId: null,
|
|
359
|
+
consultDestinationAgentJoined: true,
|
|
360
|
+
consultCallHeld: false,
|
|
361
|
+
recordingControlsAvailable: true,
|
|
362
|
+
recordingInProgress: true,
|
|
363
|
+
uiControlConfig: {
|
|
364
|
+
isEndTaskEnabled: true,
|
|
365
|
+
isEndConsultEnabled: true,
|
|
366
|
+
channelType: TASK_CHANNEL_TYPE.VOICE,
|
|
367
|
+
isRecordingEnabled: true,
|
|
368
|
+
agentId: 'agent-1',
|
|
369
|
+
},
|
|
370
|
+
uiControls: getDefaultUIControls(),
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const uiControls = computeUIControls(TaskState.CONSULTING, context, context.taskData);
|
|
374
|
+
|
|
375
|
+
// Consult leg: transfer/conference/merge/switch should be visible but disabled
|
|
376
|
+
expect(uiControls.consult.transfer).toEqual({isVisible: true, isEnabled: false});
|
|
377
|
+
expect(uiControls.consult.conference).toEqual({isVisible: true, isEnabled: false});
|
|
378
|
+
expect(uiControls.consult.switch).toEqual({isVisible: true, isEnabled: false});
|
|
379
|
+
|
|
380
|
+
// Main leg end should be visible but disabled
|
|
381
|
+
expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
|
|
382
|
+
});
|
|
383
|
+
});
|
|
@@ -72,6 +72,30 @@ describe('Voice Task', () => {
|
|
|
72
72
|
jest.clearAllMocks();
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
+
describe('emitTaskOutdialFailed', () => {
|
|
76
|
+
it('emits the failure reason string instead of the Task object', () => {
|
|
77
|
+
const taskData = createBaseData({
|
|
78
|
+
interaction: {
|
|
79
|
+
outboundType: 'OUTDIAL',
|
|
80
|
+
} as any,
|
|
81
|
+
});
|
|
82
|
+
const voice = new Voice(dummyContact, taskData, {});
|
|
83
|
+
const emitSpy = jest.spyOn(voice, 'emit');
|
|
84
|
+
|
|
85
|
+
voice.sendStateMachineEvent({
|
|
86
|
+
type: TaskEvent.OUTBOUND_FAILED,
|
|
87
|
+
taskData,
|
|
88
|
+
reason: 'CUSTOMER_BUSY',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const outdialFailedCall = emitSpy.mock.calls.find(
|
|
92
|
+
(call) => call[0] === 'task:outdialFailed'
|
|
93
|
+
);
|
|
94
|
+
expect(outdialFailedCall).toBeDefined();
|
|
95
|
+
expect(outdialFailedCall![1]).toBe('CUSTOMER_BUSY');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
75
99
|
it('hides end and endConsult when disabled', () => {
|
|
76
100
|
const voice = new Voice(dummyContact, createBaseData(), {
|
|
77
101
|
isEndTaskEnabled: false,
|