@webex/contact-center 3.12.0-next.8 → 3.12.0-next.81

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 (205) hide show
  1. package/AGENTS.md +438 -0
  2. package/ai-docs/README.md +131 -0
  3. package/ai-docs/RULES.md +455 -0
  4. package/ai-docs/patterns/event-driven-patterns.md +485 -0
  5. package/ai-docs/patterns/testing-patterns.md +480 -0
  6. package/ai-docs/patterns/typescript-patterns.md +365 -0
  7. package/ai-docs/templates/README.md +102 -0
  8. package/ai-docs/templates/documentation/create-agents-md.md +240 -0
  9. package/ai-docs/templates/documentation/create-architecture-md.md +295 -0
  10. package/ai-docs/templates/existing-service/bug-fix.md +254 -0
  11. package/ai-docs/templates/existing-service/feature-enhancement.md +450 -0
  12. package/ai-docs/templates/new-method/00-master.md +80 -0
  13. package/ai-docs/templates/new-method/01-requirements.md +232 -0
  14. package/ai-docs/templates/new-method/02-implementation.md +295 -0
  15. package/ai-docs/templates/new-method/03-tests.md +201 -0
  16. package/ai-docs/templates/new-method/04-validation.md +141 -0
  17. package/ai-docs/templates/new-service/00-master.md +109 -0
  18. package/ai-docs/templates/new-service/01-pre-questions.md +159 -0
  19. package/ai-docs/templates/new-service/02-code-generation.md +346 -0
  20. package/ai-docs/templates/new-service/03-integration.md +178 -0
  21. package/ai-docs/templates/new-service/04-test-generation.md +205 -0
  22. package/ai-docs/templates/new-service/05-validation.md +145 -0
  23. package/dist/cc.js +265 -29
  24. package/dist/cc.js.map +1 -1
  25. package/dist/config.js +6 -0
  26. package/dist/config.js.map +1 -1
  27. package/dist/constants.js +16 -1
  28. package/dist/constants.js.map +1 -1
  29. package/dist/index.js +20 -5
  30. package/dist/index.js.map +1 -1
  31. package/dist/metrics/behavioral-events.js +101 -0
  32. package/dist/metrics/behavioral-events.js.map +1 -1
  33. package/dist/metrics/constants.js +23 -4
  34. package/dist/metrics/constants.js.map +1 -1
  35. package/dist/services/ApiAiAssistant.js +74 -3
  36. package/dist/services/ApiAiAssistant.js.map +1 -1
  37. package/dist/services/UserPreference.js +427 -0
  38. package/dist/services/UserPreference.js.map +1 -0
  39. package/dist/services/config/Util.js +3 -3
  40. package/dist/services/config/Util.js.map +1 -1
  41. package/dist/services/config/constants.js +23 -2
  42. package/dist/services/config/constants.js.map +1 -1
  43. package/dist/services/config/types.js +49 -9
  44. package/dist/services/config/types.js.map +1 -1
  45. package/dist/services/core/Err.js.map +1 -1
  46. package/dist/services/core/Utils.js +107 -32
  47. package/dist/services/core/Utils.js.map +1 -1
  48. package/dist/services/core/websocket/WebSocketManager.js +2 -1
  49. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  50. package/dist/services/core/websocket/types.js.map +1 -1
  51. package/dist/services/index.js +1 -1
  52. package/dist/services/index.js.map +1 -1
  53. package/dist/services/task/Task.js +688 -0
  54. package/dist/services/task/Task.js.map +1 -0
  55. package/dist/services/task/TaskFactory.js +45 -0
  56. package/dist/services/task/TaskFactory.js.map +1 -0
  57. package/dist/services/task/TaskManager.js +725 -526
  58. package/dist/services/task/TaskManager.js.map +1 -1
  59. package/dist/services/task/TaskUtils.js +162 -26
  60. package/dist/services/task/TaskUtils.js.map +1 -1
  61. package/dist/services/task/constants.js +9 -2
  62. package/dist/services/task/constants.js.map +1 -1
  63. package/dist/services/task/dialer.js +78 -0
  64. package/dist/services/task/dialer.js.map +1 -1
  65. package/dist/services/task/digital/Digital.js +77 -0
  66. package/dist/services/task/digital/Digital.js.map +1 -0
  67. package/dist/services/task/state-machine/TaskStateMachine.js +837 -0
  68. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -0
  69. package/dist/services/task/state-machine/actions.js +543 -0
  70. package/dist/services/task/state-machine/actions.js.map +1 -0
  71. package/dist/services/task/state-machine/constants.js +161 -0
  72. package/dist/services/task/state-machine/constants.js.map +1 -0
  73. package/dist/services/task/state-machine/guards.js +340 -0
  74. package/dist/services/task/state-machine/guards.js.map +1 -0
  75. package/dist/services/task/state-machine/index.js +53 -0
  76. package/dist/services/task/state-machine/index.js.map +1 -0
  77. package/dist/services/task/state-machine/types.js +54 -0
  78. package/dist/services/task/state-machine/types.js.map +1 -0
  79. package/dist/services/task/state-machine/uiControlsComputer.js +553 -0
  80. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -0
  81. package/dist/services/task/taskDataNormalizer.js +99 -0
  82. package/dist/services/task/taskDataNormalizer.js.map +1 -0
  83. package/dist/services/task/types.js +212 -4
  84. package/dist/services/task/types.js.map +1 -1
  85. package/dist/services/task/voice/Voice.js +1042 -0
  86. package/dist/services/task/voice/Voice.js.map +1 -0
  87. package/dist/services/task/voice/WebRTC.js +149 -0
  88. package/dist/services/task/voice/WebRTC.js.map +1 -0
  89. package/dist/types/cc.d.ts +94 -1
  90. package/dist/types/config.d.ts +6 -0
  91. package/dist/types/constants.d.ts +16 -1
  92. package/dist/types/index.d.ts +21 -6
  93. package/dist/types/metrics/constants.d.ts +19 -1
  94. package/dist/types/services/ApiAiAssistant.d.ts +11 -3
  95. package/dist/types/services/UserPreference.d.ts +118 -0
  96. package/dist/types/services/config/constants.d.ts +21 -0
  97. package/dist/types/services/config/types.d.ts +171 -10
  98. package/dist/types/services/core/Err.d.ts +4 -0
  99. package/dist/types/services/core/Utils.d.ts +33 -13
  100. package/dist/types/services/core/websocket/WebSocketManager.d.ts +1 -0
  101. package/dist/types/services/core/websocket/types.d.ts +1 -1
  102. package/dist/types/services/index.d.ts +1 -1
  103. package/dist/types/services/task/Task.d.ts +157 -0
  104. package/dist/types/services/task/TaskFactory.d.ts +12 -0
  105. package/dist/types/services/task/TaskUtils.d.ts +46 -2
  106. package/dist/types/services/task/constants.d.ts +7 -0
  107. package/dist/types/services/task/dialer.d.ts +30 -0
  108. package/dist/types/services/task/digital/Digital.d.ts +22 -0
  109. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +1144 -0
  110. package/dist/types/services/task/state-machine/actions.d.ts +10 -0
  111. package/dist/types/services/task/state-machine/constants.d.ts +107 -0
  112. package/dist/types/services/task/state-machine/guards.d.ts +90 -0
  113. package/dist/types/services/task/state-machine/index.d.ts +13 -0
  114. package/dist/types/services/task/state-machine/types.d.ts +267 -0
  115. package/dist/types/services/task/state-machine/uiControlsComputer.d.ts +9 -0
  116. package/dist/types/services/task/taskDataNormalizer.d.ts +10 -0
  117. package/dist/types/services/task/types.d.ts +603 -66
  118. package/dist/types/services/task/voice/Voice.d.ts +184 -0
  119. package/dist/types/services/task/voice/WebRTC.d.ts +53 -0
  120. package/dist/types/types.d.ts +94 -0
  121. package/dist/types/webex.d.ts +1 -0
  122. package/dist/types.js +85 -0
  123. package/dist/types.js.map +1 -1
  124. package/dist/webex.js +14 -2
  125. package/dist/webex.js.map +1 -1
  126. package/package.json +15 -12
  127. package/src/cc.ts +329 -30
  128. package/src/config.ts +6 -0
  129. package/src/constants.ts +16 -1
  130. package/src/index.ts +23 -5
  131. package/src/metrics/ai-docs/AGENTS.md +348 -0
  132. package/src/metrics/ai-docs/ARCHITECTURE.md +336 -0
  133. package/src/metrics/behavioral-events.ts +106 -0
  134. package/src/metrics/constants.ts +23 -4
  135. package/src/services/ApiAiAssistant.ts +104 -3
  136. package/src/services/UserPreference.ts +509 -0
  137. package/src/services/agent/ai-docs/AGENTS.md +238 -0
  138. package/src/services/agent/ai-docs/ARCHITECTURE.md +302 -0
  139. package/src/services/ai-docs/AGENTS.md +384 -0
  140. package/src/services/config/Util.ts +3 -3
  141. package/src/services/config/ai-docs/AGENTS.md +253 -0
  142. package/src/services/config/ai-docs/ARCHITECTURE.md +424 -0
  143. package/src/services/config/constants.ts +25 -1
  144. package/src/services/config/types.ts +174 -11
  145. package/src/services/core/Err.ts +2 -0
  146. package/src/services/core/Utils.ts +123 -37
  147. package/src/services/core/ai-docs/AGENTS.md +379 -0
  148. package/src/services/core/ai-docs/ARCHITECTURE.md +696 -0
  149. package/src/services/core/websocket/WebSocketManager.ts +2 -0
  150. package/src/services/core/websocket/types.ts +1 -1
  151. package/src/services/index.ts +1 -1
  152. package/src/services/task/Task.ts +837 -0
  153. package/src/services/task/TaskFactory.ts +55 -0
  154. package/src/services/task/TaskManager.ts +738 -613
  155. package/src/services/task/TaskUtils.ts +205 -25
  156. package/src/services/task/ai-docs/AGENTS.md +455 -0
  157. package/src/services/task/ai-docs/ARCHITECTURE.md +585 -0
  158. package/src/services/task/constants.ts +7 -0
  159. package/src/services/task/dialer.ts +80 -0
  160. package/src/services/task/digital/Digital.ts +95 -0
  161. package/src/services/task/state-machine/TaskStateMachine.ts +1077 -0
  162. package/src/services/task/state-machine/actions.ts +685 -0
  163. package/src/services/task/state-machine/ai-docs/AGENTS.md +495 -0
  164. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +1135 -0
  165. package/src/services/task/state-machine/constants.ts +172 -0
  166. package/src/services/task/state-machine/guards.ts +406 -0
  167. package/src/services/task/state-machine/index.ts +28 -0
  168. package/src/services/task/state-machine/types.ts +241 -0
  169. package/src/services/task/state-machine/uiControlsComputer.ts +867 -0
  170. package/src/services/task/taskDataNormalizer.ts +137 -0
  171. package/src/services/task/types.ts +710 -71
  172. package/src/services/task/voice/Voice.ts +1267 -0
  173. package/src/services/task/voice/WebRTC.ts +187 -0
  174. package/src/types.ts +122 -2
  175. package/src/utils/AGENTS.md +276 -0
  176. package/src/webex.js +2 -0
  177. package/test/unit/spec/cc.ts +343 -23
  178. package/test/unit/spec/logger-proxy.ts +70 -0
  179. package/test/unit/spec/services/ApiAiAssistant.ts +122 -17
  180. package/test/unit/spec/services/UserPreference.ts +401 -0
  181. package/test/unit/spec/services/WebCallingService.ts +7 -1
  182. package/test/unit/spec/services/config/index.ts +30 -30
  183. package/test/unit/spec/services/core/Utils.ts +425 -8
  184. package/test/unit/spec/services/core/websocket/WebSocketManager.ts +66 -40
  185. package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
  186. package/test/unit/spec/services/task/Task.ts +477 -0
  187. package/test/unit/spec/services/task/TaskFactory.ts +62 -0
  188. package/test/unit/spec/services/task/TaskManager.ts +832 -1702
  189. package/test/unit/spec/services/task/TaskUtils.ts +206 -0
  190. package/test/unit/spec/services/task/dialer.ts +190 -0
  191. package/test/unit/spec/services/task/digital/Digital.ts +105 -0
  192. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +1825 -0
  193. package/test/unit/spec/services/task/state-machine/guards.ts +479 -0
  194. package/test/unit/spec/services/task/state-machine/types.ts +18 -0
  195. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +2020 -0
  196. package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
  197. package/test/unit/spec/services/task/voice/Voice.ts +631 -0
  198. package/test/unit/spec/services/task/voice/WebRTC.ts +235 -0
  199. package/umd/contact-center.min.js +2 -2
  200. package/umd/contact-center.min.js.map +1 -1
  201. package/dist/services/task/index.js +0 -1525
  202. package/dist/services/task/index.js.map +0 -1
  203. package/dist/types/services/task/index.d.ts +0 -650
  204. package/src/services/task/index.ts +0 -1801
  205. package/test/unit/spec/services/task/index.ts +0 -2184
