diff --git a/packages/@webex/contact-center/src/services/task/TaskManager.ts b/packages/@webex/contact-center/src/services/task/TaskManager.ts index f9c3ff3002a..5885ebeb83b 100644 --- a/packages/@webex/contact-center/src/services/task/TaskManager.ts +++ b/packages/@webex/contact-center/src/services/task/TaskManager.ts @@ -375,6 +375,12 @@ export default class TaskManager extends EventEmitter { task.sendStateMachineEvent(stateMachineEvent); } + // Emit TASK_POST_CALL_ACTIVITY for ParticipantPostCallActivity events so + // consumers (Widgets) can detect the interaction state change to post_call. + if (eventContext.eventType === CC_EVENTS.PARTICIPANT_POST_CALL_ACTIVITY) { + task.emit(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, task); + } + // Send transcript start/stop events for relevant CC events this.requestRealTimeTranscripts(eventContext.eventType, payload.interactionId); }); @@ -655,8 +661,7 @@ export default class TaskManager extends EventEmitter { const {payload} = context; let task = context.task; - if (payload.childInteractionId) { - // remove the child task from collection + if (payload.childInteractionId && this.taskCollection[payload.childInteractionId]) { this.removeTaskFromCollection(this.taskCollection[payload.childInteractionId]); } diff --git a/packages/@webex/contact-center/src/services/task/state-machine/TaskStateMachine.ts b/packages/@webex/contact-center/src/services/task/state-machine/TaskStateMachine.ts index 94e6a2fa47a..4ef7a1f23d0 100644 --- a/packages/@webex/contact-center/src/services/task/state-machine/TaskStateMachine.ts +++ b/packages/@webex/contact-center/src/services/task/state-machine/TaskStateMachine.ts @@ -12,7 +12,7 @@ import {setup} from 'xstate'; import {TaskContext, TaskEventPayload, UIControlConfig, TaskActionsMap} from './types'; import {TaskState, TaskEvent} from './constants'; import {actions, createInitialContext} from './actions'; -import {guards} from './guards'; +import {guards, shouldWrapUpForThisAgent, getTaskDataFromEvent} from './guards'; import {getIsCustomerInCall} from '../TaskUtils'; type TaskActionConfigMap = {[K in keyof typeof actions]: undefined}; @@ -323,6 +323,12 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) { [TaskState.HELD]: { on: { + [TaskEvent.PAUSE_RECORDING]: { + actions: ['updateTaskData', 'setRecordingState', 'emitTaskRecordingPaused'], + }, + [TaskEvent.RESUME_RECORDING]: { + actions: ['updateTaskData', 'setRecordingState', 'emitTaskRecordingResumed'], + }, // Click of the unhold button [TaskEvent.UNHOLD_INITIATED]: { target: TaskState.RESUME_INITIATING, @@ -467,6 +473,26 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) { target: TaskState.CONNECTED, actions: ['updateTaskData', 'clearConsultState', 'emitTaskConsultEnd'], }, + { + // Interaction terminated during consult (customer left) → WRAPPING_UP + guard: ({context, event}) => { + if (context.consultInitiator !== true) return false; + const taskData = getTaskDataFromEvent(event); + + return ( + taskData?.interaction?.isTerminated === true && + shouldWrapUpForThisAgent(context, taskData) + ); + }, + target: TaskState.WRAPPING_UP, + actions: [ + 'updateTaskData', + 'markEnded', + 'clearConsultState', + 'emitTaskWrapup', + 'requestCleanup', + ], + }, { // Initiator (no conference) → HELD guard: ({context}) => context.consultInitiator === true, @@ -587,7 +613,12 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) { // AgentConsultConferenced, ParticipantJoinedConference [TaskEvent.CONFERENCE_START]: { target: TaskState.CONFERENCING, - actions: ['handleConferenceStarted', 'clearConsultState'], + actions: [ + 'updateTaskData', + 'syncTaskDataFromEvent', + 'handleConferenceStarted', + 'clearConsultState', + ], }, }, }, @@ -597,7 +628,12 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) { // AgentConsultConferenced, ParticipantJoinedConference [TaskEvent.CONFERENCE_START]: { target: TaskState.CONFERENCING, - actions: ['handleConferenceStarted'], + actions: [ + 'updateTaskData', + 'syncTaskDataFromEvent', + 'handleConferenceStarted', + 'clearConsultState', + ], }, // AgentConsultConferenceFailed [TaskEvent.CONFERENCE_FAILED]: { @@ -610,7 +646,12 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) { [TaskState.CONFERENCING]: { on: { [TaskEvent.CONFERENCE_START]: { - actions: ['updateTaskData', 'clearConsultState', 'emitTaskConferenceStarted'], + actions: [ + 'updateTaskData', + 'syncTaskDataFromEvent', + 'clearConsultState', + 'emitTaskConferenceStarted', + ], }, [TaskEvent.EXIT_CONFERENCE_SUCCESS]: [ { @@ -633,7 +674,7 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) { actions: ['updateTaskData', 'setHoldState', 'emitTaskHold'], }, [TaskEvent.UNHOLD_SUCCESS]: { - actions: ['updateTaskData', 'setHoldState', 'emitTaskResume'], + actions: ['updateTaskData', 'syncTaskDataFromEvent', 'setHoldState', 'emitTaskResume'], }, // Start a new consult from within an active conference diff --git a/packages/@webex/contact-center/src/services/task/state-machine/actions.ts b/packages/@webex/contact-center/src/services/task/state-machine/actions.ts index 558042f5a13..bb29d8313ee 100644 --- a/packages/@webex/contact-center/src/services/task/state-machine/actions.ts +++ b/packages/@webex/contact-center/src/services/task/state-machine/actions.ts @@ -11,7 +11,13 @@ import { TaskActionArgs, RecordingStateUpdate, } from './types'; -import {TaskEvent, TaskState} from './constants'; +import { + TaskEvent, + TaskState, + INTERACTION_STATE, + CONSULT_STATE, + MEDIA_TYPE_CONSULT, +} from './constants'; import {DestinationType, TaskData} from '../types'; import {computeUIControls, getDefaultUIControls} from './uiControlsComputer'; @@ -73,6 +79,19 @@ const deriveRecordingState = (taskData?: TaskData | null): RecordingStateUpdate return update; }; +const isActiveConsultState = (taskData: TaskData | undefined, selfAgentId?: string): boolean => { + if (taskData?.interaction?.state === INTERACTION_STATE.CONSULTING) return true; + if (taskData?.interaction?.state === INTERACTION_STATE.POST_CALL && selfAgentId) { + const selfParticipant = taskData.interaction?.participants?.[selfAgentId] as any; + const hasConsultMedia = Object.values(taskData.interaction?.media ?? {}).some( + (media: any) => media?.mType === MEDIA_TYPE_CONSULT + ); + if (selfParticipant?.consultState === CONSULT_STATE.CONSULTING && hasConsultMedia) return true; + } + + return false; +}; + const deriveTaskDataUpdates = (context: TaskContext, taskData: TaskData | undefined) => taskData ? (() => { @@ -81,27 +100,30 @@ const deriveTaskDataUpdates = (context: TaskContext, taskData: TaskData | undefi ...deriveRecordingState(taskData), }; + const selfAgentId = context.uiControlConfig.agentId ?? taskData?.agentId; + const consultingActive = isActiveConsultState(taskData, selfAgentId); + if (taskData.destAgentId) { - updates.consultDestinationAgentId = taskData.destAgentId; + const isEpDnWithStoredId = + context.consultDestinationType === 'entryPoint' && context.consultDestinationAgentId; + if (!isEpDnWithStoredId) { + updates.consultDestinationAgentId = taskData.destAgentId; + } } - if (taskData.interaction?.state === 'consulting' && taskData.destinationType) { + if (consultingActive && taskData.destinationType) { updates.consultDestinationType = taskData.destinationType as DestinationType; } if (!context.consultInitiator) { - const selfAgentId = context.uiControlConfig.agentId ?? taskData?.agentId; const consultInitiator = determineConsultInitiator(taskData, selfAgentId); if (consultInitiator !== undefined) { updates.consultInitiator = consultInitiator; - } else if ( - taskData.interaction?.state === 'consulting' && - taskData.isConsulted === false - ) { + } else if (consultingActive && taskData.isConsulted === false) { updates.consultInitiator = true; } } - if (taskData.interaction?.state === 'consulting') { + if (consultingActive && taskData.interaction) { if (!context.consultDestinationAgentJoined) { const hasJoinedConsultee = Boolean( taskData.interaction.participants && @@ -230,7 +252,10 @@ export const actions: TaskActionsMap = { const taskData = getTaskDataFromEvent(event); const consultDestinationType = 'destinationType' in event ? event.destinationType ?? null : null; - const consultDestinationAgentId = 'destAgentId' in event ? event.destAgentId ?? null : null; + const consultDestinationAgentId = + ('destAgentId' in event ? event.destAgentId : null) ?? + ('destination' in event ? (event as any).destination : null) ?? + null; return { consultDestinationType, diff --git a/packages/@webex/contact-center/src/services/task/state-machine/constants.ts b/packages/@webex/contact-center/src/services/task/state-machine/constants.ts index 4115afe3597..9d2c511a529 100644 --- a/packages/@webex/contact-center/src/services/task/state-machine/constants.ts +++ b/packages/@webex/contact-center/src/services/task/state-machine/constants.ts @@ -36,6 +36,25 @@ export const MEDIA_TYPE_CONSULT = 'consult'; /** Media type for main calls */ export const MEDIA_TYPE_MAIN_CALL = 'mainCall'; +// ============================================ +// Backend Interaction State Constants +// ============================================ + +/** Backend interaction state values (from server payloads) */ +export const INTERACTION_STATE = { + CONSULTING: 'consulting', + POST_CALL: 'post_call', + CONFERENCE: 'conference', + CONNECTED: 'connected', + NEW: 'new', +} as const; + +/** Backend participant consultState values */ +export const CONSULT_STATE = { + CONSULTING: 'consulting', + CONFERENCING: 'conferencing', +} as const; + // ============================================ // State Machine Enums // ============================================ diff --git a/packages/@webex/contact-center/src/services/task/state-machine/guards.ts b/packages/@webex/contact-center/src/services/task/state-machine/guards.ts index be3bff1085a..d2788416cdc 100644 --- a/packages/@webex/contact-center/src/services/task/state-machine/guards.ts +++ b/packages/@webex/contact-center/src/services/task/state-machine/guards.ts @@ -27,7 +27,7 @@ import { getConferenceParticipantsCount, getIsConferenceInProgress, } from '../TaskUtils'; -import {TaskEvent} from './constants'; +import {TaskEvent, INTERACTION_STATE, CONSULT_STATE, MEDIA_TYPE_CONSULT} from './constants'; export const getTaskDataFromEvent = (event?: TaskEventPayload): TaskData | undefined => event && typeof event === 'object' && 'taskData' in event @@ -97,17 +97,35 @@ export const guards = { return false; }, - isInteractionConsulting: ({event}: GuardParams): boolean => { + isInteractionConsulting: ({event, context}: GuardParams): boolean => { const taskData = getTaskDataFromEvent(event); - if (taskData?.interaction?.state === 'consulting') return true; + if (taskData?.interaction?.state === INTERACTION_STATE.CONSULTING) return true; // EP_DN consulted agent: backend reports state as 'connected' but CPD indicates consult const cpd = taskData?.interaction?.callProcessingDetails; - if (cpd?.relationshipType === 'consult' && taskData?.interaction?.state === 'connected') { + if ( + cpd?.relationshipType === 'consult' && + taskData?.interaction?.state === INTERACTION_STATE.CONNECTED + ) { return true; } + // Customer left during consult: interaction state is "post_call" but consult + // between agents is still active. Detect via agent's consultState + consult media. + if (taskData?.interaction?.state === INTERACTION_STATE.POST_CALL) { + const selfAgentId = getSelfAgentId(context, taskData); + const selfParticipant = selfAgentId + ? taskData?.interaction?.participants?.[selfAgentId] + : null; + const hasConsultMedia = Object.values(taskData?.interaction?.media ?? {}).some( + (media: any) => media?.mType === MEDIA_TYPE_CONSULT + ); + if (selfParticipant?.consultState === CONSULT_STATE.CONSULTING && hasConsultMedia) { + return true; + } + } + return false; }, @@ -127,7 +145,7 @@ export const guards = { isInteractionConnected: ({event}: GuardParams): boolean => { const taskData = getTaskDataFromEvent(event); - return taskData?.interaction?.state === 'connected'; + return taskData?.interaction?.state === INTERACTION_STATE.CONNECTED; }, isConferencingByParticipants: ({event}: GuardParams): boolean => { @@ -176,7 +194,7 @@ export const guards = { if (!mainCallId) return false; // Don't downgrade while backend still reports conference. - if (taskData.interaction.state === 'conference') return false; + if (taskData.interaction.state === INTERACTION_STATE.CONFERENCE) return false; const agentParticipantsCount = getConferenceParticipantsCount(taskData.interaction, mainCallId); if (agentParticipantsCount >= 2) return false; @@ -218,7 +236,7 @@ export const guards = { return ( taskData.isConsulted === true || relationshipType === 'consult' || - taskData.interaction?.state === 'consulting' + taskData.interaction?.state === INTERACTION_STATE.CONSULTING ); }, diff --git a/packages/@webex/contact-center/src/services/task/state-machine/uiControlsComputer.ts b/packages/@webex/contact-center/src/services/task/state-machine/uiControlsComputer.ts index d56a258b292..3f778a96823 100644 --- a/packages/@webex/contact-center/src/services/task/state-machine/uiControlsComputer.ts +++ b/packages/@webex/contact-center/src/services/task/state-machine/uiControlsComputer.ts @@ -144,6 +144,12 @@ function computeVoiceInteractionUIControls( Boolean(selfAgentId) && Boolean(mainCallId) && Boolean(interaction?.media?.[mainCallId]?.participants?.includes(selfAgentId as string)); + const consultMedia = Object.values(interaction?.media ?? {}).find( + (media: any) => + media?.mediaResourceId === taskData?.consultMediaResourceId || media?.mType === 'consult' + ) as {participants?: string[]} | undefined; + const selfInConsultCall = + Boolean(selfAgentId) && Boolean(consultMedia?.participants?.includes(selfAgentId as string)); const conferenceActive = isConferencing || conferenceFromBackend || consultFromConference; // Treat consult initiator as "in conference" even if mainCall participant list lags while consulting. const inConference = conferenceActive && (isConferencing || selfInMainCall || consultInitiator); @@ -214,6 +220,7 @@ function computeVoiceInteractionUIControls( mute: (() => { if (!isWebrtc) return DISABLED; if (isWrappingUp) return DISABLED; + if (currentLeg === 'consult' && !selfInConsultCall) return DISABLED; if (isConsulting) return VISIBLE_ENABLED; if (isConnected || isHeld || isConferencing) { @@ -251,19 +258,26 @@ function computeVoiceInteractionUIControls( return DISABLED; })(), - // Transfer: connected/held, not in conference + // Transfer: connected/held/conference transfer: (() => { if (hasParallelConsultLeg) { + if (!customerPresent) return DISABLED; if (state === TaskState.CONNECTED) return VISIBLE_ENABLED; if (state === TaskState.HELD) return VISIBLE_DISABLED; } if (isConsulting) { if (!consultInitiator) return DISABLED; + if (!customerPresent) return VISIBLE_DISABLED; if (consultLegOnHold) return VISIBLE_DISABLED; return isConsultDestinationReady ? VISIBLE_ENABLED : VISIBLE_DISABLED; } - if (!hasFullControls || inConference) return DISABLED; + if (!hasFullControls) return DISABLED; + if (inConference) { + // Real conference (multiple agents): transfer is hidden + // Pending conference (only self agent): transfer remains available + return participantCount > 1 ? DISABLED : VISIBLE_ENABLED; + } if (state === TaskState.CONNECTED || state === TaskState.HELD) return VISIBLE_ENABLED; return DISABLED; @@ -278,15 +292,22 @@ function computeVoiceInteractionUIControls( return DISABLED; } - // Enabled conditions differ by state + // In conference: behavior depends on whether it's a real multi-agent conference + if (inConference) { + // Pending conference (only self agent): consult disabled + if (participantCount <= 1) return VISIBLE_DISABLED; + // Real conference: consult enabled if conditions met + const canFromConference = + !maxParticipants && customerPresent && !consultInProgress && !isConsulting; + + return {isVisible: true, isEnabled: canFromConference}; + } + + // Enabled conditions for connected/held const canFromConnected = !maxParticipants && customerPresent && !consultInProgress && !isConsulted; - const canFromConference = - !maxParticipants && customerPresent && !consultInProgress && !isConsulting; - - const isEnabled = inConference ? canFromConference : canFromConnected; - return {isVisible: true, isEnabled}; + return {isVisible: true, isEnabled: canFromConnected}; })(), // ConsultTransfer: always hidden (use transfer button) @@ -303,10 +324,16 @@ function computeVoiceInteractionUIControls( return {isVisible: true, isEnabled: consultInitiator || config.isEndConsultEnabled}; })(), - // Recording: connected/held only, not in consult/conference + // Recording: connected/held, hidden in real conference, visible in pending conference recording: (() => { if (!recordingControlsAvailable || !config.isRecordingEnabled) return DISABLED; - if (!hasFullControls || isConsulting || inConference) return DISABLED; + if (!hasFullControls || isConsulting) return DISABLED; + if (inConference) { + // Real conference (multiple agents): recording hidden + // Pending conference (only self agent): recording available + return participantCount > 1 ? DISABLED : VISIBLE_ENABLED; + } + if (hasParallelConsultLeg && !customerPresent) return DISABLED; if (state === TaskState.CONNECTED || state === TaskState.HELD) { return VISIBLE_ENABLED; } @@ -318,6 +345,7 @@ function computeVoiceInteractionUIControls( // Label changes based on leg: "Conference" on main leg, "Merge" on consult leg conference: (() => { if (hasParallelConsultLeg) { + if (!customerPresent) return DISABLED; if (state === TaskState.CONNECTED) { return maxParticipants ? VISIBLE_DISABLED : VISIBLE_ENABLED; } @@ -327,6 +355,7 @@ function computeVoiceInteractionUIControls( } if (!hasFullControls || !isConsulting) return DISABLED; if (!consultInitiator) return DISABLED; + if (!customerPresent) return VISIBLE_DISABLED; if (consultLegOnHold) return VISIBLE_DISABLED; return isConsultDestinationReady && !maxParticipants ? VISIBLE_ENABLED : VISIBLE_DISABLED; @@ -335,10 +364,11 @@ function computeVoiceInteractionUIControls( // Wrapup: wrapping up state wrapup: isWrappingUp ? VISIBLE_ENABLED : DISABLED, - // ExitConference: in conference, not consulting from conference + // ExitConference: in conference with multiple agents in main call exitConference: (() => { if (isConsulted && !isConferencing) return DISABLED; if (!inConference) return DISABLED; + if (participantCount <= 1) return DISABLED; const consultingFromConference = consultInitiator && isConsulting && conferenceFromBackend; return consultingFromConference ? VISIBLE_DISABLED : VISIBLE_ENABLED; @@ -356,6 +386,7 @@ function computeVoiceInteractionUIControls( // MergeToConference: mirrors conference control, enabled on both legs mergeToConference: (() => { if (!isConsulting || !consultInitiator) return DISABLED; + if (!customerPresent) return VISIBLE_DISABLED; if (consultLegOnHold) return VISIBLE_DISABLED; return isConsultDestinationReady && !maxParticipants ? VISIBLE_ENABLED : VISIBLE_DISABLED; @@ -363,8 +394,10 @@ function computeVoiceInteractionUIControls( // Switch: visible only on the currently active leg switch: (() => { + if (!customerPresent && hasParallelConsultLeg) return DISABLED; if (currentLeg === 'consult') { if (!isConsulting || !consultInitiator || consultCallHeld) return DISABLED; + if (!customerPresent) return VISIBLE_DISABLED; return isConsultDestinationReady ? VISIBLE_ENABLED : VISIBLE_DISABLED; } diff --git a/packages/@webex/contact-center/src/services/task/voice/Voice.ts b/packages/@webex/contact-center/src/services/task/voice/Voice.ts index b364b5c5eb9..18d56057966 100644 --- a/packages/@webex/contact-center/src/services/task/voice/Voice.ts +++ b/packages/@webex/contact-center/src/services/task/voice/Voice.ts @@ -710,17 +710,22 @@ export default class Voice extends Task implements IVoice { ? calculateDestType(this.data.interaction, this.data.agentId) : ''; + // derivedDestType is most reliable as it inspects live interaction participants + const resolvedDestinationType = + derivedDestType || + this.getStateMachineSnapshot()?.context?.consultDestinationType || + this.data.destinationType || + 'agent'; + const consultationData: consultConferencePayloadData = { agentId: this.data.agentId, - destinationType: - this.getStateMachineSnapshot()?.context?.consultDestinationType || - this.data.destinationType || - derivedDestType || - 'agent', + destinationType: resolvedDestinationType, + // derivedDestAgentId is most reliable as it resolves epId for EP_DN + // and agent ID for regular agents from live interaction data destAgentId: + derivedDestAgentId || this.getStateMachineSnapshot()?.context?.consultDestinationAgentId || - this.data.destAgentId || - derivedDestAgentId, + this.data.destAgentId, }; // Send state machine event to transition to CONF_INITIATING diff --git a/packages/@webex/contact-center/test/unit/spec/services/task/state-machine/TaskStateMachine.ts b/packages/@webex/contact-center/test/unit/spec/services/task/state-machine/TaskStateMachine.ts index d7ada05445a..1814c934936 100644 --- a/packages/@webex/contact-center/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +++ b/packages/@webex/contact-center/test/unit/spec/services/task/state-machine/TaskStateMachine.ts @@ -124,6 +124,28 @@ describe('Task state machine', () => { service.send({type: TaskEvent.RESUME_RECORDING}); expect(service.getSnapshot().context.recordingInProgress).toBe(true); }); + + it('toggles recording state while task is held', () => { + const service = startMachine(); + const taskData = createTaskData({ + interaction: { + callProcessingDetails: {recordInProgress: true}, + } as any, + }); + + service.send({type: TaskEvent.TASK_INCOMING, taskData}); + service.send({type: TaskEvent.ASSIGN, taskData}); + service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: taskData.mediaResourceId}); + service.send({type: TaskEvent.HOLD_SUCCESS, mediaResourceId: taskData.mediaResourceId}); + expect(service.getSnapshot().value).toBe(TaskState.HELD); + expect(service.getSnapshot().context.recordingInProgress).toBe(true); + + service.send({type: TaskEvent.PAUSE_RECORDING}); + expect(service.getSnapshot().context.recordingInProgress).toBe(false); + + service.send({type: TaskEvent.RESUME_RECORDING}); + expect(service.getSnapshot().context.recordingInProgress).toBe(true); + }); }); describe('wrap-up and completion flow', () => { diff --git a/packages/@webex/contact-center/test/unit/spec/services/task/state-machine/uiControlsComputer.ts b/packages/@webex/contact-center/test/unit/spec/services/task/state-machine/uiControlsComputer.ts index bcbae43f6c2..6e922783f03 100644 --- a/packages/@webex/contact-center/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +++ b/packages/@webex/contact-center/test/unit/spec/services/task/state-machine/uiControlsComputer.ts @@ -27,7 +27,7 @@ function createConsultTaskData() { currentState: 'consulting', isConsulted: true, }, - 'customer-1': {id: 'customer-1', pType: 'CUSTOMER', hasLeft: false}, + 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false}, } as any, media: { 'interaction-1': { @@ -86,6 +86,7 @@ describe('uiControlsComputer consult initiator controls', () => { expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false}); expect(uiControls.consult.hold).toEqual({isVisible: false, isEnabled: false}); + expect(uiControls.consult.mute).toEqual({isVisible: false, isEnabled: false}); expect(uiControls.consult.transfer).toEqual({isVisible: true, isEnabled: true}); expect(uiControls.consult.conference).toEqual({isVisible: true, isEnabled: true}); expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true}); @@ -109,6 +110,7 @@ describe('uiControlsComputer consult initiator controls', () => { expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false}); expect(uiControls.consult.hold).toEqual({isVisible: false, isEnabled: false}); + expect(uiControls.consult.mute).toEqual({isVisible: false, isEnabled: false}); expect(uiControls.consult.transfer).toEqual({isVisible: true, isEnabled: false}); expect(uiControls.consult.conference).toEqual({isVisible: true, isEnabled: false}); expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});