@webex/contact-center 3.12.0-task-refactor.6 → 3.12.0-task-refactor.8

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 (26) hide show
  1. package/dist/services/task/Task.js +32 -0
  2. package/dist/services/task/Task.js.map +1 -1
  3. package/dist/services/task/TaskUtils.js +3 -1
  4. package/dist/services/task/TaskUtils.js.map +1 -1
  5. package/dist/services/task/state-machine/TaskStateMachine.js +77 -1
  6. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  7. package/dist/services/task/state-machine/actions.js +113 -23
  8. package/dist/services/task/state-machine/actions.js.map +1 -1
  9. package/dist/services/task/state-machine/uiControlsComputer.js +101 -20
  10. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
  11. package/dist/types/services/task/Task.d.ts +10 -0
  12. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +110 -0
  13. package/dist/types/services/task/state-machine/actions.d.ts +2 -0
  14. package/dist/webex.js +1 -1
  15. package/package.json +1 -1
  16. package/src/services/task/Task.ts +34 -0
  17. package/src/services/task/TaskUtils.ts +5 -3
  18. package/src/services/task/state-machine/TaskStateMachine.ts +105 -1
  19. package/src/services/task/state-machine/actions.ts +151 -25
  20. package/src/services/task/state-machine/uiControlsComputer.ts +185 -29
  21. package/test/unit/spec/services/task/Task.ts +61 -0
  22. package/test/unit/spec/services/task/TaskUtils.ts +65 -0
  23. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +676 -0
  24. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +662 -1
  25. package/umd/contact-center.min.js +2 -2
  26. package/umd/contact-center.min.js.map +1 -1
@@ -66,6 +66,20 @@ export function getDefaultUIControls(): TaskUIControls {
66
66
  );
67
67
  }
68
68
 