@@ -8,6 +8,12 @@ import {
8
8
  isDigitalOutbound,
9
9
  hasAgentInitiatedOutdial,
10
10
  shouldAutoAnswerTask,
11
+ getIsCustomerInCall,
12
+ getConferenceParticipantsCount,
13
+ isSecondaryAgent,
14
+ isSecondaryEpDnAgent,
15
+ getConsultMediaResourceId,
16
+ getIsConsultInProgressForConferenceControls,
11
17
  } from '../../../../../src/services/task/TaskUtils';
12
18
  import {ITask, Interaction, TaskData} from '../../../../../src/services/task/types';
13
19
  import {LoginOption} from '../../../../../src/types';
@@ -430,4 +436,204 @@ describe('TaskUtils', () => {
430
436
  });
431
437
  });
432
438
  });
439
+
440
+ // Additional coverage for conference/consult utility functions
441
+ describe('Conference Utility Functions', () => {
442
+ const interactionId = 'interaction-123';
443
+ const createInteraction = (media: any = {}, participants: any = {}) =>
444
+ ({interactionId, mainInteractionId: interactionId, media, participants}) as any;
445
+
446
+ it('getIsCustomerInCall returns true when customer active', () => {
447
+ const interaction = createInteraction(
448
+ {[interactionId]: {mType: 'mainCall', participants: ['c1']}},
449
+ {'c1': {pType: 'Customer', hasLeft: false}}
450
+ );
451
+ expect(getIsCustomerInCall(interaction, interactionId)).toBe(true);
452
+ });
453
+
454
+ it('getIsCustomerInCall returns false when participants map is missing', () => {
455
+ const interaction = createInteraction(
456
+ {[interactionId]: {mType: 'mainCall', participants: ['c1']}},
457
+ undefined
458
+ );
459
+ expect(getIsCustomerInCall(interaction, interactionId)).toBe(false);
460
+ });
461
+
462
+ it('getConferenceParticipantsCount counts active agents only', () => {
463
+ const interaction = createInteraction(
464
+ {[interactionId]: {mType: 'mainCall', participants: ['a1', 'a2', 'c1']}},
465
+ {'a1': {pType: 'Agent', hasLeft: false}, 'a2': {pType: 'Agent', hasLeft: false}, 'c1': {pType: 'Customer', hasLeft: false}}
466
+ );
467
+ expect(getConferenceParticipantsCount(interaction, interactionId)).toBe(2);
468
+ });
469
+
470
+ it('getConferenceParticipantsCount returns 0 when participants map is missing', () => {
471
+ const interaction = createInteraction(
472
+ {[interactionId]: {mType: 'mainCall', participants: ['a1', 'a2', 'c1']}},
473
+ undefined
474
+ );
475
+ expect(getConferenceParticipantsCount(interaction, interactionId)).toBe(0);
476
+ });
477
+
478
+ it('isSecondaryAgent returns true for consult with parentInteractionId', () => {
479
+ const interaction = createInteraction();
480
+ interaction.callProcessingDetails = {relationshipType: 'consult', parentInteractionId: 'parent-456'};
481
+ expect(isSecondaryAgent(interaction)).toBe(true);
482
+ });
483
+
484
+ it('isSecondaryEpDnAgent returns true for telephony secondary agent', () => {
485
+ const interaction = createInteraction();
486
+ interaction.mediaType = 'telephony';
487
+ interaction.callProcessingDetails = {relationshipType: 'consult', parentInteractionId: 'parent-456'};
488
+ expect(isSecondaryEpDnAgent(interaction)).toBe(true);
489
+ });
490
+ });
491
+
492
+ describe('getConsultMediaResourceId', () => {
493
+ it('returns consultMediaResourceId directly when provided', () => {
494
+ const result = getConsultMediaResourceId(undefined, 'consult-media-1', 'agent1');
495
+ expect(result).toBe('consult-media-1');
496
+ });
497
+
498
+ it('returns undefined when no interaction and no consultMediaResourceId', () => {
499
+ const result = getConsultMediaResourceId(undefined, undefined, 'agent1');
500
+ expect(result).toBeUndefined();
501
+ });
502
+
503
+ it('returns undefined when no agentId and no consultMediaResourceId', () => {
504
+ const interaction = {media: {}} as any;
505
+ const result = getConsultMediaResourceId(interaction, undefined, undefined);
506
+ expect(result).toBeUndefined();
507
+ });
508
+
509
+ it('finds consult media leg by mType and agent participation', () => {
510
+ const interaction = {
511
+ media: {
512
+ 'main-media': {
513
+ mediaResourceId: 'main-media',
514
+ mType: 'mainCall',
515
+ participants: ['agent1', 'customer1'],
516
+ },
517
+ 'consult-media': {
518
+ mediaResourceId: 'consult-media',
519
+ mType: 'consult',
520
+ participants: ['agent1', 'agent2'],
521
+ },
522
+ },
523
+ } as any;
524
+ const result = getConsultMediaResourceId(interaction, undefined, 'agent1');
525
+ expect(result).toBe('consult-media');
526
+ });
527
+
528
+ it('returns undefined when no consult media leg matches the agent', () => {
529
+ const interaction = {
530
+ media: {
531
+ 'main-media': {
532
+ mediaResourceId: 'main-media',
533
+ mType: 'mainCall',
534
+ participants: ['agent1', 'customer1'],
535
+ },
536
+ 'consult-media': {
537
+ mediaResourceId: 'consult-media',
538
+ mType: 'consult',
539
+ participants: ['agent2', 'agent3'],
540
+ },
541
+ },
542
+ } as any;
543
+ const result = getConsultMediaResourceId(interaction, undefined, 'agent1');
544
+ expect(result).toBeUndefined();
545
+ });
546
+
547
+ it('returns undefined when media has no consult type entries', () => {
548
+ const interaction = {
549
+ media: {
550
+ 'main-media': {
551
+ mediaResourceId: 'main-media',
552
+ mType: 'mainCall',
553
+ participants: ['agent1', 'customer1'],
554
+ },
555
+ },
556
+ } as any;
557
+ const result = getConsultMediaResourceId(interaction, undefined, 'agent1');
558
+ expect(result).toBeUndefined();
559
+ });
560
+
561
+ it('prioritizes direct consultMediaResourceId over interaction search', () => {
562
+ const interaction = {
563
+ media: {
564
+ 'consult-media': {
565
+ mediaResourceId: 'consult-media',
566
+ mType: 'consult',
567
+ participants: ['agent1', 'agent2'],
568
+ },
569
+ },
570
+ } as any;
571
+ const result = getConsultMediaResourceId(interaction, 'direct-id', 'agent1');
572
+ expect(result).toBe('direct-id');
573
+ });
574
+ });
575
+
576
+ describe('getIsConsultInProgressForConferenceControls', () => {
577
+ it('returns false when consultee is reserved but has not joined (RONA)', () => {
578
+ const interaction = {
579
+ participants: {
580
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false, isConsulted: false},
581
+ 'agent-2': {
582
+ id: 'agent-2',
583
+ pType: 'Agent',
584
+ hasLeft: false,
585
+ hasJoined: false,
586
+ isConsulted: true,
587
+ consultState: 'consultReserved',
588
+ },
589
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
590
+ },
591
+ media: {
592
+ 'interaction-1': {
593
+ mType: 'mainCall',
594
+ participants: ['customer-1', 'agent-1'],
595
+ },
596
+ 'consult-media': {
597
+ mType: 'consult',
598
+ participants: ['agent-2', 'agent-1'],
599
+ },
600
+ },
601
+ } as any;
602
+
603
+ expect(
604
+ getIsConsultInProgressForConferenceControls(interaction, 'interaction-1', 'agent-1')
605
+ ).toBe(false);
606
+ });
607
+
608
+ it('returns true when consultee is actively consulting', () => {
609
+ const interaction = {
610
+ participants: {
611
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false, isConsulted: false},
612
+ 'agent-2': {
613
+ id: 'agent-2',
614
+ pType: 'Agent',
615
+ hasLeft: false,
616
+ hasJoined: true,
617
+ isConsulted: true,
618
+ consultState: 'consulting',
619
+ },
620
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
621
+ },
622
+ media: {
623
+ 'interaction-1': {
624
+ mType: 'mainCall',
625
+ participants: ['customer-1', 'agent-1'],
626
+ },
627
+ 'consult-media': {
628
+ mType: 'consult',
629
+ participants: ['agent-2', 'agent-1'],
630
+ },
631
+ },
632
+ } as any;
633
+
634
+ expect(
635
+ getIsConsultInProgressForConferenceControls(interaction, 'interaction-1', 'agent-1')
636
+ ).toBe(true);
637
+ });
638
+ });
433
639
  });
