diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-consult.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-consult.tsx index 87ca61e73..fdb701de7 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-consult.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-consult.tsx @@ -1,9 +1,17 @@ import React, {useState} from 'react'; import {ButtonCircle, TooltipNext, Text} from '@momentum-ui/react-collaboration'; import {Avatar, Icon} from '@momentum-design/components/dist/react'; -import {MUTE_CALL, UNMUTE_CALL} from '../../constants'; import TaskTimer from '../../TaskTimer'; import {CallControlConsultComponentsProps} from '../../task.types'; +import { + createConsultButtons, + getVisibleButtons, + handleTransferPress, + handleEndConsultPress, + handleMuteToggle, + getConsultStatusText, + createTimerKey, +} from './call-control-custom.utils'; const CallControlConsultComponent: React.FC = ({ agentName, @@ -20,91 +28,34 @@ const CallControlConsultComponent: React.FC = }) => { const [isMuteDisabled, setIsMuteDisabled] = useState(false); - const timerKey = `timer-${startTimeStamp}`; + const timerKey = createTimerKey(startTimeStamp); const handleTransfer = () => { - logger.info('CC-Widgets: CallControlConsult: transfer button clicked', { - module: 'call-control-consult.tsx', - method: 'handleTransfer', - }); - try { - if (onTransfer) { - onTransfer(); - logger.log('CC-Widgets: CallControlConsult: transfer completed', { - module: 'call-control-consult.tsx', - method: 'handleTransfer', - }); - } - } catch (error) { - throw new Error('Error transferring call:', error); - } + handleTransferPress(onTransfer, logger); }; const handleEndConsult = () => { - logger.info('CC-Widgets: CallControlConsult: end consult clicked', { - module: 'call-control-consult.tsx', - method: 'handleEndConsult', - }); - try { - endConsultCall(); - logger.log('CC-Widgets: CallControlConsult: end consult completed', { - module: 'call-control-consult.tsx', - method: 'handleEndConsult', - }); - } catch (error) { - throw new Error('Error ending consult call:', error); - } + handleEndConsultPress(endConsultCall, logger); }; const handleConsultMuteToggle = () => { - setIsMuteDisabled(true); - - try { - onToggleConsultMute(); - } catch (error) { - logger.error(`Mute toggle failed: ${error}`, { - module: 'call-control-consult.tsx', - method: 'handleConsultMuteToggle', - }); - } finally { - // Re-enable button after operation - setTimeout(() => { - setIsMuteDisabled(false); - }, 500); - } + handleMuteToggle(onToggleConsultMute, setIsMuteDisabled, logger); }; - const buttons = [ - { - key: 'mute', - icon: isMuted ? 'microphone-muted-bold' : 'microphone-bold', - onClick: handleConsultMuteToggle, - tooltip: isMuted ? UNMUTE_CALL : MUTE_CALL, - className: `${isMuted ? 'call-control-button-muted' : 'call-control-button'}`, - disabled: isMuteDisabled, - shouldShow: muteUnmute, - }, - { - key: 'transfer', - icon: 'next-bold', - tooltip: 'Transfer Consult', - onClick: handleTransfer, - className: 'call-control-button', - disabled: !consultCompleted, - shouldShow: isAgentBeingConsulted && !!onTransfer, - }, - { - key: 'cancel', - icon: 'headset-muted-bold', - tooltip: 'End Consult', - onClick: handleEndConsult, - className: 'call-control-consult-button-cancel', - shouldShow: isEndConsultEnabled || isAgentBeingConsulted, - }, - ]; + const buttons = createConsultButtons( + isMuted, + isMuteDisabled, + consultCompleted, + isAgentBeingConsulted, + isEndConsultEnabled, + muteUnmute, + onTransfer ? handleTransfer : undefined, + handleConsultMuteToggle, + handleEndConsult + ); // Filter buttons that should be shown, then map them - const visibleButtons = buttons.filter((button) => button.shouldShow); + const visibleButtons = getVisibleButtons(buttons); return (
@@ -115,7 +66,7 @@ const CallControlConsultComponent: React.FC = {agentName} - {consultCompleted ? 'Consulting' : 'Consult requested'} •  + {getConsultStatusText(consultCompleted)} • 
diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-custom.utils.ts b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-custom.utils.ts new file mode 100644 index 000000000..5486b2215 --- /dev/null +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-custom.utils.ts @@ -0,0 +1,417 @@ +import {BuddyDetails, ContactServiceQueue, ILogger} from '@webex/cc-store'; +import {MUTE_CALL, UNMUTE_CALL} from '../../constants'; + +/** + * Interface for button configuration + */ +export interface ButtonConfig { + key: string; + icon: string; + onClick: () => void; + tooltip: string; + className: string; + disabled?: boolean; + shouldShow: boolean; +} + +/** + * Interface for list item data + */ +export interface ListItemData { + id: string; + name: string; +} + +/** + * Creates the consult button configuration array + */ +export const createConsultButtons = ( + isMuted: boolean, + isMuteDisabled: boolean, + consultCompleted: boolean, + isAgentBeingConsulted: boolean, + isEndConsultEnabled: boolean, + muteUnmute: boolean, + onTransfer?: () => void, + handleConsultMuteToggle?: () => void, + handleEndConsult?: () => void +): ButtonConfig[] => { + return [ + { + key: 'mute', + icon: isMuted ? 'microphone-muted-bold' : 'microphone-bold', + onClick: handleConsultMuteToggle || (() => {}), + tooltip: isMuted ? UNMUTE_CALL : MUTE_CALL, + className: `${isMuted ? 'call-control-button-muted' : 'call-control-button'}`, + disabled: isMuteDisabled, + shouldShow: muteUnmute, + }, + { + key: 'transfer', + icon: 'next-bold', + tooltip: 'Transfer Consult', + onClick: onTransfer || (() => {}), + className: 'call-control-button', + disabled: !consultCompleted, + shouldShow: isAgentBeingConsulted && !!onTransfer, + }, + { + key: 'cancel', + icon: 'headset-muted-bold', + tooltip: 'End Consult', + onClick: handleEndConsult || (() => {}), + className: 'call-control-consult-button-cancel', + shouldShow: isEndConsultEnabled || isAgentBeingConsulted, + }, + ]; +}; + +/** + * Filters buttons that should be visible + */ +export const getVisibleButtons = (buttons: ButtonConfig[]): ButtonConfig[] => { + return buttons.filter((button) => button.shouldShow); +}; + +/** + * Creates initials from a name string + */ +export const createInitials = (name: string): string => { + return name + .split(' ') + .map((word) => word[0]) + .join('') + .slice(0, 2) + .toUpperCase(); +}; + +/** + * Handles transfer button press with logging + */ +export const handleTransferPress = (onTransfer: (() => void) | undefined, logger: ILogger): void => { + logger.info('CC-Widgets: CallControlConsult: transfer button clicked', { + module: 'call-control-consult.tsx', + method: 'handleTransfer', + }); + + try { + if (onTransfer) { + onTransfer(); + logger.log('CC-Widgets: CallControlConsult: transfer completed', { + module: 'call-control-consult.tsx', + method: 'handleTransfer', + }); + } + } catch (error) { + throw new Error(`Error transferring call: ${error}`); + } +}; + +/** + * Handles end consult button press with logging + */ +export const handleEndConsultPress = (endConsultCall: (() => void) | undefined, logger: ILogger): void => { + logger.info('CC-Widgets: CallControlConsult: end consult clicked', { + module: 'call-control-consult.tsx', + method: 'handleEndConsult', + }); + + try { + if (endConsultCall) { + endConsultCall(); + logger.log('CC-Widgets: CallControlConsult: end consult completed', { + module: 'call-control-consult.tsx', + method: 'handleEndConsult', + }); + } + } catch (error) { + throw new Error(`Error ending consult call: ${error}`); + } +}; + +/** + * Handles mute toggle with disabled state management + */ +export const handleMuteToggle = ( + onToggleConsultMute: (() => void) | undefined, + setIsMuteDisabled: (disabled: boolean) => void, + logger: ILogger +): void => { + setIsMuteDisabled(true); + + try { + if (onToggleConsultMute) { + onToggleConsultMute(); + } + } catch (error) { + logger.error(`Mute toggle failed: ${error}`, { + module: 'call-control-consult.tsx', + method: 'handleConsultMuteToggle', + }); + } finally { + // Re-enable button after operation + setTimeout(() => { + setIsMuteDisabled(false); + }, 500); + } +}; + +/** + * Gets the consult status text based on completion state + */ +export const getConsultStatusText = (consultCompleted: boolean): string => { + return consultCompleted ? 'Consulting' : 'Consult requested'; +}; + +/** + * Handles list item button press with logging + */ +export const handleListItemPress = (title: string, onButtonPress: () => void, logger: ILogger): void => { + logger.info(`CC-Widgets: ConsultTransferListComponent: button pressed: ${title}`, { + module: 'consult-transfer-list-item.tsx', + method: 'handleButtonPress', + }); + onButtonPress(); +}; + +/** + * Determines if tabs should be shown based on available data + */ +export const shouldShowTabs = (buddyAgents: BuddyDetails[], queues: ContactServiceQueue[]): boolean => { + const noAgents = !buddyAgents || buddyAgents.length === 0; + const noQueues = !queues || queues.length === 0; + return !(noAgents && noQueues); +}; + +/** + * Checks if agents list is empty + */ +export const isAgentsEmpty = (buddyAgents: BuddyDetails[]): boolean => { + return !buddyAgents || buddyAgents.length === 0; +}; + +/** + * Checks if queues list is empty + */ +export const isQueuesEmpty = (queues: ContactServiceQueue[]): boolean => { + return !queues || queues.length === 0; +}; + +/** + * Handles tab selection with logging + */ +export const handleTabSelection = (key: string, setSelectedTab: (tab: string) => void, logger: ILogger): void => { + setSelectedTab(key); + logger.log(`CC-Widgets: ConsultTransferPopover: tab selected: ${key}`, { + module: 'consult-transfer-popover.tsx', + method: 'onTabSelection', + }); +}; + +/** + * Handles agent selection with logging + */ +export const handleAgentSelection = ( + agentId: string, + agentName: string, + onAgentSelect: ((agentId: string, agentName: string) => void) | undefined, + logger: ILogger +): void => { + logger.info(`CC-Widgets: ConsultTransferPopover: agent selected: ${agentId}`, { + module: 'consult-transfer-popover.tsx', + method: 'onAgentSelect', + }); + if (onAgentSelect) { + onAgentSelect(agentId, agentName); + } +}; + +/** + * Handles queue selection with logging + */ +export const handleQueueSelection = ( + queueId: string, + queueName: string, + onQueueSelect: ((queueId: string, queueName: string) => void) | undefined, + logger: ILogger +): void => { + logger.log(`CC-Widgets: ConsultTransferPopover: queue selected: ${queueId}`, { + module: 'consult-transfer-popover.tsx', + method: 'onQueueSelect', + }); + if (onQueueSelect) { + onQueueSelect(queueId, queueName); + } +}; + +/** + * Gets the appropriate empty state message based on context + */ +export const getEmptyStateMessage = (selectedTab: string, showTabs: boolean): string => { + if (!showTabs) { + return "We can't find any queue or agent available for now."; + } + + if (selectedTab === 'Agents') { + return "We can't find any agent available for now."; + } + + return "We can't find any queue available for now."; +}; + +/** + * Creates list item data from buddy agents + */ +export const createAgentListData = (buddyAgents: BuddyDetails[]): ListItemData[] => { + return buddyAgents.map((agent) => ({ + id: agent.agentId, + name: agent.agentName, + })); +}; + +/** + * Creates list item data from queues + */ +export const createQueueListData = (queues: ContactServiceQueue[]): ListItemData[] => { + return queues.map((queue) => ({ + id: queue.id, + name: queue.name, + })); +}; + +/** + * Creates a timer key based on timestamp + */ +export const createTimerKey = (startTimeStamp: number): string => { + return `timer-${startTimeStamp}`; +}; + +/** + * Handles popover open with logging + */ +export const handlePopoverOpen = (menuType: string, setActiveMenu: (menu: string) => void, logger: ILogger): void => { + logger.info(`CC-Widgets: CallControl: opening ${menuType} popover`, { + module: 'call-control.tsx', + method: 'handlePopoverOpen', + }); + setActiveMenu(menuType); +}; + +/** + * Handles popover close with logging + */ +export const handlePopoverClose = (setActiveMenu: (menu: string | null) => void, logger: ILogger): void => { + logger.info('CC-Widgets: CallControl: closing popover', { + module: 'call-control.tsx', + method: 'handlePopoverClose', + }); + setActiveMenu(null); +}; + +/** + * Handles hold toggle with logging + */ +export const handleHoldToggle = (toggleHold: (() => void) | undefined, logger: ILogger): void => { + logger.info('CC-Widgets: CallControl: hold toggle clicked', { + module: 'call-control.tsx', + method: 'handleHoldToggle', + }); + if (toggleHold) { + toggleHold(); + } +}; + +/** + * Handles wrapup call with logging + */ +export const handleWrapupCall = (onWrapupCall: (() => void) | undefined, logger: ILogger): void => { + logger.info('CC-Widgets: CallControl: wrapup call clicked', { + module: 'call-control.tsx', + method: 'handleWrapupCall', + }); + if (onWrapupCall) { + onWrapupCall(); + } +}; + +/** + * Validates if a menu type is supported + */ +export const isValidMenuType = (menuType: string): boolean => { + const validMenuTypes = ['Consult', 'Transfer']; + return validMenuTypes.includes(menuType); +}; + +/** + * Gets button style class based on state + */ +export const getButtonStyleClass = ( + isActive: boolean, + isDisabled: boolean, + baseClass = 'call-control-button' +): string => { + if (isDisabled) { + return `${baseClass}-disabled`; + } + if (isActive) { + return `${baseClass}-active`; + } + return baseClass; +}; + +/** + * Formats elapsed time for display + */ +export const formatElapsedTime = (startTime: number): string => { + const elapsed = Date.now() - startTime; + const seconds = Math.floor(elapsed / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + if (hours > 0) { + return `${hours}:${(minutes % 60).toString().padStart(2, '0')}:${(seconds % 60).toString().padStart(2, '0')}`; + } + return `${minutes}:${(seconds % 60).toString().padStart(2, '0')}`; +}; + +/** + * Checks if an agent is available for selection + */ +export const isAgentAvailable = (agent: BuddyDetails): boolean => { + return agent && agent.agentId && agent.agentName && agent.agentName.trim().length > 0; +}; + +/** + * Checks if a queue is available for selection + */ +export const isQueueAvailable = (queue: ContactServiceQueue): boolean => { + return queue && queue.id && queue.name && queue.name.trim().length > 0; +}; + +/** + * Filters available agents + */ +export const filterAvailableAgents = (agents: BuddyDetails[]): BuddyDetails[] => { + return agents ? agents.filter(isAgentAvailable) : []; +}; + +/** + * Filters available queues + */ +export const filterAvailableQueues = (queues: ContactServiceQueue[]): ContactServiceQueue[] => { + return queues ? queues.filter(isQueueAvailable) : []; +}; + +/** + * Debounces a function call + */ +export const debounce = unknown>( + func: T, + wait: number +): ((...args: Parameters) => void) => { + let timeout: NodeJS.Timeout; + return (...args: Parameters) => { + clearTimeout(timeout); + timeout = setTimeout(() => func(...args), wait); + }; +}; diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-list-item.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-list-item.tsx index 868153ad6..aced9fa80 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-list-item.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-list-item.tsx @@ -3,23 +3,15 @@ import {ListItemBase, ListItemBaseSection, AvatarNext, Text, ButtonCircle} from import {Icon} from '@momentum-design/components/dist/react'; import classnames from 'classnames'; import {ConsultTransferListComponentProps} from '../../task.types'; +import {createInitials, handleListItemPress} from './call-control-custom.utils'; const ConsultTransferListComponent: React.FC = (props) => { const {title, subtitle, buttonIcon, onButtonPress, className, logger} = props; - const initials = title - .split(' ') - .map((word) => word[0]) - .join('') - .slice(0, 2) - .toUpperCase(); + const initials = createInitials(title); const handleButtonPress = () => { - logger.info(`CC-Widgets: ConsultTransferListComponent: button pressed: ${title}`, { - module: 'consult-transfer-list-item.tsx', - method: 'handleButtonPress', - }); - onButtonPress(); + handleListItemPress(title, onButtonPress, logger); }; return ( diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx index 717bba6e7..29078ce05 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx @@ -3,6 +3,15 @@ import {Text, TabListNext, TabNext, ListNext} from '@momentum-ui/react-collabora import ConsultTransferListComponent from './consult-transfer-list-item'; import {ConsultTransferPopoverComponentProps} from '../../task.types'; import ConsultTransferEmptyState from './consult-transfer-empty-state'; +import { + shouldShowTabs, + isAgentsEmpty, + isQueuesEmpty, + handleTabSelection, + handleAgentSelection, + handleQueueSelection, + getEmptyStateMessage, +} from './call-control-custom.utils'; const ConsultTransferPopoverComponent: React.FC = ({ heading, @@ -18,9 +27,9 @@ const ConsultTransferPopoverComponent: React.FC ( @@ -60,11 +69,7 @@ const ConsultTransferPopoverComponent: React.FC { - setSelectedTab(key as string); - logger.log(`CC-Widgets: ConsultTransferPopover: tab selected: ${key}`, { - module: 'consult-transfer-popover.tsx', - method: 'onTabSelection', - }); + handleTabSelection(key as string, setSelectedTab, logger); }} > @@ -83,16 +88,16 @@ const ConsultTransferPopoverComponent: React.FC} + {!showTabs && } {/* If agents tab is selected and empty */} {showTabs && selectedTab === 'Agents' && noAgents && ( - + )} {/* If queues tab is selected and empty */} {showTabs && selectedTab === 'Queues' && noQueues && ( - + )} {/* Render lists if not empty */} @@ -104,11 +109,7 @@ const ConsultTransferPopoverComponent: React.FC agent.agentId, (agent) => agent.agentName, (id, name) => { - logger.info(`CC-Widgets: ConsultTransferPopover: agent selected: ${id}`, { - module: 'consult-transfer-popover.tsx', - method: 'onAgentSelect', - }); - onAgentSelect(id, name); + handleAgentSelection(id, name, onAgentSelect, logger); } )} @@ -120,11 +121,7 @@ const ConsultTransferPopoverComponent: React.FC queue.id, (queue) => queue.name, (id, name) => { - logger.log(`CC-Widgets: ConsultTransferPopover: queue selected: ${id}`, { - module: 'consult-transfer-popover.tsx', - method: 'onQueueSelect', - }); - (onQueueSelect || (() => {}))(id, name); + handleQueueSelection(id, name, onQueueSelect, logger); } )} diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx index df1f30d2a..e03564c48 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx @@ -7,24 +7,23 @@ import {Icon, Button, Select, Option} from '@momentum-design/components/dist/rea import ConsultTransferPopoverComponent from './CallControlCustom/consult-transfer-popover'; import AutoWrapupTimer from '../AutoWrapupTimer/AutoWrapupTimer'; import type {MEDIA_CHANNEL as MediaChannelType} from '../task.types'; -import {getMediaTypeInfo} from '../../../utils'; import {DestinationType} from '@webex/cc-store'; +import {WRAP_UP, WRAP_UP_INTERACTION, WRAP_UP_REASON, SELECT, SUBMIT_WRAP_UP} from '../constants'; import { - RESUME_CALL, - HOLD_CALL, - CONSULT_AGENT, - TRANSFER, - PAUSE_RECORDING, - RESUME_RECORDING, - END, - WRAP_UP, - WRAP_UP_INTERACTION, - WRAP_UP_REASON, - SELECT, - SUBMIT_WRAP_UP, - MUTE_CALL, - UNMUTE_CALL, -} from '../constants'; + handleToggleHold as handleToggleHoldUtil, + handleMuteToggle as handleMuteToggleUtil, + handleWrapupCall as handleWrapupCallUtil, + handleWrapupChange as handleWrapupChangeUtil, + handleTargetSelect as handleTargetSelectUtil, + handleCloseButtonPress, + handleWrapupReasonChange, + handleAudioRef, + getMediaType, + isTelephonyMediaType, + buildCallControlButtons, + filterButtonsForConsultation, + updateCallStateFromTask, +} from './call-control.utils'; function CallControlComponent(props: CallControlComponentProps) { const [selectedWrapupReason, setSelectedWrapupReason] = useState(null); @@ -66,176 +65,75 @@ function CallControlComponent(props: CallControlComponentProps) { } = props; useEffect(() => { - if (!currentTask || !currentTask.data || !currentTask.data.interaction) return; - - const {interaction, mediaResourceId} = currentTask.data; - const {media, callProcessingDetails} = interaction; - const isHold = media && media[mediaResourceId] && media[mediaResourceId].isHold; - setIsHeld(isHold); - - if (callProcessingDetails) { - const {isPaused} = callProcessingDetails; - setIsRecording(!isPaused); - } + updateCallStateFromTask(currentTask, setIsHeld, setIsRecording); }, [currentTask]); const handletoggleHold = () => { - logger.info(`CC-Widgets: CallControl: is Call On Hold status is ${isHeld}`, { - module: 'call-control.tsx', - method: 'handletoggleHold', - }); - toggleHold(!isHeld); - setIsHeld(!isHeld); + handleToggleHoldUtil(isHeld, toggleHold, setIsHeld, logger); }; const handleMuteToggle = () => { - setIsMuteButtonDisabled(true); - - try { - toggleMute(); - } catch (error) { - logger.error(`Mute toggle failed: ${error}`, { - module: 'call-control.tsx', - method: 'handleMuteToggle', - }); - } finally { - // Re-enable button after operation - setTimeout(() => { - setIsMuteButtonDisabled(false); - }, 500); - } + handleMuteToggleUtil(toggleMute, setIsMuteButtonDisabled, logger); }; - const handleWrapupCall = () => { - logger.info('CC-Widgets: CallControl: wrap-up submitted', { - module: 'call-control.tsx', - method: 'handleWrapupCall', - }); - if (selectedWrapupReason && selectedWrapupId) { - wrapupCall(selectedWrapupReason, selectedWrapupId); - setSelectedWrapupReason(null); - setSelectedWrapupId(null); - logger.log('CC-Widgets: CallControl: wrapup completed', { - module: 'call-control.tsx', - method: 'handleWrapupCall', - }); - } + const handleWrapupCallLocal = () => { + handleWrapupCallUtil( + selectedWrapupReason, + selectedWrapupId, + wrapupCall, + setSelectedWrapupReason, + setSelectedWrapupId, + logger + ); }; const handleWrapupChange = (text, value) => { - setSelectedWrapupReason(text); - setSelectedWrapupId(value); + handleWrapupChangeUtil(text, value, setSelectedWrapupReason, setSelectedWrapupId); }; const handleTargetSelect = (id: string, name: string, type: DestinationType) => { - logger.info('CC-Widgets: CallControl: handling target agent selected', { - module: 'call-control.tsx', - method: 'handleTargetSelect', - }); - if (agentMenuType === 'Consult') { - try { - consultCall(id, type); - setConsultAgentId(id); - setConsultAgentName(name); - setLastTargetType(type); - } catch (error) { - throw new Error('Error during consult call', error); - } - } else if (agentMenuType === 'Transfer') { - try { - transferCall(id, type); - } catch (error) { - throw new Error('Error during transfer call', error); - } - } + handleTargetSelectUtil( + id, + name, + type, + agentMenuType, + consultCall, + transferCall, + setConsultAgentId, + setConsultAgentName, + setLastTargetType, + logger + ); }; - const currentMediaType = getMediaTypeInfo( + const currentMediaType = getMediaType( currentTask.data.interaction.mediaType as MediaChannelType, currentTask.data.interaction.mediaChannel as MediaChannelType ); const mediaType = currentTask.data.interaction.mediaType as MediaChannelType; - const isTelephony = mediaType === 'telephony'; + const isTelephony = isTelephonyMediaType(mediaType); - const buttons = [ - { - id: 'mute', - icon: isMuted ? 'microphone-muted-bold' : 'microphone-bold', - onClick: handleMuteToggle, - tooltip: isMuted ? UNMUTE_CALL : MUTE_CALL, - className: `${isMuted ? 'call-control-button-muted' : 'call-control-button'}`, - disabled: isMuteButtonDisabled, - isVisible: controlVisibility.muteUnmute, - }, - { - id: 'hold', - icon: isHeld ? 'play-bold' : 'pause-bold', - onClick: () => handletoggleHold(), - tooltip: isHeld ? RESUME_CALL : HOLD_CALL, - className: 'call-control-button', - disabled: false, - isVisible: controlVisibility.holdResume, - dataTestId: 'call-control:hold-toggle', - }, - { - id: 'consult', - icon: 'headset-bold', - tooltip: CONSULT_AGENT, - className: 'call-control-button', - disabled: false, - menuType: 'Consult', - isVisible: controlVisibility.consult, - dataTestId: 'call-control:consult', - }, - { - id: 'transfer', - icon: 'next-bold', - tooltip: `${TRANSFER} ${currentMediaType.labelName}`, - className: 'call-control-button', - disabled: false, - menuType: 'Transfer', - isVisible: controlVisibility.transfer, - dataTestId: 'call-control:transfer', - }, - { - id: 'record', - icon: isRecording ? 'record-paused-bold' : 'record-bold', - onClick: () => toggleRecording(), - tooltip: isRecording ? PAUSE_RECORDING : RESUME_RECORDING, - className: 'call-control-button', - disabled: false, - isVisible: controlVisibility.pauseResumeRecording, - dataTestId: 'call-control:recording-toggle', - }, - { - id: 'end', - icon: 'cancel-regular', - onClick: endCall, - tooltip: `${END} ${currentMediaType.labelName}`, - className: 'call-control-button-cancel', - disabled: isHeld, - isVisible: controlVisibility.end, - dataTestId: 'call-control:end-call', - }, - ]; + const buttons = buildCallControlButtons( + isMuted, + isHeld, + isRecording, + isMuteButtonDisabled, + currentMediaType, + controlVisibility, + handleMuteToggle, + handletoggleHold, + toggleRecording, + endCall + ); - const filteredButtons = - consultInitiated && isTelephony ? buttons.filter((button) => !['hold', 'consult'].includes(button.id)) : buttons; + const filteredButtons = filterButtonsForConsultation(buttons, consultInitiated, isTelephony); if (!currentTask) return null; return ( <> - +
{!(consultAccepted && isTelephony) && !controlVisibility.wrapup && (
@@ -272,10 +170,7 @@ function CallControlComponent(props: CallControlComponentProps) { closeButtonPlacement="top-right" closeButtonProps={{ 'aria-label': 'Close popover', - onPress: () => { - setShowAgentMenu(false); - setAgentMenuType(null); - }, + onPress: () => handleCloseButtonPress(setShowAgentMenu, setAgentMenuType), outline: true, }} triggerComponent={ @@ -364,6 +259,7 @@ function CallControlComponent(props: CallControlComponentProps) { type="button" role="button" data-testid="call-control:wrapup-button" + id="call-control-wrapup-button" > {WRAP_UP} @@ -396,11 +292,7 @@ function CallControlComponent(props: CallControlComponentProps) { className="wrapup-select" data-testid="call-control:wrapup-select" placeholder={SELECT} - onChange={(event: CustomEvent) => { - const key = event.detail.value; - const selectedItem = wrapupCodes?.find((code) => code.id === key); - handleWrapupChange(selectedItem.name, selectedItem.id); - }} + onChange={(event: CustomEvent) => handleWrapupReasonChange(event, wrapupCodes, handleWrapupChange)} > {wrapupCodes?.map((code) => (
+`; + +exports[`CallControlConsultComponent Snapshots Interactions should call mockOnToggleConsultMute when mute button is clicked 1`] = ` +
+
+ +
+ + Alice + + + Consulting +  •  + + +
+
+
+ +
+

+ Mute +

+
+ +
+

+ Transfer Consult +

+
+ +
+

+ End Consult +

+
+
+
+
+`; + +exports[`CallControlConsultComponent Snapshots Interactions should call mockOnTransfer when transfer button is clicked 1`] = ` +
+
+ +
+ + Alice + + + Consulting +  •  + + +
+
+
+ +
+

+ Mute +

+
+ +
+

+ Transfer Consult +

+
+ +
+

+ End Consult +

+
+
+
+
+`; + +exports[`CallControlConsultComponent Snapshots Rendering - Tests for UI elements and visual states of CallControlConsult component should render the component with all control buttons 1`] = ` +
+
+ +
+ + Alice + + + Consulting +  •  + + +
+
+
+ +
+

+ Mute +

+
+ +
+

+ Transfer Consult +

+
+ +
+

+ End Consult +

+
+
+
+`; + +exports[`CallControlConsultComponent Snapshots Rendering - Tests for UI elements and visual states of CallControlConsult component should render when consultCompleted is false 1`] = ` +
+
+ +
+ + Alice + + + Consult requested +  •  + + +
+
+
+ +
+

+ Mute +

+
+ +
+

+ Transfer Consult +

+
+ +
+

+ End Consult +

+
+
+
+`; + +exports[`CallControlConsultComponent Snapshots Rendering - Tests for UI elements and visual states of CallControlConsult component should render when isAgentBeingConsulted is false 1`] = ` +
+
+ +
+ + Alice + + + Consulting +  •  + + +
+
+
+ +
+

+ Mute +

+
+ +
+

+ End Consult +

+
+
+
+`; + +exports[`CallControlConsultComponent Snapshots Rendering - Tests for UI elements and visual states of CallControlConsult component should render when isEndConsultEnabled is false 1`] = ` +
+
+ +
+ + Alice + + + Consulting +  •  + + +
+
+
+ +
+

+ Mute +

+
+ +
+

+ Transfer Consult +

+
+ +
+

+ End Consult +

+
+
+
+`; + +exports[`CallControlConsultComponent Snapshots Rendering - Tests for UI elements and visual states of CallControlConsult component should render with different agent name 1`] = ` +
+
+ +
+ + Bob Johnson + + + Consulting +  •  + + +
+
+
+ +
+

+ Mute +

+
+ +
+

+ Transfer Consult +

+
+ +
+

+ End Consult +

+
+
+
+`; + +exports[`CallControlConsultComponent Snapshots Rendering - Tests for UI elements and visual states of CallControlConsult component should render with muted state 1`] = ` +
+
+ +
+ + Alice + + + Consulting +  •  + + +
+
+
+ +
+

+ Unmute +

+
+ +
+

+ Transfer Consult +

+
+ +
+

+ End Consult +

+
+
+
+`; + +exports[`CallControlConsultComponent Snapshots Rendering - Tests for UI elements and visual states of CallControlConsult component should render without mute button when muteUnmute is false 1`] = ` +
+
+ +
+ + Alice + + + Consulting +  •  + + +
+
+
+ +
+

+ Transfer Consult +

+
+ +
+

+ End Consult +

+
+
+
+`; + +exports[`CallControlConsultComponent Snapshots Rendering - Tests for UI elements and visual states of CallControlConsult component should render without transfer button when onTransfer is undefined 1`] = ` +
+
+ +
+ + Alice + + + Consulting +  •  + + +
+
+
+ +
+

+ Mute +

+
+ +
+

+ End Consult +

+
+
+
+`; + +exports[`CallControlConsultComponent Snapshots State Management should handle combination of props: no mute, no transfer, disabled end consult 1`] = ` +
+
+ +
+ + Alice + + + Consulting +  •  + + +
+
+
+ +
+

+ End Consult +

+
+
+
+`; + +exports[`CallControlConsultComponent Snapshots State Management should handle edge case with empty agent name 1`] = ` +
+
+ +
+ + + Consulting +  •  + + +
+
+
+ +
+

+ Mute +

+
+ +
+

+ Transfer Consult +

+
+ +
+

+ End Consult +

+
+
+
+`; + +exports[`CallControlConsultComponent Snapshots State Management should update when isMuted prop changes 1`] = ` +
+
+ +
+ + Alice + + + Consulting +  •  + + +
+
+
+ +
+

+ Unmute +

+
+ +
+

+ Transfer Consult +

+
+ +
+

+ End Consult +

+
+
+
+`; + +exports[`CallControlConsultComponent Snapshots State Management should update when muteUnmute prop changes 1`] = ` +
+
+ +
+ + Alice + + + Consulting +  •  + + +
+
+
+ +
+

+ Transfer Consult +

+
+ +
+

+ End Consult +

+
+
+
+`; diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-empty-state.snapshot.tsx.snap b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-empty-state.snapshot.tsx.snap new file mode 100644 index 000000000..1901a7ddc --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-empty-state.snapshot.tsx.snap @@ -0,0 +1,89 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ConsultTransferEmptyState Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferEmptyState component should render the component with default message 1`] = ` +
+
+
+ No agents or queues available +
+
+`; + +exports[`ConsultTransferEmptyState Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferEmptyState component should render with HTML characters in message 1`] = ` +
+
+
+ No agents & queues <available> +
+
+`; + +exports[`ConsultTransferEmptyState Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferEmptyState component should render with custom message 1`] = ` +
+
+
+ No items found +
+
+`; + +exports[`ConsultTransferEmptyState Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferEmptyState component should render with empty message 1`] = ` +
+
+
+
+`; + +exports[`ConsultTransferEmptyState Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferEmptyState component should render with long message 1`] = ` +
+
+
+ This is a very long message that might wrap to multiple lines to test how the component handles extended text content +
+
+`; + +exports[`ConsultTransferEmptyState Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferEmptyState component should render with special characters in message 1`] = ` +
+
+
+ No agents/queues! @#$%^&*() +
+
+`; diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-list-item.snapshot.tsx.snap b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-list-item.snapshot.tsx.snap new file mode 100644 index 000000000..0eba3b4df --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-list-item.snapshot.tsx.snap @@ -0,0 +1,1208 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ConsultTransferListComponent Snapshots Interactions should render component after button click 1`] = ` +
  • +
    + +
    +
    + + John Doe + + + Manager + +
    +
    +
    + +
    +
    +
  • +`; + +exports[`ConsultTransferListComponent Snapshots Interactions should render component after keyboard interaction 1`] = ` +
  • +
    + +
    +
    + + John Doe + + + Manager + +
    +
    +
    + +
    +
    +
  • +`; + +exports[`ConsultTransferListComponent Snapshots Interactions should render component after list item click 1`] = ` +
  • +
    + +
    +
    + + John Doe + + + Manager + +
    +
    +
    + +
    +
    +
  • +`; + +exports[`ConsultTransferListComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferListComponent component should render the component with title and subtitle 1`] = ` +
  • +
    + +
    +
    + + John Doe + + + Manager + +
    +
    +
    + +
    +
    +
  • +`; + +exports[`ConsultTransferListComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferListComponent component should render with different button icon 1`] = ` +
  • +
    + +
    +
    + + John Doe + + + Manager + +
    +
    +
    + +
    +
    +
  • +`; + +exports[`ConsultTransferListComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferListComponent component should render with different className 1`] = ` +
  • +
    + +
    +
    + + John Doe + + + Manager + +
    +
    +
    + +
    +
    +
  • +`; + +exports[`ConsultTransferListComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferListComponent component should render with long name generating different initials 1`] = ` +
  • +
    + +
    +
    + + Alexander Benjamin Christopher + + + Manager + +
    +
    +
    + +
    +
    +
  • +`; + +exports[`ConsultTransferListComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferListComponent component should render with long subtitle 1`] = ` +
  • +
    + +
    +
    + + John Doe + + + Senior Manager of Customer Relations and Business Development + +
    +
    +
    + +
    +
    +
  • +`; + +exports[`ConsultTransferListComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferListComponent component should render with single name 1`] = ` +
  • +
    + +
    +
    + + John + + + Manager + +
    +
    +
    + +
    +
    +
  • +`; + +exports[`ConsultTransferListComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferListComponent component should render with special characters in title 1`] = ` +
  • +
    + +
    +
    + + John O'Connor & Jane + + + Manager + +
    +
    +
    + +
    +
    +
  • +`; + +exports[`ConsultTransferListComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferListComponent component should render without className 1`] = ` +
  • +
    + +
    +
    + + John Doe + + + Manager + +
    +
    +
    + +
    +
    +
  • +`; + +exports[`ConsultTransferListComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferListComponent component should render without subtitle 1`] = ` +
  • +
    + +
    +
    + + John Doe + +
    +
    +
    + +
    +
    +
  • +`; + +exports[`ConsultTransferListComponent Snapshots State Management should update when buttonIcon changes 1`] = ` +
  • +
    + +
    +
    + + John Doe + + + Manager + +
    +
    +
    + +
    +
    +
  • +`; + +exports[`ConsultTransferListComponent Snapshots State Management should update when subtitle changes 1`] = ` +
  • +
    + +
    +
    + + John Doe + + + Senior Manager + +
    +
    +
    + +
    +
    +
  • +`; + +exports[`ConsultTransferListComponent Snapshots State Management should update when title changes 1`] = ` +
  • +
    + +
    +
    + + Jane Smith + + + Manager + +
    +
    +
    + +
    +
    +
  • +`; diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap new file mode 100644 index 000000000..404fb11ad --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap @@ -0,0 +1,2961 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ConsultTransferPopoverComponent Snapshots Interactions should render component after agent selection 1`] = ` +
    + + Select an Agent + +
    + + +
    +
      +
      +
    • +
      + +
      +
      + + Agent One + +
      +
      +
      + +
      +
      +
    • +
      +
      +
    • +
      + +
      +
      + + Agent Two + +
      +
      +
      + +
      +
      +
    • +
      +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots Interactions should render component after queue selection 1`] = ` +
    + + Select an Agent + +
    + + +
    +
      +
      +
    • +
      + +
      +
      + + Queue One + +
      +
      +
      + +
      +
      +
    • +
      +
      +
    • +
      + +
      +
      + + Queue Two + +
      +
      +
      + +
      +
      +
    • +
      +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots Interactions should render component after switching to queues tab 1`] = ` +
    + + Select an Agent + +
    + + +
    +
      +
      +
    • +
      + +
      +
      + + Queue One + +
      +
      +
      + +
      +
      +
    • +
      +
      +
    • +
      + +
      +
      + + Queue Two + +
      +
      +
      + +
      +
      +
    • +
      +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render the component with tabs and agents 1`] = ` +
    + + Select an Agent + +
    + + +
    +
      +
      +
    • +
      + +
      +
      + + Agent One + +
      +
      +
      + +
      +
      +
    • +
      +
      +
    • +
      + +
      +
      + + Agent Two + +
      +
      +
      + +
      +
      +
    • +
      +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with agents having different states 1`] = ` +
    + + Select an Agent + +
    + + +
    +
      +
      +
    • +
      + +
      +
      + + Available Agent + +
      +
      +
      + +
      +
      +
    • +
      +
      +
    • +
      + +
      +
      + + Busy Agent + +
      +
      +
      + +
      +
      +
    • +
      +
      +
    • +
      + +
      +
      + + Idle Agent + +
      +
      +
      + +
      +
      +
    • +
      +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with allowConsultToQueue false 1`] = ` +
    + + Select an Agent + +
    + + +
    +
      +
      +
    • +
      + +
      +
      + + Agent One + +
      +
      +
      + +
      +
      +
    • +
      +
      +
    • +
      + +
      +
      + + Agent Two + +
      +
      +
      + +
      +
      +
    • +
      +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with custom empty message 1`] = ` +
    + + Select an Agent + +
    +
    +
    + We can't find any queue or agent available for now. +
    +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with different heading 1`] = ` +
    + + Choose Transfer Target + +
    + + +
    +
      +
      +
    • +
      + +
      +
      + + Agent One + +
      +
      +
      + +
      +
      +
    • +
      +
      +
    • +
      + +
      +
      + + Agent Two + +
      +
      +
      + +
      +
      +
    • +
      +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with empty agents list 1`] = ` +
    + + Select an Agent + +
    + + +
    +
    +
    +
    + We can't find any agent available for now. +
    +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with empty queues list 1`] = ` +
    + + Select an Agent + +
    + + +
    +
      +
      +
    • +
      + +
      +
      + + Agent One + +
      +
      +
      + +
      +
      +
    • +
      +
      +
    • +
      + +
      +
      + + Agent Two + +
      +
      +
      + +
      +
      +
    • +
      +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with single agent 1`] = ` +
    + + Select an Agent + +
    + + +
    +
      +
      +
    • +
      + +
      +
      + + Single Agent + +
      +
      +
      + +
      +
      +
    • +
      +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with single queue 1`] = ` +
    + + Select an Agent + +
    + + +
    +
      +
      +
    • +
      + +
      +
      + + Agent One + +
      +
      +
      + +
      +
      +
    • +
      +
      +
    • +
      + +
      +
      + + Agent Two + +
      +
      +
      + +
      +
      +
    • +
      +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render without tabs when showTabs is false 1`] = ` +
    + + Select an Agent + +
    + + +
    +
      +
      +
    • +
      + +
      +
      + + Agent One + +
      +
      +
      + +
      +
      +
    • +
      +
      +
    • +
      + +
      +
      + + Agent Two + +
      +
      +
      + +
      +
      +
    • +
      +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots State Management should update when agents list changes 1`] = ` +
    + + Select an Agent + +
    + + +
    +
      +
      +
    • +
      + +
      +
      + + New Agent One + +
      +
      +
      + +
      +
      +
    • +
      +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots State Management should update when heading changes 1`] = ` +
    + + New Heading + +
    + + +
    +
      +
      +
    • +
      + +
      +
      + + Agent One + +
      +
      +
      + +
      +
      +
    • +
      +
      +
    • +
      + +
      +
      + + Agent Two + +
      +
      +
      + +
      +
      +
    • +
      +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots State Management should update when queues list changes 1`] = ` +
    + + Select an Agent + +
    + + +
    +
      +
      +
    • +
      + +
      +
      + + Agent One + +
      +
      +
      + +
      +
      +
    • +
      +
      +
    • +
      + +
      +
      + + Agent Two + +
      +
      +
      + +
      +
      +
    • +
      +
    +
    +`; diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/call-control-consult.snapshot.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/call-control-consult.snapshot.tsx new file mode 100644 index 000000000..dbc8e4f04 --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/call-control-consult.snapshot.tsx @@ -0,0 +1,289 @@ +import React from 'react'; +import '@testing-library/jest-dom'; +import {render, fireEvent, act} from '@testing-library/react'; +import CallControlConsultComponent from '../../../../../src/components/task/CallControl/CallControlCustom/call-control-consult'; + +const mockUIDProps = (container) => { + container + .querySelectorAll('[id^="mdc-input"]') + .forEach((el: HTMLBaseElement) => el.setAttribute('id', 'mock-input-id')); + container + .querySelectorAll('[id^="mdc-tooltip"]') + .forEach((el: HTMLBaseElement) => el.setAttribute('id', 'mock-tooltip-id')); + container + .querySelectorAll('[aria-describedby^="mdc-tooltip"]') + .forEach((el: HTMLBaseElement) => el.setAttribute('aria-describedby', 'mock-aria-describedby')); +}; + +// Mock Worker for TaskTimer component +global.Worker = class MockWorker { + onmessage: ((event: MessageEvent) => void) | null = null; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + constructor(_scriptURL: string | URL, _options?: WorkerOptions) { + // Mock worker constructor + } + + postMessage(message: {name: string; type: string}): void { + // Mock postMessage - simulate timer updates + setTimeout(() => { + if (this.onmessage) { + this.onmessage({data: {name: message.name, time: '00:00'}} as MessageEvent); + } + }, 0); + } + + terminate(): void { + // Mock terminate + } + + addEventListener(type: string, listener: EventListenerOrEventListenerObject): void { + if (type === 'message' && typeof listener === 'function') { + this.onmessage = listener as (event: MessageEvent) => void; + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + removeEventListener(type: string, _listener: EventListenerOrEventListenerObject): void { + if (type === 'message') { + this.onmessage = null; + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + dispatchEvent(_event: Event): boolean { + return true; + } +} as typeof Worker; + +// Mock URL.createObjectURL +global.URL.createObjectURL = jest.fn(() => 'mock-url'); + +describe('CallControlConsultComponent Snapshots', () => { + const mockLogger = { + log: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + trace: jest.fn(), + }; + + const mockOnTransfer = jest.fn(); + const mockEndConsultCall = jest.fn(); + const mockOnToggleConsultMute = jest.fn(); + + const defaultProps = { + agentName: 'Alice', + startTimeStamp: Date.now(), + onTransfer: mockOnTransfer, + endConsultCall: mockEndConsultCall, + onToggleConsultMute: mockOnToggleConsultMute, + consultCompleted: true, + isAgentBeingConsulted: true, + isEndConsultEnabled: true, + logger: mockLogger, + muteUnmute: true, + isMuted: false, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Rendering - Tests for UI elements and visual states of CallControlConsult component', () => { + it('should render the component with all control buttons', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-consult'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with muted state', async () => { + const mutedProps = {...defaultProps, isMuted: true}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-consult'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render without mute button when muteUnmute is false', async () => { + const propsWithoutMute = {...defaultProps, muteUnmute: false}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-consult'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render without transfer button when onTransfer is undefined', async () => { + const propsWithoutTransfer = {...defaultProps, onTransfer: undefined}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-consult'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with different agent name', async () => { + const propsWithDifferentAgent = {...defaultProps, agentName: 'Bob Johnson'}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-consult'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render when consultCompleted is false', async () => { + const propsIncompleteConsult = {...defaultProps, consultCompleted: false}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-consult'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render when isAgentBeingConsulted is false', async () => { + const propsNotBeingConsulted = {...defaultProps, isAgentBeingConsulted: false}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-consult'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render when isEndConsultEnabled is false', async () => { + const propsEndConsultDisabled = {...defaultProps, isEndConsultEnabled: false}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-consult'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + }); + + describe('Interactions', () => { + it('should call mockOnToggleConsultMute when mute button is clicked', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + const muteButton = screen.getByTestId('mute-consult-btn'); + fireEvent.click(muteButton); + + const container = screen.container.querySelector('.call-control-consult'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should call mockOnTransfer when transfer button is clicked', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + const transferButton = screen.getByTestId('transfer-consult-btn'); + fireEvent.click(transferButton); + + const container = screen.container.querySelector('.call-control-consult'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should call mockEndConsultCall when cancel button is clicked', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + const cancelButton = screen.getByTestId('cancel-consult-btn'); + fireEvent.click(cancelButton); + + const container = screen.container.querySelector('.call-control-consult'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + }); + + describe('State Management', () => { + it('should update when muteUnmute prop changes', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + screen.rerender(); + const container = screen.container.querySelector('.call-control-consult'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should update when isMuted prop changes', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + screen.rerender(); + const container = screen.container.querySelector('.call-control-consult'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should handle combination of props: no mute, no transfer, disabled end consult', async () => { + const complexProps = { + ...defaultProps, + muteUnmute: false, + onTransfer: undefined, + isEndConsultEnabled: false, + }; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-consult'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should handle edge case with empty agent name', async () => { + const propsEmptyAgent = {...defaultProps, agentName: ''}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-consult'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/call-control-consult.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/call-control-consult.tsx new file mode 100644 index 000000000..aa548b25d --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/call-control-consult.tsx @@ -0,0 +1,281 @@ +import React from 'react'; +import {render, fireEvent} from '@testing-library/react'; +import '@testing-library/jest-dom'; +import CallControlConsultComponent from '../../../../../src/components/task/CallControl/CallControlCustom/call-control-consult'; + +const loggerMock = { + log: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + trace: jest.fn(), + error: jest.fn(), + debug: jest.fn(), +}; + +// Mock Worker for TaskTimer component +class MockWorker { + public url: string; + public onmessage: ((event: MessageEvent) => void) | null; + + constructor(stringUrl: string) { + this.url = stringUrl; + this.onmessage = null; + } + + postMessage(msg: unknown) { + // Simulate worker timer behavior + if (this.onmessage) { + setTimeout(() => { + this.onmessage!({data: msg} as MessageEvent); + }, 0); + } + } + + addEventListener(type: string, listener: (event: MessageEvent) => void) { + if (type === 'message') { + this.onmessage = listener; + } + } + + removeEventListener() { + this.onmessage = null; + } + + terminate() { + // Mock terminate + } +} + +// Mock Worker and URL.createObjectURL for TaskTimer +(global as typeof globalThis).Worker = MockWorker as unknown as typeof Worker; +(global as typeof globalThis).URL.createObjectURL = jest.fn(() => 'blob:mock-url'); + +// Mock setTimeout for mute toggle tests +jest.useFakeTimers(); + +describe('CallControlConsultComponent', () => { + const mockOnTransfer = jest.fn(); + const mockEndConsultCall = jest.fn(); + const mockOnToggleConsultMute = jest.fn(); + + const defaultProps = { + agentName: 'Alice', + startTimeStamp: Date.now(), + onTransfer: mockOnTransfer, + endConsultCall: mockEndConsultCall, + onToggleConsultMute: mockOnToggleConsultMute, + consultCompleted: true, + isAgentBeingConsulted: true, + isEndConsultEnabled: true, + logger: loggerMock, + muteUnmute: true, + isMuted: false, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + it('renders consult component with agent information and control buttons', async () => { + const screen = await render(); + + // Verify main structure + expect(screen.container.querySelector('.call-control-consult')).toBeInTheDocument(); + expect(screen.container.querySelector('.consult-agent-name')).toHaveTextContent('Alice'); + + // Verify consult header structure + const consultHeader = screen.container.querySelector('.consult-header'); + expect(consultHeader).toBeInTheDocument(); + + // Verify avatar with correct attributes + const avatar = screen.container.querySelector('.task-avatar'); + expect(avatar).toBeInTheDocument(); + expect(avatar).toHaveAttribute('size', '32'); + + // Verify consult sub-text + const consultSubText = screen.container.querySelector('.consult-sub-text'); + expect(consultSubText).toBeInTheDocument(); + + // Verify all buttons are present with correct attributes + const muteButton = screen.getByTestId('mute-consult-btn'); + expect(muteButton).toBeInTheDocument(); + expect(muteButton).toHaveAttribute('data-disabled', 'false'); + expect(muteButton).toHaveAttribute('type', 'button'); + expect(muteButton).toHaveAttribute('data-color', 'primary'); + expect(muteButton).toHaveAttribute('data-size', '40'); + expect(muteButton).toHaveClass('call-control-button'); + + const transferButton = screen.getByTestId('transfer-consult-btn'); + expect(transferButton).toBeInTheDocument(); + expect(transferButton).toHaveAttribute('data-disabled', 'false'); // enabled when consultCompleted is true + expect(transferButton).toHaveAttribute('type', 'button'); + expect(transferButton).toHaveAttribute('data-color', 'primary'); + expect(transferButton).toHaveAttribute('data-size', '40'); + expect(transferButton).toHaveClass('call-control-button'); + + const cancelButton = screen.getByTestId('cancel-consult-btn'); + expect(cancelButton).toBeInTheDocument(); + expect(cancelButton).toHaveAttribute('data-disabled', 'false'); + expect(cancelButton).toHaveAttribute('type', 'button'); + expect(cancelButton).toHaveAttribute('data-color', 'primary'); + expect(cancelButton).toHaveAttribute('data-size', '40'); + expect(cancelButton).toHaveClass('call-control-consult-button-cancel'); + + // Verify button container + const buttonContainer = screen.container.querySelector('.consult-buttons-container'); + expect(buttonContainer).toBeInTheDocument(); + + // Verify icons are present + expect(screen.container.querySelector('.call-control-button-icon')).toBeInTheDocument(); + expect(screen.container.querySelector('.call-control-consult-button-cancel-icon')).toBeInTheDocument(); + }); + + it('handles button clicks correctly', async () => { + const screen = await render(); + + // Test mute button + const muteButton = screen.getByTestId('mute-consult-btn'); + expect(muteButton).toHaveAttribute('data-disabled', 'false'); + expect(muteButton).toHaveAttribute('type', 'button'); + expect(muteButton).toHaveAttribute('data-color', 'primary'); + fireEvent.click(muteButton); + expect(mockOnToggleConsultMute).toHaveBeenCalledTimes(1); + + // Test transfer button + const transferButton = screen.getByTestId('transfer-consult-btn'); + expect(transferButton).toHaveAttribute('data-disabled', 'false'); // Should be enabled when consultCompleted is true + expect(transferButton).toHaveAttribute('type', 'button'); + expect(transferButton).toHaveAttribute('data-color', 'primary'); + fireEvent.click(transferButton); + expect(mockOnTransfer).toHaveBeenCalledTimes(1); + + // Test end consult button + const cancelButton = screen.getByTestId('cancel-consult-btn'); + expect(cancelButton).toBeInTheDocument(); + expect(cancelButton).toHaveAttribute('data-disabled', 'false'); + expect(cancelButton).toHaveAttribute('type', 'button'); + fireEvent.click(cancelButton); + expect(mockEndConsultCall).toHaveBeenCalledTimes(1); + }); + + it('conditionally renders buttons based on props', async () => { + const propsWithoutMute = {...defaultProps, muteUnmute: false}; + const screen = await render(); + + // Mute button should not be rendered when muteUnmute is false + expect(screen.queryByTestId('mute-consult-btn')).not.toBeInTheDocument(); + + // Verify remaining buttons and their attributes + const transferButton = screen.getByTestId('transfer-consult-btn'); + expect(transferButton).toBeInTheDocument(); + expect(transferButton).toHaveAttribute('data-disabled', 'false'); + expect(transferButton).toHaveAttribute('type', 'button'); + expect(transferButton).toHaveClass('call-control-button'); + + const cancelButton = screen.getByTestId('cancel-consult-btn'); + expect(cancelButton).toBeInTheDocument(); + expect(cancelButton).toHaveAttribute('data-disabled', 'false'); + expect(cancelButton).toHaveAttribute('type', 'button'); + expect(cancelButton).toHaveClass('call-control-consult-button-cancel'); + }); + + it('handles case when onTransfer is undefined (covers line 52)', async () => { + const propsWithoutTransfer = {...defaultProps, onTransfer: undefined}; + const screen = await render(); + + // Component should still render without transfer functionality + expect(screen.container.querySelector('.call-control-consult')).toBeInTheDocument(); + + // Verify remaining buttons and their attributes + const muteButton = screen.getByTestId('mute-consult-btn'); + expect(muteButton).toBeInTheDocument(); + expect(muteButton).toHaveAttribute('data-disabled', 'false'); + expect(muteButton).toHaveAttribute('type', 'button'); + expect(muteButton).toHaveClass('call-control-button'); + + const cancelButton = screen.getByTestId('cancel-consult-btn'); + expect(cancelButton).toBeInTheDocument(); + expect(cancelButton).toHaveAttribute('data-disabled', 'false'); + expect(cancelButton).toHaveAttribute('type', 'button'); + expect(cancelButton).toHaveClass('call-control-consult-button-cancel'); + + // Transfer button should NOT be present when onTransfer is undefined + expect(screen.queryByTestId('transfer-consult-btn')).not.toBeInTheDocument(); + }); + + it('renders with muted state correctly', async () => { + const mutedProps = {...defaultProps, isMuted: true}; + const screen = await render(); + + const muteButton = screen.getByTestId('mute-consult-btn'); + expect(muteButton).toBeInTheDocument(); + expect(muteButton).toHaveAttribute('data-disabled', 'false'); + expect(muteButton).toHaveAttribute('type', 'button'); + expect(muteButton).toHaveAttribute('data-color', 'primary'); + expect(muteButton).toHaveAttribute('data-size', '40'); + expect(muteButton).toHaveClass('call-control-button-muted'); + + // Verify the muted icon is present + const mutedIcon = screen.container.querySelector('.call-control-button-muted-icon'); + expect(mutedIcon).toBeInTheDocument(); + }); + + it('tests button disabled states and tooltips', async () => { + const propsWithIncompleteConsult = {...defaultProps, consultCompleted: false}; + const screen = await render(); + + // Transfer button should be disabled when consultCompleted is false + const transferButton = screen.getByTestId('transfer-consult-btn'); + expect(transferButton).toHaveAttribute('data-disabled', 'true'); + expect(transferButton).toHaveAttribute('disabled', ''); + expect(transferButton).toHaveAttribute('type', 'button'); + expect(transferButton).toHaveAttribute('data-color', 'primary'); + + // Verify tooltip containers are present + const tooltips = screen.container.querySelectorAll('.md-tooltip-label'); + expect(tooltips.length).toBeGreaterThan(0); + }); + + it('verifies button icons and classes in different states', async () => { + const screen = await render(); + + // Verify unmuted microphone icon + const muteIcon = screen.container.querySelector('.call-control-button-icon'); + expect(muteIcon).toBeInTheDocument(); + + // Verify transfer icon (next-bold) + const transferIcon = screen.container.querySelector('.call-control-button-icon'); + expect(transferIcon).toBeInTheDocument(); + + // Verify cancel/end consult icon + const cancelIcon = screen.container.querySelector('.call-control-consult-button-cancel-icon'); + expect(cancelIcon).toBeInTheDocument(); + }); + + it('verifies accessibility attributes for all buttons', async () => { + const screen = await render(); + + // Check mute button accessibility + const muteButton = screen.getByTestId('mute-consult-btn'); + expect(muteButton).toHaveAttribute('aria-describedby'); + + // Check transfer button accessibility + const transferButton = screen.getByTestId('transfer-consult-btn'); + expect(transferButton).toHaveAttribute('aria-describedby'); + + // Check cancel button accessibility + const cancelButton = screen.getByTestId('cancel-consult-btn'); + expect(cancelButton).toHaveAttribute('aria-describedby'); + + // Verify tooltip labels exist and have content + const tooltipLabels = screen.container.querySelectorAll('.md-tooltip-label p'); + expect(tooltipLabels.length).toBe(3); + expect(tooltipLabels[0]).toHaveTextContent('Mute'); + expect(tooltipLabels[1]).toHaveTextContent('Transfer Consult'); + expect(tooltipLabels[2]).toHaveTextContent('End Consult'); + }); +}); diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/call-control-custom.util.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/call-control-custom.util.tsx new file mode 100644 index 000000000..c89750dea --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/call-control-custom.util.tsx @@ -0,0 +1,951 @@ +import '@testing-library/jest-dom'; +import {BuddyDetails, ContactServiceQueue} from '@webex/cc-store'; +import { + createConsultButtons, + getVisibleButtons, + createInitials, + handleTransferPress, + handleEndConsultPress, + handleMuteToggle, + getConsultStatusText, + handleListItemPress, + shouldShowTabs, + isAgentsEmpty, + isQueuesEmpty, + handleTabSelection, + handleAgentSelection, + handleQueueSelection, + getEmptyStateMessage, + createAgentListData, + createQueueListData, + createTimerKey, + handlePopoverOpen, + handlePopoverClose, + handleHoldToggle, + handleWrapupCall, + isValidMenuType, + getButtonStyleClass, + formatElapsedTime, + isAgentAvailable, + isQueueAvailable, + filterAvailableAgents, + filterAvailableQueues, + debounce, +} from '../../../../../src/components/task/CallControl/CallControlCustom/call-control-custom.utils'; + +const loggerMock = { + info: jest.fn(), + log: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + trace: jest.fn(), +}; + +const mockBuddyAgents: BuddyDetails[] = [ + { + agentId: 'agent1', + agentName: 'John Doe', + state: 'Available', + teamId: 'team1', + dn: 'dn1', + siteId: 'site1', + }, + { + agentId: 'agent2', + agentName: 'Jane Smith', + state: 'Available', + teamId: 'team1', + dn: 'dn2', + siteId: 'site1', + }, + { + agentId: 'agent3', + agentName: '', + state: 'Available', + teamId: 'team1', + dn: 'dn3', + siteId: 'site1', + }, + { + agentId: '', + agentName: 'Invalid Agent', + state: 'Available', + teamId: 'team1', + dn: 'dn4', + siteId: 'site1', + }, +]; + +const mockQueues: ContactServiceQueue[] = [ + { + id: 'queue1', + name: 'Support Queue', + description: 'Support Queue Description', + queueType: 'inbound', + checkAgentAvailability: true, + channelType: 'telephony', + } as ContactServiceQueue, + { + id: 'queue2', + name: 'Sales Queue', + description: 'Sales Queue Description', + queueType: 'inbound', + checkAgentAvailability: true, + channelType: 'telephony', + } as ContactServiceQueue, + { + id: 'queue3', + name: '', + description: 'Empty Name Queue', + queueType: 'inbound', + checkAgentAvailability: true, + channelType: 'telephony', + } as ContactServiceQueue, + { + id: '', + name: 'Invalid Queue', + description: 'Invalid Queue Description', + queueType: 'inbound', + checkAgentAvailability: true, + channelType: 'telephony', + } as ContactServiceQueue, +]; + +describe('Call Control Custom Utils', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('createConsultButtons', () => { + const defaultParams = { + isMuted: false, + isMuteDisabled: false, + consultCompleted: true, + isAgentBeingConsulted: true, + isEndConsultEnabled: true, + muteUnmute: true, + }; + + it('should create button configuration array with all buttons visible', () => { + const mockTransfer = jest.fn(); + const mockMuteToggle = jest.fn(); + const mockEndConsult = jest.fn(); + + const buttons = createConsultButtons( + defaultParams.isMuted, + defaultParams.isMuteDisabled, + defaultParams.consultCompleted, + defaultParams.isAgentBeingConsulted, + defaultParams.isEndConsultEnabled, + defaultParams.muteUnmute, + mockTransfer, + mockMuteToggle, + mockEndConsult + ); + + expect(buttons).toHaveLength(3); + expect(buttons[0].key).toBe('mute'); + expect(buttons[1].key).toBe('transfer'); + expect(buttons[2].key).toBe('cancel'); + }); + + it('should configure mute button correctly when muted', () => { + const buttons = createConsultButtons( + true, // isMuted + false, + defaultParams.consultCompleted, + defaultParams.isAgentBeingConsulted, + defaultParams.isEndConsultEnabled, + defaultParams.muteUnmute + ); + + const muteButton = buttons.find((b) => b.key === 'mute'); + expect(muteButton?.icon).toBe('microphone-muted-bold'); + expect(muteButton?.className).toBe('call-control-button-muted'); + expect(muteButton?.tooltip).toBe('Unmute'); + }); + + it('should configure mute button correctly when not muted', () => { + const buttons = createConsultButtons( + false, // isMuted + false, + defaultParams.consultCompleted, + defaultParams.isAgentBeingConsulted, + defaultParams.isEndConsultEnabled, + defaultParams.muteUnmute + ); + + const muteButton = buttons.find((b) => b.key === 'mute'); + expect(muteButton?.icon).toBe('microphone-bold'); + expect(muteButton?.className).toBe('call-control-button'); + expect(muteButton?.tooltip).toBe('Mute'); + }); + + it('should disable transfer button when consult not completed', () => { + const buttons = createConsultButtons( + defaultParams.isMuted, + defaultParams.isMuteDisabled, + false, // consultCompleted + defaultParams.isAgentBeingConsulted, + defaultParams.isEndConsultEnabled, + defaultParams.muteUnmute + ); + + const transferButton = buttons.find((b) => b.key === 'transfer'); + expect(transferButton?.disabled).toBe(true); + }); + + it('should hide transfer button when not agent being consulted or no onTransfer', () => { + const buttons = createConsultButtons( + defaultParams.isMuted, + defaultParams.isMuteDisabled, + defaultParams.consultCompleted, + false, // isAgentBeingConsulted + defaultParams.isEndConsultEnabled, + defaultParams.muteUnmute + ); + + const transferButton = buttons.find((b) => b.key === 'transfer'); + expect(transferButton?.shouldShow).toBe(false); + }); + + it('should hide mute button when muteUnmute is false', () => { + const buttons = createConsultButtons( + defaultParams.isMuted, + defaultParams.isMuteDisabled, + defaultParams.consultCompleted, + defaultParams.isAgentBeingConsulted, + defaultParams.isEndConsultEnabled, + false // muteUnmute + ); + + const muteButton = buttons.find((b) => b.key === 'mute'); + expect(muteButton?.shouldShow).toBe(false); + }); + }); + + describe('getVisibleButtons', () => { + it('should filter buttons that should be shown', () => { + const buttons = [ + {key: 'btn1', shouldShow: true, icon: '', onClick: jest.fn(), tooltip: '', className: ''}, + {key: 'btn2', shouldShow: false, icon: '', onClick: jest.fn(), tooltip: '', className: ''}, + {key: 'btn3', shouldShow: true, icon: '', onClick: jest.fn(), tooltip: '', className: ''}, + ]; + + const visible = getVisibleButtons(buttons); + expect(visible).toHaveLength(2); + expect(visible[0].key).toBe('btn1'); + expect(visible[1].key).toBe('btn3'); + }); + + it('should return empty array when no buttons should be shown', () => { + const buttons = [{key: 'btn1', shouldShow: false, icon: '', onClick: jest.fn(), tooltip: '', className: ''}]; + + const visible = getVisibleButtons(buttons); + expect(visible).toHaveLength(0); + }); + }); + + describe('createInitials', () => { + it('should create initials from full name', () => { + expect(createInitials('John Doe')).toBe('JD'); + }); + + it('should create initials from single name', () => { + expect(createInitials('John')).toBe('J'); + }); + + it('should create initials from multiple names, taking first two', () => { + expect(createInitials('John Michael Doe')).toBe('JM'); + }); + + it('should handle empty string', () => { + expect(createInitials('')).toBe(''); + }); + + it('should convert to uppercase', () => { + expect(createInitials('john doe')).toBe('JD'); + }); + + it('should handle names with extra spaces', () => { + expect(createInitials(' John Doe ')).toBe('JD'); + }); + }); + + describe('handleTransferPress', () => { + it('should call onTransfer and log when provided', () => { + const mockOnTransfer = jest.fn(); + + handleTransferPress(mockOnTransfer, loggerMock); + + expect(loggerMock.info).toHaveBeenCalledWith('CC-Widgets: CallControlConsult: transfer button clicked', { + module: 'call-control-consult.tsx', + method: 'handleTransfer', + }); + expect(mockOnTransfer).toHaveBeenCalled(); + expect(loggerMock.log).toHaveBeenCalledWith('CC-Widgets: CallControlConsult: transfer completed', { + module: 'call-control-consult.tsx', + method: 'handleTransfer', + }); + }); + + it('should not call onTransfer when not provided', () => { + expect(() => { + handleTransferPress(undefined, loggerMock); + }).not.toThrow(); + + expect(loggerMock.info).toHaveBeenCalled(); + expect(loggerMock.log).not.toHaveBeenCalled(); + }); + + it('should throw error when onTransfer throws', () => { + const mockOnTransfer = jest.fn(() => { + throw new Error('Transfer failed'); + }); + + expect(() => { + handleTransferPress(mockOnTransfer, loggerMock); + }).toThrow('Error transferring call: Error: Transfer failed'); + }); + }); + + describe('handleEndConsultPress', () => { + it('should call endConsultCall and log when provided', () => { + const mockEndConsult = jest.fn(); + + handleEndConsultPress(mockEndConsult, loggerMock); + + expect(loggerMock.info).toHaveBeenCalledWith('CC-Widgets: CallControlConsult: end consult clicked', { + module: 'call-control-consult.tsx', + method: 'handleEndConsult', + }); + expect(mockEndConsult).toHaveBeenCalled(); + expect(loggerMock.log).toHaveBeenCalledWith('CC-Widgets: CallControlConsult: end consult completed', { + module: 'call-control-consult.tsx', + method: 'handleEndConsult', + }); + }); + + it('should throw error when endConsultCall throws', () => { + const mockEndConsult = jest.fn(() => { + throw new Error('End consult failed'); + }); + + expect(() => { + handleEndConsultPress(mockEndConsult, loggerMock); + }).toThrow('Error ending consult call: Error: End consult failed'); + }); + }); + + describe('handleMuteToggle', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should disable button, call toggle, and re-enable after timeout', () => { + const mockToggleMute = jest.fn(); + const mockSetDisabled = jest.fn(); + + handleMuteToggle(mockToggleMute, mockSetDisabled, loggerMock); + + expect(mockSetDisabled).toHaveBeenCalledWith(true); + expect(mockToggleMute).toHaveBeenCalled(); + + jest.advanceTimersByTime(500); + + expect(mockSetDisabled).toHaveBeenCalledWith(false); + }); + + it('should handle error and still re-enable button', () => { + const mockToggleMute = jest.fn(() => { + throw new Error('Mute failed'); + }); + const mockSetDisabled = jest.fn(); + + handleMuteToggle(mockToggleMute, mockSetDisabled, loggerMock); + + expect(loggerMock.error).toHaveBeenCalledWith('Mute toggle failed: Error: Mute failed', { + module: 'call-control-consult.tsx', + method: 'handleConsultMuteToggle', + }); + + jest.advanceTimersByTime(500); + expect(mockSetDisabled).toHaveBeenCalledWith(false); + }); + + it('should not call toggle when not provided', () => { + const mockSetDisabled = jest.fn(); + + expect(() => { + handleMuteToggle(undefined, mockSetDisabled, loggerMock); + }).not.toThrow(); + + expect(mockSetDisabled).toHaveBeenCalledWith(true); + }); + }); + + describe('getConsultStatusText', () => { + it('should return "Consulting" when completed', () => { + expect(getConsultStatusText(true)).toBe('Consulting'); + }); + + it('should return "Consult requested" when not completed', () => { + expect(getConsultStatusText(false)).toBe('Consult requested'); + }); + }); + + describe('handleListItemPress', () => { + it('should call onButtonPress and log', () => { + const mockButtonPress = jest.fn(); + const title = 'Test Agent'; + + handleListItemPress(title, mockButtonPress, loggerMock); + + expect(loggerMock.info).toHaveBeenCalledWith( + `CC-Widgets: ConsultTransferListComponent: button pressed: ${title}`, + { + module: 'consult-transfer-list-item.tsx', + method: 'handleButtonPress', + } + ); + expect(mockButtonPress).toHaveBeenCalled(); + }); + }); + + describe('shouldShowTabs', () => { + it('should return true when agents exist', () => { + expect(shouldShowTabs(mockBuddyAgents, [])).toBe(true); + }); + + it('should return true when queues exist', () => { + expect(shouldShowTabs([], mockQueues)).toBe(true); + }); + + it('should return true when both exist', () => { + expect(shouldShowTabs(mockBuddyAgents, mockQueues)).toBe(true); + }); + + it('should return false when both are empty', () => { + expect(shouldShowTabs([], [])).toBe(false); + }); + + it('should return false when both are null/undefined', () => { + expect(shouldShowTabs(null!, undefined!)).toBe(false); + }); + }); + + describe('isAgentsEmpty', () => { + it('should return false when agents exist', () => { + expect(isAgentsEmpty(mockBuddyAgents)).toBe(false); + }); + + it('should return true when agents array is empty', () => { + expect(isAgentsEmpty([])).toBe(true); + }); + + it('should return true when agents is null', () => { + expect(isAgentsEmpty(null!)).toBe(true); + }); + + it('should return true when agents is undefined', () => { + expect(isAgentsEmpty(undefined!)).toBe(true); + }); + }); + + describe('isQueuesEmpty', () => { + it('should return false when queues exist', () => { + expect(isQueuesEmpty(mockQueues)).toBe(false); + }); + + it('should return true when queues array is empty', () => { + expect(isQueuesEmpty([])).toBe(true); + }); + + it('should return true when queues is null', () => { + expect(isQueuesEmpty(null!)).toBe(true); + }); + + it('should return true when queues is undefined', () => { + expect(isQueuesEmpty(undefined!)).toBe(true); + }); + }); + + describe('handleTabSelection', () => { + it('should set selected tab and log', () => { + const mockSetSelectedTab = jest.fn(); + const key = 'Agents'; + + handleTabSelection(key, mockSetSelectedTab, loggerMock); + + expect(mockSetSelectedTab).toHaveBeenCalledWith(key); + expect(loggerMock.log).toHaveBeenCalledWith(`CC-Widgets: ConsultTransferPopover: tab selected: ${key}`, { + module: 'consult-transfer-popover.tsx', + method: 'onTabSelection', + }); + }); + }); + + describe('handleAgentSelection', () => { + it('should call onAgentSelect and log when provided', () => { + const mockOnAgentSelect = jest.fn(); + const agentId = 'agent1'; + const agentName = 'John Doe'; + + handleAgentSelection(agentId, agentName, mockOnAgentSelect, loggerMock); + + expect(loggerMock.info).toHaveBeenCalledWith(`CC-Widgets: ConsultTransferPopover: agent selected: ${agentId}`, { + module: 'consult-transfer-popover.tsx', + method: 'onAgentSelect', + }); + expect(mockOnAgentSelect).toHaveBeenCalledWith(agentId, agentName); + }); + + it('should not call onAgentSelect when not provided', () => { + expect(() => { + handleAgentSelection('agent1', 'John Doe', undefined, loggerMock); + }).not.toThrow(); + + expect(loggerMock.info).toHaveBeenCalled(); + }); + }); + + describe('handleQueueSelection', () => { + it('should call onQueueSelect and log when provided', () => { + const mockOnQueueSelect = jest.fn(); + const queueId = 'queue1'; + const queueName = 'Support Queue'; + + handleQueueSelection(queueId, queueName, mockOnQueueSelect, loggerMock); + + expect(loggerMock.log).toHaveBeenCalledWith(`CC-Widgets: ConsultTransferPopover: queue selected: ${queueId}`, { + module: 'consult-transfer-popover.tsx', + method: 'onQueueSelect', + }); + expect(mockOnQueueSelect).toHaveBeenCalledWith(queueId, queueName); + }); + + it('should not call onQueueSelect when not provided', () => { + expect(() => { + handleQueueSelection('queue1', 'Support Queue', undefined, loggerMock); + }).not.toThrow(); + + expect(loggerMock.log).toHaveBeenCalled(); + }); + }); + + describe('getEmptyStateMessage', () => { + it('should return general message when tabs are not shown', () => { + const message = getEmptyStateMessage('Agents', false); + expect(message).toBe("We can't find any queue or agent available for now."); + }); + + it('should return agents message when Agents tab is selected', () => { + const message = getEmptyStateMessage('Agents', true); + expect(message).toBe("We can't find any agent available for now."); + }); + + it('should return queues message when Queues tab is selected', () => { + const message = getEmptyStateMessage('Queues', true); + expect(message).toBe("We can't find any queue available for now."); + }); + + it('should return queues message for any other tab', () => { + const message = getEmptyStateMessage('SomeOtherTab', true); + expect(message).toBe("We can't find any queue available for now."); + }); + }); + + describe('createAgentListData', () => { + it('should transform buddy agents to list data', () => { + const result = createAgentListData(mockBuddyAgents); + + expect(result).toHaveLength(4); + expect(result[0]).toEqual({id: 'agent1', name: 'John Doe'}); + expect(result[1]).toEqual({id: 'agent2', name: 'Jane Smith'}); + }); + + it('should handle empty array', () => { + const result = createAgentListData([]); + expect(result).toEqual([]); + }); + }); + + describe('createQueueListData', () => { + it('should transform queues to list data', () => { + const result = createQueueListData(mockQueues); + + expect(result).toHaveLength(4); + expect(result[0]).toEqual({id: 'queue1', name: 'Support Queue'}); + expect(result[1]).toEqual({id: 'queue2', name: 'Sales Queue'}); + }); + + it('should handle empty array', () => { + const result = createQueueListData([]); + expect(result).toEqual([]); + }); + }); + + describe('createTimerKey', () => { + it('should create timer key with timestamp', () => { + const timestamp = 1234567890; + expect(createTimerKey(timestamp)).toBe('timer-1234567890'); + }); + + it('should handle zero timestamp', () => { + expect(createTimerKey(0)).toBe('timer-0'); + }); + }); + + describe('handlePopoverOpen', () => { + it('should set active menu and log', () => { + const mockSetActiveMenu = jest.fn(); + const menuType = 'Consult'; + + handlePopoverOpen(menuType, mockSetActiveMenu, loggerMock); + + expect(loggerMock.info).toHaveBeenCalledWith(`CC-Widgets: CallControl: opening ${menuType} popover`, { + module: 'call-control.tsx', + method: 'handlePopoverOpen', + }); + expect(mockSetActiveMenu).toHaveBeenCalledWith(menuType); + }); + }); + + describe('handlePopoverClose', () => { + it('should set active menu to null and log', () => { + const mockSetActiveMenu = jest.fn(); + + handlePopoverClose(mockSetActiveMenu, loggerMock); + + expect(loggerMock.info).toHaveBeenCalledWith('CC-Widgets: CallControl: closing popover', { + module: 'call-control.tsx', + method: 'handlePopoverClose', + }); + expect(mockSetActiveMenu).toHaveBeenCalledWith(null); + }); + }); + + describe('handleHoldToggle', () => { + it('should call toggleHold and log when provided', () => { + const mockToggleHold = jest.fn(); + + handleHoldToggle(mockToggleHold, loggerMock); + + expect(loggerMock.info).toHaveBeenCalledWith('CC-Widgets: CallControl: hold toggle clicked', { + module: 'call-control.tsx', + method: 'handleHoldToggle', + }); + expect(mockToggleHold).toHaveBeenCalled(); + }); + + it('should not call toggleHold when not provided', () => { + expect(() => { + handleHoldToggle(undefined, loggerMock); + }).not.toThrow(); + + expect(loggerMock.info).toHaveBeenCalled(); + }); + }); + + describe('handleWrapupCall', () => { + it('should call onWrapupCall and log when provided', () => { + const mockWrapupCall = jest.fn(); + + handleWrapupCall(mockWrapupCall, loggerMock); + + expect(loggerMock.info).toHaveBeenCalledWith('CC-Widgets: CallControl: wrapup call clicked', { + module: 'call-control.tsx', + method: 'handleWrapupCall', + }); + expect(mockWrapupCall).toHaveBeenCalled(); + }); + + it('should not call onWrapupCall when not provided', () => { + expect(() => { + handleWrapupCall(undefined, loggerMock); + }).not.toThrow(); + + expect(loggerMock.info).toHaveBeenCalled(); + }); + }); + + describe('isValidMenuType', () => { + it('should return true for valid menu types', () => { + expect(isValidMenuType('Consult')).toBe(true); + expect(isValidMenuType('Transfer')).toBe(true); + }); + + it('should return false for invalid menu types', () => { + expect(isValidMenuType('Invalid')).toBe(false); + expect(isValidMenuType('')).toBe(false); + expect(isValidMenuType('consult')).toBe(false); // case sensitive + }); + }); + + describe('getButtonStyleClass', () => { + it('should return disabled class when disabled', () => { + expect(getButtonStyleClass(false, true)).toBe('call-control-button-disabled'); + }); + + it('should return active class when active and not disabled', () => { + expect(getButtonStyleClass(true, false)).toBe('call-control-button-active'); + }); + + it('should return base class when neither active nor disabled', () => { + expect(getButtonStyleClass(false, false)).toBe('call-control-button'); + }); + + it('should use custom base class', () => { + expect(getButtonStyleClass(false, false, 'custom-button')).toBe('custom-button'); + expect(getButtonStyleClass(true, false, 'custom-button')).toBe('custom-button-active'); + expect(getButtonStyleClass(false, true, 'custom-button')).toBe('custom-button-disabled'); + }); + }); + + describe('formatElapsedTime', () => { + beforeEach(() => { + jest.spyOn(Date, 'now').mockImplementation(() => 1000000); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should format minutes and seconds', () => { + const startTime = 1000000 - 90000; // 90 seconds ago + expect(formatElapsedTime(startTime)).toBe('1:30'); + }); + + it('should format hours, minutes and seconds', () => { + const startTime = 1000000 - 3661000; // 1 hour 1 minute 1 second ago + expect(formatElapsedTime(startTime)).toBe('1:01:01'); + }); + + it('should handle zero elapsed time', () => { + const startTime = 1000000; + expect(formatElapsedTime(startTime)).toBe('0:00'); + }); + + it('should pad single digits correctly', () => { + const startTime = 1000000 - 9000; // 9 seconds ago + expect(formatElapsedTime(startTime)).toBe('0:09'); + }); + }); + + describe('isAgentAvailable', () => { + it('should return true for valid agent', () => { + expect( + isAgentAvailable({ + agentId: 'agent1', + agentName: 'John Doe', + state: 'Available', + teamId: 'team1', + dn: 'dn1', + siteId: 'site1', + }) + ).toBe(true); + }); + + it('should return false for missing agentId', () => { + expect( + isAgentAvailable({ + agentId: '', + agentName: 'John Doe', + state: 'Available', + teamId: 'team1', + dn: 'dn1', + siteId: 'site1', + }) + ).toBeFalsy(); + }); + + it('should return false for missing agentName', () => { + expect( + isAgentAvailable({ + agentId: 'agent1', + agentName: '', + state: 'Available', + teamId: 'team1', + dn: 'dn1', + siteId: 'site1', + } as BuddyDetails) + ).toBeFalsy(); + }); + + it('should return false for whitespace-only agentName', () => { + expect( + isAgentAvailable({ + agentId: 'agent1', + agentName: ' ', + state: 'Available', + teamId: 'team1', + dn: 'dn1', + siteId: 'site1', + } as BuddyDetails) + ).toBeFalsy(); + }); + + it('should return false for null agent', () => { + expect(isAgentAvailable(null!)).toBeFalsy(); + }); + + it('should return false for undefined agent', () => { + expect(isAgentAvailable(undefined!)).toBeFalsy(); + }); + }); + + describe('isQueueAvailable', () => { + it('should return true for valid queue', () => { + expect( + isQueueAvailable({ + id: 'queue1', + name: 'Support Queue', + description: 'Support Queue Description', + queueType: 'inbound', + checkAgentAvailability: true, + channelType: 'telephony', + } as ContactServiceQueue) + ).toBe(true); + }); + + it('should return false for missing id', () => { + expect( + isQueueAvailable({ + id: '', + name: 'Support Queue', + description: 'Support Queue Description', + queueType: 'inbound', + checkAgentAvailability: true, + channelType: 'telephony', + } as ContactServiceQueue) + ).toBeFalsy(); + }); + + it('should return false for missing name', () => { + expect( + isQueueAvailable({ + id: 'queue1', + name: '', + description: 'Support Queue Description', + queueType: 'inbound', + checkAgentAvailability: true, + channelType: 'telephony', + } as ContactServiceQueue) + ).toBeFalsy(); + }); + + it('should return false for whitespace-only name', () => { + expect( + isQueueAvailable({ + id: 'queue1', + name: ' ', + description: 'Support Queue Description', + queueType: 'inbound', + checkAgentAvailability: true, + channelType: 'telephony', + } as ContactServiceQueue) + ).toBeFalsy(); + }); + + it('should return false for null queue', () => { + expect(isQueueAvailable(null!)).toBeFalsy(); + }); + + it('should return false for undefined queue', () => { + expect(isQueueAvailable(undefined!)).toBeFalsy(); + }); + }); + + describe('filterAvailableAgents', () => { + it('should filter out invalid agents', () => { + const result = filterAvailableAgents(mockBuddyAgents); + expect(result).toHaveLength(2); + expect(result[0]).toEqual(expect.objectContaining({agentId: 'agent1', agentName: 'John Doe'})); + expect(result[1]).toEqual(expect.objectContaining({agentId: 'agent2', agentName: 'Jane Smith'})); + }); + + it('should return empty array for null input', () => { + expect(filterAvailableAgents(null!)).toEqual([]); + }); + + it('should return empty array for undefined input', () => { + expect(filterAvailableAgents(undefined!)).toEqual([]); + }); + }); + + describe('filterAvailableQueues', () => { + it('should filter out invalid queues', () => { + const result = filterAvailableQueues(mockQueues); + expect(result).toHaveLength(2); + expect(result[0]).toEqual(expect.objectContaining({id: 'queue1', name: 'Support Queue'})); + expect(result[1]).toEqual(expect.objectContaining({id: 'queue2', name: 'Sales Queue'})); + }); + + it('should return empty array for null input', () => { + expect(filterAvailableQueues(null!)).toEqual([]); + }); + + it('should return empty array for undefined input', () => { + expect(filterAvailableQueues(undefined!)).toEqual([]); + }); + }); + + describe('debounce', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should debounce function calls', () => { + const mockFn = jest.fn(); + const debouncedFn = debounce(mockFn, 100); + + debouncedFn('arg1'); + debouncedFn('arg2'); + debouncedFn('arg3'); + + expect(mockFn).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(100); + + expect(mockFn).toHaveBeenCalledTimes(1); + expect(mockFn).toHaveBeenCalledWith('arg3'); + }); + + it('should clear previous timeout on new call', () => { + const mockFn = jest.fn(); + const debouncedFn = debounce(mockFn, 100); + + debouncedFn('arg1'); + jest.advanceTimersByTime(50); + + debouncedFn('arg2'); + jest.advanceTimersByTime(50); + + expect(mockFn).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(50); + + expect(mockFn).toHaveBeenCalledTimes(1); + expect(mockFn).toHaveBeenCalledWith('arg2'); + }); + + it('should handle multiple arguments', () => { + const mockFn = jest.fn(); + const debouncedFn = debounce(mockFn, 100); + + debouncedFn('arg1', 'arg2', 'arg3'); + + jest.advanceTimersByTime(100); + + expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2', 'arg3'); + }); + }); +}); diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-empty-state.snapshot.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-empty-state.snapshot.tsx new file mode 100644 index 000000000..d9543b946 --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-empty-state.snapshot.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import '@testing-library/jest-dom'; +import {render, act} from '@testing-library/react'; +import ConsultTransferEmptyState from '../../../../../src/components/task/CallControl/CallControlCustom/consult-transfer-empty-state'; + +const mockUIDProps = (container) => { + container + .querySelectorAll('[id^="mdc-input"]') + .forEach((el: HTMLBaseElement) => el.setAttribute('id', 'mock-input-id')); + container + .querySelectorAll('[id^="mdc-tooltip"]') + .forEach((el: HTMLBaseElement) => el.setAttribute('id', 'mock-tooltip-id')); + container + .querySelectorAll('[aria-describedby^="mdc-tooltip"]') + .forEach((el: HTMLBaseElement) => el.setAttribute('aria-describedby', 'mock-aria-describedby')); +}; + +describe('ConsultTransferEmptyState Snapshots', () => { + const defaultProps = { + message: 'No agents or queues available', + }; + + describe('Rendering - Tests for UI elements and visual states of ConsultTransferEmptyState component', () => { + it('should render the component with default message', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.consult-empty-state'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with custom message', async () => { + const customProps = {...defaultProps, message: 'No items found'}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.consult-empty-state'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with long message', async () => { + const longMessageProps = { + ...defaultProps, + message: + 'This is a very long message that might wrap to multiple lines to test how the component handles extended text content', + }; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.consult-empty-state'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with empty message', async () => { + const emptyMessageProps = {...defaultProps, message: ''}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.consult-empty-state'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with HTML characters in message', async () => { + const htmlMessageProps = {...defaultProps, message: 'No agents & queues '}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.consult-empty-state'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with special characters in message', async () => { + const specialMessageProps = {...defaultProps, message: 'No agents/queues! @#$%^&*()'}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.consult-empty-state'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-empty-state.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-empty-state.tsx new file mode 100644 index 000000000..bff2e7bcd --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-empty-state.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import {render} from '@testing-library/react'; +import '@testing-library/jest-dom'; +import ConsultTransferEmptyState from '../../../../../src/components/task/CallControl/CallControlCustom/consult-transfer-empty-state'; + +describe('ConsultTransferEmptyState', () => { + it('renders empty state with message', async () => { + const message = 'No agents or queues available'; + const screen = await render(); + + // Verify main container + const emptyStateContainer = screen.container.querySelector('.consult-empty-state'); + expect(emptyStateContainer).toBeInTheDocument(); + + // Verify SVG placeholder + const svgPlaceholder = screen.container.querySelector('.consult-empty-state-svg'); + expect(svgPlaceholder).toBeInTheDocument(); + + // Verify message + const messageElement = screen.container.querySelector('.consult-empty-message'); + expect(messageElement).toBeInTheDocument(); + expect(messageElement).toHaveTextContent(message); + }); + + it('renders with different message', async () => { + const customMessage = 'No items found'; + const screen = await render(); + + const messageElement = screen.container.querySelector('.consult-empty-message'); + expect(messageElement).toHaveTextContent(customMessage); + }); +}); diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-list-item.snapshot.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-list-item.snapshot.tsx new file mode 100644 index 000000000..af012e5af --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-list-item.snapshot.tsx @@ -0,0 +1,239 @@ +import React from 'react'; +import '@testing-library/jest-dom'; +import {render, fireEvent, act} from '@testing-library/react'; +import ConsultTransferListComponent from '../../../../../src/components/task/CallControl/CallControlCustom/consult-transfer-list-item'; + +const mockUIDProps = (container) => { + container + .querySelectorAll('[id^="mdc-input"]') + .forEach((el: HTMLBaseElement) => el.setAttribute('id', 'mock-input-id')); + container + .querySelectorAll('[id^="mdc-tooltip"]') + .forEach((el: HTMLBaseElement) => el.setAttribute('id', 'mock-tooltip-id')); + container + .querySelectorAll('[aria-describedby^="mdc-tooltip"]') + .forEach((el: HTMLBaseElement) => el.setAttribute('aria-describedby', 'mock-aria-describedby')); +}; + +// Mock lottie-web +jest.mock('lottie-web'); + +describe('ConsultTransferListComponent Snapshots', () => { + const mockLogger = { + log: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + trace: jest.fn(), + }; + + const mockOnButtonPress = jest.fn(); + + const defaultProps = { + title: 'John Doe', + subtitle: 'Manager', + buttonIcon: 'test-icon', + onButtonPress: mockOnButtonPress, + className: 'custom-class', + logger: mockLogger, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Rendering - Tests for UI elements and visual states of ConsultTransferListComponent component', () => { + it('should render the component with title and subtitle', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-list-item'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with long name generating different initials', async () => { + const longNameProps = {...defaultProps, title: 'Alexander Benjamin Christopher'}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-list-item'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with single name', async () => { + const singleNameProps = {...defaultProps, title: 'John'}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-list-item'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render without subtitle', async () => { + const noSubtitleProps = {...defaultProps, subtitle: ''}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-list-item'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with different className', async () => { + const customClassProps = {...defaultProps, className: 'different-custom-class'}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-list-item'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with different button icon', async () => { + const differentIconProps = {...defaultProps, buttonIcon: 'different-icon'}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-list-item'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render without className', async () => { + const noClassProps = {...defaultProps, className: undefined}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-list-item'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with special characters in title', async () => { + const specialCharsProps = {...defaultProps, title: "John O'Connor & Jane"}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-list-item'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with long subtitle', async () => { + const longSubtitleProps = { + ...defaultProps, + subtitle: 'Senior Manager of Customer Relations and Business Development', + }; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-list-item'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + }); + + describe('Interactions', () => { + it('should render component after button click', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + const button = screen.container.querySelector('button'); + if (button) { + fireEvent.click(button); + } + + const container = screen.container.querySelector('.call-control-list-item'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render component after list item click', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + const listItem = screen.container.querySelector('.call-control-list-item'); + fireEvent.click(listItem); + + mockUIDProps(listItem); + expect(listItem).toMatchSnapshot(); + }); + + it('should render component after keyboard interaction', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + const listItem = screen.container.querySelector('.call-control-list-item'); + fireEvent.keyDown(listItem, {key: 'Enter', code: 'Enter'}); + + mockUIDProps(listItem); + expect(listItem).toMatchSnapshot(); + }); + }); + + describe('State Management', () => { + it('should update when title changes', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + screen.rerender(); + const container = screen.container.querySelector('.call-control-list-item'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should update when subtitle changes', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + screen.rerender(); + const container = screen.container.querySelector('.call-control-list-item'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should update when buttonIcon changes', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + screen.rerender(); + const container = screen.container.querySelector('.call-control-list-item'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-list-item.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-list-item.tsx new file mode 100644 index 000000000..95603e202 --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-list-item.tsx @@ -0,0 +1,148 @@ +import React from 'react'; +import {render, fireEvent} from '@testing-library/react'; +import '@testing-library/jest-dom'; +import ConsultTransferListComponent from '../../../../../src/components/task/CallControl/CallControlCustom/consult-transfer-list-item'; + +const loggerMock = { + log: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + trace: jest.fn(), + error: jest.fn(), +}; + +jest.mock('lottie-web'); + +beforeAll(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); +}); + +afterAll(() => { + (console.error as jest.Mock).mockRestore(); +}); + +// This test suite was previously skipped but is now enabled for 100% coverage +describe('CallControlListItemPresentational', () => { + const mockOnButtonPress = jest.fn(); + const defaultProps = { + title: 'John Doe', + subtitle: 'Manager', + buttonIcon: 'test-icon', + onButtonPress: mockOnButtonPress, + className: 'custom-class', + logger: loggerMock, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders with title, subtitle, and correct initials', async () => { + const screen = await render(); + + // Verify main list item container + const listItem = screen.container.querySelector('.call-control-list-item'); + expect(listItem).toBeInTheDocument(); + expect(listItem).toHaveClass('custom-class'); + expect(listItem).toHaveAttribute('role', 'listitem'); + expect(listItem).toHaveAttribute('aria-label', 'John Doe'); + expect(listItem).toHaveAttribute('data-size', '50'); + expect(listItem).toHaveAttribute('data-padded', 'true'); + expect(listItem).toHaveAttribute('data-interactive', 'true'); + expect(listItem).toHaveAttribute('data-disabled', 'false'); + expect(listItem).toHaveAttribute('data-shape', 'rectangle'); + expect(listItem).toHaveAttribute('tabindex', '0'); + + // Verify avatar section + const avatarWrapper = screen.container.querySelector('.md-avatar-wrapper'); + expect(avatarWrapper).toBeInTheDocument(); + expect(avatarWrapper).toHaveAttribute('role', 'img'); + expect(avatarWrapper).toHaveAttribute('data-size', '32'); + expect(avatarWrapper).toHaveAttribute('data-color', 'default'); + expect(avatarWrapper).toHaveAttribute('aria-hidden', 'false'); + + // Verify initials display + const initialsSpan = screen.container.querySelector('.md-avatar-wrapper-children'); + expect(initialsSpan).toBeInTheDocument(); + expect(initialsSpan).toHaveTextContent('JD'); + expect(initialsSpan).toHaveAttribute('aria-hidden', 'true'); + + // Verify middle section with title and subtitle + const middleSection = screen.container.querySelector('[data-position="middle"]'); + expect(middleSection).toBeInTheDocument(); + expect(middleSection).toHaveStyle({ + flex: '1', + display: 'flex', + flexDirection: 'column', + marginLeft: '8px', + minWidth: '0', + overflow: 'hidden', + }); + + // Verify title text + const titleElements = screen.container.querySelectorAll('mdc-text'); + expect(titleElements[0]).toHaveTextContent('John Doe'); + expect(titleElements[0]).toHaveAttribute('tagname', 'p'); + expect(titleElements[0]).toHaveAttribute('type', 'body-large-regular'); + expect(titleElements[0]).toHaveStyle({ + margin: '0px', + lineHeight: '1.2', + }); + + // Verify subtitle text + expect(titleElements[1]).toHaveTextContent('Manager'); + expect(titleElements[1]).toHaveAttribute('tagname', 'p'); + expect(titleElements[1]).toHaveAttribute('type', 'body-midsize-regular'); + expect(titleElements[1]).toHaveStyle({ + margin: '0px', + lineHeight: '1.2', + }); + + // Verify end section with button + const endSection = screen.container.querySelector('[data-position="end"]'); + expect(endSection).toBeInTheDocument(); + + // Verify hover button container + const hoverButton = screen.container.querySelector('.hover-button'); + expect(hoverButton).toBeInTheDocument(); + + // Verify button attributes + const button = screen.container.querySelector('button'); + expect(button).toBeInTheDocument(); + expect(button).toHaveAttribute('data-color', 'join'); + expect(button).toHaveAttribute('data-size', '28'); + expect(button).toHaveAttribute('data-disabled', 'false'); + expect(button).toHaveAttribute('tabindex', '-1'); + expect(button).toHaveAttribute('type', 'button'); + + // Verify icon + const icon = screen.container.querySelector('mdc-icon'); + expect(icon).toBeInTheDocument(); + expect(icon).toHaveAttribute('name', 'test-icon'); + }); + + it('handles button click correctly', async () => { + const screen = await render(); + + // Click the button + const button = screen.container.querySelector('button'); + fireEvent.click(button!); + + // Verify onButtonPress was called + expect(mockOnButtonPress).toHaveBeenCalledTimes(1); + }); + + it('renders without subtitle when not provided', async () => { + const propsWithoutSubtitle = { + ...defaultProps, + subtitle: undefined, + }; + + const screen = await render(); + + // Should only have one mdc-text element (title only) + const textElements = screen.container.querySelectorAll('mdc-text'); + expect(textElements).toHaveLength(1); + expect(textElements[0]).toHaveTextContent('John Doe'); + }); +}); diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.snapshot.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.snapshot.tsx new file mode 100644 index 000000000..baf2dc497 --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.snapshot.tsx @@ -0,0 +1,339 @@ +import React from 'react'; +import '@testing-library/jest-dom'; +import {render, fireEvent, act} from '@testing-library/react'; +import ConsultTransferPopoverComponent from '../../../../../src/components/task/CallControl/CallControlCustom/consult-transfer-popover'; +import {ContactServiceQueue} from '@webex/cc-store'; + +const mockUIDProps = (container) => { + container + .querySelectorAll('[id^="mdc-input"]') + .forEach((el: HTMLBaseElement) => el.setAttribute('id', 'mock-input-id')); + container + .querySelectorAll('[id^="mdc-tooltip"]') + .forEach((el: HTMLBaseElement) => el.setAttribute('id', 'mock-tooltip-id')); + container + .querySelectorAll('[aria-describedby^="mdc-tooltip"]') + .forEach((el: HTMLBaseElement) => el.setAttribute('aria-describedby', 'mock-aria-describedby')); +}; + +describe('ConsultTransferPopoverComponent Snapshots', () => { + const mockLogger = { + log: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + trace: jest.fn(), + }; + + const mockOnAgentSelect = jest.fn(); + const mockOnQueueSelect = jest.fn(); + + const defaultProps = { + heading: 'Select an Agent', + buttonIcon: 'agent-icon', + buddyAgents: [ + { + agentId: 'agent1', + agentName: 'Agent One', + dn: '1001', + state: 'Available', + teamId: 'team1', + siteId: 'site1', + }, + { + agentId: 'agent2', + agentName: 'Agent Two', + dn: '1002', + state: 'Available', + teamId: 'team1', + siteId: 'site1', + }, + ], + queues: [ + {id: 'queue1', name: 'Queue One'} as ContactServiceQueue, + {id: 'queue2', name: 'Queue Two'} as ContactServiceQueue, + ], + onAgentSelect: mockOnAgentSelect, + onQueueSelect: mockOnQueueSelect, + allowConsultToQueue: true, + logger: mockLogger, + showTabs: true, + emptyMessage: 'No agents or queues available', + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component', () => { + it('should render the component with tabs and agents', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render without tabs when showTabs is false', async () => { + const noTabsProps = {...defaultProps, showTabs: false}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with different heading', async () => { + const customHeadingProps = {...defaultProps, heading: 'Choose Transfer Target'}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with empty agents list', async () => { + const emptyAgentsProps = {...defaultProps, buddyAgents: []}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with empty queues list', async () => { + const emptyQueuesProps = {...defaultProps, queues: []}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with allowConsultToQueue false', async () => { + const noQueueConsultProps = {...defaultProps, allowConsultToQueue: false}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with custom empty message', async () => { + const customEmptyProps = { + ...defaultProps, + buddyAgents: [], + queues: [], + emptyMessage: 'Custom empty state message', + }; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with single agent', async () => { + const singleAgentProps = { + ...defaultProps, + buddyAgents: [ + { + agentId: 'agent1', + agentName: 'Single Agent', + dn: '1001', + state: 'Available', + teamId: 'team1', + siteId: 'site1', + }, + ], + }; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with single queue', async () => { + const singleQueueProps = { + ...defaultProps, + queues: [{id: 'queue1', name: 'Single Queue'} as ContactServiceQueue], + }; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with agents having different states', async () => { + const mixedStateAgentsProps = { + ...defaultProps, + buddyAgents: [ + { + agentId: 'agent1', + agentName: 'Available Agent', + dn: '1001', + state: 'Available', + teamId: 'team1', + siteId: 'site1', + }, + { + agentId: 'agent2', + agentName: 'Busy Agent', + dn: '1002', + state: 'Busy', + teamId: 'team1', + siteId: 'site1', + }, + { + agentId: 'agent3', + agentName: 'Idle Agent', + dn: '1003', + state: 'Idle', + teamId: 'team1', + siteId: 'site1', + }, + ], + }; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + }); + + describe('Interactions', () => { + it('should render component after switching to queues tab', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + const queueTab = screen.container.querySelector('.queue-tab'); + fireEvent.click(queueTab); + + const container = screen.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render component after agent selection', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + const agentItems = screen.container.querySelectorAll('.call-control-list-item'); + if (agentItems.length > 0) { + fireEvent.click(agentItems[0]); + } + + const container = screen.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render component after queue selection', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + // Switch to queues tab first + const queueTab = screen.container.querySelector('.queue-tab'); + fireEvent.click(queueTab); + + // Then click on a queue + const queueItems = screen.container.querySelectorAll('.call-control-list-item'); + if (queueItems.length > 0) { + fireEvent.click(queueItems[0]); + } + + const container = screen.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + }); + + describe('State Management', () => { + it('should update when heading changes', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + screen.rerender(); + const container = screen.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should update when agents list changes', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + const newAgents = [ + { + agentId: 'newAgent1', + agentName: 'New Agent One', + dn: '2001', + state: 'Available', + teamId: 'team2', + siteId: 'site2', + }, + ]; + screen.rerender(); + const container = screen.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should update when queues list changes', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + const newQueues = [{id: 'newQueue1', name: 'New Queue One'} as ContactServiceQueue]; + screen.rerender(); + const container = screen.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx new file mode 100644 index 000000000..a8954ce0e --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx @@ -0,0 +1,208 @@ +import React from 'react'; +import {render, fireEvent} from '@testing-library/react'; +import '@testing-library/jest-dom'; +import ConsultTransferPopoverComponent from '../../../../../src/components/task/CallControl/CallControlCustom/consult-transfer-popover'; +import {ContactServiceQueue} from '@webex/cc-store'; +import * as utils from '../../../../..//src/components/task/CallControl/CallControlCustom/call-control-custom.utils'; + +const loggerMock = { + log: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + trace: jest.fn(), + error: jest.fn(), +}; + +beforeAll(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); +}); + +afterAll(() => { + (console.error as jest.Mock).mockRestore(); +}); + +// This test suite was previously skipped but is now enabled for 100% coverage +describe('ConsultTransferPopoverComponent', () => { + const mockOnAgentSelect = jest.fn(); + const mockOnQueueSelect = jest.fn(); + const baseProps = { + heading: 'Select an Agent', + buttonIcon: 'agent-icon', + buddyAgents: [ + { + agentId: 'agent1', + agentName: 'Agent One', + dn: '1001', + state: 'Available', + teamId: 'team1', + siteId: 'site1', + }, + { + agentId: 'agent2', + agentName: 'Agent Two', + dn: '1002', + state: 'Available', + teamId: 'team1', + siteId: 'site1', + }, + ], + queues: [ + {id: 'queue1', name: 'Queue One'} as ContactServiceQueue, + {id: 'queue2', name: 'Queue Two'} as ContactServiceQueue, + ], + onAgentSelect: mockOnAgentSelect, + onQueueSelect: mockOnQueueSelect, + allowConsultToQueue: true, + logger: loggerMock, + showTabs: true, + emptyMessage: 'No agents or queues available', + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders heading and tabs when showTabs is true', async () => { + const screen = await render(); + + // Verify main container + expect(screen.container.querySelector('.agent-popover-content')).toBeInTheDocument(); + + // Verify heading - it's wrapped in mdc-text component + const heading = screen.container.querySelector('.agent-popover-title'); + expect(heading).toBeInTheDocument(); + expect(heading).toHaveTextContent('Select an Agent'); + expect(heading?.tagName.toLowerCase()).toBe('mdc-text'); + expect(heading).toHaveAttribute('tagname', 'h3'); + expect(heading).toHaveAttribute('type', 'body-large-bold'); + + // Verify tabs container + const tabList = screen.container.querySelector('.agent-tablist'); + expect(tabList).toBeInTheDocument(); + expect(tabList).toHaveAttribute('role', 'tablist'); + expect(tabList).toHaveAttribute('aria-label', 'Tabs'); + expect(tabList).toHaveAttribute('data-orientation', 'horizontal'); + + // Verify Agents tab (active by default) + const agentTab = screen.container.querySelector('.agent-tab'); + expect(agentTab).toBeInTheDocument(); + expect(agentTab).toHaveAttribute('role', 'tab'); + expect(agentTab).toHaveAttribute('data-active', 'true'); + expect(agentTab).toHaveTextContent('Agents'); + + // Verify Queues tab (inactive by default) + const queueTab = screen.container.querySelector('.queue-tab'); + expect(queueTab).toBeInTheDocument(); + expect(queueTab).toHaveAttribute('role', 'tab'); + expect(queueTab).toHaveAttribute('data-active', 'false'); + expect(queueTab).toHaveAttribute('tabindex', '-1'); + expect(queueTab).toHaveTextContent('Queues'); + + // Verify agent list + const agentList = screen.container.querySelector('.agent-list'); + expect(agentList).toBeInTheDocument(); + + // Verify agent list items + const listItems = screen.container.querySelectorAll('.call-control-list-item'); + expect(listItems).toHaveLength(2); + expect(listItems[0]).toHaveTextContent('Agent One'); + expect(listItems[1]).toHaveTextContent('Agent Two'); + + // Verify list item containers have correct styling + const listItemContainers = screen.container.querySelectorAll('[style*="cursor: pointer"]'); + expect(listItemContainers).toHaveLength(2); + }); + + it('handles interactions and tab switching correctly', async () => { + const screen = await render(); + + // Test agent selection - click on the button inside the first agent item + const firstAgentButton = screen.container.querySelectorAll('.call-control-list-item button')[0]; + fireEvent.click(firstAgentButton); + expect(mockOnAgentSelect).toHaveBeenCalledWith('agent1', 'Agent One'); + + // Test onMouseDown event handler (covers line 39) - just trigger the event + const listItemContainer = screen.container.querySelector('[style*="cursor: pointer"]'); + fireEvent.mouseDown(listItemContainer!); + + // Test tab switching + const queueTab = screen.container.querySelector('.queue-tab'); + fireEvent.click(queueTab!); + + // Test queue selection after switching tabs - click on the button inside the first queue item + const firstQueueButton = screen.container.querySelectorAll('.call-control-list-item button')[0]; + fireEvent.click(firstQueueButton); + expect(mockOnQueueSelect).toHaveBeenCalledWith('queue1', 'Queue One'); + }); + + it('handles edge cases and conditional rendering', async () => { + // Test empty state when both lists are completely empty + const emptyProps = { + ...baseProps, + buddyAgents: [], + queues: [], + }; + + let screen = await render(); + expect(screen.container.querySelector('.agent-tablist')).not.toBeInTheDocument(); + expect(screen.container.querySelector('.consult-empty-state')).toBeInTheDocument(); + expect(screen.container.querySelector('.consult-empty-message')).toBeInTheDocument(); + + // Test empty agents tab with tabs showing (covers lines 94-96) + const emptyAgentsProps = { + ...baseProps, + buddyAgents: [], + }; + + screen = await render(); + expect(screen.container.querySelector('.agent-tablist')).toBeInTheDocument(); + expect(screen.container.querySelector('.consult-empty-state')).toBeInTheDocument(); + + // Test empty queues tab when switched to queues (covers lines 98-100) + const emptyQueuesProps = { + ...baseProps, + queues: [], + }; + + screen = await render(); + + // Switch to queues tab + const queueTab = screen.container.querySelector('.queue-tab'); + fireEvent.click(queueTab!); + + // Should show empty state for queues (covers lines 98-100) + expect(screen.container.querySelector('.consult-empty-state')).toBeInTheDocument(); + + // Test hidden queue tab when allowConsultToQueue is false + const propsWithoutQueue = { + ...baseProps, + allowConsultToQueue: false, + }; + + screen = await render(); + const hiddenQueueTab = screen.container.querySelector('.queue-tab'); + expect(hiddenQueueTab).toHaveStyle('display: none'); + }); + + it('covers edge case for empty items in renderList (line 50)', async () => { + // Mock isAgentsEmpty to return false even with empty array to force renderList call + const mockIsAgentsEmpty = jest.spyOn(utils, 'isAgentsEmpty').mockReturnValue(false); + const mockShouldShowTabs = jest.spyOn(utils, 'shouldShowTabs').mockReturnValue(true); + + try { + const propsWithEmptyAgents = { + ...baseProps, + buddyAgents: [], // Empty array but mocked to return false for isEmpty + }; + + const screen = await render(); + + // Should render the "No agents found" text (line 50) + expect(screen.getByText('No agents found')).toBeInTheDocument(); + } finally { + // Restore original functions + mockIsAgentsEmpty.mockRestore(); + mockShouldShowTabs.mockRestore(); + } + }); +}); diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/__snapshots__/call-control.snapshot.tsx.snap b/packages/contact-center/cc-components/tests/components/task/CallControl/__snapshots__/call-control.snapshot.tsx.snap new file mode 100644 index 000000000..d6f51fa04 --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/__snapshots__/call-control.snapshot.tsx.snap @@ -0,0 +1,3074 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CallControlComponent Snapshots Interactions should handle end call button click 1`] = ` +
    +
    + +
    +

    + Mute +

    +
    + +
    +

    + Hold the call +

    +
    + +
    +

    + Consult with another agent +

    +
    + +
    +

    + Transfer Call +

    +
    + +
    +

    + Resume Recording +

    +
    + +
    +

    + End Call +

    +
    +
    +
    +
    +`; + +exports[`CallControlComponent Snapshots Interactions should handle hold button click 1`] = ` +
    +
    + +
    +

    + Mute +

    +
    + +
    +

    + Hold the call +

    +
    + +
    +

    + Consult with another agent +

    +
    + +
    +

    + Transfer Call +

    +
    + +
    +

    + Resume Recording +

    +
    + +
    +

    + End Call +

    +
    +
    +
    +
    +`; + +exports[`CallControlComponent Snapshots Interactions should handle mute button click 1`] = ` +
    +
    + +
    +

    + Mute +

    +
    + +
    +

    + Hold the call +

    +
    + +
    +

    + Consult with another agent +

    +
    + +
    +

    + Transfer Call +

    +
    + +
    +

    + Resume Recording +

    +
    + +
    +

    + End Call +

    +
    +
    +
    +
    +`; + +exports[`CallControlComponent Snapshots Interactions should handle record button click 1`] = ` +
    +
    + +
    +

    + Mute +

    +
    + +
    +

    + Hold the call +

    +
    + +
    +

    + Consult with another agent +

    +
    + +
    +

    + Transfer Call +

    +
    + +
    +

    + Resume Recording +

    +
    + +
    +

    + End Call +

    +
    +
    +
    +
    +`; + +exports[`CallControlComponent Snapshots Interactions should render wrapup button in wrapup mode 1`] = ` +
    +
    + +
    +
    +`; + +exports[`CallControlComponent Snapshots Rendering - Tests for UI elements and visual states of CallControl component should render the component with default call control buttons 1`] = ` +
    +
    + +
    +

    + Mute +

    +
    + +
    +

    + Hold the call +

    +
    + +
    +

    + Consult with another agent +

    +
    + +
    +

    + Transfer Call +

    +
    + +
    +

    + Resume Recording +

    +
    + +
    +

    + End Call +

    +
    +
    +
    +`; + +exports[`CallControlComponent Snapshots Rendering - Tests for UI elements and visual states of CallControl component should render with chat media type 1`] = ` +
    +
    + +
    +

    + Mute +

    +
    + +
    +

    + Hold the call +

    +
    + +
    +

    + Consult with another agent +

    +
    + +
    +

    + Transfer Chat +

    +
    + +
    +

    + Resume Recording +

    +
    + +
    +

    + End Chat +

    +
    +
    +
    +`; + +exports[`CallControlComponent Snapshots Rendering - Tests for UI elements and visual states of CallControl component should render with consultation accepted 1`] = ` +
    +`; + +exports[`CallControlComponent Snapshots Rendering - Tests for UI elements and visual states of CallControl component should render with consultation initiated 1`] = ` +
    +
    + +
    +

    + Mute +

    +
    + +
    +

    + Transfer Call +

    +
    + +
    +

    + Resume Recording +

    +
    + +
    +

    + End Call +

    +
    +
    +
    +`; + +exports[`CallControlComponent Snapshots Rendering - Tests for UI elements and visual states of CallControl component should render with held state 1`] = ` +
    +
    + +
    +

    + Mute +

    +
    + +
    +

    + Resume the call +

    +
    + +
    +

    + Consult with another agent +

    +
    + +
    +

    + Transfer Call +

    +
    + +
    +

    + Resume Recording +

    +
    + +
    +

    + End Call +

    +
    +
    +
    +`; + +exports[`CallControlComponent Snapshots Rendering - Tests for UI elements and visual states of CallControl component should render with limited control visibility 1`] = ` +
    +
    + +
    +

    + Mute +

    +
    + +
    +

    + End Call +

    +
    +
    +
    +`; + +exports[`CallControlComponent Snapshots Rendering - Tests for UI elements and visual states of CallControl component should render with muted state 1`] = ` +
    +
    + +
    +

    + Unmute +

    +
    + +
    +

    + Hold the call +

    +
    + +
    +

    + Consult with another agent +

    +
    + +
    +

    + Transfer Call +

    +
    + +
    +

    + Resume Recording +

    +
    + +
    +

    + End Call +

    +
    +
    +
    +`; + +exports[`CallControlComponent Snapshots Rendering - Tests for UI elements and visual states of CallControl component should render with recording state 1`] = ` +
    +
    + +
    +

    + Mute +

    +
    + +
    +

    + Hold the call +

    +
    + +
    +

    + Consult with another agent +

    +
    + +
    +

    + Transfer Call +

    +
    + +
    +

    + Pause Recording +

    +
    + +
    +

    + End Call +

    +
    +
    +
    +`; + +exports[`CallControlComponent Snapshots Rendering - Tests for UI elements and visual states of CallControl component should render with wrapup mode 1`] = ` +
    +
    + +
    +
    +`; + +exports[`CallControlComponent Snapshots State Management should handle combination of states: muted, held, and recording 1`] = ` +
    +
    + +
    +

    + Unmute +

    +
    + +
    +

    + Resume the call +

    +
    + +
    +

    + Consult with another agent +

    +
    + +
    +

    + Transfer Call +

    +
    + +
    +

    + Pause Recording +

    +
    + +
    +

    + End Call +

    +
    +
    +
    +`; + +exports[`CallControlComponent Snapshots State Management should handle edge case with no buddy agents 1`] = ` +
    +
    + +
    +

    + Mute +

    +
    + +
    +

    + Hold the call +

    +
    + +
    +

    + Consult with another agent +

    +
    + +
    +

    + Transfer Call +

    +
    + +
    +

    + Resume Recording +

    +
    + +
    +

    + End Call +

    +
    +
    +
    +`; + +exports[`CallControlComponent Snapshots State Management should handle edge case with no queues 1`] = ` +
    +
    + +
    +

    + Mute +

    +
    + +
    +

    + Hold the call +

    +
    + +
    +

    + Consult with another agent +

    +
    + +
    +

    + Transfer Call +

    +
    + +
    +

    + Resume Recording +

    +
    + +
    +

    + End Call +

    +
    +
    +
    +`; + +exports[`CallControlComponent Snapshots State Management should handle edge case with no wrapup codes 1`] = ` +
    +
    + +
    +
    +`; + +exports[`CallControlComponent Snapshots State Management should update when hold state changes 1`] = ` +
    +
    + +
    +

    + Mute +

    +
    + +
    +

    + Resume the call +

    +
    + +
    +

    + Consult with another agent +

    +
    + +
    +

    + Transfer Call +

    +
    + +
    +

    + Resume Recording +

    +
    + +
    +

    + End Call +

    +
    +
    +
    +`; + +exports[`CallControlComponent Snapshots State Management should update when mute state changes 1`] = ` +
    +
    + +
    +

    + Unmute +

    +
    + +
    +

    + Hold the call +

    +
    + +
    +

    + Consult with another agent +

    +
    + +
    +

    + Transfer Call +

    +
    + +
    +

    + Resume Recording +

    +
    + +
    +

    + End Call +

    +
    +
    +
    +`; + +exports[`CallControlComponent Snapshots State Management should update when recording state changes 1`] = ` +
    +
    + +
    +

    + Mute +

    +
    + +
    +

    + Hold the call +

    +
    + +
    +

    + Consult with another agent +

    +
    + +
    +

    + Transfer Call +

    +
    + +
    +

    + Pause Recording +

    +
    + +
    +

    + End Call +

    +
    +
    +
    +`; diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control-consult.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control-consult.tsx deleted file mode 100644 index abf565113..000000000 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control-consult.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import React from 'react'; -import {render, screen, fireEvent} from '@testing-library/react'; -import '@testing-library/jest-dom'; -import CallControlConsultComponent from '../../../../src/components/task/CallControl/CallControlCustom/call-control-consult'; - -const loggerMock = { - log: jest.fn(), - info: jest.fn(), - warn: jest.fn(), - trace: jest.fn(), - error: jest.fn(), -}; - -// eslint-disable-next-line react/display-name -jest.mock('../../../../src/components/task/TaskTimer', () => () => 00:00); - -beforeAll(() => { - jest.spyOn(console, 'error').mockImplementation(() => {}); -}); - -afterAll(() => { - (console.error as jest.Mock).mockRestore(); -}); - -// This test suite is skipped because we have removed the :broken from the command -// line in the package.json scripts to run these tests in pipeline -describe.skip('CallControlConsultComponent', () => { - const mockOnTransfer = jest.fn(); - const mockEndConsultCall = jest.fn(); - const defaultProps = { - agentName: 'Alice', - startTimeStamp: Date.now(), - onTransfer: mockOnTransfer, - endConsultCall: mockEndConsultCall, - consultCompleted: true, - isAgentBeingConsulted: true, - isEndConsultEnabled: true, - logger: loggerMock, - muteUnmute: false, - isMuted: false, - }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('renders agent name, consult label and task timer', () => { - render(); - expect(screen.getByText('Alice')).toBeInTheDocument(); - const consultText = screen.getByText( - (content, element) => element?.className.includes('consult-sub-text') && content.includes('Consult') - ); - expect(consultText).toBeInTheDocument(); - }); - - it('calls onTransfer when transfer button is clicked', () => { - render(); - const transferButton = screen.getByTestId('transfer-consult-btn'); - fireEvent.click(transferButton); - expect(mockOnTransfer).toHaveBeenCalled(); - }); - - it('calls endConsultCall when cancel button is clicked', () => { - render(); - const cancelButton = screen.getByTestId('cancel-consult-btn'); - fireEvent.click(cancelButton); - expect(mockEndConsultCall).toHaveBeenCalled(); - }); - - it('displays correct state when consult is not completed', () => { - render(); - const transferButton = screen.getByTestId('transfer-consult-btn'); - - // Check the disabled attribute directly - expect(transferButton).toHaveAttribute('disabled', ''); - - const consultText = screen.getByText( - (content, element) => element?.className.includes('consult-sub-text') && content.includes('Consult requested') - ); - expect(consultText).toBeInTheDocument(); - }); - - it('handles error when transfer button click fails', () => { - const errorMockOnTransfer = jest.fn().mockImplementation(() => { - throw new Error('Transfer failed'); - }); - - const errorHandler = jest.fn(); - window.addEventListener('error', errorHandler); - - render(); - const transferButton = screen.getByTestId('transfer-consult-btn'); - - fireEvent.click(transferButton); - expect(errorMockOnTransfer).toHaveBeenCalled(); - }); - - it('handles error when end consult button click fails', () => { - const errorMockEndConsultCall = jest.fn().mockImplementation(() => { - throw new Error('End consult failed'); - }); - - const errorHandler = jest.fn(); - window.addEventListener('error', errorHandler); - - render(); - const cancelButton = screen.getByTestId('cancel-consult-btn'); - fireEvent.click(cancelButton); - expect(errorMockEndConsultCall).toHaveBeenCalled(); - }); - - it('does not render transfer button when isAgentBeingConsulted is false', () => { - render(); - expect(screen.queryByTestId('transfer-consult-btn')).not.toBeInTheDocument(); - }); - - it('does not render cancel button when both isEndConsultEnabled and isAgentBeingConsulted are false', () => { - render(); - expect(screen.queryByTestId('cancel-consult-btn')).not.toBeInTheDocument(); - }); - - it('renders cancel button when isEndConsultEnabled is true even if isAgentBeingConsulted is false', () => { - render(); - expect(screen.getByTestId('cancel-consult-btn')).toBeInTheDocument(); - }); - - it('logs transfer button click', () => { - render(); - const transferButton = screen.getByTestId('transfer-consult-btn'); - fireEvent.click(transferButton); - expect(loggerMock.log).toHaveBeenCalledWith('CC-Widgets: CallControlConsult: transfer completed', { - module: 'call-control-consult.tsx', - method: 'handleTransfer', - }); - }); - - it('logs end consult button click', () => { - render(); - const cancelButton = screen.getByTestId('cancel-consult-btn'); - fireEvent.click(cancelButton); - expect(loggerMock.log).toHaveBeenCalledWith('CC-Widgets: CallControlConsult: end consult completed', { - module: 'call-control-consult.tsx', - method: 'handleEndConsult', - }); - }); -}); diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.snapshot.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.snapshot.tsx new file mode 100644 index 000000000..edd7d9a88 --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.snapshot.tsx @@ -0,0 +1,455 @@ +import React from 'react'; +import '@testing-library/jest-dom'; +import {render, fireEvent, act} from '@testing-library/react'; +import CallControlComponent from '../../../../src/components/task/CallControl/call-control'; +import {CallControlComponentProps} from '../../../../src/components/task/task.types'; +import {mockTask, mockQueueDetails, mockAgents, mockProfile, mockCC} from '@webex/test-fixtures'; +import {BuddyDetails, ContactServiceQueue, IWrapupCode} from '@webex/cc-store'; + +const mockUIDProps = (container) => { + container + .querySelectorAll('[id^="mdc-input"]') + .forEach((el: HTMLBaseElement) => el.setAttribute('id', 'mock-input-id')); + container + .querySelectorAll('[id^="mdc-tooltip"]') + .forEach((el: HTMLBaseElement) => el.setAttribute('id', 'mock-tooltip-id')); + container + .querySelectorAll('[aria-describedby^="mdc-tooltip"]') + .forEach((el: HTMLBaseElement) => el.setAttribute('aria-describedby', 'mock-aria-describedby')); + container + .querySelectorAll('[id^="mdc-popover"]') + .forEach((el: HTMLBaseElement) => el.setAttribute('id', 'mock-popover-id')); + container + .querySelectorAll('[id^="mdc-select"]') + .forEach((el: HTMLBaseElement) => el.setAttribute('id', 'mock-select-id')); +}; + +// Mock MediaStream for testing +Object.defineProperty(window, 'MediaStream', { + writable: true, + value: jest.fn().mockImplementation(() => ({ + getTracks: jest.fn(() => []), + addTrack: jest.fn(), + removeTrack: jest.fn(), + })), +}); + +describe('CallControlComponent Snapshots', () => { + const mockLogger = mockCC.LoggerProxy; + + const mockCurrentTask = { + ...mockTask, + id: `task-${mockProfile.agentId}`, + data: { + ...mockTask.data, + interaction: { + ...mockTask.data.interaction, + mediaType: 'telephony', + mediaChannel: 'telephony', + }, + }, + status: 'connected', + isHeld: false, + recording: {isRecording: false}, + wrapUpReason: null, + autoWrapup: undefined, + }; + + const mockWrapupCodes: IWrapupCode[] = mockProfile.wrapupCodes; + + const mockBuddyAgents: BuddyDetails[] = mockAgents.map((agent) => ({ + ...agent, + firstName: agent.name.split(' ')[0] || agent.name, + lastName: agent.name.split(' ')[1] || '', + teamName: mockProfile.teams[0]?.teamName || 'Team 1', + siteName: `Site-${mockProfile.siteId}`, + profileId: mockProfile.agentProfileID, + agentSessionId: `session-${mockProfile.agentId}`, + stateChangeTime: mockProfile.lastStateChangeTimestamp, + auxiliaryCodeId: mockProfile.lastStateAuxCodeId, + teamIds: [mockProfile.teams[0]?.teamId || 'team1'], + })) as BuddyDetails[]; + + const mockQueues: ContactServiceQueue[] = mockQueueDetails.map((queue) => ({ + id: queue.id, + name: queue.name, + description: queue.description, + queueType: queue.queueType, + checkAgentAvailability: queue.checkAgentAvailability, + channelType: queue.channelType, + })) as ContactServiceQueue[]; + + const defaultProps: CallControlComponentProps = { + currentTask: mockCurrentTask, + wrapupCodes: mockWrapupCodes, + toggleHold: jest.fn(), + toggleRecording: jest.fn(), + toggleMute: jest.fn(), + isMuted: false, + endCall: jest.fn(), + wrapupCall: jest.fn(), + isHeld: false, + setIsHeld: jest.fn(), + isRecording: false, + setIsRecording: jest.fn(), + buddyAgents: mockBuddyAgents, + loadBuddyAgents: jest.fn(), + transferCall: jest.fn(), + consultCall: jest.fn(), + endConsultCall: jest.fn(), + consultInitiated: false, + consultTransfer: jest.fn(), + consultCompleted: false, + consultAccepted: false, + consultStartTimeStamp: undefined, + callControlAudio: null, + consultAgentName: '', + setConsultAgentName: jest.fn(), + consultAgentId: '', + setConsultAgentId: jest.fn(), + holdTime: 0, + callControlClassName: '', + callControlConsultClassName: '', + startTimestamp: Date.now(), + queues: mockQueues, + loadQueues: jest.fn(), + isEndConsultEnabled: mockProfile.isEndConsultEnabled, + allowConsultToQueue: mockProfile.allowConsultToQueue, + lastTargetType: 'agent', + setLastTargetType: jest.fn(), + controlVisibility: { + accept: true, + decline: true, + end: true, + muteUnmute: true, + holdResume: true, + consult: true, + transfer: true, + conference: true, + wrapup: false, + pauseResumeRecording: true, + endConsult: true, + recordingIndicator: true, + }, + logger: mockLogger, + secondsUntilAutoWrapup: undefined, + cancelAutoWrapup: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Rendering - Tests for UI elements and visual states of CallControl component', () => { + it('should render the component with default call control buttons', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with muted state', async () => { + const mutedProps = {...defaultProps, isMuted: true}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with held state', async () => { + const heldProps = {...defaultProps, isHeld: true}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with recording state', async () => { + const recordingProps = {...defaultProps, isRecording: true}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with wrapup mode', async () => { + const wrapupProps = { + ...defaultProps, + controlVisibility: {...defaultProps.controlVisibility, wrapup: true}, + }; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with consultation initiated', async () => { + const consultProps = {...defaultProps, consultInitiated: true}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with consultation accepted', async () => { + const consultAcceptedProps = {...defaultProps, consultAccepted: true}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with limited control visibility', async () => { + const limitedControlsProps = { + ...defaultProps, + controlVisibility: { + accept: true, + decline: true, + end: true, + muteUnmute: true, + holdResume: false, + consult: false, + transfer: false, + conference: false, + wrapup: false, + pauseResumeRecording: false, + endConsult: true, + recordingIndicator: true, + }, + }; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render with chat media type', async () => { + const chatProps = { + ...defaultProps, + currentTask: { + ...defaultProps.currentTask, + data: { + ...defaultProps.currentTask.data, + interaction: { + ...defaultProps.currentTask.data.interaction, + mediaType: 'chat', + mediaChannel: 'chat', + }, + }, + }, + }; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + }); + + describe('Interactions', () => { + it('should handle mute button click', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + const muteButton = screen.getByTestId('call-control:mute-toggle'); + fireEvent.click(muteButton); + + const container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should handle hold button click', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + const holdButton = screen.getByTestId('call-control:hold-toggle'); + fireEvent.click(holdButton); + + const container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should handle record button click', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + const recordButton = screen.getByTestId('call-control:recording-toggle'); + fireEvent.click(recordButton); + + const container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should handle end call button click', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + const endCallButton = screen.getByTestId('call-control:end-call'); + fireEvent.click(endCallButton); + + const container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should render wrapup button in wrapup mode', async () => { + const wrapupProps = { + ...defaultProps, + controlVisibility: {...defaultProps.controlVisibility, wrapup: true}, + }; + let screen; + await act(async () => { + screen = render(); + }); + + // The wrapup submit button is inside a popover that needs to be opened + const wrapupButton = screen.getByTestId('call-control:wrapup-button'); + expect(wrapupButton).toBeInTheDocument(); + + const container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + }); + + describe('State Management', () => { + it('should update when hold state changes', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + screen.rerender(); + const container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should update when mute state changes', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + screen.rerender(); + const container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should update when recording state changes', async () => { + let screen; + await act(async () => { + screen = render(); + }); + + screen.rerender(); + const container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should handle combination of states: muted, held, and recording', async () => { + const combinedStateProps = { + ...defaultProps, + isMuted: true, + isHeld: true, + isRecording: true, + }; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should handle edge case with no buddy agents', async () => { + const noBuddyAgentsProps = {...defaultProps, buddyAgents: []}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should handle edge case with no queues', async () => { + const noQueuesProps = {...defaultProps, queues: []}; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + + it('should handle edge case with no wrapup codes', async () => { + const noWrapupCodesProps = { + ...defaultProps, + wrapupCodes: [], + controlVisibility: {...defaultProps.controlVisibility, wrapup: true}, + }; + let screen; + await act(async () => { + screen = render(); + }); + + const container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx index 052809673..5907c47b4 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx @@ -1,208 +1,494 @@ -/* eslint-disable react/prop-types */ import React from 'react'; -import {render, screen, fireEvent} from '@testing-library/react'; +import {render, fireEvent} from '@testing-library/react'; import '@testing-library/jest-dom'; import CallControlComponent from '../../../../src/components/task/CallControl/call-control'; +import {CallControlComponentProps, CallControlMenuType} from '../../../../src/components/task/task.types'; +import * as callControlUtils from '../../../../src/components/task/CallControl/call-control.utils'; import {mockTask} from '@webex/test-fixtures'; -jest.mock('@webex/cc-store', () => ({ - DestinationType: { - AGENT: 'agent', - QUEUE: 'queue', - }, -})); - -jest.mock('../../../../src/components/task/CallControl/CallControlCustom/consult-transfer-popover', () => { - const MockPopover = (props) => ( -
    - -
    - ); - MockPopover.displayName = 'ConsultTransferPopoverPresentational'; - return MockPopover; +// Mock MediaStream for testing +Object.defineProperty(window, 'MediaStream', { + writable: true, + value: jest.fn().mockImplementation(() => ({ + getTracks: jest.fn(() => []), + addTrack: jest.fn(), + removeTrack: jest.fn(), + })), }); -jest.mock('../../../../src/components/task/CallControl/CallControlCustom/call-control-consult', () => { - // eslint-disable-next-line react/display-name - return (props) => ( -
    - CallControlConsultComponent -
    - ); -}); - -const loggerMock = { - log: jest.fn(), - info: jest.fn(), - warn: jest.fn(), - trace: jest.fn(), - error: jest.fn(), -}; - -beforeAll(() => { - jest.spyOn(console, 'error').mockImplementation(() => {}); -}); +describe('CallControlComponent', () => { + const mockLogger = { + log: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + trace: jest.fn(), + }; -afterAll(() => { - (console.error as jest.Mock).mockRestore(); -}); + const mockCurrentTask = { + ...mockTask, + id: 'task-123', + mediaType: 'telephony', + status: 'connected', + isHeld: false, + recording: {isRecording: false}, + wrapUpReason: null, + }; -// This test suite is skipped because we have removed the :broken from the command -// line in the package.json scripts to run these tests in pipeline -describe.skip('CallControlPresentational', () => { - const mockToggleHold = jest.fn(); - const mockToggleRecording = jest.fn(); - const mockEndCall = jest.fn(); - const mockWrapupCall = jest.fn(); const mockWrapupCodes = [ - {id: '1', name: 'Reason 1'}, - {id: '2', name: 'Reason 2'}, + {id: 'wrap1', name: 'Customer Issue', isSystem: false}, + {id: 'wrap2', name: 'Technical Support', isSystem: false}, + ]; + + const mockBuddyAgents = [ + { + agentId: 'agent1', + id: 'agent1', + firstName: 'John', + lastName: 'Doe', + agentName: 'John Doe', + dn: '1001', + teamId: 'team1', + teamName: 'Support Team', + siteId: 'site1', + siteName: 'Main Site', + profileId: 'profile1', + agentSessionId: 'session1', + state: 'Available', + stateChangeTime: 1234567890, + auxiliaryCodeId: null, + teamIds: ['team1'], + }, ]; - const mockLoadBuddyAgents = jest.fn(); - const mockLoadQueues = jest.fn(); - const mockConsultCall = jest.fn(); - const mockTransferCall = jest.fn(); - const mockSetConsultAgentId = jest.fn(); - const mockSetConsultAgentName = jest.fn(); - const setIsHeld = jest.fn(); - - const defaultProps = { - currentTask: mockTask, - toggleHold: mockToggleHold, - toggleRecording: mockToggleRecording, - endCall: mockEndCall, - wrapupCall: mockWrapupCall, + + const mockControlVisibility = { + accept: true, + decline: true, + end: true, + muteUnmute: true, + holdResume: true, + consult: true, + transfer: true, + conference: true, + wrapup: false, // Set to false by default to show buttons + pauseResumeRecording: true, + endConsult: true, + recordingIndicator: true, + }; + + const defaultProps: CallControlComponentProps = { + currentTask: mockCurrentTask, wrapupCodes: mockWrapupCodes, + toggleHold: jest.fn(), + toggleRecording: jest.fn(), + toggleMute: jest.fn(), + isMuted: false, + endCall: jest.fn(), + wrapupCall: jest.fn(), isHeld: false, - setIsHeld: setIsHeld, + setIsHeld: jest.fn(), isRecording: false, setIsRecording: jest.fn(), - buddyAgents: [], - loadBuddyAgents: mockLoadBuddyAgents, - transferCall: mockTransferCall, - consultCall: mockConsultCall, + buddyAgents: mockBuddyAgents, + loadBuddyAgents: jest.fn(), + queues: [], + loadQueues: jest.fn(), + transferCall: jest.fn(), + consultCall: jest.fn(), endConsultCall: jest.fn(), consultInitiated: false, consultTransfer: jest.fn(), consultCompleted: false, consultAccepted: false, - consultStartTimeStamp: 0, - callControlAudio: null, + consultStartTimeStamp: Date.now(), + callControlAudio: null as unknown as MediaStream, consultAgentName: '', - setConsultAgentName: mockSetConsultAgentName, + setConsultAgentName: jest.fn(), consultAgentId: '', - setConsultAgentId: mockSetConsultAgentId, + setConsultAgentId: jest.fn(), holdTime: 0, callControlClassName: '', callControlConsultClassName: '', - startTimestamp: 0, - queues: [], - loadQueues: mockLoadQueues, + startTimestamp: Date.now(), isEndConsultEnabled: true, allowConsultToQueue: true, - lastTargetType: 'agent' as 'agent' | 'queue', + lastTargetType: 'agent', setLastTargetType: jest.fn(), - controlVisibility: { - accept: false, - decline: false, - end: true, - muteUnmute: false, - holdResume: true, - consult: true, - transfer: true, - conference: false, - wrapup: false, - pauseResumeRecording: true, - endConsult: false, - recordingIndicator: false, - }, - logger: loggerMock, + controlVisibility: mockControlVisibility, + logger: mockLogger, + secondsUntilAutoWrapup: null, cancelAutoWrapup: jest.fn(), - isMuted: false, - muteUnmute: false, - toggleMute: jest.fn(), }; + // Utility function spies + let buildCallControlButtonsSpy: jest.SpyInstance; + let getMediaTypeSpy: jest.SpyInstance; + let isTelephonyMediaTypeSpy: jest.SpyInstance; + let updateCallStateFromTaskSpy: jest.SpyInstance; + beforeEach(() => { + // Mock utility functions with proper return values + getMediaTypeSpy = jest.spyOn(callControlUtils, 'getMediaType').mockReturnValue({ + labelName: 'Voice', + }); + + isTelephonyMediaTypeSpy = jest.spyOn(callControlUtils, 'isTelephonyMediaType').mockReturnValue(true); + buildCallControlButtonsSpy = jest.spyOn(callControlUtils, 'buildCallControlButtons').mockReturnValue([ + { + id: 'mute', + icon: 'mute', + onClick: jest.fn(), + tooltip: 'Mute', + className: 'mute-btn', + disabled: false, + isVisible: true, + }, + { + id: 'hold', + icon: 'hold', + onClick: jest.fn(), + tooltip: 'Hold', + className: 'hold-btn', + disabled: false, + isVisible: true, + }, + { + id: 'transfer', + icon: 'next-bold', + tooltip: 'Transfer', + className: 'call-control-button', + disabled: false, + menuType: 'Transfer', + isVisible: true, + }, + ]); + updateCallStateFromTaskSpy = jest.spyOn(callControlUtils, 'updateCallStateFromTask').mockImplementation(() => {}); + + // Reset all mocks jest.clearAllMocks(); }); - it('renders the component with call control container', () => { - render(); - expect(screen.getByTestId('call-control-container')).toBeInTheDocument(); + afterEach(() => { + jest.restoreAllMocks(); }); + describe('Rendering', () => { + it('renders mute and hold buttons and responds to user interactions', async () => { + const modifiedProps = { + ...defaultProps, + isMuted: false, + isHeld: false, + controlVisibility: { + ...mockControlVisibility, + muteUnmute: true, + holdResume: true, + }, + }; - it('calls toggleHold with correct value when hold button is clicked', () => { - render(); - const buttons = screen.getAllByTestId('ButtonCircle'); - fireEvent.click(buttons[0]); - expect(mockToggleHold).toHaveBeenCalledWith(true); - }); + const screen = render(); - it('calls endCall when end call button is clicked', () => { - render(); - const buttons = screen.getAllByTestId('ButtonCircle'); - fireEvent.click(buttons[4]); - expect(mockEndCall).toHaveBeenCalled(); - }); + // Perform user interactions + const muteButton = screen.getByLabelText('Mute'); + fireEvent.click(muteButton); - it('calls consultCall when a consult agent is selected', () => { - render(); - const buttons = screen.getAllByTestId('ButtonCircle'); - fireEvent.click(buttons[1]); - const agentSelectButton = screen.getByTestId('AgentSelectButton'); - fireEvent.click(agentSelectButton); - expect(mockConsultCall).toHaveBeenCalledWith('agent1', 'agent'); - expect(mockSetConsultAgentId).toHaveBeenCalledWith('agent1'); - expect(mockLoadQueues).toHaveBeenCalled(); - expect(mockTransferCall).not.toHaveBeenCalled(); - }); + const holdButton = screen.getByLabelText('Hold'); + fireEvent.click(holdButton); - it('calls transferCall when a transfer agent is selected', () => { - render(); - const buttons = screen.getAllByTestId('ButtonCircle'); - fireEvent.click(buttons[2]); - const agentSelectButton = screen.getByTestId('AgentSelectButton'); - fireEvent.click(agentSelectButton); - expect(mockTransferCall).toHaveBeenCalledWith('agent1', 'agent'); - expect(mockLoadQueues).toHaveBeenCalled(); - expect(mockConsultCall).not.toHaveBeenCalled(); - }); + // Verify mute button functionality + expect(muteButton).toBeInTheDocument(); + expect(muteButton).toHaveAttribute('aria-label', 'Mute'); + expect(muteButton).toHaveAttribute('data-disabled', 'false'); + + // Verify hold button functionality + expect(holdButton).toBeInTheDocument(); + expect(holdButton).toHaveAttribute('aria-label', 'Hold'); + expect(holdButton).toHaveAttribute('data-disabled', 'false'); + }); + it('displays wrapup button when call is muted and held', async () => { + const modifiedProps = { + ...defaultProps, + isMuted: true, + isHeld: true, + controlVisibility: { + ...mockControlVisibility, + wrapup: true, + }, + }; + + const screen = await render(); + + // Verify wrapup button is available when conditions are met + const wrapupButton = screen.getByTestId('call-control:wrapup-button'); + expect(wrapupButton).toBeInTheDocument(); + expect(wrapupButton).toHaveAttribute('aria-expanded', 'false'); + expect(wrapupButton).toHaveAttribute('aria-haspopup', 'dialog'); + expect(wrapupButton).toHaveAttribute('type', 'button'); + expect(wrapupButton).toHaveTextContent('Wrap up'); + }); + it('renders all call control elements with proper attributes and accessibility features', async () => { + const screen = await render(); + + // Verify main container structure + const callControlContainer = screen.getByTestId('call-control-container'); + expect(callControlContainer).toBeInTheDocument(); + expect(callControlContainer).toHaveAttribute('class', 'call-control-container'); + expect(callControlContainer).toHaveAttribute('data-testid', 'call-control-container'); + + // Verify audio element for call playback + const remoteAudio = screen.container.querySelector('#remote-audio'); + expect(remoteAudio).toBeInTheDocument(); + expect(remoteAudio).toHaveAttribute('autoplay', ''); + expect(remoteAudio).toHaveAttribute('id', 'remote-audio'); + + // Verify button group container + const buttonGroup = callControlContainer.querySelector('.button-group'); + expect(buttonGroup).toBeInTheDocument(); + expect(buttonGroup).toHaveAttribute('class', 'button-group'); + + // Verify mute button and its properties + const muteButton = screen.getByLabelText('Mute'); + expect(muteButton).toBeInTheDocument(); + expect(muteButton).toHaveAttribute('type', 'button'); + expect(muteButton).toHaveAttribute('aria-label', 'Mute'); + expect(muteButton).toHaveAttribute('class', 'md-button-circle-wrapper mute-btn md-button-simple-wrapper'); + expect(muteButton).toHaveAttribute('data-color', 'primary'); + expect(muteButton).toHaveAttribute('data-disabled', 'false'); + expect(muteButton).toHaveAttribute('data-size', '40'); + + // Verify mute button icon + const muteIcon = muteButton.querySelector('mdc-icon'); + expect(muteIcon).toBeInTheDocument(); + expect(muteIcon).toHaveAttribute('class', 'mute-btn-icon'); + expect(muteIcon).toHaveAttribute('name', 'mute'); - it('logs hold button click', () => { - render(); - const buttons = screen.getAllByTestId('ButtonCircle'); - fireEvent.click(buttons[0]); - expect(loggerMock.info).toHaveBeenCalledWith('CC-Widgets: CallControl: is Call On Hold status is false', { - module: 'call-control.tsx', - method: 'handletoggleHold', + // Verify mute button tooltip + const muteTooltip = screen.getByText('Mute').closest('.md-tooltip-label'); + expect(muteTooltip).toBeInTheDocument(); + expect(muteTooltip).toHaveAttribute('class', 'md-tooltip-label'); + + // Verify hold button and its properties + const holdButton = screen.getByLabelText('Hold'); + expect(holdButton).toBeInTheDocument(); + expect(holdButton).toHaveAttribute('type', 'button'); + expect(holdButton).toHaveAttribute('aria-label', 'Hold'); + expect(holdButton).toHaveAttribute('class', 'md-button-circle-wrapper hold-btn md-button-simple-wrapper'); + expect(holdButton).toHaveAttribute('data-color', 'primary'); + expect(holdButton).toHaveAttribute('data-disabled', 'false'); + expect(holdButton).toHaveAttribute('data-size', '40'); + + // Verify hold button icon + const holdIcon = holdButton.querySelector('mdc-icon'); + expect(holdIcon).toBeInTheDocument(); + expect(holdIcon).toHaveAttribute('class', 'hold-btn-icon'); + expect(holdIcon).toHaveAttribute('name', 'hold'); + + // Verify hold button tooltip + const holdTooltip = screen.getByText('Hold').closest('.md-tooltip-label'); + expect(holdTooltip).toBeInTheDocument(); + expect(holdTooltip).toHaveAttribute('class', 'md-tooltip-label'); + + // Verify transfer button and its properties + const transferButton = screen.getByLabelText('Transfer'); + expect(transferButton).toBeInTheDocument(); + expect(transferButton).toHaveAttribute('type', 'button'); + expect(transferButton).toHaveAttribute('aria-label', 'Transfer'); + expect(transferButton).toHaveAttribute('aria-expanded', 'false'); + expect(transferButton).toHaveAttribute('aria-haspopup', 'dialog'); + expect(transferButton).toHaveAttribute( + 'class', + 'md-button-circle-wrapper call-control-button md-button-simple-wrapper' + ); + expect(transferButton).toHaveAttribute('data-color', 'primary'); + expect(transferButton).toHaveAttribute('data-disabled', 'false'); + expect(transferButton).toHaveAttribute('data-size', '40'); + + // Verify transfer button icon + const transferIcon = transferButton.querySelector('mdc-icon'); + expect(transferIcon).toBeInTheDocument(); + expect(transferIcon).toHaveAttribute('class', 'call-control-button-icon'); + expect(transferIcon).toHaveAttribute('name', 'next-bold'); + + // Verify transfer button tooltip + const transferTooltip = screen.getByText('Transfer').closest('.md-tooltip-label'); + expect(transferTooltip).toBeInTheDocument(); + expect(transferTooltip).toHaveAttribute('class', 'md-tooltip-label'); + + // Verify utility functions were called + expect(getMediaTypeSpy).toHaveBeenCalled(); + expect(isTelephonyMediaTypeSpy).toHaveBeenCalled(); + expect(buildCallControlButtonsSpy).toHaveBeenCalled(); + expect(updateCallStateFromTaskSpy).toHaveBeenCalled(); }); }); + describe('Button interactions', () => { + it('responds to hover events and maintains button state during interactions', async () => { + // Use the default buildCallControlButtons to ensure mute and hold buttons are rendered + const modifiedProps = { + ...defaultProps, + consultInitiated: false, + controlVisibility: { + ...mockControlVisibility, + wrapup: false, + consult: true, + transfer: true, + muteUnmute: true, + holdResume: true, + }, + buddyAgents: mockBuddyAgents, + }; + + const screen = await render(); + + // Test hover interactions on mute button + const muteButton = screen.getByLabelText('Mute'); + fireEvent.mouseEnter(muteButton); + fireEvent.mouseOver(muteButton); + expect(muteButton).toBeInTheDocument(); + fireEvent.mouseLeave(muteButton); + + // Test hover interactions on hold button + const holdButton = screen.getByLabelText('Hold'); + fireEvent.mouseEnter(holdButton); + fireEvent.mouseOver(holdButton); + expect(holdButton).toBeInTheDocument(); + fireEvent.mouseLeave(holdButton); + + // Test hover and click interactions on transfer button + const transferButton = screen.getByLabelText('Transfer'); + + // Verify accessibility attributes before interaction + expect(transferButton).toHaveAttribute('aria-haspopup', 'dialog'); + expect(transferButton).toHaveAttribute('aria-expanded', 'false'); + + fireEvent.mouseEnter(transferButton); + fireEvent.mouseOver(transferButton); + fireEvent.click(transferButton); // Trigger potential popover functionality + fireEvent.mouseLeave(transferButton); + + // After clicking, the popover should be expanded + expect(transferButton).toHaveAttribute('aria-expanded', 'true'); + + // Verify buttons maintain their CSS classes after interactions + expect(transferButton).toHaveClass('call-control-button'); + expect(muteButton).toHaveClass('mute-btn'); + expect(holdButton).toHaveClass('hold-btn'); + }); + it('handles various user interaction patterns on consultation buttons', async () => { + const modifiedProps = { + ...defaultProps, + consultInitiated: false, + controlVisibility: { + ...mockControlVisibility, + wrapup: false, + consult: true, + }, + buddyAgents: mockBuddyAgents, + }; + + // Mock filterButtonsForConsultation to return a consult button + jest.spyOn(callControlUtils, 'filterButtonsForConsultation').mockReturnValue([ + { + id: 'consult', + icon: 'consult', + tooltip: 'Consult', + className: 'call-control-button', + disabled: false, + menuType: 'Consult', + isVisible: true, + dataTestId: 'consult-button', + }, + ]); + + const screen = await render(); + + // Locate consultation button for testing + const consultButton = screen.getByLabelText('Consult'); + expect(consultButton).toBeInTheDocument(); + + // Test keyboard interactions + fireEvent.focus(consultButton); + fireEvent.keyDown(consultButton, {key: 'Enter', code: 'Enter'}); + fireEvent.keyUp(consultButton, {key: 'Enter', code: 'Enter'}); - // TODO - We do not have tests for CAD Component. Will move these while writing test cases for it - // it('renders consult UI with consultAccepted prop', () => { - // const props = { - // ...defaultProps, - // consultAccepted: true, - // consultInitiated: false, - // }; - // render(); - // const consultContainer = document.querySelector('.call-control-consult-container'); - // expect(consultContainer).toBeInTheDocument(); - // expect(consultContainer).toHaveClass('no-border'); - // }); - - // it('renders consult UI with consultInitiated prop', () => { - // const props = { - // ...defaultProps, - // consultAccepted: false, - // consultInitiated: true, - // }; - // render(); - // const consultContainer = document.querySelector('.call-control-consult-container'); - // expect(consultContainer).toBeInTheDocument(); - // expect(consultContainer).not.toHaveClass('no-border'); - // }); + fireEvent.focus(consultButton); + fireEvent.keyDown(consultButton, {key: ' ', code: 'Space'}); + fireEvent.keyUp(consultButton, {key: ' ', code: 'Space'}); + + // Test mouse interactions + fireEvent.mouseDown(consultButton); + fireEvent.mouseUp(consultButton); + fireEvent.click(consultButton); + + // Test touch interactions for mobile compatibility + fireEvent.touchStart(consultButton); + fireEvent.touchEnd(consultButton); + + // Verify button maintains accessibility and functionality + expect(consultButton).toHaveAttribute('type', 'button'); + expect(consultButton).toHaveAttribute('aria-label', 'Consult'); + + // Verify button parent structure exists + const buttonParent = consultButton.parentElement; + expect(buttonParent).toBeInTheDocument(); + }); + + it('manages popover functionality for consultation and transfer operations', async () => { + // Configure consultation button for popover testing + const consultButton = { + id: 'consult', + icon: 'consult', + tooltip: 'Consult', + className: 'call-control-button', + disabled: false, + menuType: 'Consult' as CallControlMenuType, + isVisible: true, + dataTestId: 'consult-button', + onClick: jest.fn(), + }; + + jest.spyOn(callControlUtils, 'filterButtonsForConsultation').mockReturnValue([consultButton]); + + // Mock popover event handlers + const mockHandleCloseButtonPress = jest.fn(); + + jest.spyOn(callControlUtils, 'handleCloseButtonPress').mockImplementation(mockHandleCloseButtonPress); + + const modifiedProps = { + ...defaultProps, + consultInitiated: false, + controlVisibility: { + ...mockControlVisibility, + wrapup: false, + consult: true, + }, + buddyAgents: mockBuddyAgents, + }; + + const screen = await render(); + + // Locate and interact with consultation button + const consultButtonElement = screen.getByLabelText('Consult'); + expect(consultButtonElement).toBeInTheDocument(); + + // Verify popover accessibility attributes before interaction + expect(consultButtonElement).toHaveAttribute('aria-haspopup', 'dialog'); + expect(consultButtonElement).toHaveAttribute('aria-expanded', 'false'); + + // Simulate user click to trigger popover functionality + fireEvent.click(consultButtonElement); + + // After clicking, the popover should be expanded + expect(consultButtonElement).toHaveAttribute('aria-expanded', 'true'); + + // Test additional interactions that exercise popover behavior + fireEvent.mouseEnter(consultButtonElement); + fireEvent.focus(consultButtonElement); + fireEvent.blur(consultButtonElement); + fireEvent.mouseLeave(consultButtonElement); + + // Verify button integrity after interactions + expect(consultButtonElement).toBeInTheDocument(); + expect(consultButtonElement).toHaveClass('call-control-button'); + }); + }); }); diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.utils.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.utils.tsx new file mode 100644 index 000000000..bbc77b0a0 --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.utils.tsx @@ -0,0 +1,865 @@ +import '@testing-library/jest-dom'; +import {ITask} from '@webex/cc-store'; +import { + handleToggleHold, + handleMuteToggle, + handleWrapupCall, + handleWrapupChange, + handleTargetSelect, + getMediaType, + isTelephonyMediaType, + buildCallControlButtons, + filterButtonsForConsultation, + updateCallStateFromTask, + handleCloseButtonPress, + handleWrapupReasonChange, + handleAudioRef, +} from '../../../../src/components/task/CallControl/call-control.utils'; +import * as utils from '../../../../src/utils'; + +// Mock the external utilities +jest.mock('../../../../src/utils', () => ({ + getMediaTypeInfo: jest.fn(), +})); + +const loggerMock = { + info: jest.fn(), + log: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + trace: jest.fn(), +}; + +describe('CallControl Utils', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + const mockCurrentTask = { + data: { + interaction: { + mediaType: 'telephony', + mediaChannel: 'telephony', + media: { + 'media-resource-1': { + isHold: false, + }, + }, + callProcessingDetails: { + isPaused: false, + }, + }, + mediaResourceId: 'media-resource-1', + }, + }; + + const mockControlVisibility = { + accept: true, + decline: true, + end: true, + muteUnmute: true, + holdResume: true, + consult: true, + transfer: true, + conference: true, + wrapup: true, + pauseResumeRecording: true, + endConsult: true, + recordingIndicator: true, + }; + + const mockMediaTypeInfo = { + labelName: 'Call', + }; + + describe('handleToggleHold', () => { + it('should toggle hold from false to true', () => { + const mockToggleHold = jest.fn(); + const mockSetIsHeld = jest.fn(); + + handleToggleHold(false, mockToggleHold, mockSetIsHeld, loggerMock); + + expect(loggerMock.info).toHaveBeenCalledWith('CC-Widgets: CallControl: is Call On Hold status is false', { + module: 'call-control.tsx', + method: 'handletoggleHold', + }); + expect(mockToggleHold).toHaveBeenCalledWith(true); + expect(mockSetIsHeld).toHaveBeenCalledWith(true); + }); + + it('should toggle hold from true to false', () => { + const mockToggleHold = jest.fn(); + const mockSetIsHeld = jest.fn(); + + handleToggleHold(true, mockToggleHold, mockSetIsHeld, loggerMock); + + expect(loggerMock.info).toHaveBeenCalledWith('CC-Widgets: CallControl: is Call On Hold status is true', { + module: 'call-control.tsx', + method: 'handletoggleHold', + }); + expect(mockToggleHold).toHaveBeenCalledWith(false); + expect(mockSetIsHeld).toHaveBeenCalledWith(false); + }); + }); + + describe('handleMuteToggle', () => { + it('should disable button, call toggleMute, and re-enable button after timeout', () => { + const mockToggleMute = jest.fn(); + const mockSetIsMuteButtonDisabled = jest.fn(); + + handleMuteToggle(mockToggleMute, mockSetIsMuteButtonDisabled, loggerMock); + + expect(mockSetIsMuteButtonDisabled).toHaveBeenCalledWith(true); + expect(mockToggleMute).toHaveBeenCalled(); + + // Fast-forward timer + jest.advanceTimersByTime(500); + + expect(mockSetIsMuteButtonDisabled).toHaveBeenCalledWith(false); + }); + + it('should handle error and still re-enable button', () => { + const mockToggleMute = jest.fn().mockImplementation(() => { + throw new Error('Mute failed'); + }); + const mockSetIsMuteButtonDisabled = jest.fn(); + + handleMuteToggle(mockToggleMute, mockSetIsMuteButtonDisabled, loggerMock); + + expect(mockSetIsMuteButtonDisabled).toHaveBeenCalledWith(true); + expect(loggerMock.error).toHaveBeenCalledWith('Mute toggle failed: Error: Mute failed', { + module: 'call-control.tsx', + method: 'handleMuteToggle', + }); + + // Fast-forward timer + jest.advanceTimersByTime(500); + + expect(mockSetIsMuteButtonDisabled).toHaveBeenCalledWith(false); + }); + }); + + describe('handleWrapupCall', () => { + it('should call wrapupCall and reset state when both reason and id are provided', () => { + const mockWrapupCall = jest.fn(); + const mockSetSelectedWrapupReason = jest.fn(); + const mockSetSelectedWrapupId = jest.fn(); + + handleWrapupCall( + 'Test Reason', + 'test-id', + mockWrapupCall, + mockSetSelectedWrapupReason, + mockSetSelectedWrapupId, + loggerMock + ); + + expect(loggerMock.info).toHaveBeenCalledWith('CC-Widgets: CallControl: wrap-up submitted', { + module: 'call-control.tsx', + method: 'handleWrapupCall', + }); + expect(mockWrapupCall).toHaveBeenCalledWith('Test Reason', 'test-id'); + expect(mockSetSelectedWrapupReason).toHaveBeenCalledWith(null); + expect(mockSetSelectedWrapupId).toHaveBeenCalledWith(null); + expect(loggerMock.log).toHaveBeenCalledWith('CC-Widgets: CallControl: wrapup completed', { + module: 'call-control.tsx', + method: 'handleWrapupCall', + }); + }); + + it('should not call wrapupCall when reason is null', () => { + const mockWrapupCall = jest.fn(); + const mockSetSelectedWrapupReason = jest.fn(); + const mockSetSelectedWrapupId = jest.fn(); + + handleWrapupCall( + null, + 'test-id', + mockWrapupCall, + mockSetSelectedWrapupReason, + mockSetSelectedWrapupId, + loggerMock + ); + + expect(loggerMock.info).toHaveBeenCalled(); + expect(mockWrapupCall).not.toHaveBeenCalled(); + expect(mockSetSelectedWrapupReason).not.toHaveBeenCalled(); + expect(mockSetSelectedWrapupId).not.toHaveBeenCalled(); + expect(loggerMock.log).not.toHaveBeenCalled(); + }); + + it('should not call wrapupCall when id is null', () => { + const mockWrapupCall = jest.fn(); + const mockSetSelectedWrapupReason = jest.fn(); + const mockSetSelectedWrapupId = jest.fn(); + + handleWrapupCall( + 'Test Reason', + null, + mockWrapupCall, + mockSetSelectedWrapupReason, + mockSetSelectedWrapupId, + loggerMock + ); + + expect(loggerMock.info).toHaveBeenCalled(); + expect(mockWrapupCall).not.toHaveBeenCalled(); + expect(mockSetSelectedWrapupReason).not.toHaveBeenCalled(); + expect(mockSetSelectedWrapupId).not.toHaveBeenCalled(); + expect(loggerMock.log).not.toHaveBeenCalled(); + }); + }); + + describe('handleWrapupChange', () => { + it('should call both setter functions with correct values', () => { + const mockSetSelectedWrapupReason = jest.fn(); + const mockSetSelectedWrapupId = jest.fn(); + + handleWrapupChange('New Reason', 'new-id', mockSetSelectedWrapupReason, mockSetSelectedWrapupId); + + expect(mockSetSelectedWrapupReason).toHaveBeenCalledWith('New Reason'); + expect(mockSetSelectedWrapupId).toHaveBeenCalledWith('new-id'); + }); + }); + + describe('handleTargetSelect', () => { + const mockConsultCall = jest.fn(); + const mockTransferCall = jest.fn(); + const mockSetConsultAgentId = jest.fn(); + const mockSetConsultAgentName = jest.fn(); + const mockSetLastTargetType = jest.fn(); + + beforeEach(() => { + mockConsultCall.mockClear(); + mockTransferCall.mockClear(); + mockSetConsultAgentId.mockClear(); + mockSetConsultAgentName.mockClear(); + mockSetLastTargetType.mockClear(); + }); + + it('should handle consult call successfully', () => { + handleTargetSelect( + 'agent-123', + 'John Doe', + 'agent', + 'Consult', + mockConsultCall, + mockTransferCall, + mockSetConsultAgentId, + mockSetConsultAgentName, + mockSetLastTargetType, + loggerMock + ); + + expect(loggerMock.info).toHaveBeenCalledWith('CC-Widgets: CallControl: handling target agent selected', { + module: 'call-control.tsx', + method: 'handleTargetSelect', + }); + expect(mockConsultCall).toHaveBeenCalledWith('agent-123', 'agent'); + expect(mockSetConsultAgentId).toHaveBeenCalledWith('agent-123'); + expect(mockSetConsultAgentName).toHaveBeenCalledWith('John Doe'); + expect(mockSetLastTargetType).toHaveBeenCalledWith('agent'); + expect(mockTransferCall).not.toHaveBeenCalled(); + }); + + it('should handle transfer call successfully', () => { + handleTargetSelect( + 'queue-456', + 'Support Queue', + 'queue', + 'Transfer', + mockConsultCall, + mockTransferCall, + mockSetConsultAgentId, + mockSetConsultAgentName, + mockSetLastTargetType, + loggerMock + ); + + expect(loggerMock.info).toHaveBeenCalledWith('CC-Widgets: CallControl: handling target agent selected', { + module: 'call-control.tsx', + method: 'handleTargetSelect', + }); + expect(mockTransferCall).toHaveBeenCalledWith('queue-456', 'queue'); + expect(mockConsultCall).not.toHaveBeenCalled(); + expect(mockSetConsultAgentId).not.toHaveBeenCalled(); + expect(mockSetConsultAgentName).not.toHaveBeenCalled(); + expect(mockSetLastTargetType).not.toHaveBeenCalled(); + }); + + it('should handle consult call error', () => { + mockConsultCall.mockImplementation(() => { + throw new Error('Consult failed'); + }); + + expect(() => { + handleTargetSelect( + 'agent-123', + 'John Doe', + 'agent', + 'Consult', + mockConsultCall, + mockTransferCall, + mockSetConsultAgentId, + mockSetConsultAgentName, + mockSetLastTargetType, + loggerMock + ); + }).toThrow('Error during consult call'); + + expect(loggerMock.error).toHaveBeenCalledWith('Error during consult call: Error: Consult failed', { + module: 'call-control.tsx', + method: 'handleTargetSelect', + }); + }); + + it('should handle transfer call error', () => { + mockTransferCall.mockImplementation(() => { + throw new Error('Transfer failed'); + }); + + expect(() => { + handleTargetSelect( + 'queue-456', + 'Support Queue', + 'queue', + 'Transfer', + mockConsultCall, + mockTransferCall, + mockSetConsultAgentId, + mockSetConsultAgentName, + mockSetLastTargetType, + loggerMock + ); + }).toThrow('Error during transfer call'); + + expect(loggerMock.error).toHaveBeenCalledWith('Error during transfer call: Error: Transfer failed', { + module: 'call-control.tsx', + method: 'handleTargetSelect', + }); + }); + + it('should do nothing when agentMenuType is null', () => { + handleTargetSelect( + 'agent-123', + 'John Doe', + 'agent', + null, + mockConsultCall, + mockTransferCall, + mockSetConsultAgentId, + mockSetConsultAgentName, + mockSetLastTargetType, + loggerMock + ); + + expect(mockConsultCall).not.toHaveBeenCalled(); + expect(mockTransferCall).not.toHaveBeenCalled(); + }); + }); + + describe('handlePopoverOpen', () => { + const mockSetShowAgentMenu = jest.fn(); + const mockSetAgentMenuType = jest.fn(); + const mockLoadBuddyAgents = jest.fn(); + const mockLoadQueues = jest.fn(); + + beforeEach(() => { + mockSetShowAgentMenu.mockClear(); + mockSetAgentMenuType.mockClear(); + mockLoadBuddyAgents.mockClear(); + mockLoadQueues.mockClear(); + }); + + it('should handle popover open event', () => { + // TODO: Add actual test implementation + expect(true).toBe(true); + }); + }); + + describe('getMediaType', () => { + it('should call getMediaTypeInfo with correct parameters', () => { + const mockGetMediaTypeInfo = utils.getMediaTypeInfo as jest.Mock; + mockGetMediaTypeInfo.mockReturnValue(mockMediaTypeInfo); + + const result = getMediaType('telephony', 'telephony'); + + expect(mockGetMediaTypeInfo).toHaveBeenCalledWith('telephony', 'telephony'); + expect(result).toBe(mockMediaTypeInfo); + }); + }); + + describe('isTelephonyMediaType', () => { + it('should return true for telephony media type', () => { + const result = isTelephonyMediaType('telephony'); + expect(result).toBe(true); + }); + + it('should return false for non-telephony media type', () => { + const result = isTelephonyMediaType('chat'); + expect(result).toBe(false); + }); + + it('should return false for email media type', () => { + const result = isTelephonyMediaType('email'); + expect(result).toBe(false); + }); + }); + + describe('buildCallControlButtons', () => { + const mockFunctions = { + handleMuteToggleFunc: jest.fn(), + handleToggleHoldFunc: jest.fn(), + toggleRecording: jest.fn(), + endCall: jest.fn(), + }; + + it('should build buttons with correct configuration when muted', () => { + const buttons = buildCallControlButtons( + true, // isMuted + false, // isHeld + true, // isRecording + false, // isMuteButtonDisabled + mockMediaTypeInfo, + mockControlVisibility, + mockFunctions.handleMuteToggleFunc, + mockFunctions.handleToggleHoldFunc, + mockFunctions.toggleRecording, + mockFunctions.endCall + ); + + expect(buttons).toHaveLength(6); + + // Check mute button + const muteButton = buttons.find((b) => b.id === 'mute'); + expect(muteButton).toEqual({ + id: 'mute', + icon: 'microphone-muted-bold', + onClick: mockFunctions.handleMuteToggleFunc, + tooltip: 'Unmute', + className: 'call-control-button-muted', + disabled: false, + isVisible: true, + dataTestId: 'call-control:mute-toggle', + }); + + // Check hold button + const holdButton = buttons.find((b) => b.id === 'hold'); + expect(holdButton).toEqual({ + id: 'hold', + icon: 'pause-bold', + onClick: mockFunctions.handleToggleHoldFunc, + tooltip: 'Hold the call', + className: 'call-control-button', + disabled: false, + isVisible: true, + dataTestId: 'call-control:hold-toggle', + }); + }); + + it('should build buttons with correct configuration when not muted and held', () => { + const buttons = buildCallControlButtons( + false, // isMuted + true, // isHeld + false, // isRecording + true, // isMuteButtonDisabled + mockMediaTypeInfo, + mockControlVisibility, + mockFunctions.handleMuteToggleFunc, + mockFunctions.handleToggleHoldFunc, + mockFunctions.toggleRecording, + mockFunctions.endCall + ); + + // Check mute button + const muteButton = buttons.find((b) => b.id === 'mute'); + expect(muteButton).toEqual({ + id: 'mute', + icon: 'microphone-bold', + onClick: mockFunctions.handleMuteToggleFunc, + tooltip: 'Mute', + className: 'call-control-button', + disabled: true, + isVisible: true, + dataTestId: 'call-control:mute-toggle', + }); + + // Check hold button + const holdButton = buttons.find((b) => b.id === 'hold'); + expect(holdButton).toEqual({ + id: 'hold', + icon: 'play-bold', + onClick: mockFunctions.handleToggleHoldFunc, + tooltip: 'Resume the call', + className: 'call-control-button', + disabled: false, + isVisible: true, + dataTestId: 'call-control:hold-toggle', + }); + + // Check end button - should be disabled when held + const endButton = buttons.find((b) => b.id === 'end'); + expect(endButton?.disabled).toBe(true); + }); + + it('should build consult and transfer buttons with menu types', () => { + const buttons = buildCallControlButtons( + false, + false, + false, + false, + mockMediaTypeInfo, + mockControlVisibility, + mockFunctions.handleMuteToggleFunc, + mockFunctions.handleToggleHoldFunc, + mockFunctions.toggleRecording, + mockFunctions.endCall + ); + + const consultButton = buttons.find((b) => b.id === 'consult'); + expect(consultButton).toEqual({ + id: 'consult', + icon: 'headset-bold', + tooltip: 'Consult with another agent', + className: 'call-control-button', + disabled: false, + menuType: 'Consult', + isVisible: true, + dataTestId: 'call-control:consult', + }); + + const transferButton = buttons.find((b) => b.id === 'transfer'); + expect(transferButton).toEqual({ + id: 'transfer', + icon: 'next-bold', + tooltip: 'Transfer Call', + className: 'call-control-button', + disabled: false, + menuType: 'Transfer', + isVisible: true, + dataTestId: 'call-control:transfer', + }); + }); + + it('should build record button with correct states', () => { + // When recording + let buttons = buildCallControlButtons( + false, + false, + true, // isRecording + false, + mockMediaTypeInfo, + mockControlVisibility, + mockFunctions.handleMuteToggleFunc, + mockFunctions.handleToggleHoldFunc, + mockFunctions.toggleRecording, + mockFunctions.endCall + ); + + let recordButton = buttons.find((b) => b.id === 'record'); + expect(recordButton?.icon).toBe('record-paused-bold'); + expect(recordButton?.tooltip).toBe('Pause Recording'); + + // When not recording + buttons = buildCallControlButtons( + false, + false, + false, // isRecording + false, + mockMediaTypeInfo, + mockControlVisibility, + mockFunctions.handleMuteToggleFunc, + mockFunctions.handleToggleHoldFunc, + mockFunctions.toggleRecording, + mockFunctions.endCall + ); + + recordButton = buttons.find((b) => b.id === 'record'); + expect(recordButton?.icon).toBe('record-bold'); + expect(recordButton?.tooltip).toBe('Resume Recording'); + }); + }); + + describe('filterButtonsForConsultation', () => { + const mockButtons = [ + {id: 'mute', icon: '', tooltip: '', className: '', disabled: false, isVisible: true}, + {id: 'hold', icon: '', tooltip: '', className: '', disabled: false, isVisible: true}, + {id: 'consult', icon: '', tooltip: '', className: '', disabled: false, isVisible: true}, + {id: 'transfer', icon: '', tooltip: '', className: '', disabled: false, isVisible: true}, + {id: 'record', icon: '', tooltip: '', className: '', disabled: false, isVisible: true}, + {id: 'end', icon: '', tooltip: '', className: '', disabled: false, isVisible: true}, + ]; + + it('should filter out hold and consult buttons when consultation is initiated and telephony', () => { + const result = filterButtonsForConsultation(mockButtons, true, true); + + expect(result).toHaveLength(4); + expect(result.map((b) => b.id)).toEqual(['mute', 'transfer', 'record', 'end']); + }); + + it('should not filter buttons when consultation is not initiated', () => { + const result = filterButtonsForConsultation(mockButtons, false, true); + + expect(result).toHaveLength(6); + expect(result).toBe(mockButtons); + }); + + it('should not filter buttons when not telephony', () => { + const result = filterButtonsForConsultation(mockButtons, true, false); + + expect(result).toHaveLength(6); + expect(result).toBe(mockButtons); + }); + + it('should not filter buttons when neither consultation initiated nor telephony', () => { + const result = filterButtonsForConsultation(mockButtons, false, false); + + expect(result).toHaveLength(6); + expect(result).toBe(mockButtons); + }); + }); + + describe('updateCallStateFromTask', () => { + const mockSetIsHeld = jest.fn(); + const mockSetIsRecording = jest.fn(); + + beforeEach(() => { + mockSetIsHeld.mockClear(); + mockSetIsRecording.mockClear(); + }); + + it('should update hold and recording state from task data', () => { + updateCallStateFromTask(mockCurrentTask as unknown as ITask, mockSetIsHeld, mockSetIsRecording); + + expect(mockSetIsHeld).toHaveBeenCalledWith(false); + expect(mockSetIsRecording).toHaveBeenCalledWith(true); // !isPaused = !false = true + }); + + it('should handle task with hold state true', () => { + const taskWithHold = { + ...mockCurrentTask, + data: { + ...mockCurrentTask.data, + interaction: { + ...mockCurrentTask.data.interaction, + media: { + 'media-resource-1': { + isHold: true, + }, + }, + }, + }, + }; + + updateCallStateFromTask(taskWithHold as unknown as ITask, mockSetIsHeld, mockSetIsRecording); + + expect(mockSetIsHeld).toHaveBeenCalledWith(true); + }); + + it('should handle task with recording paused', () => { + const taskWithPausedRecording = { + ...mockCurrentTask, + data: { + ...mockCurrentTask.data, + interaction: { + ...mockCurrentTask.data.interaction, + callProcessingDetails: { + isPaused: true, + }, + }, + }, + }; + + updateCallStateFromTask(taskWithPausedRecording as unknown as ITask, mockSetIsHeld, mockSetIsRecording); + + expect(mockSetIsRecording).toHaveBeenCalledWith(false); // !isPaused = !true = false + }); + + it('should return early when currentTask is null', () => { + updateCallStateFromTask(null as unknown as ITask, mockSetIsHeld, mockSetIsRecording); + + expect(mockSetIsHeld).not.toHaveBeenCalled(); + expect(mockSetIsRecording).not.toHaveBeenCalled(); + }); + + it('should return early when currentTask.data is null', () => { + const invalidTask = {data: null}; + + updateCallStateFromTask(invalidTask as unknown as ITask, mockSetIsHeld, mockSetIsRecording); + + expect(mockSetIsHeld).not.toHaveBeenCalled(); + expect(mockSetIsRecording).not.toHaveBeenCalled(); + }); + + it('should return early when currentTask.data.interaction is null', () => { + const invalidTask = { + data: { + interaction: null, + }, + }; + + updateCallStateFromTask(invalidTask as unknown as ITask, mockSetIsHeld, mockSetIsRecording); + + expect(mockSetIsHeld).not.toHaveBeenCalled(); + expect(mockSetIsRecording).not.toHaveBeenCalled(); + }); + + it('should handle missing media resource', () => { + const taskWithoutMedia = { + ...mockCurrentTask, + data: { + ...mockCurrentTask.data, + interaction: { + ...mockCurrentTask.data.interaction, + media: {}, + }, + }, + }; + + updateCallStateFromTask(taskWithoutMedia as unknown as ITask, mockSetIsHeld, mockSetIsRecording); + + expect(mockSetIsHeld).toHaveBeenCalledWith(undefined); // undefined && undefined && undefined = falsy + }); + + it('should handle missing callProcessingDetails', () => { + const taskWithoutCallProcessing = { + ...mockCurrentTask, + data: { + ...mockCurrentTask.data, + interaction: { + ...mockCurrentTask.data.interaction, + callProcessingDetails: null, + }, + }, + }; + + updateCallStateFromTask(taskWithoutCallProcessing as unknown as ITask, mockSetIsHeld, mockSetIsRecording); + + expect(mockSetIsHeld).toHaveBeenCalledWith(false); + expect(mockSetIsRecording).not.toHaveBeenCalled(); + }); + }); + + describe('handleCloseButtonPress', () => { + it('should set showAgentMenu to false and agentMenuType to null', () => { + const mockSetShowAgentMenu = jest.fn(); + const mockSetAgentMenuType = jest.fn(); + + handleCloseButtonPress(mockSetShowAgentMenu, mockSetAgentMenuType); + + expect(mockSetShowAgentMenu).toHaveBeenCalledWith(false); + expect(mockSetAgentMenuType).toHaveBeenCalledWith(null); + }); + }); + + describe('handleWrapupReasonChange', () => { + const mockHandleWrapupChange = jest.fn(); + + beforeEach(() => { + mockHandleWrapupChange.mockClear(); + }); + + it('should handle wrapup reason change with valid selection', () => { + const mockEvent = { + detail: { + value: 'code-1', + }, + } as CustomEvent; + + const mockWrapupCodes = [ + {id: 'code-1', name: 'Technical Issue'}, + {id: 'code-2', name: 'Customer Inquiry'}, + ]; + + handleWrapupReasonChange(mockEvent, mockWrapupCodes, mockHandleWrapupChange); + + expect(mockHandleWrapupChange).toHaveBeenCalledWith('Technical Issue', 'code-1'); + }); + + it('should handle wrapup reason change with unknown selection', () => { + const mockEvent = { + detail: { + value: 'unknown-code', + }, + } as CustomEvent; + + const mockWrapupCodes = [ + {id: 'code-1', name: 'Technical Issue'}, + {id: 'code-2', name: 'Customer Inquiry'}, + ]; + + handleWrapupReasonChange(mockEvent, mockWrapupCodes, mockHandleWrapupChange); + + expect(mockHandleWrapupChange).not.toHaveBeenCalled(); + }); + + it('should handle wrapup reason change with undefined wrapupCodes', () => { + const mockEvent = { + detail: { + value: 'code-1', + }, + } as CustomEvent; + + handleWrapupReasonChange(mockEvent, undefined, mockHandleWrapupChange); + + expect(mockHandleWrapupChange).not.toHaveBeenCalled(); + }); + + it('should handle wrapup reason change with empty wrapupCodes array', () => { + const mockEvent = { + detail: { + value: 'code-1', + }, + } as CustomEvent; + + const mockWrapupCodes: Array<{id: string; name: string}> = []; + + handleWrapupReasonChange(mockEvent, mockWrapupCodes, mockHandleWrapupChange); + + expect(mockHandleWrapupChange).not.toHaveBeenCalled(); + }); + }); + + describe('handleAudioRef', () => { + it('should set srcObject when both audioElement and callControlAudio are provided', () => { + const mockAudioElement = { + srcObject: null, + } as HTMLAudioElement; + + const mockCallControlAudio = {} as MediaStream; + + handleAudioRef(mockAudioElement, mockCallControlAudio); + + expect(mockAudioElement.srcObject).toBe(mockCallControlAudio); + }); + + it('should not set srcObject when audioElement is null', () => { + const mockCallControlAudio = {} as MediaStream; + + handleAudioRef(null, mockCallControlAudio); + + // No assertion needed as the function should not throw and do nothing + }); + + it('should not set srcObject when callControlAudio is null', () => { + const mockAudioElement = { + srcObject: null, + } as HTMLAudioElement; + + handleAudioRef(mockAudioElement, null); + + expect(mockAudioElement.srcObject).toBe(null); + }); + + it('should not set srcObject when both audioElement and callControlAudio are null', () => { + handleAudioRef(null, null); + + // No assertion needed as the function should not throw and do nothing + }); + }); +}); diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/consult-transfer-list-item.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/consult-transfer-list-item.tsx deleted file mode 100644 index ab1d7d5ef..000000000 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/consult-transfer-list-item.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; -import {render, screen, fireEvent} from '@testing-library/react'; -import '@testing-library/jest-dom'; -import ConsultTransferListComponent from '../../../../src/components/task/CallControl/CallControlCustom/consult-transfer-list-item'; - -const loggerMock = { - log: jest.fn(), - info: jest.fn(), - warn: jest.fn(), - trace: jest.fn(), - error: jest.fn(), -}; - -jest.mock('lottie-web'); - -beforeAll(() => { - jest.spyOn(console, 'error').mockImplementation(() => {}); -}); - -afterAll(() => { - (console.error as jest.Mock).mockRestore(); -}); - -// This test suite is skipped because we have removed the :broken from the command -// line in the package.json scripts to run these tests in pipeline -describe.skip('CallControlListItemPresentational', () => { - const mockOnButtonPress = jest.fn(); - const defaultProps = { - title: 'John Doe', - subtitle: 'Manager', - buttonIcon: 'test-icon', - onButtonPress: mockOnButtonPress, - className: 'custom-class', - logger: loggerMock, - }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('renders with title, subtitle, and correct initials', () => { - render(); - expect(screen.getByText('John Doe')).toBeInTheDocument(); - expect(screen.getByText('Manager')).toBeInTheDocument(); - expect(screen.getByTestId('AvatarNext')).toHaveTextContent('JD'); - expect(screen.getByTestId('Icon')).toHaveTextContent('test-icon'); - }); - - it('renders without subtitle when not provided', () => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const {subtitle, ...propsWithoutSubtitle} = defaultProps; - render(); - expect(screen.getByText('John Doe')).toBeInTheDocument(); - expect(screen.queryByText('Manager')).not.toBeInTheDocument(); - }); - - it('calls onButtonPress when button is clicked', () => { - render(); - const button = screen.getByTestId('ButtonCircle'); - fireEvent.click(button); - expect(mockOnButtonPress).toHaveBeenCalled(); - }); -}); diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/consult-transfer-popover.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/consult-transfer-popover.tsx deleted file mode 100644 index 5661d89f4..000000000 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/consult-transfer-popover.tsx +++ /dev/null @@ -1,158 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import React from 'react'; -import {render, screen, fireEvent} from '@testing-library/react'; -import '@testing-library/jest-dom'; -import ConsultTransferPopoverComponent from '../../../../src/components/task/CallControl/CallControlCustom/consult-transfer-popover'; -import ConsultTransferEmptyState from '../../../../src/components/task/CallControl/CallControlCustom/consult-transfer-empty-state'; -import {ContactServiceQueue} from '@webex/cc-store'; - -const loggerMock = { - log: jest.fn(), - info: jest.fn(), - warn: jest.fn(), - trace: jest.fn(), - error: jest.fn(), -}; - -jest.mock('../../../../src/components/task/CallControl/CallControlCustom/consult-transfer-list-item', () => { - const MockListItem = (props: any) => ( -
    - {props.title} -
    - ); - MockListItem.displayName = 'ConsultTransferListComponent'; - return MockListItem; -}); - -beforeAll(() => { - jest.spyOn(console, 'error').mockImplementation(() => {}); -}); - -afterAll(() => { - (console.error as jest.Mock).mockRestore(); -}); - -// This test suite is skipped because we have removed the :broken from the command -// line in the package.json scripts to run these tests in pipeline -describe.skip('ConsultTransferPopoverComponent', () => { - const mockOnAgentSelect = jest.fn(); - const mockOnQueueSelect = jest.fn(); - const baseProps = { - heading: 'Select an Agent', - buttonIcon: 'agent-icon', - buddyAgents: [ - { - agentId: 'agent1', - agentName: 'Agent One', - dn: '1001', - state: 'Available', - teamId: 'team1', - siteId: 'site1', - }, - { - agentId: 'agent2', - agentName: 'Agent Two', - dn: '1002', - state: 'Available', - teamId: 'team1', - siteId: 'site1', - }, - ], - queues: [ - {id: 'queue1', name: 'Queue One'} as ContactServiceQueue, - {id: 'queue2', name: 'Queue Two'} as ContactServiceQueue, - ], - onAgentSelect: mockOnAgentSelect, - onQueueSelect: mockOnQueueSelect, - allowConsultToQueue: true, - logger: loggerMock, - showTabs: true, - emptyMessage: 'No agents or queues available', - }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('renders heading and tabs when showTabs is true', () => { - render(); - expect(screen.getByText('Select an Agent')).toBeInTheDocument(); - expect(screen.getByText('Agents')).toBeInTheDocument(); - expect(screen.getByText('Queues')).toBeInTheDocument(); - }); - - it('does not render tabs when showTabs is false (both lists empty)', () => { - render(); - expect(screen.queryByText('Agents')).not.toBeInTheDocument(); - expect(screen.queryByText('Queues')).not.toBeInTheDocument(); - }); - - it('renders queues list and allows selecting a queue', () => { - render(); - fireEvent.click(screen.getByText('Queues')); - expect(screen.getByText('Queue One')).toBeInTheDocument(); - fireEvent.click(screen.getByText('Queue One')); - expect(mockOnQueueSelect).toHaveBeenCalledWith('queue1', 'Queue One'); - }); - - it('renders queues list and allows selecting a queue', () => { - render(); - fireEvent.click(screen.getByText('Queues')); - expect(screen.getByText('Queue One')).toBeInTheDocument(); - fireEvent.click(screen.getByText('Queue One')); - expect(mockOnQueueSelect).toHaveBeenCalledWith('queue1', 'Queue One'); - }); - - it('shows ConsultTransferEmptyState when both buddyAgents and queues are empty', () => { - render(); - expect(screen.getByText('We can’t find any queue or agent available for now.')).toBeInTheDocument(); - }); - - it('shows ConsultTransferEmptyState when only buddyAgents is empty and Agents tab is active', () => { - render(); - expect(screen.getByText('We can’t find any agent available for now.')).toBeInTheDocument(); - }); - - it('shows ConsultTransferEmptyState when only queues is empty and Queues tab is active', () => { - render(); - // Switch to the Queues tab - fireEvent.click(screen.getByText('Queues')); - expect(screen.getByText('We can’t find any queue available for now.')).toBeInTheDocument(); - }); - - it('hides queues tab when allowConsultToQueue is false', () => { - render(); - const queuesTab = screen.getByText('Queues'); - // Check that the tab is hidden (display: none) - expect(queuesTab.closest('button')).toHaveStyle('display: none'); - }); - - it('shows queues tab when allowConsultToQueue is true', () => { - render(); - expect(screen.getByText('Queues')).toBeInTheDocument(); - }); - - it('renders ConsultTransferEmptyState component with correct message', () => { - render(); - expect(screen.getByText('No available agents')).toBeInTheDocument(); - }); - - it('renders queues list and allows selecting a queue', () => { - render( - - ); - // Switch to the Queues tab if not already selected - fireEvent.click(screen.getByText('Queues')); - expect(screen.getByText('Queue One')).toBeInTheDocument(); - fireEvent.click(screen.getByText('Queue One')); - expect(mockOnQueueSelect).toHaveBeenCalledWith('queue1', 'Queue One'); - }); -}); diff --git a/packages/contact-center/cc-components/tests/components/task/CallControlCAD/__snapshots__/call-control-cad.snapshot.tsx.snap b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/__snapshots__/call-control-cad.snapshot.tsx.snap new file mode 100644 index 000000000..1a564b737 --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/__snapshots__/call-control-cad.snapshot.tsx.snap @@ -0,0 +1,2457 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CallControlCADComponent Snapshots should handle edge cases and control visibility: custom-css-classes 1`] = ` +
    +
    +
    +
    + +
    +
    + + 555-123-4567 + +
    + + Voice + - + + 00:00 + + +
    +
    +
    +
    +
    + +
    +
    +`; + +exports[`CallControlCADComponent Snapshots should handle edge cases and control visibility: limited-control-visibility 1`] = ` +
    +
    +
    + +
    +
    + + 555-123-4567 + +
    + + Voice + - + + 00:00 + + +
    +
    +
    +
    +
    +`; + +exports[`CallControlCADComponent Snapshots should handle edge cases and control visibility: no-customer-information 1`] = ` +
    +
    +
    + +
    +
    + + No Caller ID + +
    + + Voice + - + + 00:00 + + +
    +
    +
    +
    +
    + +
    +
    +`; + +exports[`CallControlCADComponent Snapshots should render basic call states and media types: chat-media-type 1`] = ` +
    +
    +
    + +
    +
    + + chat-customer@example.com + + + + chat-customer@example.com + + +
    + + Chat + - + + 00:00 + + +
    +
    +
    +
    +
    + +
    +
    +`; + +exports[`CallControlCADComponent Snapshots should render basic call states and media types: combined-states 1`] = ` +
    +
    +
    + +
    +
    + + 555-123-4567 + +
    + + Voice + - + + 00:00 + + +
    + + • + +
    + + + On hold - + + 02:05 + +
    +
    +
    +
    +
    +
    + +
    +
    +`; + +exports[`CallControlCADComponent Snapshots should render basic call states and media types: default-telephony 1`] = ` +
    +
    +
    + +
    +
    + + 555-123-4567 + +
    + + Voice + - + + 00:00 + + +
    +
    +
    +
    +
    + +
    +
    +`; + +exports[`CallControlCADComponent Snapshots should render basic call states and media types: social-media-type 1`] = ` +
    +
    +
    +
    +
    + + Social Customer + + + + Social Customer + + +
    + + Social + - + + 00:00 + + +
    +
    +
    +
    +
    + +
    +
    +`; + +exports[`CallControlCADComponent Snapshots should render consultation and wrapup modes: consultation-accepted 1`] = ` +
    +
    +
    +
    + +
    +
    + + 555-123-4567 + +
    + + Voice + - + + 00:00 + + +
    +
    +
    +
    +
    + +
    +
    +`; + +exports[`CallControlCADComponent Snapshots should render consultation and wrapup modes: consultation-hidden-in-wrapup 1`] = ` +
    +
    +
    + +
    +
    + + 555-123-4567 + +
    + + Voice + - + + 00:00 + + +
    +
    +
    +
    +
    +`; + +exports[`CallControlCADComponent Snapshots should render consultation and wrapup modes: consultation-initiated 1`] = ` +
    +
    +
    +
    + +
    +
    + + 555-123-4567 + +
    + + Voice + - + + 00:00 + + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    + + Consult Agent + + + Consult requested +  •  + + 00:00 + + +
    +
    +
    + +
    +

    + Mute +

    +
    + +
    +

    + Transfer Consult +

    +
    + +
    +

    + End Consult +

    +
    +
    +
    +
    +
    +`; + +exports[`CallControlCADComponent Snapshots should render consultation and wrapup modes: wrapup-mode 1`] = ` +
    +
    +
    + +
    +
    + + 555-123-4567 + +
    + + Voice + - + + 00:00 + + +
    +
    +
    +
    +
    +`; diff --git a/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.snapshot.tsx b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.snapshot.tsx new file mode 100644 index 000000000..8a6aa9711 --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.snapshot.tsx @@ -0,0 +1,371 @@ +import React from 'react'; +import {render} from '@testing-library/react'; +import CallControlCADComponent from '../../../../src/components/task/CallControlCAD/call-control-cad'; +import {CallControlComponentProps} from '../../../../src/components/task/task.types'; +import {mockTask} from '@webex/test-fixtures'; +import {ContactServiceQueue, BuddyDetails} from '@webex/cc-store'; +import '@testing-library/jest-dom'; + +const mockUIDProps = (container: Element) => { + container.querySelectorAll('[id^="mdc-input"]').forEach((el: Element) => el.setAttribute('id', 'mock-input-id')); + container.querySelectorAll('[id^="mdc-tooltip"]').forEach((el: Element) => el.setAttribute('id', 'mock-tooltip-id')); + container + .querySelectorAll('[aria-describedby^="mdc-tooltip"]') + .forEach((el: Element) => el.setAttribute('aria-describedby', 'mock-aria-describedby')); + container.querySelectorAll('[id^="mdc-popover"]').forEach((el: Element) => el.setAttribute('id', 'mock-popover-id')); + container.querySelectorAll('[id^="mdc-select"]').forEach((el: Element) => el.setAttribute('id', 'mock-select-id')); + container.querySelectorAll('[id^="mdc-button"]').forEach((el: Element) => el.setAttribute('id', 'mock-button-id')); +}; + +// Mock MediaStream for testing +Object.defineProperty(window, 'MediaStream', { + writable: true, + value: jest.fn().mockImplementation(() => ({ + getTracks: jest.fn(() => []), + addTrack: jest.fn(), + removeTrack: jest.fn(), + })), +}); + +// Mock TaskTimer component to avoid Worker issues in Jest +jest.mock('../../../../src/components/task/TaskTimer/index', () => + // eslint-disable-next-line react/display-name + () => 00:00 +); + +// Mock utilities that require external dependencies +jest.mock('../../../../src/utils', () => ({ + getMediaTypeInfo: jest.fn((mediaType) => ({ + labelName: mediaType === 'telephony' ? 'Voice' : mediaType === 'chat' ? 'Chat' : 'Social', + iconName: mediaType === 'telephony' ? 'headset' : mediaType === 'chat' ? 'chat' : 'facebook', + className: mediaType === 'telephony' ? 'voice-media' : mediaType === 'chat' ? 'chat-media' : 'social-media', + isBrandVisual: mediaType === 'social', + })), +})); + +describe('CallControlCADComponent Snapshots', () => { + const mockLogger = { + log: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + trace: jest.fn(), + }; + + const mockCurrentTask = { + ...mockTask, + id: 'task-123', + data: { + ...mockTask.data, + interaction: { + ...mockTask.data.interaction, + mediaType: 'telephony', + mediaChannel: 'telephony', + interactionId: 'interaction-123', + callAssociatedDetails: { + customerName: 'John Doe', + ani: '555-123-4567', + virtualTeamName: 'Support Team', + ronaTimeout: '30', + }, + }, + }, + status: 'connected', + isHeld: false, + recording: {isRecording: false}, + wrapUpReason: null, + autoWrapup: undefined, + }; + + const mockWrapupCodes = [ + {id: 'wrap1', name: 'Customer Issue'}, + {id: 'wrap2', name: 'Technical Support'}, + ]; + + const mockBuddyAgents: BuddyDetails[] = [ + { + agentId: 'agent1', + agentName: 'John Doe', + state: 'Available', + teamId: 'team1', + dn: '1001', + siteId: 'site1', + }, + { + agentId: 'agent2', + agentName: 'Jane Smith', + state: 'Available', + teamId: 'team1', + dn: '1002', + siteId: 'site1', + }, + ] as BuddyDetails[]; + + const mockQueues: ContactServiceQueue[] = [ + { + id: 'queue1', + name: 'Support Queue', + }, + { + id: 'queue2', + name: 'Sales Queue', + }, + ] as ContactServiceQueue[]; + + const defaultProps: CallControlComponentProps = { + currentTask: mockCurrentTask, + wrapupCodes: mockWrapupCodes, + toggleHold: jest.fn(), + toggleRecording: jest.fn(), + toggleMute: jest.fn(), + isMuted: false, + endCall: jest.fn(), + wrapupCall: jest.fn(), + isHeld: false, + setIsHeld: jest.fn(), + isRecording: false, + setIsRecording: jest.fn(), + buddyAgents: mockBuddyAgents, + loadBuddyAgents: jest.fn(), + transferCall: jest.fn(), + consultCall: jest.fn(), + endConsultCall: jest.fn(), + consultInitiated: false, + consultTransfer: jest.fn(), + consultCompleted: false, + consultAccepted: false, + consultStartTimeStamp: 1234567890000, + callControlAudio: null, + consultAgentName: '', + setConsultAgentName: jest.fn(), + consultAgentId: '', + setConsultAgentId: jest.fn(), + holdTime: 0, + callControlClassName: '', + callControlConsultClassName: '', + startTimestamp: 1234567890000, + queues: mockQueues, + loadQueues: jest.fn(), + isEndConsultEnabled: true, + allowConsultToQueue: true, + lastTargetType: 'agent', + setLastTargetType: jest.fn(), + controlVisibility: { + accept: true, + decline: true, + end: true, + muteUnmute: true, + holdResume: true, + consult: true, + transfer: true, + conference: true, + wrapup: false, + pauseResumeRecording: true, + endConsult: true, + recordingIndicator: true, + }, + logger: mockLogger, + secondsUntilAutoWrapup: undefined, + cancelAutoWrapup: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render basic call states and media types', async () => { + // Default telephony state + let screen = render(); + let container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container!); + expect(container).toMatchSnapshot('default-telephony'); + screen.unmount(); + + // Muted, held, and recording states combined + const combinedStateProps = { + ...defaultProps, + isMuted: true, + isHeld: true, + isRecording: true, + holdTime: 125, + }; + screen = render(); + container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container!); + expect(container).toMatchSnapshot('combined-states'); + screen.unmount(); + + // Chat media type + const chatProps = { + ...defaultProps, + currentTask: { + ...defaultProps.currentTask, + data: { + ...defaultProps.currentTask.data, + interaction: { + ...defaultProps.currentTask.data.interaction, + mediaType: 'chat', + mediaChannel: 'chat', + interactionId: 'chat-interaction-123', + callAssociatedDetails: { + customerName: 'Chat Customer', + ani: 'chat-customer@example.com', + virtualTeamName: 'Chat Support Team', + ronaTimeout: '45', + }, + }, + }, + }, + }; + screen = render(); + container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container!); + expect(container).toMatchSnapshot('chat-media-type'); + screen.unmount(); + + // Social media type + const socialProps = { + ...defaultProps, + currentTask: { + ...defaultProps.currentTask, + data: { + ...defaultProps.currentTask.data, + interaction: { + ...defaultProps.currentTask.data.interaction, + mediaType: 'social', + mediaChannel: 'social', + interactionId: 'social-interaction-123', + callAssociatedDetails: { + customerName: 'Social Customer', + ani: '@socialuser', + virtualTeamName: 'Social Media Team', + ronaTimeout: '60', + }, + }, + }, + }, + }; + screen = render(); + container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container!); + expect(container).toMatchSnapshot('social-media-type'); + }); + + it('should render consultation and wrapup modes', async () => { + // Consultation initiated + const consultProps = {...defaultProps, consultInitiated: true, consultAgentName: 'Consult Agent'}; + let screen = render(); + let mainContainer = screen.container.querySelector('.call-control-container'); + let consultContainer = screen.container.querySelector('.call-control-consult-container'); + mockUIDProps(mainContainer!); + if (consultContainer) { + mockUIDProps(consultContainer); + } + expect(screen.container).toMatchSnapshot('consultation-initiated'); + screen.unmount(); + + // Consultation accepted + const consultAcceptedProps = { + ...defaultProps, + consultAccepted: true, + consultAgentName: 'Consult Agent', + consultStartTimeStamp: 1234567890000, + }; + screen = render(); + mainContainer = screen.container.querySelector('.call-control-container'); + consultContainer = screen.container.querySelector('.call-control-consult-container'); + mockUIDProps(mainContainer!); + if (consultContainer) { + mockUIDProps(consultContainer); + } + expect(screen.container).toMatchSnapshot('consultation-accepted'); + screen.unmount(); + + // Wrapup mode + const wrapupProps = { + ...defaultProps, + controlVisibility: {...defaultProps.controlVisibility, wrapup: true}, + }; + screen = render(); + const container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container!); + expect(container).toMatchSnapshot('wrapup-mode'); + screen.unmount(); + + // Consultation with wrapup (consultation should be hidden) + const consultWrapupProps = { + ...defaultProps, + consultAccepted: true, + consultAgentName: 'Consult Agent', + controlVisibility: {...defaultProps.controlVisibility, wrapup: true}, + }; + screen = render(); + const wrapupContainer = screen.container.querySelector('.call-control-container'); + mockUIDProps(wrapupContainer!); + expect(wrapupContainer).toMatchSnapshot('consultation-hidden-in-wrapup'); + }); + + it('should handle edge cases and control visibility', async () => { + // No customer information + const noCustomerProps = { + ...defaultProps, + currentTask: { + ...defaultProps.currentTask, + data: { + ...defaultProps.currentTask.data, + interaction: { + ...defaultProps.currentTask.data.interaction, + callAssociatedDetails: {}, + }, + }, + }, + }; + let screen = render(); + let container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container!); + expect(container).toMatchSnapshot('no-customer-information'); + screen.unmount(); + + // Limited control visibility + const limitedControlsProps = { + ...defaultProps, + controlVisibility: { + accept: true, + decline: true, + end: true, + muteUnmute: true, + holdResume: false, + consult: false, + transfer: false, + conference: false, + wrapup: false, + pauseResumeRecording: false, + endConsult: true, + recordingIndicator: false, + }, + }; + screen = render(); + container = screen.container.querySelector('.call-control-container'); + mockUIDProps(container!); + expect(container).toMatchSnapshot('limited-control-visibility'); + screen.unmount(); + + // Custom CSS classes with consultation + const customProps = { + ...defaultProps, + callControlClassName: 'custom-call-control', + callControlConsultClassName: 'custom-consult-control', + consultAccepted: true, + consultAgentName: 'Consult Agent', + }; + screen = render(); + const mainContainer = screen.container.querySelector('.call-control-container'); + const consultContainer = screen.container.querySelector('.call-control-consult-container'); + mockUIDProps(mainContainer!); + if (consultContainer) { + mockUIDProps(consultContainer); + } + expect(screen.container).toMatchSnapshot('custom-css-classes'); + }); +}); diff --git a/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.tsx b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.tsx new file mode 100644 index 000000000..69906ac67 --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.tsx @@ -0,0 +1,321 @@ +import React from 'react'; +import {render} from '@testing-library/react'; +import CallControlCADComponent from '../../../../src/components/task/CallControlCAD/call-control-cad'; +import {CallControlComponentProps} from '../../../../src/components/task/task.types'; +import {mockTask} from '@webex/test-fixtures'; +import {ContactServiceQueue, BuddyDetails} from '@webex/cc-store'; +import '@testing-library/jest-dom'; + +// Mock MediaStream for testing +Object.defineProperty(window, 'MediaStream', { + writable: true, + value: jest.fn().mockImplementation(() => ({ + getTracks: jest.fn(() => []), + addTrack: jest.fn(), + removeTrack: jest.fn(), + })), +}); + +// Mock TaskTimer component to avoid Worker issues in Jest +jest.mock('../../../../src/components/task/TaskTimer/index', () => + // eslint-disable-next-line react/display-name + () => 00:00 +); + +// Mock utilities that require external dependencies +jest.mock('../../../../src/utils', () => ({ + getMediaTypeInfo: jest.fn((mediaType) => ({ + labelName: mediaType === 'telephony' ? 'Voice' : 'Chat', + iconName: mediaType === 'telephony' ? 'headset' : 'chat', + className: mediaType === 'telephony' ? 'voice-media' : 'chat-media', + isBrandVisual: false, + })), +})); + +describe('CallControlCADComponent', () => { + const mockLogger = { + log: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + trace: jest.fn(), + }; + + const mockCurrentTask = { + ...mockTask, + id: 'task-123', + data: { + ...mockTask.data, + interaction: { + ...mockTask.data.interaction, + mediaType: 'telephony', + mediaChannel: 'telephony', + interactionId: 'interaction-123', + callAssociatedDetails: { + customerName: 'John Doe', + ani: '555-123-4567', + virtualTeamName: 'Support Team', + ronaTimeout: '30', + }, + }, + }, + status: 'connected', + isHeld: false, + recording: {isRecording: false}, + wrapUpReason: null, + }; + + const mockWrapupCodes = [ + {id: 'wrap1', name: 'Customer Issue', isSystem: false}, + {id: 'wrap2', name: 'Technical Support', isSystem: false}, + ]; + + const mockBuddyAgents: BuddyDetails[] = [ + { + agentId: 'agent1', + agentName: 'John Doe', + dn: '1001', + teamId: 'team1', + siteId: 'site1', + state: 'Available', + } as BuddyDetails, + ]; + + const mockControlVisibility = { + accept: true, + decline: true, + end: true, + muteUnmute: true, + holdResume: true, + consult: true, + transfer: true, + conference: true, + wrapup: false, + pauseResumeRecording: true, + endConsult: true, + recordingIndicator: true, + }; + + const defaultProps: CallControlComponentProps = { + currentTask: mockCurrentTask, + wrapupCodes: mockWrapupCodes, + toggleHold: jest.fn(), + toggleRecording: jest.fn(), + toggleMute: jest.fn(), + isMuted: false, + endCall: jest.fn(), + wrapupCall: jest.fn(), + isHeld: false, + setIsHeld: jest.fn(), + isRecording: false, + setIsRecording: jest.fn(), + buddyAgents: mockBuddyAgents, + loadBuddyAgents: jest.fn(), + queues: [] as ContactServiceQueue[], + loadQueues: jest.fn(), + transferCall: jest.fn(), + consultCall: jest.fn(), + endConsultCall: jest.fn(), + consultInitiated: false, + consultTransfer: jest.fn(), + consultCompleted: false, + consultAccepted: false, + consultStartTimeStamp: Date.now(), + callControlAudio: null as unknown as MediaStream, + consultAgentName: '', + setConsultAgentName: jest.fn(), + consultAgentId: '', + setConsultAgentId: jest.fn(), + holdTime: 0, + callControlClassName: '', + callControlConsultClassName: '', + startTimestamp: Date.now(), + isEndConsultEnabled: true, + allowConsultToQueue: true, + lastTargetType: 'agent', + setLastTargetType: jest.fn(), + controlVisibility: mockControlVisibility, + logger: mockLogger, + secondsUntilAutoWrapup: undefined, + cancelAutoWrapup: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render telephony call control with all basic information', () => { + const screen = render(); + + // Verify main structure + const container = screen.container.querySelector('.call-control-container'); + expect(container).toBeInTheDocument(); + + // Verify call information display + expect(screen.getByText('Queue:')).toBeInTheDocument(); + expect(screen.getByText('Support Team')).toBeInTheDocument(); + expect(screen.getByText('Phone Number:')).toBeInTheDocument(); + expect(screen.getByText('RONA:')).toBeInTheDocument(); + expect(screen.getByText('30')).toBeInTheDocument(); + + // Verify media icon and timer + const mediaIcon = screen.container.querySelector('.media-icon.voice-media'); + expect(mediaIcon).toBeInTheDocument(); + const timerElement = screen.container.querySelector('.call-timer'); + expect(timerElement).toBeInTheDocument(); + + // Verify recording indicator + const recordingIndicator = screen.container.querySelector('.recording-indicator'); + expect(recordingIndicator).toBeInTheDocument(); + + // Verify phone numbers displayed + const phoneNumbers = screen.getAllByText('555-123-4567'); + expect(phoneNumbers.length).toBeGreaterThan(0); + }); + + it('should handle different states and media types', () => { + // Test held state with hold time + const heldProps = { + ...defaultProps, + isHeld: true, + holdTime: 65, + }; + const heldScreen = render(); + expect(heldScreen.getByText(/On hold/)).toBeInTheDocument(); + expect(heldScreen.getByText(/01:05/)).toBeInTheDocument(); + const holdIcon = heldScreen.container.querySelector('.call-hold-filled-icon'); + expect(holdIcon).toBeInTheDocument(); + heldScreen.unmount(); + + // Test social media interaction + const socialProps = { + ...defaultProps, + currentTask: { + ...defaultProps.currentTask, + data: { + ...defaultProps.currentTask.data, + interaction: { + ...defaultProps.currentTask.data.interaction, + mediaType: 'social', + mediaChannel: 'social', + callAssociatedDetails: { + customerName: 'Social Customer', + ani: '555-123-4567', + virtualTeamName: 'Support Team', + ronaTimeout: '30', + }, + }, + }, + }, + }; + const socialScreen = render(); + expect(socialScreen.getByText('Customer Name')).toBeInTheDocument(); + const socialCustomerNames = socialScreen.getAllByText('Social Customer'); + expect(socialCustomerNames.length).toBeGreaterThan(0); + socialScreen.unmount(); + + // Test consultation functionality for telephony + const consultProps = { + ...defaultProps, + consultAccepted: true, + consultAgentName: 'Consult Agent', + consultStartTimeStamp: Date.now(), + }; + const consultScreen = render(); + const consultContainer = consultScreen.container.querySelector('.call-control-consult-container'); + expect(consultContainer).toBeInTheDocument(); + consultScreen.unmount(); + + // Test that consultation is hidden for non-telephony + const chatConsultProps = { + ...defaultProps, + currentTask: { + ...defaultProps.currentTask, + data: { + ...defaultProps.currentTask.data, + interaction: { + ...defaultProps.currentTask.data.interaction, + mediaType: 'chat', + mediaChannel: 'chat', + }, + }, + }, + consultAccepted: true, + }; + const chatConsultScreen = render(); + const chatConsultContainer = chatConsultScreen.container.querySelector('.call-control-consult-container'); + expect(chatConsultContainer).not.toBeInTheDocument(); + chatConsultScreen.unmount(); + }); + + it('should handle wrapup mode and edge cases', () => { + // Test wrapup mode hides elements + const wrapupProps = { + ...defaultProps, + controlVisibility: { + ...mockControlVisibility, + wrapup: true, + }, + isHeld: true, + isRecording: true, + consultAccepted: true, + }; + const screen = render(); + + // Verify elements are hidden in wrapup mode + expect(screen.queryByText('On Hold')).not.toBeInTheDocument(); + const recordingIndicator = screen.container.querySelector('.recording-indicator'); + expect(recordingIndicator).not.toBeInTheDocument(); + const consultContainer = screen.container.querySelector('.call-control-consult-container'); + expect(consultContainer).not.toBeInTheDocument(); + screen.unmount(); + + // Test fallback values when data is missing + const noDataProps = { + ...defaultProps, + currentTask: { + ...defaultProps.currentTask, + data: { + ...defaultProps.currentTask.data, + interaction: { + ...defaultProps.currentTask.data.interaction, + callAssociatedDetails: {}, + }, + }, + }, + }; + const noDataScreen = render(); + expect(noDataScreen.getByText('No Caller ID')).toBeInTheDocument(); + expect(noDataScreen.getByText('No Team Name')).toBeInTheDocument(); + expect(noDataScreen.getByText('No Phone Number')).toBeInTheDocument(); + expect(noDataScreen.getByText('No RONA')).toBeInTheDocument(); + noDataScreen.unmount(); + + // Test controlVisibility hiding recording indicator + const noRecordingProps = { + ...defaultProps, + controlVisibility: { + ...mockControlVisibility, + recordingIndicator: false, + }, + }; + const noRecordingScreen = render(); + const hiddenRecordingIndicator = noRecordingScreen.container.querySelector('.recording-indicator'); + expect(hiddenRecordingIndicator).not.toBeInTheDocument(); + noRecordingScreen.unmount(); + + // Test custom CSS classes + const customProps = { + ...defaultProps, + callControlClassName: 'custom-call-control', + callControlConsultClassName: 'custom-consult-control', + consultAccepted: true, + }; + const customScreen = render(); + const container = customScreen.container.querySelector('.call-control-container'); + expect(container).toHaveClass('custom-call-control'); + const customConsultContainer = customScreen.container.querySelector('.call-control-consult-container'); + expect(customConsultContainer).toHaveClass('custom-consult-control'); + }); +});