69
+ /** Consult media must exist on the interaction payload, not only as a stale resource id. */
70
+ function hasConsultMediaInInteraction(
71
+ interaction: TaskData['interaction'] | undefined,
72
+ consultMediaResourceId?: string
73
+ ): boolean {
74
+ const media = interaction?.media ?? {};
75
+ const hasConsultLeg = Object.values(media).some((entry: any) => entry?.mType === 'consult');
76
+
77
+ if (hasConsultLeg) return true;
78
+ if (!consultMediaResourceId) return false;
79
+
80
+ return Boolean(media[consultMediaResourceId]);
81
+ }
82
+
69
83
  function computeVoiceInteractionUIControls(
70
84
  state: TaskState,
71
85
  context: TaskContext,
@@ -78,8 +92,8 @@ function computeVoiceInteractionUIControls(
78
92
  return getDefaultInteractionUIControls();
79
93
  }
80
94
 
81
- // Essential data
82
- const taskData = context.taskData ?? fallbackTaskData ?? null;
95
+ // Prefer live task.data (fallback) over stale state-machine snapshot during multi-login sync.
96
+ const taskData = fallbackTaskData ?? context.taskData ?? null;
83
97
  const interaction = taskData?.interaction;
84
98
  const mainCallId = interaction?.mainInteractionId || taskData?.interactionId;
85
99
  const isWebrtc = config.voiceVariant === VOICE_VARIANT.WEBRTC;
@@ -125,7 +139,6 @@ function computeVoiceInteractionUIControls(
125
139
  // Backend sends destinationType as 'EP-DN'; SDK method uses 'entryPoint' — check both.
126
140
  const isEpDnConsult =
127
141
  consultDestinationType === 'entryPoint' || consultDestinationType === ('EP-DN' as any);
128
- const isConsultDestinationReady = consultDestinationAgentJoined || isEpDnConsult;
129
142
 
130
143
  const stateImpliesHeld = state === TaskState.HELD || state === TaskState.RESUME_INITIATING;
131
144
  const stateImpliesConnected =
@@ -151,9 +164,32 @@ function computeVoiceInteractionUIControls(
151
164
  const selfParticipant = selfAgentId ? interaction?.participants?.[selfAgentId] : null;
152
165
  const selfInConsultCall =
153
166
  Boolean(selfAgentId) && Boolean(consultMedia?.participants?.includes(selfAgentId as string));
154
- const conferenceActive = isConferencing || conferenceFromBackend || consultFromConference;
167
+ const hasConsultMedia = hasConsultMediaInInteraction(
168
+ interaction,
169
+ taskData?.consultMediaResourceId
170
+ );
171
+ const isConsultEndedForSelf =
172
+ taskData?.type === 'AgentConsultEnded' ||
173
+ taskData?.type === 'AgentConsultFailed' ||
174
+ (selfParticipant?.consultState === 'consultCompleted' &&
175
+ !hasConsultMedia &&
176
+ taskData?.isConsulted === false);
177
+ const effectiveConsultInitiator = isConsultEndedForSelf ? false : consultInitiator;
178
+ const effectiveConsultCallHeld = isConsultEndedForSelf ? false : consultCallHeld;
179
+ const effectiveConsultFromConference = isConsultEndedForSelf ? false : consultFromConference;
180
+ const effectiveConsultDestinationAgentJoined = isConsultEndedForSelf
181
+ ? false
182
+ : consultDestinationAgentJoined;
183
+ // After a consult ends for self, the backend stops sending consult media but the merged
184
+ // task.data can still carry the stale consult-media entry (reconcileData never deletes keys).
185
+ // Treat it as gone so post-consult main-leg controls (consult retry) are not blocked.
186
+ const effectiveHasConsultMedia = isConsultEndedForSelf ? false : hasConsultMedia;
187
+ const isConsultDestinationReady = effectiveConsultDestinationAgentJoined || isEpDnConsult;
188
+ const conferenceActive =
189
+ isConferencing || conferenceFromBackend || effectiveConsultFromConference;
155
190
  // Treat consult initiator as "in conference" even if mainCall participant list lags while consulting.
156
- const inConference = conferenceActive && (isConferencing || selfInMainCall || consultInitiator);
191
+ const inConference =
192
+ conferenceActive && (isConferencing || selfInMainCall || effectiveConsultInitiator);
157
193
 
158
194
  // Check if this is a consulted agent (must be after isConsulting is computed).
159
195
  const isSoleAgentOnCall =
@@ -162,7 +198,7 @@ function computeVoiceInteractionUIControls(
162
198
  inConference || isSoleAgentOnCall
163
199
  ? false
164
200
  : getIsConsultedAgentForControls(taskData, context, isConsulting) ||
165
- (!consultInitiator &&
201
+ (!effectiveConsultInitiator &&
166
202
  (selfParticipant?.isConsulted === true ||
167
203
  selfParticipant?.consultState === 'consulting'));
168
204
 
@@ -177,13 +213,10 @@ function computeVoiceInteractionUIControls(
177
213
 
178
214
  // Consulted agents have limited controls until they're in conference or wrapup
179
215
  // Use inConference (not isConferencing) so controls remain enabled after state downgrade
180
- const hasFullControls = !isConsulted || consultInitiator || inConference || isWrappingUp;
216
+ const hasFullControls = !isConsulted || effectiveConsultInitiator || inConference || isWrappingUp;
181
217
  const consultOwnedBySelf =
182
- consultInitiator || (Boolean(selfAgentId) && taskData?.consultingAgentId === selfAgentId);
183
- const hasConsultMedia = Boolean(
184
- taskData?.consultMediaResourceId ||
185
- Object.values(interaction?.media ?? {}).some((media: any) => media?.mType === 'consult')
186
- );
218
+ effectiveConsultInitiator ||
219
+ (Boolean(selfAgentId) && taskData?.consultingAgentId === selfAgentId);
187
220
  const selfConsultPendingOnConsultMedia =
188
221
  selfParticipant?.consultState === 'consultInitiated' &&
189
222
  !taskData?.isConsulted &&
@@ -207,17 +240,18 @@ function computeVoiceInteractionUIControls(
207
240
  })
208
241
  );
209
242
  const isHydratedConferenceConsultPending =
210
- inConference && selfConsultPendingOnConsultMedia && !consultDestinationAgentJoined;
243
+ inConference && selfConsultPendingOnConsultMedia && !effectiveConsultDestinationAgentJoined;
211
244
  const hasParallelConsultLeg =
245
+ !isConsultEndedForSelf &&
212
246
  consultOwnedBySelf &&
213
247
  !isConsulting &&
214
248
  !isConsulted &&
215
- (consultInProgress || consultCallHeld || hasConsultMedia);
216
- const activeLegForConferenceConsult = consultCallHeld ? 'main' : 'consult';
249
+ (consultInProgress || effectiveConsultCallHeld || hasConsultMedia);
250
+ const activeLegForConferenceConsult = effectiveConsultCallHeld ? 'main' : 'consult';
217
251
  const isCurrentLegActive = currentLeg === activeLegForConferenceConsult;
218
252
  const isConferenceConsultTransferContext =
219
- inConference && consultInitiator && hasConsultMedia && isConsultDestinationReady;
220
- const consultLegOnHold = isConsulting && consultCallHeld;
253
+ inConference && effectiveConsultInitiator && hasConsultMedia && isConsultDestinationReady;
254
+ const consultLegOnHold = isConsulting && effectiveConsultCallHeld;
221
255
  const callProcessingDetails = interaction?.callProcessingDetails as
222
256
  | {conferenceHoldParticipant?: boolean | string}
223
257
  | undefined;
@@ -259,6 +293,17 @@ function computeVoiceInteractionUIControls(
259
293
  (isConsulting ||
260
294
  taskData?.type === 'AgentConsulting' ||
261
295
  selfParticipant?.consultState === 'consulting');
296
+ const hideExitConferenceOnMainLegForEpDnConsultFromConference =
297
+ currentLeg === 'main' &&
298
+ inConference &&
299
+ consultFromConference &&
300
+ consultInitiator &&
301
+ (isConsulting ||
302
+ consultInProgress ||
303
+ taskData?.type === 'AgentConsultCreated' ||
304
+ taskData?.type === 'AgentConsulting' ||
305
+ selfParticipant?.consultState === 'consultInitiated' ||
306
+ selfParticipant?.consultState === 'consulting');
262
307
  const forceHeldPostConsultControls =
263
308
  !hideExitConferenceWhileConsultPending &&
264
309
  (postDeclineHeldMainLeg || postConsultCompletedHeldMainLeg);
@@ -269,6 +314,61 @@ function computeVoiceInteractionUIControls(
269
314
  currentLeg === 'main' && inConference && consultInProgress && !selfOnConsultLeg;
270
315
  const allowHeldMainLegControlsForNonInitiator =
271
316
  showMainLegConferenceControlsDuringConsult && !isHydratedConferenceConsultPending;
317
+ const isConsultRequestedPhase =
318
+ isConsultPendingBeforeJoin &&
319
+ !isConsulted &&
320
+ (consultInitiator ||
321
+ (Boolean(selfAgentId) &&
322
+ selfParticipant?.consultState === 'consultInitiated' &&
323
+ taskData?.agentId === selfAgentId));
324
+ const isConsultUnansweredFailure =
325
+ currentLeg === 'main' &&
326
+ !inConference &&
327
+ !taskData?.isConsulted &&
328
+ !effectiveHasConsultMedia &&
329
+ isHeld &&
330
+ selfParticipant?.consultState === 'consultCompleted' &&
331
+ (taskData?.type === 'AgentConsultFailed' ||
332
+ taskData?.type === 'AgentConsultEnded' ||
333
+ isConsultEndedForSelf);
334
+
335
+ if (isConsultUnansweredFailure) {
336
+ const recordingControl =
337
+ recordingControlsAvailable && config.isRecordingEnabled && !inConference
338
+ ? VISIBLE_ENABLED
339
+ : DISABLED;
340
+
341
+ return {
342
+ ...getDefaultInteractionUIControls(),
343
+ hold: VISIBLE_ENABLED,
344
+ transfer: VISIBLE_ENABLED,
345
+ consult: VISIBLE_ENABLED,
346
+ recording: recordingControl,
347
+ end: VISIBLE_DISABLED,
348
+ };
349
+ }
350
+
351
+ if (isConsultRequestedPhase) {
352
+ if (currentLeg === 'main') {
353
+ return {
354
+ ...getDefaultInteractionUIControls(),
355
+ transfer: VISIBLE_DISABLED,
356
+ conference: VISIBLE_DISABLED,
357
+ end: VISIBLE_DISABLED,
358
+ };
359
+ }
360
+
361
+ if (currentLeg === 'consult') {
362
+ return {
363
+ ...getDefaultInteractionUIControls(),
364
+ endConsult: VISIBLE_ENABLED,
365
+ switch: VISIBLE_DISABLED,
366
+ transfer: VISIBLE_DISABLED,
367
+ transferConference: inConference ? VISIBLE_DISABLED : DISABLED,
368
+ mergeToConference: VISIBLE_DISABLED,
369
+ };
370
+ }
371
+ }
272
372
 
273
373
  return {
274
374
  // Accept/Decline: Voice tasks in offered state
@@ -289,7 +389,10 @@ function computeVoiceInteractionUIControls(
289
389
  hold: (() => {
290
390
  if (!hasFullControls) return DISABLED;
291
391
  if (forceHeldPostConsultControls) return VISIBLE_ENABLED;
292
- if (consultOwnedBySelf && (isConsulting || hasParallelConsultLeg || consultCallHeld)) {
392
+ if (
393
+ consultOwnedBySelf &&
394
+ (isConsulting || hasParallelConsultLeg || effectiveConsultCallHeld)
395
+ ) {
293
396
  return DISABLED;
294
397
  }
295
398
  if (hasParallelConsultLeg) return DISABLED;
@@ -308,6 +411,17 @@ function computeVoiceInteractionUIControls(
308
411
  if (!isWebrtc) return DISABLED;
309
412
  if (isWrappingUp) return DISABLED;
310
413
  if (currentLeg === 'consult' && !selfInConsultCall) return DISABLED;
414
+ // The consulted agent has no separate consult leg; their main leg is the active call, so
415
+ // mute stays available while they consult (the heuristic below targets the initiator's
416
+ // inactive leg, not the consultee's only leg).
417
+ const isConsultedActiveMainLeg = isConsulted && currentLeg === 'main';
418
+ if (
419
+ (isConsulting || hasParallelConsultLeg) &&
420
+ !isCurrentLegActive &&
421
+ !isConsultedActiveMainLeg
422
+ ) {
423
+ return VISIBLE_DISABLED;
424
+ }
311
425
  if (isConsulting) return VISIBLE_ENABLED;
312
426
 
313
427
  if (isConnected || isHeld || isConferencing) {
@@ -383,6 +497,18 @@ function computeVoiceInteractionUIControls(
383
497
  consult: (() => {
384
498
  const isConnectedOrHeld = state === TaskState.CONNECTED || state === TaskState.HELD;
385
499
 
500
+ if (
501
+ isHeld &&
502
+ !inConference &&
503
+ !effectiveHasConsultMedia &&
504
+ selfParticipant?.consultState === 'consultCompleted' &&
505
+ !taskData?.isConsulted
506
+ ) {
507
+ const canRetryConsult = !maxParticipants && customerPresent && !otherAgentConsultInProgress;
508
+
509
+ return {isVisible: true, isEnabled: canRetryConsult};
510
+ }
511
+
386
512
  if (inConference && nonOwnerPostConsultCompletedHeldMainLeg) return VISIBLE_DISABLED;
387
513
  if (hasParallelConsultLeg) return DISABLED;
388
514
  if (!hasFullControls || !(isConnectedOrHeld || inConference)) {
@@ -469,6 +595,7 @@ function computeVoiceInteractionUIControls(
469
595
  // ExitConference: in conference with multiple agents in main call
470
596
  exitConference: (() => {
471
597
  if (hideExitConferenceWhileConsultPending) return DISABLED;
598
+ if (hideExitConferenceOnMainLegForEpDnConsultFromConference) return DISABLED;
472
599
  if (allowHeldMainLegControlsForNonInitiator) return VISIBLE_ENABLED;
473
600
  if (showMainLegConferenceControlsDuringConsult) return VISIBLE_DISABLED;
474
601
  if (hideExitConferenceDuringActiveConsultFromConference) return DISABLED;
@@ -578,10 +705,21 @@ function getVoiceLegState(
578
705
  };
579
706
  }
580
707
 
581
- const taskData = context.taskData ?? fallbackTaskData ?? null;
708
+ const taskData = fallbackTaskData ?? context.taskData ?? null;
582
709
  const interaction = taskData?.interaction;
583
710
  const mainCallId = interaction?.mainInteractionId || taskData?.interactionId;
584
711
  const selfAgentId = config.agentId ?? taskData?.agentId;
712
+ const selfParticipant = selfAgentId ? interaction?.participants?.[selfAgentId] : null;
713
+ const hasConsultMedia = hasConsultMediaInInteraction(
714
+ interaction,
715
+ taskData?.consultMediaResourceId
716
+ );
717
+ const isConsultEndedForSelf =
718
+ taskData?.type === 'AgentConsultEnded' ||
719
+ taskData?.type === 'AgentConsultFailed' ||
720
+ (selfParticipant?.consultState === 'consultCompleted' &&
721
+ !hasConsultMedia &&
722
+ taskData?.isConsulted === false);
585
723
  const consultInProgress = getIsConsultInProgressForConferenceControls(
586
724
  interaction,
587
725
  mainCallId,
@@ -594,30 +732,48 @@ function getVoiceLegState(
594
732
  const consultOwnedBySelf =
595
733
  context.consultInitiator ||
596
734
  (Boolean(selfAgentId) && taskData?.consultingAgentId === selfAgentId);
597
- const hasConsultMedia = Boolean(
598
- taskData?.consultMediaResourceId ||
599
- Object.values(interaction?.media ?? {}).some((media: any) => media?.mType === 'consult')
600
- );
601
- const selfParticipant = selfAgentId ? interaction?.participants?.[selfAgentId] : null;
602
735
  const selfConsultPendingOnConsultMedia =
603
736
  selfParticipant?.consultState === 'consultInitiated' &&
604
737
  !taskData?.isConsulted &&
605
738
  hasConsultMedia;
606
739
  const selfConsultingOnConsultMedia =
607
740
  selfParticipant?.consultState === 'consulting' && hasConsultMedia;
741
+ const isConsultUnansweredFailure =
742
+ !taskData?.isConsulted &&
743
+ !hasConsultMedia &&
744
+ selfParticipant?.consultState === 'consultCompleted' &&
745
+ Boolean(mainCallId && taskData?.interaction?.media?.[mainCallId]?.isHold === true) &&
746
+ taskData?.interaction?.state !== 'conference' &&
747
+ !getIsConferenceInProgress(taskData);
608
748
  const hasConsultLeg = Boolean(
609
- !interaction?.isTerminated &&
610
- ((consultOwnedBySelf && !taskData?.isConsulted) ||
611
- selfConsultingOnConsultMedia ||
612
- selfConsultPendingOnConsultMedia) &&
749
+ !isConsultEndedForSelf &&
750
+ !interaction?.isTerminated &&
751
+ !isConsultUnansweredFailure &&
752
+ (consultOwnedBySelf || selfConsultingOnConsultMedia || selfConsultPendingOnConsultMedia) &&
613
753
  (consultInProgress || isConsultingState || context.consultCallHeld || hasConsultMedia)
614
754
  );
615
755
 
616
756
  if (!hasConsultLeg) {
757
+ let mainState = currentState;
758
+ if (isConsultEndedForSelf && taskData?.interaction) {
759
+ const resolvedMainId = taskData.interaction.mainInteractionId || taskData.interactionId;
760
+ const isMainHeld = Boolean(
761
+ resolvedMainId && taskData.interaction.media?.[resolvedMainId]?.isHold === true
762
+ );
763
+
764
+ if (taskData.interaction.state === 'conference' || getIsConferenceInProgress(taskData)) {
765
+ mainState = TaskState.CONFERENCING;
766
+ } else if (isMainHeld || taskData.interaction.state === 'hold') {
767
+ mainState = TaskState.HELD;
768
+ } else {
769
+ mainState = TaskState.CONNECTED;
770
+ }
771
+ }
772
+
617
773
  return {
618
774
  hasConsultLeg: false,
619
775
  activeLeg: 'main',
620
- mainState: currentState,
776
+ mainState,
621
777
  consultState: TaskState.CONSULTING,
622
778
  };
623
779
  }
@@ -89,6 +89,67 @@ describe('Task (base class)', () => {
89
89
  expect((task.data as any).foo).toBeUndefined();
90
90
  });
91
91
 
92
+ it('drops stale interaction.media/participants entries the backend no longer sends (merge keeps other fields)', () => {
93
+ const withConsult = createTaskData({
94
+ interaction: {
95
+ callAssociatedData: {a: {value: 'keep-me'}},
96
+ media: {
97
+ main: {mediaResourceId: 'main', mType: 'mainCall', isHold: true},
98
+ consult: {mediaResourceId: 'consult', mType: 'consult', isHold: false},
99
+ },
100
+ participants: {
101
+ 'customer-1': {id: 'customer-1', pType: 'Customer'},
102
+ 'agent-1': {id: 'agent-1', pType: 'Agent'},
103
+ 'agent-2': {id: 'agent-2', pType: 'Agent', consultState: 'consulting'},
104
+ },
105
+ } as any,
106
+ }) as unknown as TaskData;
107
+ task.updateTaskData(withConsult, true);
108
+
109
+ // Backend resume snapshot: consult leg gone (only main media + main participants).
110
+ const resumeData = createTaskData({
111
+ interaction: {
112
+ media: {main: {mediaResourceId: 'main', mType: 'mainCall', isHold: false}},
113
+ participants: {
114
+ 'customer-1': {id: 'customer-1', pType: 'Customer'},
115
+ 'agent-1': {id: 'agent-1', pType: 'Agent', consultState: null},
116
+ },
117
+ } as any,
118
+ }) as unknown as TaskData;
119
+ task.updateTaskData(resumeData);
120
+
121
+ const interaction = (task.data as any).interaction;
122
+ // Stale consult media + consultee participant (absent from the resume snapshot) are removed.
123
+ expect(interaction.media.consult).toBeUndefined();
124
+ expect(interaction.participants['agent-2']).toBeUndefined();
125
+ // Entries still present in the incoming snapshot survive and reflect the new values.
126
+ expect(interaction.media.main).toBeDefined();
127
+ expect(interaction.media.main.isHold).toBe(false);
128
+ expect(interaction.participants['agent-1']).toBeDefined();
129
+ expect(interaction.participants['customer-1']).toBeDefined();
130
+ // Unrelated merge-able fields (CAD) are preserved.
131
+ expect(interaction.callAssociatedData.a.value).toBe('keep-me');
132
+ });
133
+
134
+ it('does not touch interaction maps when the incoming payload omits them', () => {
135
+ const withConsult = createTaskData({
136
+ interaction: {
137
+ media: {main: {mediaResourceId: 'main'}, consult: {mediaResourceId: 'consult'}},
138
+ participants: {'agent-1': {id: 'agent-1'}, 'agent-2': {id: 'agent-2'}},
139
+ } as any,
140
+ }) as unknown as TaskData;
141
+ task.updateTaskData(withConsult, true);
142
+
143
+ // A partial update with no interaction maps must not prune existing media/participants.
144
+ task.updateTaskData({foo: 'changed'} as unknown as TaskData);
145
+
146
+ const interaction = (task.data as any).interaction;
147
+ expect(interaction.media.main).toBeDefined();
148
+ expect(interaction.media.consult).toBeDefined();
149
+ expect(interaction.participants['agent-1']).toBeDefined();
150
+ expect(interaction.participants['agent-2']).toBeDefined();
151
+ });
152
+
92
153
  it('getUIControls returns default controls shape for idle voice task', () => {
93
154
  const controls = task.uiControls;
94
155
  const mainControls = controls.main;
@@ -13,6 +13,7 @@ import {
13
13
  isSecondaryAgent,
14
14
  isSecondaryEpDnAgent,
15
15
  getConsultMediaResourceId,
16
+ getIsConsultInProgressForConferenceControls,
16
17
  } from '../../../../../src/services/task/TaskUtils';
17
18
  import {ITask, Interaction, TaskData} from '../../../../../src/services/task/types';
18
19
  import {LoginOption} from '../../../../../src/types';
@@ -571,4 +572,68 @@ describe('TaskUtils', () => {
571
572
  expect(result).toBe('direct-id');
572
573
  });
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
+ });
574
639
  });