@@ -239,5 +239,195 @@ describe('AQM routing dialer', () => {
239
239
  ).rejects.toThrow('Request Timeout');
240
240
  });
241
241
  });
242
+
243
+ describe('skipPreviewContact', () => {
244
+ it('should construct the correct URL with campaignId and interactionId', () => {
245
+ const dialer = aqmDialer(fakeAqm as any);
246
+ const config = dialer.skipPreviewContact({data: previewPayload}) as any;
247
+
248
+ expect(config.url).toBe(
249
+ `/v1/dialer/campaign/${previewPayload.campaignId}/preview-task/${previewPayload.interactionId}/skip`
250
+ );
251
+ });
252
+
253
+ it('should URL-encode campaignId when it contains reserved characters', () => {
254
+ const dialer = aqmDialer(fakeAqm as any);
255
+ const payloadWithSpecialChars = {
256
+ interactionId: 'interaction-456',
257
+ campaignId: 'My Campaign/Test #1',
258
+ };
259
+ const config = dialer.skipPreviewContact({data: payloadWithSpecialChars}) as any;
260
+
261
+ expect(config.url).toBe(
262
+ `/v1/dialer/campaign/${encodeURIComponent(
263
+ payloadWithSpecialChars.campaignId
264
+ )}/preview-task/${payloadWithSpecialChars.interactionId}/skip`
265
+ );
266
+ expect(config.url).toContain('My%20Campaign%2FTest%20%231');
267
+ });
268
+
269
+ it('should call the skipPreviewContact api', () => {
270
+ const fakeAqm = {
271
+ req: () =>
272
+ jest.fn().mockResolvedValue(() => {
273
+ Promise.resolve({data: 'skip preview success'});
274
+ }),
275
+ evt: jest.fn(),
276
+ };
277
+
278
+ const dialer = aqmDialer(fakeAqm as any);
279
+
280
+ dialer
281
+ .skipPreviewContact({data: previewPayload})
282
+ .then((response) => {
283
+ expect(response.data).toBe('skip preview success');
284
+ })
285
+ .catch(() => {
286
+ expect(true).toBe(true);
287
+ });
288
+
289
+ expect(dialer.skipPreviewContact).toHaveBeenCalled();
290
+ });
291
+
292
+ it('should handle network errors', () => {
293
+ const fakeAqm = {
294
+ req: () => jest.fn().mockRejectedValue(new Error('Network Error')),
295
+ evt: jest.fn(),
296
+ };
297
+
298
+ const dialer = aqmDialer(fakeAqm as any);
299
+
300
+ return expect(
301
+ dialer.skipPreviewContact({
302
+ data: previewPayload,
303
+ })
304
+ ).rejects.toThrow('Network Error');
305
+ });
306
+
307
+ it('should handle server errors', () => {
308
+ const fakeAqm = {
309
+ req: () => jest.fn().mockRejectedValue(new Error('Server Error')),
310
+ evt: jest.fn(),
311
+ };
312
+
313
+ const dialer = aqmDialer(fakeAqm as any);
314
+
315
+ return expect(
316
+ dialer.skipPreviewContact({
317
+ data: previewPayload,
318
+ })
319
+ ).rejects.toThrow('Server Error');
320
+ });
321
+
322
+ it('should handle timeout scenarios', () => {
323
+ const fakeAqm = {
324
+ req: () => jest.fn().mockRejectedValue(new Error('Request Timeout')),
325
+ evt: jest.fn(),
326
+ };
327
+
328
+ const dialer = aqmDialer(fakeAqm as any);
329
+
330
+ return expect(
331
+ dialer.skipPreviewContact({
332
+ data: previewPayload,
333
+ })
334
+ ).rejects.toThrow('Request Timeout');
335
+ });
336
+ });
337
+
338
+ describe('removePreviewContact', () => {
339
+ it('should construct the correct URL with campaignId and interactionId', () => {
340
+ const dialer = aqmDialer(fakeAqm as any);
341
+ const config = dialer.removePreviewContact({data: previewPayload}) as any;
342
+
343
+ expect(config.url).toBe(
344
+ `/v1/dialer/campaign/${previewPayload.campaignId}/preview-task/${previewPayload.interactionId}/remove`
345
+ );
346
+ });
347
+
348
+ it('should URL-encode campaignId when it contains reserved characters', () => {
349
+ const dialer = aqmDialer(fakeAqm as any);
350
+ const payloadWithSpecialChars = {
351
+ interactionId: 'interaction-456',
352
+ campaignId: 'My Campaign/Test #1',
353
+ };
354
+ const config = dialer.removePreviewContact({data: payloadWithSpecialChars}) as any;
355
+
356
+ expect(config.url).toBe(
357
+ `/v1/dialer/campaign/${encodeURIComponent(
358
+ payloadWithSpecialChars.campaignId
359
+ )}/preview-task/${payloadWithSpecialChars.interactionId}/remove`
360
+ );
361
+ expect(config.url).toContain('My%20Campaign%2FTest%20%231');
362
+ });
363
+
364
+ it('should call the removePreviewContact api', () => {
365
+ const fakeAqm = {
366
+ req: () =>
367
+ jest.fn().mockResolvedValue(() => {
368
+ Promise.resolve({data: 'remove preview success'});
369
+ }),
370
+ evt: jest.fn(),
371
+ };
372
+
373
+ const dialer = aqmDialer(fakeAqm as any);
374
+
375
+ dialer
376
+ .removePreviewContact({data: previewPayload})
377
+ .then((response) => {
378
+ expect(response.data).toBe('remove preview success');
379
+ })
380
+ .catch(() => {
381
+ expect(true).toBe(true);
382
+ });
383
+
384
+ expect(dialer.removePreviewContact).toHaveBeenCalled();
385
+ });
386
+
387
+ it('should handle network errors', () => {
388
+ const fakeAqm = {
389
+ req: () => jest.fn().mockRejectedValue(new Error('Network Error')),
390
+ evt: jest.fn(),
391
+ };
392
+
393
+ const dialer = aqmDialer(fakeAqm as any);
394
+
395
+ return expect(
396
+ dialer.removePreviewContact({
397
+ data: previewPayload,
398
+ })
399
+ ).rejects.toThrow('Network Error');
400
+ });
401
+
402
+ it('should handle server errors', () => {
403
+ const fakeAqm = {
404
+ req: () => jest.fn().mockRejectedValue(new Error('Server Error')),
405
+ evt: jest.fn(),
406
+ };
407
+
408
+ const dialer = aqmDialer(fakeAqm as any);
409
+
410
+ return expect(
411
+ dialer.removePreviewContact({
412
+ data: previewPayload,
413
+ })
414
+ ).rejects.toThrow('Server Error');
415
+ });
416
+
417
+ it('should handle timeout scenarios', () => {
418
+ const fakeAqm = {
419
+ req: () => jest.fn().mockRejectedValue(new Error('Request Timeout')),
420
+ evt: jest.fn(),
421
+ };
422
+
423
+ const dialer = aqmDialer(fakeAqm as any);
424
+
425
+ return expect(
426
+ dialer.removePreviewContact({
427
+ data: previewPayload,
428
+ })
429
+ ).rejects.toThrow('Request Timeout');
430
+ });
431
+ });
242
432
  });
243
433
  });
@@ -0,0 +1,105 @@
1
+ import Digital from '../../../../../../src/services/task/digital/Digital';
2
+ import {MEDIA_CHANNEL, TaskData, TaskResponse} from '../../../../../../src/services/task/types';
3
+ import {TaskEvent, TaskEventPayload} from '../../../../../../src/services/task/state-machine';
4
+
5
+ jest.mock('../../../../../../src/services/core/WebexRequest', () => ({
6
+ __esModule: true,
7
+ default: {
8
+ getInstance: () => ({ uploadLogs: jest.fn() }),
9
+ },
10
+ }));
11
+
12
+ const sendStateEvents = (task: Digital, events: TaskEventPayload[]) => {
13
+ events.forEach((event) => {
14
+ if (!event) {
15
+ throw new Error('Task event payload is required');
16
+ }
17
+ task.stateMachineService?.send(event);
18
+ });
19
+ };
20
+
21
+ describe('Digital Task', () => {
22
+ const dummyData = {
23
+ interactionId: 'dig1',
24
+ interaction: {isTerminated: false, mediaType: MEDIA_CHANNEL.CHAT},
25
+ } as TaskData;
26
+ let dummyContact: { accept: jest.Mock<Promise<TaskResponse>> };
27
+
28
+ beforeEach(() => {
29
+ dummyContact = {
30
+ accept: jest.fn().mockResolvedValue({ status: 'ok' }),
31
+ };
32
+ });
33
+
34
+ it('accept() calls contact.accept with interactionId', async () => {
35
+ const task = new Digital(dummyContact, dummyData);
36
+ const res = await task.accept();
37
+ expect(dummyContact.accept).toHaveBeenCalledWith({ interactionId: 'dig1' });
38
+ expect(res).toEqual({ status: 'ok' });
39
+ });
40
+
41
+ it('accept() throws an error when contact.accept rejects', async () => {
42
+ const error = new Error('Error while performing accept');
43
+ (dummyContact.accept as jest.Mock).mockRejectedValue(error);
44
+ const task = new Digital(dummyContact, dummyData);
45
+ await expect(task.accept()).rejects.toThrow('Error while performing accept');
46
+ });
47
+
48
+ it('constructor shows accept when offered', () => {
49
+ const task = new Digital(dummyContact, dummyData);
50
+ sendStateEvents(task, [{type: TaskEvent.TASK_INCOMING, taskData: dummyData}]);
51
+ expect(task.uiControls.main.accept.isVisible).toBe(true);
52
+ expect(task.uiControls.main.accept.isEnabled).toBe(true);
53
+ });
54
+
55
+ describe('UI controls derived from state machine events', () => {
56
+ it('connected state shows transfer and end', () => {
57
+ const task = new Digital(dummyContact, dummyData);
58
+ sendStateEvents(task, [
59
+ {type: TaskEvent.TASK_INCOMING, taskData: dummyData},
60
+ {type: TaskEvent.ASSIGN, taskData: dummyData},
61
+ ]);
62
+ expect(task.uiControls.main.accept.isVisible).toBe(false);
63
+ expect(task.uiControls.main.transfer.isVisible).toBe(true);
64
+ expect(task.uiControls.main.end.isVisible).toBe(true);
65
+ expect(task.uiControls.main.wrapup.isVisible).toBe(false);
66
+ });
67
+
68
+ it('wrapup state hides transfer/end and shows wrapup button', () => {
69
+ const task = new Digital(dummyContact, dummyData);
70
+ sendStateEvents(task, [
71
+ {type: TaskEvent.TASK_INCOMING, taskData: dummyData},
72
+ {type: TaskEvent.ASSIGN, taskData: dummyData},
73
+ {type: TaskEvent.TASK_WRAPUP},
74
+ ]);
75
+ expect(task.uiControls.main.transfer.isVisible).toBe(false);
76
+ expect(task.uiControls.main.end.isVisible).toBe(false);
77
+ expect(task.uiControls.main.wrapup.isVisible).toBe(true);
78
+ });
79
+
80
+ it('terminated interaction toggles wrapup visibility even before END event', () => {
81
+ const task = new Digital(dummyContact, dummyData);
82
+ const terminatedData = {
83
+ ...dummyData,
84
+ interaction: {...(dummyData.interaction as any), isTerminated: true},
85
+ } as TaskData;
86
+ task.updateTaskData(terminatedData);
87
+ sendStateEvents(task, [
88
+ {type: TaskEvent.TASK_INCOMING, taskData: dummyData},
89
+ {type: TaskEvent.ASSIGN, taskData: terminatedData},
90
+ ]);
91
+ expect(task.uiControls.main.wrapup.isVisible).toBe(true);
92
+ });
93
+
94
+ it('rona hides accept controls', () => {
95
+ const task = new Digital(dummyContact, dummyData);
96
+ sendStateEvents(task, [
97
+ {type: TaskEvent.TASK_INCOMING, taskData: dummyData},
98
+ {type: TaskEvent.RONA},
99
+ ]);
100
+ expect(task.uiControls.main.accept.isVisible).toBe(false);
101
+ expect(task.uiControls.main.transfer.isVisible).toBe(false);
102
+ expect(task.uiControls.main.end.isVisible).toBe(false);
103
+ });
104
+ });
105
+ });