diff --git a/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.tsx b/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.tsx index 87fe6a0f7..5766b9140 100644 --- a/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.tsx +++ b/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.tsx @@ -1,66 +1,38 @@ import React from 'react'; import {IncomingTaskComponentProps, MEDIA_CHANNEL} from '../task.types'; import Task from '../Task'; +import {extractIncomingTaskData} from './incoming-task.utils'; const IncomingTaskComponent: React.FunctionComponent = (props) => { - const {incomingTask, isBrowser, accept, reject, logger} = props; + const {incomingTask, isBrowser, accept, reject} = props; if (!incomingTask) { return <>; // hidden component } - //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 - const callAssociationDetails = incomingTask?.data?.interaction?.callAssociatedDetails; - const ani = callAssociationDetails?.ani; - const customerName = callAssociationDetails?.customerName; - const virtualTeamName = callAssociationDetails?.virtualTeamName; - const ronaTimeout = callAssociationDetails?.ronaTimeout ? Number(callAssociationDetails?.ronaTimeout) : null; - const startTimeStamp = incomingTask?.data?.interaction?.createdTimestamp; - const mediaType = incomingTask.data.interaction.mediaType; - const mediaChannel = incomingTask.data.interaction.mediaChannel; - const isTelephony = mediaType === MEDIA_CHANNEL.TELEPHONY; - const isSocial = mediaType === MEDIA_CHANNEL.SOCIAL; - const acceptText = !incomingTask.data.wrapUpRequired - ? isTelephony && !isBrowser - ? 'Ringing...' - : 'Accept' - : undefined; - const declineText = !incomingTask.data.wrapUpRequired && isTelephony && isBrowser ? 'Decline' : undefined; + // Extract all task data using the utility function + const taskData = extractIncomingTaskData(incomingTask, isBrowser); return ( { - logger.info( - `CC-Widgets: IncomingTask: accept clicked for task with interactionID: ${incomingTask.data.interactionId}`, - { - module: 'incoming-task.tsx', - method: 'acceptTask', - } - ); accept(incomingTask); }} declineTask={() => { - logger.info( - `CC-Widgets: IncomingTask: decline clicked for task with interactionID: ${incomingTask.data.interactionId}`, - { - module: 'incoming-task.tsx', - method: 'declineTask', - } - ); reject(incomingTask); }} - ronaTimeout={ronaTimeout} - acceptText={acceptText} - disableAccept={isTelephony && !isBrowser} - declineText={declineText} + ronaTimeout={taskData.ronaTimeout} + acceptText={taskData.acceptText} + disableAccept={taskData.disableAccept} + declineText={taskData.declineText} styles="task-list-hover" - mediaType={mediaType} - mediaChannel={mediaChannel} + mediaType={taskData.mediaType as MEDIA_CHANNEL} + mediaChannel={taskData.mediaChannel as MEDIA_CHANNEL} /> ); }; diff --git a/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.utils.tsx b/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.utils.tsx new file mode 100644 index 000000000..51a0d063a --- /dev/null +++ b/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.utils.tsx @@ -0,0 +1,72 @@ +import {MEDIA_CHANNEL} from '../task.types'; +import {ITask} from '@webex/cc-store'; + +export interface IncomingTaskData { + ani: string; + customerName: string; + virtualTeamName: string; + ronaTimeout: number | null; + startTimeStamp: number; + mediaType: string; + mediaChannel: string; + isTelephony: boolean; + isSocial: boolean; + acceptText: string | undefined; + declineText: string | undefined; + title: string; + disableAccept: boolean; +} + +/** + * Extracts and processes all data needed for rendering an incoming task + * @param incomingTask - The incoming task object + * @param isBrowser - Whether the device type is browser + * @returns Processed task data with computed values + */ +export const extractIncomingTaskData = (incomingTask: ITask, isBrowser: boolean): IncomingTaskData => { + // Extract basic data from task + //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 + const callAssociationDetails = incomingTask?.data?.interaction?.callAssociatedDetails; + const ani = callAssociationDetails?.ani; + const customerName = callAssociationDetails?.customerName; + const virtualTeamName = callAssociationDetails?.virtualTeamName; + const ronaTimeout = callAssociationDetails?.ronaTimeout ? Number(callAssociationDetails?.ronaTimeout) : null; + const startTimeStamp = incomingTask?.data?.interaction?.createdTimestamp; + const mediaType = incomingTask.data.interaction.mediaType; + const mediaChannel = incomingTask.data.interaction.mediaChannel; + + // Compute media type flags + const isTelephony = mediaType === MEDIA_CHANNEL.TELEPHONY; + const isSocial = mediaType === MEDIA_CHANNEL.SOCIAL; + + // Compute button text based on conditions + const acceptText = !incomingTask.data.wrapUpRequired + ? isTelephony && !isBrowser + ? 'Ringing...' + : 'Accept' + : undefined; + + const declineText = !incomingTask.data.wrapUpRequired && isTelephony && isBrowser ? 'Decline' : undefined; + + // Compute title based on media type + const title = isSocial ? customerName : ani; + + // Compute disable state for accept button + const disableAccept = isTelephony && !isBrowser; + + return { + ani, + customerName, + virtualTeamName, + ronaTimeout, + startTimeStamp, + mediaType, + mediaChannel, + isTelephony, + isSocial, + acceptText, + declineText, + title, + disableAccept, + }; +}; diff --git a/packages/contact-center/cc-components/src/components/task/Task/index.tsx b/packages/contact-center/cc-components/src/components/task/Task/index.tsx index 7d9ed1014..564c3b72a 100644 --- a/packages/contact-center/cc-components/src/components/task/Task/index.tsx +++ b/packages/contact-center/cc-components/src/components/task/Task/index.tsx @@ -3,11 +3,11 @@ import {ButtonPill, ListItemBase, ListItemBaseSection, Text} from '@momentum-ui/ import {Avatar, Brandvisual, Tooltip} from '@momentum-design/components/dist/react'; import {PressEvent} from '@react-types/shared'; import TaskTimer from '../TaskTimer'; -import {getMediaTypeInfo} from '../../../utils'; import type {MEDIA_CHANNEL as MediaChannelType} from '../task.types'; +import {extractTaskComponentData, getTaskListItemClasses} from './task.utils'; import './styles.scss'; -interface TaskProps { +export interface TaskProps { interactionId?: string; title?: string; state?: string; @@ -46,24 +46,18 @@ const Task: React.FC = ({ mediaType, mediaChannel, }) => { - const capitalizeFirstWord = (str: string) => { - return str.replace(/^\s*(\w)/, (match, firstLetter) => firstLetter.toUpperCase()); - }; - const currentMediaType = getMediaTypeInfo(mediaType, mediaChannel); - const isNonVoiceMedia = currentMediaType.labelName !== 'Call'; - // Create unique IDs for tooltip trigger and tooltip - const tooltipTriggerId = `tooltip-trigger-${interactionId}`; - const tooltipId = `tooltip-${interactionId}`; - // Helper function to get the correct CSS class - const getTitleClassName = () => { - if (isNonVoiceMedia && isIncomingTask) { - return 'incoming-digital-task-title'; - } - if (isNonVoiceMedia && !isIncomingTask) { - return 'task-digital-title'; - } - return 'task-title'; - }; + // Extract all computed data using the utility function + const taskData = extractTaskComponentData({ + mediaType, + mediaChannel, + isIncomingTask, + interactionId, + state, + queue, + ronaTimeout, + startTimeStamp, + }); + const renderTitle = () => { if (!title) return null; @@ -71,25 +65,25 @@ const Task: React.FC = ({ {title} ); - if (isNonVoiceMedia) { + if (taskData.isNonVoiceMedia) { return ( <> {textComponent} {title} @@ -103,48 +97,66 @@ const Task: React.FC = ({ return ( - {currentMediaType.isBrandVisual ? ( + {taskData.currentMediaType.isBrandVisual ? (
- +
) : ( - + )}
{renderTitle()} - {state && !isIncomingTask && ( - - {capitalizeFirstWord(state)} + {taskData.shouldShowState && ( + + {taskData.capitalizedState} )} - {queue && isIncomingTask && ( - - {capitalizeFirstWord(queue)} + {taskData.shouldShowQueue && ( + + {taskData.capitalizedQueue} )} {/* Handle Time should render if it's an incoming call without ronaTimeout OR if it's not an incoming call */} - {(isIncomingTask && !ronaTimeout) || !isIncomingTask - ? startTimeStamp && ( - - Handle Time: {' '} - - - ) - : null} + {taskData.shouldShowHandleTime && ( + + Handle Time: {' '} + + + )} {/* Time Left should render if it's an incoming call with ronaTimeout */} - {isIncomingTask && ronaTimeout && ( - + {taskData.shouldShowTimeLeft && ( + Time Left: {' '} diff --git a/packages/contact-center/cc-components/src/components/task/Task/task.utils.ts b/packages/contact-center/cc-components/src/components/task/Task/task.utils.ts new file mode 100644 index 000000000..7740411f4 --- /dev/null +++ b/packages/contact-center/cc-components/src/components/task/Task/task.utils.ts @@ -0,0 +1,160 @@ +import type {MEDIA_CHANNEL as MediaChannelType, TaskComponentData} from '../task.types'; +import {getMediaTypeInfo} from '../../../utils'; + +/** + * Capitalizes the first word of a string + * @param str - The string to capitalize + * @returns The string with the first word capitalized + */ +export const capitalizeFirstWord = (str: string): string => { + return str.replace(/^\s*(\w)/, (match, firstLetter) => firstLetter.toUpperCase()); +}; + +/** + * Gets the correct CSS class name for the task title + * @param isNonVoiceMedia - Whether the media type is non-voice + * @param isIncomingTask - Whether this is an incoming task + * @returns The appropriate CSS class name + */ +export const getTitleClassName = (isNonVoiceMedia: boolean, isIncomingTask: boolean): string => { + if (isNonVoiceMedia && isIncomingTask) { + return 'incoming-digital-task-title'; + } + if (isNonVoiceMedia && !isIncomingTask) { + return 'task-digital-title'; + } + return 'task-title'; +}; + +/** + * Creates unique IDs for tooltip elements + * @param interactionId - The interaction ID + * @returns Object with tooltip trigger and tooltip IDs + */ +export const createTooltipIds = (interactionId?: string) => { + return { + tooltipTriggerId: `tooltip-trigger-${interactionId}`, + tooltipId: `tooltip-${interactionId}`, + }; +}; + +/** + * Determines if the state should be shown + * @param state - The task state + * @param isIncomingTask - Whether this is an incoming task + * @returns Whether to show the state + */ +export const shouldShowState = (state?: string, isIncomingTask?: boolean): boolean => { + return Boolean(state && !isIncomingTask); +}; + +/** + * Determines if the queue should be shown + * @param queue - The queue name + * @param isIncomingTask - Whether this is an incoming task + * @returns Whether to show the queue + */ +export const shouldShowQueue = (queue?: string, isIncomingTask?: boolean): boolean => { + return Boolean(queue && isIncomingTask); +}; + +/** + * Determines if handle time should be shown + * @param isIncomingTask - Whether this is an incoming task + * @param ronaTimeout - The RONA timeout value + * @param startTimeStamp - The start timestamp + * @returns Whether to show handle time + */ +export const shouldShowHandleTime = ( + isIncomingTask?: boolean, + ronaTimeout?: number, + startTimeStamp?: number +): boolean => { + if (!startTimeStamp) return false; + return (isIncomingTask && !ronaTimeout) || !isIncomingTask; +}; + +/** + * Determines if time left should be shown + * @param isIncomingTask - Whether this is an incoming task + * @param ronaTimeout - The RONA timeout value + * @returns Whether to show time left + */ +export const shouldShowTimeLeft = (isIncomingTask?: boolean, ronaTimeout?: number): boolean => { + return Boolean(isIncomingTask && ronaTimeout); +}; + +/** + * Gets the task list item CSS classes + * @param selected - Whether the task is selected + * @param styles - Additional styles + * @returns The combined CSS class string + */ +export const getTaskListItemClasses = (selected?: boolean, styles?: string): string => { + const baseClass = 'task-list-item'; + const selectedClass = selected ? 'task-list-item--selected' : ''; + const additionalStyles = styles || ''; + + return `${baseClass} ${selectedClass} ${additionalStyles}`.trim(); +}; + +/** + * Extracts and processes all data needed for rendering the Task component + * @param props - The Task component props + * @returns Processed task data with computed values + */ +export const extractTaskComponentData = ({ + mediaType, + mediaChannel, + isIncomingTask = false, + interactionId, + state, + queue, + ronaTimeout, + startTimeStamp, +}: { + mediaType?: MediaChannelType; + mediaChannel?: MediaChannelType; + isIncomingTask?: boolean; + interactionId?: string; + state?: string; + queue?: string; + ronaTimeout?: number; + startTimeStamp?: number; + acceptText?: string; + declineText?: string; +}): TaskComponentData => { + // Get media type information + const currentMediaType = getMediaTypeInfo(mediaType, mediaChannel); + const isNonVoiceMedia = currentMediaType.labelName !== 'Call'; + + // Create tooltip IDs + const {tooltipTriggerId, tooltipId} = createTooltipIds(interactionId); + + // Get title CSS class + const titleClassName = getTitleClassName(isNonVoiceMedia, isIncomingTask); + + // Determine what elements should be shown + const shouldShowStateElement = shouldShowState(state, isIncomingTask); + const shouldShowQueueElement = shouldShowQueue(queue, isIncomingTask); + const shouldShowHandleTimeElement = shouldShowHandleTime(isIncomingTask, ronaTimeout, startTimeStamp); + const shouldShowTimeLeftElement = shouldShowTimeLeft(isIncomingTask, ronaTimeout); + + // Capitalize text values + const capitalizedState = state ? capitalizeFirstWord(state) : ''; + const capitalizedQueue = queue ? capitalizeFirstWord(queue) : ''; + + return { + currentMediaType, + isNonVoiceMedia, + tooltipTriggerId, + tooltipId, + titleClassName, + shouldShowState: shouldShowStateElement, + shouldShowQueue: shouldShowQueueElement, + shouldShowHandleTime: shouldShowHandleTimeElement, + shouldShowTimeLeft: shouldShowTimeLeftElement, + capitalizedState, + capitalizedQueue, + }; +}; diff --git a/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx b/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx index 7ec913b67..ca0e0f504 100644 --- a/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx +++ b/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx @@ -1,39 +1,32 @@ import React from 'react'; import {TaskListComponentProps, MEDIA_CHANNEL} from '../task.types'; import Task from '../Task'; +import { + extractTaskListItemData, + isTaskListEmpty, + getTasksArray, + createTaskSelectHandler, + isCurrentTaskSelected, +} from './task-list.utils'; import './styles.scss'; const TaskListComponent: React.FunctionComponent = (props) => { const {currentTask, taskList, acceptTask, declineTask, isBrowser, onTaskSelect, logger} = props; - if (!taskList || Object.keys(taskList).length === 0) { + // Early return for empty task list + if (isTaskListEmpty(taskList)) { return <>; // hidden component } + + // Get tasks as array for mapping + const tasks = getTasksArray(taskList!); return (
    - {Object.values(taskList)?.map((task, index) => { - //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 - const callAssociationDetails = task?.data?.interaction?.callAssociatedDetails; - const ani = callAssociationDetails?.ani; - const customerName = callAssociationDetails?.customerName; - const virtualTeamName = callAssociationDetails?.virtualTeamName; - // rona timeout is not always available in the callAssociatedDetails object - const ronaTimeout = callAssociationDetails?.ronaTimeout ? Number(callAssociationDetails?.ronaTimeout) : null; - const taskState = task.data.interaction.state; - const startTimeStamp = task.data.interaction.createdTimestamp; - const isIncomingTask = taskState === 'new' || taskState === 'consult'; - const mediaType = task.data.interaction.mediaType; - const mediaChannel = task.data.interaction.mediaChannel; - const isTelephony = mediaType === MEDIA_CHANNEL.TELEPHONY; - const isSocial = mediaType === MEDIA_CHANNEL.SOCIAL; - const acceptText = - isIncomingTask && !task.data.wrapUpRequired - ? isTelephony && !isBrowser - ? 'Ringing...' - : 'Accept' - : undefined; - const declineText = - isIncomingTask && !task.data.wrapUpRequired && isTelephony && isBrowser ? 'Decline' : undefined; + {tasks.map((task, index) => { + // Extract all task data using the utility function + const taskData = extractTaskListItemData(task, isBrowser); + + // Log task rendering logger.info('CC-Widgets: TaskList: rendering task list', { module: 'task-list.tsx', method: 'renderItem', @@ -41,33 +34,22 @@ const TaskListComponent: React.FunctionComponent = (prop return ( acceptTask(task)} declineTask={() => declineTask(task)} - ronaTimeout={isIncomingTask ? ronaTimeout : null} - onTaskSelect={() => { - logger.log(`CC-Widgets: TaskList: select task clicked for interactionId: ${task.data.interactionId}`, { - module: 'task-list.tsx', - method: 'onTaskSelect', - }); - if ( - currentTask?.data.interactionId !== task.data.interactionId && - !(isIncomingTask && !task.data.wrapUpRequired) - ) { - onTaskSelect(task); - } - }} - acceptText={acceptText} - disableAccept={isIncomingTask && isTelephony && !isBrowser} - declineText={declineText} - mediaType={mediaType} - mediaChannel={mediaChannel} + ronaTimeout={taskData.ronaTimeout} + onTaskSelect={createTaskSelectHandler(task, currentTask, onTaskSelect)} + acceptText={taskData.acceptText} + disableAccept={taskData.disableAccept} + declineText={taskData.declineText} + mediaType={taskData.mediaType as MEDIA_CHANNEL} + mediaChannel={taskData.mediaChannel as MEDIA_CHANNEL} /> ); })} diff --git a/packages/contact-center/cc-components/src/components/task/TaskList/task-list.utils.ts b/packages/contact-center/cc-components/src/components/task/TaskList/task-list.utils.ts new file mode 100644 index 000000000..f902c1b1d --- /dev/null +++ b/packages/contact-center/cc-components/src/components/task/TaskList/task-list.utils.ts @@ -0,0 +1,134 @@ +import {MEDIA_CHANNEL, TaskListItemData} from '../task.types'; +import {ITask} from '@webex/cc-store'; + +/** + * Extracts and processes data from a task for rendering in the task list + * @param task - The task object + * @param isBrowser - Whether the device type is browser + * @returns Processed task data with computed values + */ +export const extractTaskListItemData = (task: ITask, isBrowser: boolean): TaskListItemData => { + // Extract basic data from task + //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 + const callAssociationDetails = task?.data?.interaction?.callAssociatedDetails; + const ani = callAssociationDetails?.ani; + const customerName = callAssociationDetails?.customerName; + const virtualTeamName = callAssociationDetails?.virtualTeamName; + + // rona timeout is not always available in the callAssociatedDetails object + const rawRonaTimeout = callAssociationDetails?.ronaTimeout ? Number(callAssociationDetails?.ronaTimeout) : null; + + const taskState = task.data.interaction.state; + const startTimeStamp = task.data.interaction.createdTimestamp; + const isIncomingTask = taskState === 'new' || taskState === 'consult'; + const mediaType = task.data.interaction.mediaType; + const mediaChannel = task.data.interaction.mediaChannel; + + // Compute media type flags + const isTelephony = mediaType === MEDIA_CHANNEL.TELEPHONY; + const isSocial = mediaType === MEDIA_CHANNEL.SOCIAL; + + // Compute button text based on conditions + const acceptText = + isIncomingTask && !task.data.wrapUpRequired ? (isTelephony && !isBrowser ? 'Ringing...' : 'Accept') : undefined; + + const declineText = isIncomingTask && !task.data.wrapUpRequired && isTelephony && isBrowser ? 'Decline' : undefined; + + // Compute title based on media type + const title = isSocial ? customerName : ani; + + // Compute disable state for accept button + const disableAccept = isIncomingTask && isTelephony && !isBrowser; + + const ronaTimeout = isIncomingTask ? rawRonaTimeout : null; + + // Compute display state + const displayState = !isIncomingTask ? taskState : ''; + + return { + ani, + customerName, + virtualTeamName, + ronaTimeout, + taskState, + startTimeStamp, + isIncomingTask, + mediaType, + mediaChannel, + isTelephony, + isSocial, + acceptText, + declineText, + title, + disableAccept, + displayState, + }; +}; + +/** + * Determines if a task should be selectable + * @param task - The task object + * @param currentTask - The currently selected task + * @param taskData - Processed task data + * @returns Whether the task should be selectable + */ +export const isTaskSelectable = (task: ITask, currentTask: ITask | null, taskData: TaskListItemData): boolean => { + const isDifferentTask = currentTask?.data.interactionId !== task.data.interactionId; + const isNotIncomingWithoutWrapUp = !(taskData.isIncomingTask && !task.data.wrapUpRequired); + + return isDifferentTask && isNotIncomingWithoutWrapUp; +}; + +/** + * Determines if the current task is selected + * @param task - The task object + * @param currentTask - The currently selected task + * @returns Whether this task is currently selected + */ +export const isCurrentTaskSelected = (task: ITask, currentTask: ITask | null): boolean => { + return currentTask?.data.interactionId === task.data.interactionId; +}; + +/** + * Validates if a task list is empty or invalid + * @param taskList - The task list object + * @returns Whether the task list is empty or invalid + */ +export const isTaskListEmpty = (taskList: Record | null | undefined): boolean => { + return !taskList || Object.keys(taskList).length === 0; +}; + +/** + * Gets tasks as an array from the task list object + * @param taskList - The task list object + * @returns Array of tasks + */ +export const getTasksArray = (taskList: Record | null | undefined): ITask[] => { + if (!taskList) { + return []; + } + return Object.values(taskList); +}; + +/** + * Creates task select handler with logging + * @param task - The task to select + * @param currentTask - The currently selected task + * @param onTaskSelect - The task select function + * @param logger - The logger instance + * @returns Task select handler function + */ +export const createTaskSelectHandler = ( + task: ITask, + currentTask: ITask | null, + onTaskSelect: (task: ITask) => void +) => { + return () => { + // Logging moved to helper.ts + const taskData = extractTaskListItemData(task, true); // Use browser=true for selection logic + + if (isTaskSelectable(task, currentTask, taskData)) { + onTaskSelect(task); + } + }; +}; diff --git a/packages/contact-center/cc-components/src/components/task/task.types.ts b/packages/contact-center/cc-components/src/components/task/task.types.ts index 5414a5773..195f4ccc3 100644 --- a/packages/contact-center/cc-components/src/components/task/task.types.ts +++ b/packages/contact-center/cc-components/src/components/task/task.types.ts @@ -117,12 +117,14 @@ export interface TaskProps { logger: ILogger; } -export type IncomingTaskComponentProps = Pick; +export type IncomingTaskComponentProps = Pick & + Partial>; export type TaskListComponentProps = Pick< TaskProps, - 'currentTask' | 'taskList' | 'isBrowser' | 'acceptTask' | 'declineTask' | 'onTaskSelect' | 'logger' ->; + 'isBrowser' | 'acceptTask' | 'declineTask' | 'onTaskSelect' | 'logger' +> & + Partial>; /** * Interface representing the properties for control actions on a task. @@ -551,3 +553,64 @@ export interface AutoWrapupTimerProps { allowCancelAutoWrapup?: boolean; handleCancelWrapup: () => void; } + +export interface TaskComponentData { + currentMediaType: { + labelName: string; + iconName: string; + className: string; + isBrandVisual: boolean; + }; + isNonVoiceMedia: boolean; + tooltipTriggerId: string; + tooltipId: string; + titleClassName: string; + shouldShowState: boolean; + shouldShowQueue: boolean; + shouldShowHandleTime: boolean; + shouldShowTimeLeft: boolean; + capitalizedState: string; + capitalizedQueue: string; +} + +export interface TaskListItemData { + ani: string; + customerName: string; + virtualTeamName: string; + ronaTimeout: number | null; + taskState: string; + startTimeStamp: number; + isIncomingTask: boolean; + mediaType: string; + mediaChannel: string; + isTelephony: boolean; + isSocial: boolean; + acceptText: string | undefined; + declineText: string | undefined; + title: string; + disableAccept: boolean; + displayState: string; +} + +export enum TaskState { + NEW = 'new', + ACTIVE = 'active', + CONNECTED = 'connected', + HOLD = 'hold', + CONSULT = 'consult', + CONFERENCE = 'conference', + WRAP_UP = 'wrap_up', + ENDED = 'ended', + TRANSFERRED = 'transferred', + DECLINED = 'declined', +} + +export enum TaskQueue { + SUPPORT = 'support', + SALES = 'sales', + TECHNICAL = 'technical', + BILLING = 'billing', + GENERAL = 'general', + VIP = 'vip', + ESCALATION = 'escalation', +} diff --git a/packages/contact-center/cc-components/tests/components/task/IncomingTask/__snapshots__/incoming-task.snapshot.tsx.snap b/packages/contact-center/cc-components/tests/components/task/IncomingTask/__snapshots__/incoming-task.snapshot.tsx.snap new file mode 100644 index 000000000..a06c9a034 --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/IncomingTask/__snapshots__/incoming-task.snapshot.tsx.snap @@ -0,0 +1,1203 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`IncomingTaskComponent Actions should call acceptTask when accept button is clicked 1`] = ` +
    +
  • +
    + +
    +
    +
    + + 1234567890 + + + Support Team + + + Time Left: + + + +
    +
    +
    +
    + + +
    +
    +
  • +
    +`; + +exports[`IncomingTaskComponent Actions should call acceptTask when accept button is clicked 2`] = ` +
    +
  • +
    + +
    +
    +
    + + 1234567890 + + + Support Team + + + Time Left: + + + +
    +
    +
    +
    + + +
    +
    +
  • +
    +`; + +exports[`IncomingTaskComponent Actions should call declineTask when decline button is clicked 1`] = ` +
    +
  • +
    + +
    +
    +
    + + 1234567890 + + + Support Team + + + Time Left: + + + +
    +
    +
    +
    + + +
    +
    +
  • +
    +`; + +exports[`IncomingTaskComponent Actions should call declineTask when decline button is clicked 2`] = ` +
    +
  • +
    + +
    +
    +
    + + 1234567890 + + + Support Team + + + Time Left: + + + +
    +
    +
    +
    + + +
    +
    +
  • +
    +`; + +exports[`IncomingTaskComponent Empty Component Scenarios should return empty component when incomingTask is null or undefined 1`] = `
    `; + +exports[`IncomingTaskComponent Empty Component Scenarios should return empty component when incomingTask is null or undefined 2`] = `
    `; + +exports[`IncomingTaskComponent Rendering with Incoming Tasks should render digital incoming task with Accept button only 1`] = ` +
    +
  • +
    + +
    +
    +
    + + Chat Customer + + + Chat Customer + + + Chat Support + + + Time Left: + + + +
    +
    +
    +
    + +
    +
    +
  • +
    +`; + +exports[`IncomingTaskComponent Rendering with Incoming Tasks should render multiple incoming tasks with different media types and their corresponding icons and labels 1`] = ` +
    +
  • +
    + +
    +
    +
    + + 1234567890 + + + Support Team + + + Time Left: + + + +
    +
    +
    +
    + + +
    +
    +
  • +
    +`; + +exports[`IncomingTaskComponent Rendering with Incoming Tasks should render multiple incoming tasks with different media types and their corresponding icons and labels 2`] = ` +
    +
  • +
    + +
    +
    +
    + + 1234567890 + + + Mobile Support + + + Time Left: + + + +
    +
    +
    +
    + +
    +
    +
  • +
    +`; + +exports[`IncomingTaskComponent Rendering with Incoming Tasks should render multiple incoming tasks with different media types and their corresponding icons and labels 3`] = ` +
    +
  • +
    + +
    +
    +
    + + Chat Customer + + + Chat Customer + + + Chat Support + + + Time Left: + + + +
    +
    +
    +
    + +
    +
    +
  • +
    +`; + +exports[`IncomingTaskComponent Rendering with Incoming Tasks should render multiple incoming tasks with different media types and their corresponding icons and labels 4`] = ` +
    +
  • +
    +
    +
    +
    +
    +
    + + Social Customer + + + Social Customer + + + Social Team + + + Time Left: + + + +
    +
    +
    +
    + +
    +
    +
  • +
    +`; + +exports[`IncomingTaskComponent Rendering with Incoming Tasks should render telephony incoming task with Accept/Decline buttons in WebRTC mode 1`] = ` +
    +
  • +
    + +
    +
    +
    + + 1234567890 + + + Support Team + + + Time Left: + + + +
    +
    +
    +
    + + +
    +
    +
  • +
    +`; + +exports[`IncomingTaskComponent Rendering with Incoming Tasks should render telephony incoming task with disabled Accept button in Extension mode 1`] = ` +
    +
  • +
    + +
    +
    +
    + + 1234567890 + + + Mobile Support + + + Time Left: + + + +
    +
    +
    +
    + +
    +
    +
  • +
    +`; diff --git a/packages/contact-center/cc-components/tests/components/task/IncomingTask/incoming-task.snapshot.tsx b/packages/contact-center/cc-components/tests/components/task/IncomingTask/incoming-task.snapshot.tsx new file mode 100644 index 000000000..90d0aaf25 --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/IncomingTask/incoming-task.snapshot.tsx @@ -0,0 +1,331 @@ +import React from 'react'; +import {render, fireEvent} from '@testing-library/react'; +import '@testing-library/jest-dom'; +import {mockTask, mockIncomingTaskData} from '@webex/test-fixtures'; +import IncomingTaskComponent from '../../../../src/components/task/IncomingTask/incoming-task'; +import {IncomingTaskComponentProps, MEDIA_CHANNEL} from '../../../../src/components/task/task.types'; +import * as incomingTaskUtils from '../../../../src/components/task/IncomingTask/incoming-task.utils'; + +// Simple Worker mock +Object.defineProperty(global, 'Worker', { + writable: true, + value: class MockWorker { + constructor() {} + postMessage = jest.fn(); + addEventListener = jest.fn(); + removeEventListener = jest.fn(); + terminate = jest.fn(); + }, +}); + +Object.defineProperty(global, 'URL', { + writable: true, + value: { + createObjectURL: jest.fn(() => 'blob:mock-url'), + revokeObjectURL: jest.fn(), + }, +}); + +describe('IncomingTaskComponent', () => { + // Mock functions + const mockAccept = jest.fn(); + const mockReject = jest.fn(); + + // Default props using IncomingTaskComponentProps interface + const defaultProps: IncomingTaskComponentProps = { + incomingTask: null, + isBrowser: true, + accept: mockAccept, + reject: mockReject, + }; + + // Utility function spies + const extractIncomingTaskDataSpy = jest.spyOn(incomingTaskUtils, 'extractIncomingTaskData'); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + extractIncomingTaskDataSpy.mockRestore(); + }); + + describe('Empty Component Scenarios', () => { + it('should return empty component when incomingTask is null or undefined', async () => { + // Test with null + const propsWithNull: IncomingTaskComponentProps = { + ...defaultProps, + incomingTask: null, + }; + + const {container: containerNull} = await render(); + expect(containerNull).toMatchSnapshot(); + + // Test with undefined + const propsWithUndefined: IncomingTaskComponentProps = { + ...defaultProps, + incomingTask: undefined, + }; + + const {container: containerUndefined} = await render(); + expect(containerUndefined).toMatchSnapshot(); + }); + }); + + describe('Rendering with Incoming Tasks', () => { + const sampleIncomingTask = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'incoming-task-123', + interaction: { + ...mockTask.data.interaction, + state: 'new', + mediaType: MEDIA_CHANNEL.TELEPHONY, + callAssociatedDetails: { + ani: '1234567890', + customerName: 'John Doe', + virtualTeamName: 'Support Team', + ronaTimeout: '30', + }, + }, + wrapUpRequired: false, + }, + }; + + it('should render multiple incoming tasks with different media types and their corresponding icons and labels', async () => { + // Create telephony WebRTC task + const telephonyWebRTCTask = { + ...sampleIncomingTask, + data: { + ...sampleIncomingTask.data, + interactionId: 'telephony-webrtc-task', + interaction: { + ...sampleIncomingTask.data.interaction, + state: 'new', + mediaType: MEDIA_CHANNEL.TELEPHONY, + callAssociatedDetails: { + ani: '1234567890', + customerName: 'Telephony Customer', + virtualTeamName: 'Support Team', + ronaTimeout: '30', + }, + }, + }, + }; + + // Create telephony Extension task + const telephonyExtensionTask = { + ...sampleIncomingTask, + data: { + ...sampleIncomingTask.data, + interactionId: 'telephony-extension-task', + interaction: { + ...sampleIncomingTask.data.interaction, + state: 'new', + mediaType: MEDIA_CHANNEL.TELEPHONY, + callAssociatedDetails: { + ani: '0987654321', + customerName: 'Extension Customer', + virtualTeamName: 'Mobile Support', + ronaTimeout: '30', + }, + }, + }, + }; + + // Create chat task + const chatTask = { + ...sampleIncomingTask, + data: { + ...sampleIncomingTask.data, + interactionId: 'chat-task', + interaction: { + ...sampleIncomingTask.data.interaction, + state: 'new', + mediaType: MEDIA_CHANNEL.CHAT, + callAssociatedDetails: { + ani: 'chat-user-123', + customerName: 'Chat Customer', + virtualTeamName: 'Chat Support', + ronaTimeout: '60', + }, + }, + }, + }; + + // Create social (Facebook) task + const socialTask = { + ...sampleIncomingTask, + data: { + ...sampleIncomingTask.data, + interactionId: 'social-task', + interaction: { + ...sampleIncomingTask.data.interaction, + state: 'new', + mediaType: MEDIA_CHANNEL.SOCIAL, + mediaChannel: MEDIA_CHANNEL.FACEBOOK, + callAssociatedDetails: { + ani: 'facebook-user-456', + customerName: 'Social Customer', + virtualTeamName: 'Social Team', + ronaTimeout: '45', + }, + }, + }, + }; + + // Mock different return values for each media type and device mode + extractIncomingTaskDataSpy + .mockReturnValueOnce(mockIncomingTaskData.webRTC) // WebRTC telephony + .mockReturnValueOnce(mockIncomingTaskData.extension) // Extension telephony + .mockReturnValueOnce(mockIncomingTaskData.chat) // Chat + .mockReturnValueOnce(mockIncomingTaskData.social); // Social + + // Test WebRTC telephony task (isBrowser: true) + const {container: webRTCContainer} = await render( + + ); + expect(webRTCContainer).toMatchSnapshot(); + + // Test Extension telephony task (isBrowser: false) + const {container: extensionContainer} = await render( + + ); + expect(extensionContainer).toMatchSnapshot(); + + // Test chat task + const {container: chatContainer} = await render( + + ); + expect(chatContainer).toMatchSnapshot(); + + // Test social task + const {container: socialContainer} = await render( + + ); + expect(socialContainer).toMatchSnapshot(); + }); + + it('should render telephony incoming task with Accept/Decline buttons in WebRTC mode', async () => { + extractIncomingTaskDataSpy.mockReturnValue(mockIncomingTaskData.webRTC); + + const webRTCTask = { + ...sampleIncomingTask, + data: { + ...sampleIncomingTask.data, + interactionId: 'webrtc-telephony-task', + interaction: { + ...sampleIncomingTask.data.interaction, + state: 'new', + mediaType: MEDIA_CHANNEL.TELEPHONY, + }, + wrapUpRequired: false, + }, + }; + + const {container} = await render( + + ); + expect(container).toMatchSnapshot(); + }); + + it('should render telephony incoming task with disabled Accept button in Extension mode', async () => { + extractIncomingTaskDataSpy.mockReturnValue(mockIncomingTaskData.extension); + + const extensionTask = { + ...sampleIncomingTask, + data: { + ...sampleIncomingTask.data, + interactionId: 'extension-telephony-task', + interaction: { + ...sampleIncomingTask.data.interaction, + state: 'new', + mediaType: MEDIA_CHANNEL.TELEPHONY, + }, + wrapUpRequired: false, + }, + }; + + const {container} = await render( + + ); + expect(container).toMatchSnapshot(); + }); + + it('should render digital incoming task with Accept button only', async () => { + extractIncomingTaskDataSpy.mockReturnValue(mockIncomingTaskData.chat); + + const incomingChatTask = { + ...sampleIncomingTask, + data: { + ...sampleIncomingTask.data, + interactionId: 'incoming-chat-task', + interaction: { + ...sampleIncomingTask.data.interaction, + state: 'new', + mediaType: MEDIA_CHANNEL.CHAT, + }, + wrapUpRequired: false, + }, + }; + + const {container} = await render( + + ); + expect(container).toMatchSnapshot(); + }); + }); + + describe('Actions', () => { + const actionTask = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'action-task', + interaction: { + ...mockTask.data.interaction, + state: 'new', + mediaType: MEDIA_CHANNEL.TELEPHONY, + callAssociatedDetails: { + ani: '1234567890', + customerName: 'Action Customer', + virtualTeamName: 'Action Team', + ronaTimeout: '30', + }, + }, + wrapUpRequired: false, + }, + }; + + beforeEach(() => { + extractIncomingTaskDataSpy.mockReturnValue(mockIncomingTaskData.webRTC); + }); + + it('should call acceptTask when accept button is clicked', async () => { + const {container} = await render( + + ); + + expect(container).toMatchSnapshot(); + + const acceptButton = container.querySelector('[data-testid="task:accept-button"]') as HTMLElement; + fireEvent.click(acceptButton); + + expect(container).toMatchSnapshot(); + }); + + it('should call declineTask when decline button is clicked', async () => { + const {container} = await render( + + ); + + expect(container).toMatchSnapshot(); + + const declineButton = container.querySelector('[data-testid="task:decline-button"]') as HTMLElement; + fireEvent.click(declineButton); + + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/contact-center/cc-components/tests/components/task/IncomingTask/incoming-task.tsx b/packages/contact-center/cc-components/tests/components/task/IncomingTask/incoming-task.tsx index ecbd64984..87dafb3a5 100644 --- a/packages/contact-center/cc-components/tests/components/task/IncomingTask/incoming-task.tsx +++ b/packages/contact-center/cc-components/tests/components/task/IncomingTask/incoming-task.tsx @@ -1,34 +1,497 @@ import React from 'react'; -import {render, screen, cleanup} from '@testing-library/react'; +import {render, fireEvent, getByTestId} from '@testing-library/react'; import '@testing-library/jest-dom'; +import {mockTask, mockIncomingTaskData} from '@webex/test-fixtures'; import IncomingTaskComponent from '../../../../src/components/task/IncomingTask/incoming-task'; -import {mockTask} from '@webex/test-fixtures'; - -// 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('IncomingTaskComponent', () => { - afterEach(cleanup); - - it('renders incoming call for browser option', () => { - const props = { - incomingTask: mockTask, - accept: jest.fn(), - reject: jest.fn(), - isBrowser: true, - isAnswered: false, - isEnded: false, - isMissed: false, - logger: { - log: jest.fn(), - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - trace: jest.fn(), +import {IncomingTaskComponentProps, MEDIA_CHANNEL} from '../../../../src/components/task/task.types'; +import * as incomingTaskUtils from '../../../../src/components/task/IncomingTask/incoming-task.utils'; + +// Simple Worker mock +Object.defineProperty(global, 'Worker', { + writable: true, + value: class MockWorker { + constructor() {} + postMessage = jest.fn(); + addEventListener = jest.fn(); + removeEventListener = jest.fn(); + terminate = jest.fn(); + }, +}); + +Object.defineProperty(global, 'URL', { + writable: true, + value: { + createObjectURL: jest.fn(() => 'blob:mock-url'), + revokeObjectURL: jest.fn(), + }, +}); + +describe('IncomingTaskComponent', () => { + // Mock functions + const mockAccept = jest.fn(); + const mockReject = jest.fn(); + + // Default props using IncomingTaskComponentProps interface + const defaultProps: IncomingTaskComponentProps = { + incomingTask: null, + isBrowser: true, + accept: mockAccept, + reject: mockReject, + }; + + // Utility function spies + const extractIncomingTaskDataSpy = jest.spyOn(incomingTaskUtils, 'extractIncomingTaskData'); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + extractIncomingTaskDataSpy.mockRestore(); + }); + + describe('Empty Component Scenarios', () => { + it('should return empty component when incomingTask is null or undefined', async () => { + // Test with null + const propsWithNull: IncomingTaskComponentProps = { + ...defaultProps, + incomingTask: null, + }; + + const {container: containerNull} = await render(); + expect(containerNull).toBeEmptyDOMElement(); + expect(extractIncomingTaskDataSpy).not.toHaveBeenCalled(); + + jest.clearAllMocks(); + + // Test with undefined + const propsWithUndefined: IncomingTaskComponentProps = { + ...defaultProps, + incomingTask: undefined, + }; + + const {container: containerUndefined} = await render(); + expect(containerUndefined).toBeEmptyDOMElement(); + expect(extractIncomingTaskDataSpy).not.toHaveBeenCalled(); + }); + }); + + describe('Rendering with Incoming Tasks', () => { + const sampleIncomingTask = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'incoming-task-123', + interaction: { + ...mockTask.data.interaction, + state: 'new', + mediaType: MEDIA_CHANNEL.TELEPHONY, + callAssociatedDetails: { + ani: '1234567890', + customerName: 'John Doe', + virtualTeamName: 'Support Team', + ronaTimeout: '30', + }, + }, + wrapUpRequired: false, + }, + }; + + it('should render multiple incoming tasks with different media types and their corresponding icons and labels', async () => { + // Create telephony WebRTC task + const telephonyWebRTCTask = { + ...sampleIncomingTask, + data: { + ...sampleIncomingTask.data, + interactionId: 'telephony-webrtc-task', + interaction: { + ...sampleIncomingTask.data.interaction, + state: 'new', + mediaType: MEDIA_CHANNEL.TELEPHONY, + callAssociatedDetails: { + ani: '1234567890', + customerName: 'Telephony Customer', + virtualTeamName: 'Support Team', + ronaTimeout: '30', + }, + }, + }, + }; + + // Create telephony Extension task + const telephonyExtensionTask = { + ...sampleIncomingTask, + data: { + ...sampleIncomingTask.data, + interactionId: 'telephony-extension-task', + interaction: { + ...sampleIncomingTask.data.interaction, + state: 'new', + mediaType: MEDIA_CHANNEL.TELEPHONY, + callAssociatedDetails: { + ani: '0987654321', + customerName: 'Extension Customer', + virtualTeamName: 'Mobile Support', + ronaTimeout: '30', + }, + }, + }, + }; + + // Create chat task + const chatTask = { + ...sampleIncomingTask, + data: { + ...sampleIncomingTask.data, + interactionId: 'chat-task', + interaction: { + ...sampleIncomingTask.data.interaction, + state: 'new', + mediaType: MEDIA_CHANNEL.CHAT, + callAssociatedDetails: { + ani: 'chat-user-123', + customerName: 'Chat Customer', + virtualTeamName: 'Chat Support', + ronaTimeout: '60', + }, + }, + }, + }; + + // Create social (Facebook) task + const socialTask = { + ...sampleIncomingTask, + data: { + ...sampleIncomingTask.data, + interactionId: 'social-task', + interaction: { + ...sampleIncomingTask.data.interaction, + state: 'new', + mediaType: MEDIA_CHANNEL.SOCIAL, + mediaChannel: MEDIA_CHANNEL.FACEBOOK, + callAssociatedDetails: { + ani: 'facebook-user-456', + customerName: 'Social Customer', + virtualTeamName: 'Social Team', + ronaTimeout: '45', + }, + }, + }, + }; + + // Mock different return values for each media type and device mode + extractIncomingTaskDataSpy + .mockReturnValueOnce(mockIncomingTaskData.webRTC) // WebRTC telephony + .mockReturnValueOnce(mockIncomingTaskData.extension) // Extension telephony + .mockReturnValueOnce(mockIncomingTaskData.chat) // Chat + .mockReturnValueOnce(mockIncomingTaskData.social); // Social + + // Test WebRTC telephony task (isBrowser: true) + const {container: webRTCContainer} = await render( + + ); + + // Test Extension telephony task (isBrowser: false) + const {container: extensionContainer} = await render( + + ); + + // Test chat task + const {container: chatContainer} = await render( + + ); + + // Test social task + const {container: socialContainer} = await render( + + ); + + // Verify utility function was called correctly for each task + expect(extractIncomingTaskDataSpy).toHaveBeenCalledTimes(4); + expect(extractIncomingTaskDataSpy).toHaveBeenNthCalledWith(1, telephonyWebRTCTask, true); + expect(extractIncomingTaskDataSpy).toHaveBeenNthCalledWith(2, telephonyExtensionTask, false); + expect(extractIncomingTaskDataSpy).toHaveBeenNthCalledWith(3, chatTask, true); + expect(extractIncomingTaskDataSpy).toHaveBeenNthCalledWith(4, socialTask, true); + + // === WebRTC Telephony Task Assertions === + const webRTCListItem = webRTCContainer.querySelector('li.task-list-item'); + expect(webRTCListItem).toBeInTheDocument(); + expect(webRTCListItem).toHaveClass('task-list-item'); + expect(webRTCListItem).toHaveClass('task-list-hover'); + expect(webRTCListItem).toHaveAttribute('data-allow-text-select', 'false'); + expect(webRTCListItem).toHaveAttribute('data-disabled', 'false'); + expect(webRTCListItem).toHaveAttribute('data-interactive', 'true'); + expect(webRTCListItem).toHaveAttribute('data-padded', 'false'); + expect(webRTCListItem).toHaveAttribute('data-shape', 'rectangle'); + expect(webRTCListItem).toHaveAttribute('data-size', '40'); + expect(webRTCListItem).toHaveAttribute('id', 'telephony-webrtc-task'); + expect(webRTCListItem).toHaveAttribute('role', 'listitem'); + expect(webRTCListItem).toHaveAttribute('tabindex', '0'); + + // WebRTC avatar and icon + const webRTCAvatar = webRTCContainer.querySelector('mdc-avatar'); + expect(webRTCAvatar).toBeInTheDocument(); + expect(webRTCAvatar).toHaveClass('telephony'); + expect(webRTCAvatar).toHaveAttribute('icon-name', 'handset-filled'); + expect(webRTCAvatar).toHaveAttribute('size', '32'); + + // WebRTC task details + const webRTCTitle = webRTCContainer.querySelector('.task-title'); + expect(webRTCTitle).toBeInTheDocument(); + expect(webRTCTitle).toHaveTextContent('1234567890'); // ANI as title + expect(webRTCTitle).toHaveAttribute('type', 'body-large-medium'); + + const webRTCTeam = webRTCContainer.querySelectorAll('.task-text')[0]; + expect(webRTCTeam).toHaveTextContent('Support Team'); + + // WebRTC time display + const webRTCTimeElement = webRTCContainer.querySelector('time'); + expect(webRTCTimeElement).toBeInTheDocument(); + expect(webRTCTimeElement).toHaveClass('task-text--secondary'); + expect(webRTCTimeElement).toHaveAttribute('datetime', '00:00'); + + // WebRTC buttons - Both Accept and Decline + const webRTCAcceptButton = webRTCContainer.querySelector('[data-testid="task:accept-button"]'); + const webRTCDeclineButton = webRTCContainer.querySelector('[data-testid="task:decline-button"]'); + expect(webRTCAcceptButton).toBeInTheDocument(); + expect(webRTCDeclineButton).toBeInTheDocument(); + expect(webRTCAcceptButton).toHaveClass('md-button-pill-wrapper'); + expect(webRTCAcceptButton).toHaveClass('md-button-simple-wrapper'); + expect(webRTCAcceptButton).toHaveTextContent('Accept'); + expect(webRTCAcceptButton).toHaveAttribute('data-color', 'join'); + expect(webRTCAcceptButton).toHaveAttribute('data-disabled', 'false'); + expect(webRTCAcceptButton).toHaveAttribute('data-disabled-outline', 'false'); + expect(webRTCAcceptButton).toHaveAttribute('data-ghost', 'false'); + expect(webRTCAcceptButton).toHaveAttribute('data-grown', 'false'); + expect(webRTCAcceptButton).toHaveAttribute('data-inverted', 'false'); + expect(webRTCAcceptButton).toHaveAttribute('data-outline', 'false'); + expect(webRTCAcceptButton).toHaveAttribute('data-shallow-disabled', 'false'); + expect(webRTCAcceptButton).toHaveAttribute('data-size', '40'); + expect(webRTCAcceptButton).toHaveAttribute('data-testid', 'task:accept-button'); + expect(webRTCAcceptButton).toHaveAttribute('tabindex', '-1'); + expect(webRTCAcceptButton).toHaveAttribute('type', 'button'); + + expect(webRTCDeclineButton).toHaveAttribute('data-color', 'cancel'); + expect(webRTCDeclineButton).toHaveTextContent('Decline'); + expect(webRTCDeclineButton).toHaveAttribute('data-testid', 'task:decline-button'); + + // === Extension Telephony Task Assertions === + const extensionListItem = extensionContainer.querySelector('li.task-list-item'); + expect(extensionListItem).toBeInTheDocument(); + expect(extensionListItem).toHaveClass('task-list-item'); + expect(extensionListItem).toHaveClass('task-list-hover'); + expect(extensionListItem).toHaveAttribute('data-allow-text-select', 'false'); + expect(extensionListItem).toHaveAttribute('data-disabled', 'false'); + expect(extensionListItem).toHaveAttribute('data-interactive', 'true'); + expect(extensionListItem).toHaveAttribute('data-padded', 'false'); + expect(extensionListItem).toHaveAttribute('data-shape', 'rectangle'); + expect(extensionListItem).toHaveAttribute('data-size', '40'); + expect(extensionListItem).toHaveAttribute('id', 'telephony-extension-task'); + expect(extensionListItem).toHaveAttribute('role', 'listitem'); + + // Extension avatar and icon (same as WebRTC) + const extensionAvatar = extensionContainer.querySelector('mdc-avatar'); + expect(extensionAvatar).toBeInTheDocument(); + expect(extensionAvatar).toHaveClass('telephony'); + expect(extensionAvatar).toHaveAttribute('icon-name', 'handset-filled'); + + // Extension task details + const extensionTitle = extensionContainer.querySelector('.task-title'); + expect(extensionTitle).toHaveTextContent('1234567890'); // ANI as title + + const extensionTeam = extensionContainer.querySelectorAll('.task-text')[0]; + expect(extensionTeam).toHaveTextContent('Mobile Support'); + + // Extension buttons - Only Accept (disabled) button + const extensionAcceptButton = extensionContainer.querySelector('[data-testid="task:accept-button"]'); + const extensionDeclineButton = extensionContainer.querySelector('[data-testid="task:decline-button"]'); + expect(extensionAcceptButton).toBeInTheDocument(); + expect(extensionDeclineButton).not.toBeInTheDocument(); // No decline button + expect(extensionAcceptButton).toHaveAttribute('data-disabled', 'true'); + expect(extensionAcceptButton).toHaveAttribute('disabled', ''); + expect(extensionAcceptButton).toHaveTextContent('Ringing...'); + + // === Chat Task Assertions === + const chatListItem = chatContainer.querySelector('li.task-list-item'); + expect(chatListItem).toBeInTheDocument(); + expect(chatListItem).toHaveAttribute('id', 'chat-task'); + + // Chat avatar and icon + const chatAvatar = chatContainer.querySelector('mdc-avatar'); + expect(chatAvatar).toBeInTheDocument(); + expect(chatAvatar).toHaveClass('chat'); + expect(chatAvatar).toHaveAttribute('icon-name', 'chat-filled'); + + // Chat task details with tooltip + const chatTitle = chatContainer.querySelector('.incoming-digital-task-title'); + expect(chatTitle).toBeInTheDocument(); + expect(chatTitle).toHaveTextContent('Chat Customer'); // Customer name as title + expect(chatTitle).toHaveAttribute('aria-describedby', 'tooltip-chat-task'); + expect(chatTitle).toHaveAttribute('id', 'tooltip-trigger-chat-task'); + + // Chat tooltip + const chatTooltip = chatContainer.querySelector('mdc-tooltip'); + expect(chatTooltip).toBeInTheDocument(); + expect(chatTooltip).toHaveAttribute('id', 'tooltip-chat-task'); + expect(chatTooltip).toHaveAttribute('triggerid', 'tooltip-trigger-chat-task'); + expect(chatTooltip).toHaveTextContent('Chat Customer'); + + const chatTeam = chatContainer.querySelectorAll('.task-text')[0]; + expect(chatTeam).toHaveTextContent('Chat Support'); + + // Chat buttons - Only Accept button + const chatAcceptButton = chatContainer.querySelector('[data-testid="task:accept-button"]'); + const chatDeclineButton = chatContainer.querySelector('[data-testid="task:decline-button"]'); + expect(chatAcceptButton).toBeInTheDocument(); + expect(chatDeclineButton).not.toBeInTheDocument(); // No decline button + expect(chatAcceptButton).toHaveAttribute('data-disabled', 'false'); + expect(chatAcceptButton).toHaveTextContent('Accept'); + + // === Social Task Assertions === + const socialListItem = socialContainer.querySelector('li.task-list-item'); + expect(socialListItem).toBeInTheDocument(); + expect(socialListItem).toHaveAttribute('id', 'social-task'); + + // Social brand visual (different from avatar) + const socialBrandBackground = socialContainer.querySelector('.brand-visual-background'); + expect(socialBrandBackground).toBeInTheDocument(); + + const socialBrandVisual = socialContainer.querySelector('mdc-brandvisual'); + expect(socialBrandVisual).toBeInTheDocument(); + expect(socialBrandVisual).toHaveClass('facebook'); + expect(socialBrandVisual).toHaveAttribute('name', 'social-facebook-color'); + + // Social task details with tooltip + const socialTitle = socialContainer.querySelector('.incoming-digital-task-title'); + expect(socialTitle).toBeInTheDocument(); + expect(socialTitle).toHaveTextContent('Social Customer'); // Customer name as title + expect(socialTitle).toHaveAttribute('aria-describedby', 'tooltip-social-task'); + expect(socialTitle).toHaveAttribute('id', 'tooltip-trigger-social-task'); + + // Social tooltip + const socialTooltip = socialContainer.querySelector('mdc-tooltip'); + expect(socialTooltip).toBeInTheDocument(); + expect(socialTooltip).toHaveAttribute('id', 'tooltip-social-task'); + expect(socialTooltip).toHaveAttribute('triggerid', 'tooltip-trigger-social-task'); + expect(socialTooltip).toHaveTextContent('Social Customer'); + + const socialTeam = socialContainer.querySelectorAll('.task-text')[0]; + expect(socialTeam).toHaveTextContent('Social Team'); + + // Social buttons - Only Accept button + const socialAcceptButton = socialContainer.querySelector('[data-testid="task:accept-button"]'); + const socialDeclineButton = socialContainer.querySelector('[data-testid="task:decline-button"]'); + expect(socialAcceptButton).toBeInTheDocument(); + expect(socialDeclineButton).not.toBeInTheDocument(); // No decline button + expect(socialAcceptButton).toHaveAttribute('data-disabled', 'false'); + expect(socialAcceptButton).toHaveTextContent('Accept'); + + // === Media Type Specific Assertions === + // Verify different icon types + expect(webRTCAvatar).toHaveAttribute('icon-name', 'handset-filled'); // Telephony + expect(extensionAvatar).toHaveAttribute('icon-name', 'handset-filled'); // Telephony + expect(chatAvatar).toHaveAttribute('icon-name', 'chat-filled'); // Chat + expect(socialBrandVisual).toHaveAttribute('name', 'social-facebook-color'); // Social + + // Verify different avatar classes + expect(webRTCAvatar).toHaveClass('telephony'); + expect(extensionAvatar).toHaveClass('telephony'); + expect(chatAvatar).toHaveClass('chat'); + expect(socialBrandVisual).toHaveClass('facebook'); + + // Verify title display logic (ANI vs Customer Name) + expect(webRTCTitle).toHaveTextContent('1234567890'); // ANI for telephony + expect(extensionTitle).toHaveTextContent('1234567890'); // ANI for telephony + expect(chatTitle).toHaveTextContent('Chat Customer'); // Customer name for digital + expect(socialTitle).toHaveTextContent('Social Customer'); // Customer name for digital + + // Verify digital tasks have tooltips + expect(chatTitle).toHaveClass('incoming-digital-task-title'); + expect(socialTitle).toHaveClass('incoming-digital-task-title'); + expect(webRTCTitle).toHaveClass('task-title'); // Regular title for telephony + expect(extensionTitle).toHaveClass('task-title'); // Regular title for telephony + + // Verify button behavior differences + expect(webRTCAcceptButton).not.toHaveAttribute('disabled'); // WebRTC enabled + expect(extensionAcceptButton).toHaveAttribute('disabled', ''); // Extension disabled + expect(chatAcceptButton).not.toHaveAttribute('disabled'); // Chat enabled + expect(socialAcceptButton).not.toHaveAttribute('disabled'); // Social enabled + + // Verify decline button presence + expect(webRTCDeclineButton).toBeInTheDocument(); // WebRTC has decline + expect(extensionDeclineButton).not.toBeInTheDocument(); // Extension no decline + expect(chatDeclineButton).not.toBeInTheDocument(); // Chat no decline + expect(socialDeclineButton).not.toBeInTheDocument(); // Social no decline + }); + }); + + describe('Actions', () => { + const actionTask = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'action-task', + interaction: { + ...mockTask.data.interaction, + state: 'new', + mediaType: MEDIA_CHANNEL.TELEPHONY, + callAssociatedDetails: { + ani: '1234567890', + customerName: 'Action Customer', + virtualTeamName: 'Action Team', + ronaTimeout: '30', + }, + }, + wrapUpRequired: false, }, }; - render(); + beforeEach(() => { + extractIncomingTaskDataSpy.mockReturnValue(mockIncomingTaskData.webRTC); + }); + + it('should call acceptTask when accept button is clicked', async () => { + const {container} = await render( + + ); + + // Verify accept button exists + const acceptButton = getByTestId(container, 'task:accept-button'); + expect(acceptButton).toBeInTheDocument(); + expect(acceptButton).toHaveTextContent('Accept'); + expect(acceptButton).not.toHaveAttribute('disabled'); + + // Click the accept button + fireEvent.click(acceptButton); + + // Verify accept function was called with correct task + expect(mockAccept).toHaveBeenCalledTimes(1); + expect(mockAccept).toHaveBeenCalledWith(actionTask); + + // Verify reject was not called + expect(mockReject).not.toHaveBeenCalled(); + }); + + it('should call declineTask when decline button is clicked', async () => { + const {container} = await render( + + ); + + // Verify decline button exists + const declineButton = getByTestId(container, 'task:decline-button'); + expect(declineButton).toBeInTheDocument(); + expect(declineButton).toHaveTextContent('Decline'); + expect(declineButton).not.toHaveAttribute('disabled'); + + // Click the decline button + fireEvent.click(declineButton); + + // Verify reject function was called with correct task + expect(mockReject).toHaveBeenCalledTimes(1); + expect(mockReject).toHaveBeenCalledWith(actionTask); - expect(screen.getAllByTestId('ListItemBase')).toHaveLength(1); + // Verify accept was not called + expect(mockAccept).not.toHaveBeenCalled(); + }); }); }); diff --git a/packages/contact-center/cc-components/tests/components/task/IncomingTask/incoming-task.utils.tsx b/packages/contact-center/cc-components/tests/components/task/IncomingTask/incoming-task.utils.tsx new file mode 100644 index 000000000..0198dd810 --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/IncomingTask/incoming-task.utils.tsx @@ -0,0 +1,148 @@ +import {extractIncomingTaskData} from '../../../../src/components/task/IncomingTask/incoming-task.utils'; +import {MEDIA_CHANNEL} from '../../../../src/components/task/task.types'; +import {mockTask} from '@webex/test-fixtures'; + +describe('incoming-task.utils', () => { + beforeEach(() => { + // Reset mockTask to default state before each test + jest.clearAllMocks(); + }); + + describe('extractIncomingTaskData', () => { + describe('Telephony tasks', () => { + it('should extract correct data for browser telephony task', () => { + const result = extractIncomingTaskData(mockTask, true); + + expect(result.isTelephony).toBe(true); + expect(result.isSocial).toBe(false); + expect(result.acceptText).toBe('Accept'); + expect(result.declineText).toBe('Decline'); + expect(result.disableAccept).toBe(false); + expect(result.mediaType).toBe(mockTask.data.interaction.mediaType); + expect(result.startTimeStamp).toBe(mockTask.data.interaction.createdTimestamp); + expect(result.title).toBe(result.ani); // ANI for telephony + }); + + it('should extract correct data for non-browser telephony task', () => { + const result = extractIncomingTaskData(mockTask, false); + + expect(result.isTelephony).toBe(true); + expect(result.isSocial).toBe(false); + expect(result.acceptText).toBe('Ringing...'); + expect(result.declineText).toBeUndefined(); + expect(result.disableAccept).toBe(true); + expect(result.mediaType).toBe(mockTask.data.interaction.mediaType); + expect(result.startTimeStamp).toBe(mockTask.data.interaction.createdTimestamp); + }); + + it('should handle telephony task with wrap up required', () => { + const originalWrapUpRequired = mockTask.data.wrapUpRequired; + mockTask.data.wrapUpRequired = true; + + const result = extractIncomingTaskData(mockTask, true); + + expect(result.acceptText).toBeUndefined(); + expect(result.declineText).toBeUndefined(); + expect(result.isTelephony).toBe(true); + + // Restore original wrapUpRequired + mockTask.data.wrapUpRequired = originalWrapUpRequired; + }); + }); + + describe('Digital media tasks', () => { + it('should extract correct data for social media task', () => { + const originalMediaType = mockTask.data.interaction.mediaType; + mockTask.data.interaction.mediaType = MEDIA_CHANNEL.SOCIAL; + + const result = extractIncomingTaskData(mockTask, true); + + expect(result.isTelephony).toBe(false); + expect(result.isSocial).toBe(true); + expect(result.acceptText).toBe('Accept'); + expect(result.declineText).toBeUndefined(); + expect(result.disableAccept).toBe(false); + expect(result.mediaType).toBe(MEDIA_CHANNEL.SOCIAL); + expect(result.title).toBe(result.customerName); // Customer name for social + + // Restore original mediaType + mockTask.data.interaction.mediaType = originalMediaType; + }); + + it('should extract correct data for chat media type', () => { + const originalMediaType = mockTask.data.interaction.mediaType; + mockTask.data.interaction.mediaType = MEDIA_CHANNEL.CHAT; + + const result = extractIncomingTaskData(mockTask, true); + + expect(result.isTelephony).toBe(false); + expect(result.isSocial).toBe(false); + expect(result.mediaType).toBe(MEDIA_CHANNEL.CHAT); + expect(result.acceptText).toBe('Accept'); + expect(result.declineText).toBeUndefined(); + expect(result.disableAccept).toBe(false); + + // Restore original mediaType + mockTask.data.interaction.mediaType = originalMediaType; + }); + + it('should handle digital media task with wrap up required', () => { + const originalMediaType = mockTask.data.interaction.mediaType; + const originalWrapUpRequired = mockTask.data.wrapUpRequired; + + mockTask.data.interaction.mediaType = MEDIA_CHANNEL.SOCIAL; + mockTask.data.wrapUpRequired = true; + + const result = extractIncomingTaskData(mockTask, true); + + expect(result.acceptText).toBeUndefined(); + expect(result.declineText).toBeUndefined(); + expect(result.isSocial).toBe(true); + + // Restore original values + mockTask.data.interaction.mediaType = originalMediaType; + mockTask.data.wrapUpRequired = originalWrapUpRequired; + }); + }); + + describe('Edge cases', () => { + it('should handle missing call association details', () => { + const originalCallAssociatedDetails = mockTask.data.interaction.callAssociatedDetails; + mockTask.data.interaction.callAssociatedDetails = undefined; + + const result = extractIncomingTaskData(mockTask, true); + + expect(result.ani).toBeUndefined(); + expect(result.customerName).toBeUndefined(); + expect(result.virtualTeamName).toBeUndefined(); + + mockTask.data.interaction.callAssociatedDetails = originalCallAssociatedDetails; + }); + + it('should handle missing and invalid ronaTimeout values', () => { + const originalCallAssociatedDetails = mockTask.data.interaction.callAssociatedDetails; + + // Test missing ronaTimeout + mockTask.data.interaction.callAssociatedDetails = { + ...originalCallAssociatedDetails, + ronaTimeout: undefined, + }; + + let result = extractIncomingTaskData(mockTask, true); + expect(result.ronaTimeout).toBeNull(); + + // Test invalid ronaTimeout + mockTask.data.interaction.callAssociatedDetails = { + ...originalCallAssociatedDetails, + ronaTimeout: 'invalid-number', + }; + + result = extractIncomingTaskData(mockTask, true); + expect(result.ronaTimeout).toBeNaN(); + + // Restore original values + mockTask.data.interaction.callAssociatedDetails = originalCallAssociatedDetails; + }); + }); + }); +}); diff --git a/packages/contact-center/cc-components/tests/components/task/TaskList/__snapshots__/task-list.snapshot.tsx.snap b/packages/contact-center/cc-components/tests/components/task/TaskList/__snapshots__/task-list.snapshot.tsx.snap new file mode 100644 index 000000000..58023a027 --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/TaskList/__snapshots__/task-list.snapshot.tsx.snap @@ -0,0 +1,1707 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TaskListComponent Actions should call acceptTask when accept button is clicked 1`] = ` +
    +
      +
    • +
      + +
      +
      +
      + + 1111111111 + + + Action Team + + + Time Left: + + + +
      +
      +
      +
      + + +
      +
      +
    • +
    +
    +`; + +exports[`TaskListComponent Actions should call acceptTask when accept button is clicked 2`] = ` +
    +
      +
    • +
      + +
      +
      +
      + + 1111111111 + + + Action Team + + + Time Left: + + + +
      +
      +
      +
      + + +
      +
      +
    • +
    +
    +`; + +exports[`TaskListComponent Actions should call declineTask when decline button is clicked 1`] = ` +
    +
      +
    • +
      + +
      +
      +
      + + 1111111111 + + + Action Team + + + Time Left: + + + +
      +
      +
      +
      + + +
      +
      +
    • +
    +
    +`; + +exports[`TaskListComponent Actions should call declineTask when decline button is clicked 2`] = ` +
    +
      +
    • +
      + +
      +
      +
      + + 1111111111 + + + Action Team + + + Time Left: + + + +
      +
      +
      +
      + + +
      +
      +
    • +
    +
    +`; + +exports[`TaskListComponent Actions should call onTaskSelect when task is clicked 1`] = ` +
    +
      +
    • +
      + +
      +
      +
      + + 1111111111 + + + Active + + + Handle Time: + + + +
      +
      +
      +
      +
      +
    • +
    +
    +`; + +exports[`TaskListComponent Actions should call onTaskSelect when task is clicked 2`] = ` +
    +
      +
    • +
      + +
      +
      +
      + + 1111111111 + + + Active + + + Handle Time: + + + +
      +
      +
      +
      +
      +
    • +
    +
    +`; + +exports[`TaskListComponent Actions should handle task selection between multiple tasks 1`] = ` +
    +
      +
    • +
      + +
      +
      +
      + + 1111111111 + + + Selected Team + + + Time Left: + + + +
      +
      +
      +
      + + +
      +
      +
    • +
    • +
      + +
      +
      +
      + + Unselected Chat Customer + + + Unselected Chat Customer + + + Active + + + Handle Time: + + + +
      +
      +
      +
      +
      +
    • +
    +
    +`; + +exports[`TaskListComponent Actions should handle task selection between multiple tasks 2`] = ` +
    +
      +
    • +
      + +
      +
      +
      + + 1111111111 + + + Selected Team + + + Time Left: + + + +
      +
      +
      +
      + + +
      +
      +
    • +
    • +
      + +
      +
      +
      + + Unselected Chat Customer + + + Unselected Chat Customer + + + Active + + + Handle Time: + + + +
      +
      +
      +
      +
      +
    • +
    +
    +`; + +exports[`TaskListComponent Empty TaskList Scenarios should return empty component when taskList is empty object 1`] = `
    `; + +exports[`TaskListComponent Empty TaskList Scenarios should return empty component when taskList is null or undefined 1`] = `
    `; + +exports[`TaskListComponent Empty TaskList Scenarios should return empty component when taskList is null or undefined 2`] = `
    `; + +exports[`TaskListComponent Rendering with Tasks should render digital incoming task with Accept button only 1`] = ` +
    +
      +
    • +
      + +
      +
      +
      + + Chat Customer + + + Chat Customer + + + Chat Support + + + Time Left: + + + +
      +
      +
      +
      + +
      +
      +
    • +
    +
    +`; + +exports[`TaskListComponent Rendering with Tasks should render multiple tasks with different media types and their corresponding icons and labels 1`] = ` +
    +
      +
    • +
      + +
      +
      +
      + + 1234567890 + + + Active + + + Handle Time: + + + +
      +
      +
      +
      +
      +
    • +
    • +
      + +
      +
      +
      + + Chat Customer + + + Chat Customer + + + Active + + + Handle Time: + + + +
      +
      +
      +
      +
      +
    • +
    • +
      +
      +
      +
      +
      +
      + + Facebook Customer + + + Facebook Customer + + + Active + + + Handle Time: + + + +
      +
      +
      +
      +
      +
    • +
    +
    +`; + +exports[`TaskListComponent Rendering with Tasks should render telephony incoming task with Accept/Decline buttons in WebRTC mode 1`] = ` +
    +
      +
    • +
      + +
      +
      +
      + + 1234567890 + + + Support + + + Time Left: + + + +
      +
      +
      +
      + + +
      +
      +
    • +
    +
    +`; + +exports[`TaskListComponent Rendering with Tasks should render telephony incoming task with disabled Accept button in Extension mode 1`] = ` +
    +
      +
    • +
      + +
      +
      +
      + + 1234567890 + + + Support + + + Time Left: + + + +
      +
      +
      +
      + +
      +
      +
    • +
    +
    +`; + +exports[`TaskListComponent Rendering with Tasks should show selected state with bold title 1`] = ` +
    +
      +
    • +
      + +
      +
      +
      + + 1234567890 + + + Active + + + Handle Time: + + + +
      +
      +
      +
      +
      +
    • +
    +
    +`; diff --git a/packages/contact-center/cc-components/tests/components/task/TaskList/task-list.snapshot.tsx b/packages/contact-center/cc-components/tests/components/task/TaskList/task-list.snapshot.tsx new file mode 100644 index 000000000..8a6588a0d --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/TaskList/task-list.snapshot.tsx @@ -0,0 +1,387 @@ +import React from 'react'; +import {render, fireEvent} from '@testing-library/react'; +import '@testing-library/jest-dom'; +import {mockTask, mockTaskData} from '@webex/test-fixtures'; +import TaskListComponent from '../../../../src/components/task/TaskList/task-list'; +import {TaskListComponentProps, MEDIA_CHANNEL} from '../../../../src/components/task/task.types'; +import type {ILogger} from '@webex/cc-store'; +import * as taskListUtils from '../../../../src/components/task/TaskList/task-list.utils'; + +// Simple Worker mock +Object.defineProperty(global, 'Worker', { + writable: true, + value: class MockWorker { + constructor() {} + postMessage = jest.fn(); + addEventListener = jest.fn(); + removeEventListener = jest.fn(); + terminate = jest.fn(); + }, +}); + +Object.defineProperty(global, 'URL', { + writable: true, + value: { + createObjectURL: jest.fn(() => 'blob:mock-url'), + revokeObjectURL: jest.fn(), + }, +}); + +describe('TaskListComponent', () => { + // Mock functions + const mockAcceptTask = jest.fn(); + const mockDeclineTask = jest.fn(); + const mockOnTaskSelect = jest.fn(); + const mockLogger: ILogger = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + log: jest.fn(), + trace: jest.fn(), + }; + + // Default props using TaskListComponentProps interface + const defaultProps: TaskListComponentProps = { + currentTask: null, + taskList: null, + acceptTask: mockAcceptTask, + declineTask: mockDeclineTask, + isBrowser: true, + onTaskSelect: mockOnTaskSelect, + logger: mockLogger, + }; + + // Utility function spies + const isTaskListEmptySpy = jest.spyOn(taskListUtils, 'isTaskListEmpty'); + const getTasksArraySpy = jest.spyOn(taskListUtils, 'getTasksArray'); + const extractTaskListItemDataSpy = jest.spyOn(taskListUtils, 'extractTaskListItemData'); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + isTaskListEmptySpy.mockRestore(); + getTasksArraySpy.mockRestore(); + extractTaskListItemDataSpy.mockRestore(); + }); + + describe('Empty TaskList Scenarios', () => { + it('should return empty component when taskList is null or undefined', async () => { + // Test with null + const propsWithNull: TaskListComponentProps = { + ...defaultProps, + taskList: null, + }; + + const {container: containerNull} = await render(); + expect(containerNull).toMatchSnapshot(); + + // Test with undefined + const propsWithUndefined: TaskListComponentProps = { + ...defaultProps, + taskList: undefined, + }; + + const {container: containerUndefined} = await render(); + expect(containerUndefined).toMatchSnapshot(); + }); + + it('should return empty component when taskList is empty object', async () => { + const propsWithEmptyObject: TaskListComponentProps = { + ...defaultProps, + taskList: {}, // Empty object + }; + + const {container} = await render(); + expect(container).toMatchSnapshot(); + }); + }); + + describe('Rendering with Tasks', () => { + const sampleTask = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'test-task-123', + interaction: { + ...mockTask.data.interaction, + state: 'active', + mediaType: MEDIA_CHANNEL.TELEPHONY, + callAssociatedDetails: { + ani: '1234567890', + customerName: 'John Doe', + virtualTeamName: 'Support Team', + }, + }, + }, + }; + + beforeEach(() => { + extractTaskListItemDataSpy.mockReturnValue(mockTaskData.active.telephony); + }); + + it('should render multiple tasks with different media types and their corresponding icons and labels', async () => { + // Create telephony task + const telephonyTask = { + ...sampleTask, + data: { + ...sampleTask.data, + interactionId: 'telephony-task', + interaction: { + ...sampleTask.data.interaction, + state: 'active', + mediaType: MEDIA_CHANNEL.TELEPHONY, + callAssociatedDetails: { + ani: '1234567890', + customerName: 'Telephony Customer', + virtualTeamName: 'Telephony Team', + }, + }, + }, + }; + + // Create chat task + const chatTask = { + ...sampleTask, + data: { + ...sampleTask.data, + interactionId: 'chat-task', + interaction: { + ...sampleTask.data.interaction, + state: 'active', + mediaType: MEDIA_CHANNEL.CHAT, + callAssociatedDetails: { + ani: 'chat-user-123', + customerName: 'Chat Customer', + virtualTeamName: 'Chat Team', + }, + }, + }, + }; + + // Create social (Facebook) task + const socialTask = { + ...sampleTask, + data: { + ...sampleTask.data, + interactionId: 'social-task', + interaction: { + ...sampleTask.data.interaction, + state: 'active', + mediaType: MEDIA_CHANNEL.SOCIAL, + mediaChannel: MEDIA_CHANNEL.FACEBOOK, + callAssociatedDetails: { + ani: 'facebook-user-456', + customerName: 'Facebook Customer', + virtualTeamName: 'Social Team', + }, + }, + }, + }; + + // Mock different return values for each media type using common data + extractTaskListItemDataSpy + .mockReturnValueOnce(mockTaskData.active.telephony) + .mockReturnValueOnce(mockTaskData.active.chat) + .mockReturnValueOnce(mockTaskData.active.facebook); + + const taskList = { + 'telephony-task': telephonyTask, + 'chat-task': chatTask, + 'social-task': socialTask, + }; + + const {container} = await render(); + expect(container).toMatchSnapshot(); + }); + + it('should show selected state with bold title', async () => { + const taskList = {'test-task-123': sampleTask}; + const {container} = await render( + + ); + expect(container).toMatchSnapshot(); + }); + + it('should render telephony incoming task with Accept/Decline buttons in WebRTC mode', async () => { + extractTaskListItemDataSpy.mockReturnValue(mockTaskData.incoming.webrtcTelephony); + + const incomingTask = { + ...sampleTask, + data: { + ...sampleTask.data, + interactionId: 'webrtc-telephony-task', + interaction: { + ...sampleTask.data.interaction, + state: 'new', + mediaType: MEDIA_CHANNEL.TELEPHONY, + }, + wrapUpRequired: false, + }, + }; + + const {container} = await render( + + ); + expect(container).toMatchSnapshot(); + }); + + it('should render telephony incoming task with disabled Accept button in Extension mode', async () => { + extractTaskListItemDataSpy.mockReturnValue(mockTaskData.incoming.extensionTelephony); + + const incomingTask = { + ...sampleTask, + data: { + ...sampleTask.data, + interactionId: 'extension-telephony-task', + interaction: { + ...sampleTask.data.interaction, + state: 'new', + mediaType: MEDIA_CHANNEL.TELEPHONY, + }, + wrapUpRequired: false, + }, + }; + + const {container} = await render( + + ); + expect(container).toMatchSnapshot(); + }); + + it('should render digital incoming task with Accept button only', async () => { + extractTaskListItemDataSpy.mockReturnValue(mockTaskData.incoming.chat); + + const incomingChatTask = { + ...sampleTask, + data: { + ...sampleTask.data, + interactionId: 'incoming-chat-task', + interaction: { + ...sampleTask.data.interaction, + state: 'new', + mediaType: MEDIA_CHANNEL.CHAT, + }, + wrapUpRequired: false, + }, + }; + + const {container} = await render( + + ); + expect(container).toMatchSnapshot(); + }); + }); + + describe('Actions', () => { + const actionTask = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'action-task', + interaction: { + ...mockTask.data.interaction, + state: 'new', + mediaType: MEDIA_CHANNEL.TELEPHONY, + }, + wrapUpRequired: false, + }, + }; + + beforeEach(() => { + extractTaskListItemDataSpy.mockReturnValue(mockTaskData.action.telephony); + }); + + it('should call acceptTask when accept button is clicked', async () => { + const taskList = {'action-task': actionTask}; + const {container} = await render(); + + expect(container).toMatchSnapshot(); + + const acceptButton = container.querySelector('[data-testid="task:accept-button"]') as HTMLElement; + fireEvent.click(acceptButton); + + expect(container).toMatchSnapshot(); + }); + + it('should call declineTask when decline button is clicked', async () => { + const taskList = {'action-task': actionTask}; + const {container} = await render(); + + expect(container).toMatchSnapshot(); + + const declineButton = container.querySelector('[data-testid="task:decline-button"]') as HTMLElement; + fireEvent.click(declineButton); + + expect(container).toMatchSnapshot(); + }); + + it('should call onTaskSelect when task is clicked', async () => { + extractTaskListItemDataSpy.mockReturnValue(mockTaskData.action.activeTask); + + const activeTask = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'active-task', + interaction: { + ...mockTask.data.interaction, + state: 'active', + mediaType: MEDIA_CHANNEL.TELEPHONY, + }, + }, + }; + + const taskList = {'active-task': activeTask}; + const {container} = await render(); + + expect(container).toMatchSnapshot(); + + const taskElement = container.querySelector('[role="listitem"]') as HTMLElement; + fireEvent.click(taskElement); + + expect(container).toMatchSnapshot(); + }); + + it('should handle task selection between multiple tasks', async () => { + // Create second task as CHAT task with different mock data + const secondTask = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'unselected-chat-task', + interaction: { + ...mockTask.data.interaction, + state: 'active', + mediaType: MEDIA_CHANNEL.CHAT, + }, + }, + }; + + // Mock different return values for each task using common data + extractTaskListItemDataSpy + .mockReturnValueOnce(mockTaskData.selection.selectedTelephony) + .mockReturnValueOnce(mockTaskData.selection.unselectedChat); + + const taskList = { + 'action-task': actionTask, + 'unselected-chat-task': secondTask, + }; + + const {container} = await render( + + ); + + // Capture initial state with selected telephony task + expect(container).toMatchSnapshot(); + + const taskElements = container.querySelectorAll('[role="listitem"]'); + const unselectedTaskElement = taskElements[1] as HTMLElement; + fireEvent.click(unselectedTaskElement); + + // Capture state after clicking unselected chat task + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/contact-center/cc-components/tests/components/task/TaskList/task-list.tsx b/packages/contact-center/cc-components/tests/components/task/TaskList/task-list.tsx index 3b7117e17..06e1c62b0 100644 --- a/packages/contact-center/cc-components/tests/components/task/TaskList/task-list.tsx +++ b/packages/contact-center/cc-components/tests/components/task/TaskList/task-list.tsx @@ -1,64 +1,716 @@ import React from 'react'; -import {render, screen, cleanup} from '@testing-library/react'; +import {render, fireEvent} from '@testing-library/react'; import '@testing-library/jest-dom'; +import {mockTask, mockTaskData} from '@webex/test-fixtures'; import TaskListComponent from '../../../../src/components/task/TaskList/task-list'; -import {TaskListComponentProps} from '../../../../src/components/task/task.types'; -import {mockTask} from '@webex/test-fixtures'; - -// 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('TaskListPresentational Component', () => { - afterEach(cleanup); - - it('renders a list of tasks when taskList is not empty', () => { - const loggerMock = { - log: jest.fn(), - error: jest.fn(), - warn: jest.fn(), - info: jest.fn(), - trace: jest.fn(), +import {TaskListComponentProps, MEDIA_CHANNEL} from '../../../../src/components/task/task.types'; +import type {ILogger} from '@webex/cc-store'; +import * as taskListUtils from '../../../../src/components/task/TaskList/task-list.utils'; + +// Simple Worker mock +Object.defineProperty(global, 'Worker', { + writable: true, + value: class MockWorker { + constructor() {} + postMessage = jest.fn(); + addEventListener = jest.fn(); + removeEventListener = jest.fn(); + terminate = jest.fn(); + }, +}); + +Object.defineProperty(global, 'URL', { + writable: true, + value: { + createObjectURL: jest.fn(() => 'blob:mock-url'), + revokeObjectURL: jest.fn(), + }, +}); + +describe('TaskListComponent', () => { + // Mock functions + const mockAcceptTask = jest.fn(); + const mockDeclineTask = jest.fn(); + const mockOnTaskSelect = jest.fn(); + const mockLogger: ILogger = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + log: jest.fn(), + trace: jest.fn(), + }; + + // Default props using TaskListComponentProps interface + const defaultProps: TaskListComponentProps = { + currentTask: null, + taskList: null, + acceptTask: mockAcceptTask, + declineTask: mockDeclineTask, + isBrowser: true, + onTaskSelect: mockOnTaskSelect, + logger: mockLogger, + }; + + // Utility function spies + const isTaskListEmptySpy = jest.spyOn(taskListUtils, 'isTaskListEmpty'); + const getTasksArraySpy = jest.spyOn(taskListUtils, 'getTasksArray'); + const extractTaskListItemDataSpy = jest.spyOn(taskListUtils, 'extractTaskListItemData'); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + isTaskListEmptySpy.mockRestore(); + getTasksArraySpy.mockRestore(); + extractTaskListItemDataSpy.mockRestore(); + }); + + describe('Empty TaskList Scenarios', () => { + it('should return empty component when taskList is null or undefined', async () => { + // Test with null + const propsWithNull: TaskListComponentProps = { + ...defaultProps, + taskList: null, + }; + + const screenNull = await render(); + + expect(isTaskListEmptySpy).toHaveBeenCalledWith(null); + expect(screenNull.container).toBeEmptyDOMElement(); + expect(screenNull.queryByTestId('task-list')).not.toBeInTheDocument(); + expect(getTasksArraySpy).not.toHaveBeenCalled(); + + jest.clearAllMocks(); + + // Test with undefined + const propsWithUndefined: TaskListComponentProps = { + ...defaultProps, + taskList: undefined, + }; + + const screenUndefined = await render(); + + expect(isTaskListEmptySpy).toHaveBeenCalledWith(undefined); + expect(screenUndefined.container).toBeEmptyDOMElement(); + expect(screenUndefined.queryByTestId('task-list')).not.toBeInTheDocument(); + expect(getTasksArraySpy).not.toHaveBeenCalled(); + }); + + it('should return empty component when taskList is empty object', async () => { + const propsWithEmptyObject: TaskListComponentProps = { + ...defaultProps, + taskList: {}, // Empty object + }; + + const screen = await render(); + + expect(isTaskListEmptySpy).toHaveBeenCalledWith({}); + expect(screen.container).toBeEmptyDOMElement(); + expect(screen.queryByTestId('task-list')).not.toBeInTheDocument(); + expect(getTasksArraySpy).not.toHaveBeenCalled(); + }); + }); + + describe('Rendering with Tasks', () => { + const sampleTask = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'test-task-123', + interaction: { + ...mockTask.data.interaction, + state: 'active', + mediaType: MEDIA_CHANNEL.TELEPHONY, + callAssociatedDetails: { + ani: '1234567890', + customerName: 'John Doe', + virtualTeamName: 'Support Team', + }, + }, + }, }; - const props: TaskListComponentProps = { - currentTask: mockTask, - taskList: { - '1': { - ...mockTask, - id: '1', - data: { - interaction: { - //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 - callAssociatedDetails: { - ani: '1234567890', - dn: '9876543210', - virtualTeamName: 'Sales Team', - }, + beforeEach(() => { + extractTaskListItemDataSpy.mockReturnValue(mockTaskData.active.telephony); + }); + + it('should render multiple tasks with different media types and their corresponding icons and labels', async () => { + // Create telephony task + const telephonyTask = { + ...sampleTask, + data: { + ...sampleTask.data, + interactionId: 'telephony-task', + interaction: { + ...sampleTask.data.interaction, + state: 'active', + mediaType: MEDIA_CHANNEL.TELEPHONY, + callAssociatedDetails: { + ani: '1234567890', + customerName: 'Telephony Customer', + virtualTeamName: 'Telephony Team', }, }, }, - '2': { - ...mockTask, - id: '2', - data: { - interaction: { - //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 - callAssociatedDetails: { - ani: '0987654321', - dn: '8765432109', - virtualTeamName: 'Support Team', - }, + }; + + // Create chat task + const chatTask = { + ...sampleTask, + data: { + ...sampleTask.data, + interactionId: 'chat-task', + interaction: { + ...sampleTask.data.interaction, + state: 'active', + mediaType: MEDIA_CHANNEL.CHAT, + callAssociatedDetails: { + ani: 'chat-user-123', + customerName: 'Chat Customer', + virtualTeamName: 'Chat Team', + }, + }, + }, + }; + + // Create social (Facebook) task + const socialTask = { + ...sampleTask, + data: { + ...sampleTask.data, + interactionId: 'social-task', + interaction: { + ...sampleTask.data.interaction, + state: 'active', + mediaType: MEDIA_CHANNEL.SOCIAL, + mediaChannel: MEDIA_CHANNEL.FACEBOOK, + callAssociatedDetails: { + ani: 'facebook-user-456', + customerName: 'Facebook Customer', + virtualTeamName: 'Social Team', }, }, }, + }; + + // Mock different return values for each media type using common data + extractTaskListItemDataSpy + .mockReturnValueOnce(mockTaskData.active.telephony) + .mockReturnValueOnce(mockTaskData.active.chat) + .mockReturnValueOnce(mockTaskData.active.facebook); + + const taskList = { + 'telephony-task': telephonyTask, + 'chat-task': chatTask, + 'social-task': socialTask, + }; + + const props: TaskListComponentProps = { + ...defaultProps, + taskList, + }; + + const screen = await render(); + // Verify task list structure + const taskListElement = screen.getByTestId('task-list'); + expect(taskListElement).toBeInTheDocument(); + expect(taskListElement.tagName).toBe('UL'); + expect(taskListElement).toHaveClass('task-list'); + + // Verify correct number of tasks rendered + expect(getTasksArraySpy).toHaveBeenCalledWith(taskList); + expect(extractTaskListItemDataSpy).toHaveBeenCalledTimes(3); + + const taskItems = screen.getAllByRole('listitem'); + expect(taskItems).toHaveLength(3); + + // Test each task has common attributes + taskItems.forEach((taskItem) => { + expect(taskItem).toHaveClass('task-list-item', 'md-list-item-base-wrapper'); + expect(taskItem).toHaveAttribute('data-interactive', 'true'); + expect(taskItem).toHaveAttribute('data-size', '40'); + expect(taskItem).toHaveAttribute('role', 'listitem'); + expect(taskItem).toHaveAttribute('tabindex', '0'); + }); + + // Verify telephony task (first task) + const telephonyTaskElement = taskItems[0]; + expect(telephonyTaskElement).toHaveAttribute('id', 'telephony-task'); + expect(telephonyTaskElement).toHaveClass('task-list-item'); + expect(telephonyTaskElement).toHaveAttribute('data-allow-text-select', 'false'); + expect(telephonyTaskElement).toHaveAttribute('data-disabled', 'false'); + expect(telephonyTaskElement).toHaveAttribute('data-padded', 'false'); + expect(telephonyTaskElement).toHaveAttribute('data-shape', 'rectangle'); + + const telephonyAvatar = telephonyTaskElement.querySelector('mdc-avatar') as HTMLElement; + expect(telephonyAvatar).toHaveClass('telephony'); + expect(telephonyAvatar).toHaveAttribute('icon-name', 'handset-filled'); + expect(telephonyAvatar).toHaveAttribute('size', '32'); + + const telephonyTitle = telephonyTaskElement.querySelector('.task-title') as HTMLElement; + expect(telephonyTitle).toHaveTextContent('1234567890'); + expect(telephonyTitle).toHaveAttribute('type', 'body-large-medium'); + expect(telephonyTitle).toHaveAttribute('tagname', 'span'); + + // Verify chat task (second task) + const chatTaskElement = taskItems[1]; + expect(chatTaskElement).toHaveAttribute('id', 'chat-task'); + const chatAvatar = chatTaskElement.querySelector('mdc-avatar') as HTMLElement; + expect(chatAvatar).toHaveClass('chat'); + expect(chatAvatar).toHaveAttribute('icon-name', 'chat-filled'); + expect(chatAvatar).toHaveAttribute('size', '32'); + + const chatTitle = chatTaskElement.querySelector('.task-digital-title') as HTMLElement; + expect(chatTitle).toHaveTextContent('Chat Customer'); + expect(chatTitle).toHaveAttribute('type', 'body-large-medium'); + expect(chatTitle).toHaveAttribute('aria-describedby', 'tooltip-chat-task'); + expect(chatTitle).toHaveAttribute('id', 'tooltip-trigger-chat-task'); + + // Verify chat tooltip + const chatTooltip = chatTaskElement.querySelector('mdc-tooltip') as HTMLElement; + expect(chatTooltip).toBeInTheDocument(); + expect(chatTooltip).toHaveAttribute('role', 'tooltip'); + expect(chatTooltip).toHaveAttribute('id', 'tooltip-chat-task'); + expect(chatTooltip).toHaveTextContent('Chat Customer'); + + // Verify social (Facebook) task (third task) - Different structure! + const socialTaskElement = taskItems[2]; + expect(socialTaskElement).toHaveAttribute('id', 'social-task'); + + // Facebook uses brand-visual-background instead of mdc-avatar + const brandVisualBackground = socialTaskElement.querySelector('.brand-visual-background') as HTMLElement; + expect(brandVisualBackground).toBeInTheDocument(); + + const socialBrandVisual = socialTaskElement.querySelector('mdc-brandvisual') as HTMLElement; + expect(socialBrandVisual).toHaveClass('facebook'); + expect(socialBrandVisual).toHaveAttribute('name', 'social-facebook-color'); + + const socialTitle = socialTaskElement.querySelector('.task-digital-title') as HTMLElement; + expect(socialTitle).toHaveTextContent('Facebook Customer'); + expect(socialTitle).toHaveAttribute('type', 'body-large-medium'); + expect(socialTitle).toHaveAttribute('aria-describedby', 'tooltip-social-task'); + expect(socialTitle).toHaveAttribute('id', 'tooltip-trigger-social-task'); + + // Verify social tooltip + const socialTooltip = socialTaskElement.querySelector('mdc-tooltip') as HTMLElement; + expect(socialTooltip).toBeInTheDocument(); + expect(socialTooltip).toHaveAttribute('role', 'tooltip'); + expect(socialTooltip).toHaveAttribute('id', 'tooltip-social-task'); + expect(socialTooltip).toHaveTextContent('Facebook Customer'); + + // Verify each task has proper layout structure + taskItems.forEach((taskItem) => { + const startDiv = taskItem.querySelector('[data-position="start"]') as HTMLElement; + const fillDiv = taskItem.querySelector('[data-position="fill"]') as HTMLElement; + const endDiv = taskItem.querySelector('[data-position="end"]') as HTMLElement; + + expect(startDiv).toBeInTheDocument(); + expect(fillDiv).toBeInTheDocument(); + expect(endDiv).toBeInTheDocument(); + + // Each task should have task details section + const taskDetails = taskItem.querySelector('.task-details') as HTMLElement; + expect(taskDetails.tagName).toBe('SECTION'); + expect(fillDiv).toContainElement(taskDetails); + + // Each task should have empty button container for active tasks + const buttonContainer = taskItem.querySelector('.task-button-container') as HTMLElement; + expect(buttonContainer).toBeInTheDocument(); + expect(buttonContainer.children).toHaveLength(0); // No buttons for active tasks + }); + + // Verify task status and time elements for all tasks + taskItems.forEach((taskItem) => { + const taskTextElements = taskItem.querySelectorAll('mdc-text.task-text'); + expect(taskTextElements[0]).toHaveTextContent('Active'); + expect(taskTextElements[1]).toHaveTextContent('Handle Time:'); + + const timeElement = taskItem.querySelector('time') as HTMLElement; + expect(timeElement).toHaveTextContent('00:00'); + expect(timeElement).toHaveAttribute('datetime', '00:00'); + expect(timeElement).toHaveClass('task-text', 'task-text--secondary'); + }); + + // Verify utility function calls + const extractedData = extractTaskListItemDataSpy.mock.results; + expect(extractedData[0].value.mediaType).toBe(MEDIA_CHANNEL.TELEPHONY); + expect(extractedData[0].value.isTelephony).toBe(true); + expect(extractedData[1].value.mediaType).toBe(MEDIA_CHANNEL.CHAT); + expect(extractedData[1].value.isTelephony).toBe(false); + expect(extractedData[2].value.mediaType).toBe(MEDIA_CHANNEL.SOCIAL); + expect(extractedData[2].value.mediaChannel).toBe(MEDIA_CHANNEL.FACEBOOK); + expect(extractedData[2].value.isSocial).toBe(true); + }); + + it('should show selected state with bold title', async () => { + const taskList = {'test-task-123': sampleTask}; + const props: TaskListComponentProps = { + ...defaultProps, + taskList, + currentTask: sampleTask, + }; + + const screen = await render(); + + const taskElement = screen.getByRole('listitem'); + expect(taskElement).toHaveClass('task-list-item--selected'); + + const taskTitle = screen.container.querySelector('.task-title') as HTMLElement; + expect(taskTitle).toHaveAttribute('type', 'body-large-bold'); + }); + + it('should render telephony incoming task with Accept/Decline buttons in WebRTC mode', async () => { + extractTaskListItemDataSpy.mockReturnValue(mockTaskData.incoming.webrtcTelephony); + + const incomingTask = { + ...sampleTask, + data: { + ...sampleTask.data, + interactionId: 'webrtc-telephony-task', + interaction: { + ...sampleTask.data.interaction, + state: 'new', + mediaType: MEDIA_CHANNEL.TELEPHONY, + }, + wrapUpRequired: false, + }, + }; + + const props: TaskListComponentProps = { + ...defaultProps, + taskList: {'webrtc-telephony-task': incomingTask}, + isBrowser: true, // WebRTC mode + }; + + const screen = await render(); + + // Verify both buttons are present + const acceptButton = screen.getByTestId('task:accept-button'); + const declineButton = screen.getByTestId('task:decline-button'); + + expect(acceptButton).toHaveAttribute('data-color', 'join'); + expect(acceptButton).toHaveTextContent('Accept'); + expect(acceptButton).toHaveAttribute('data-disabled', 'false'); + + expect(declineButton).toHaveAttribute('data-color', 'cancel'); + expect(declineButton).toHaveTextContent('Decline'); + + // Verify button container has both buttons + const buttonContainer = screen.container.querySelector('.task-button-container') as HTMLElement; + expect(buttonContainer.children).toHaveLength(2); + + // Verify task content for incoming tasks + const taskTextElements = screen.container.querySelectorAll('mdc-text.task-text'); + expect(taskTextElements[0]).toHaveTextContent('Support'); // Queue name + expect(taskTextElements[1]).toHaveTextContent('Time Left:'); // Incoming task timing + }); + + it('should render telephony incoming task with disabled Accept button in Extension mode', async () => { + extractTaskListItemDataSpy.mockReturnValue(mockTaskData.incoming.extensionTelephony); + + const incomingTask = { + ...sampleTask, + data: { + ...sampleTask.data, + interactionId: 'extension-telephony-task', + interaction: { + ...sampleTask.data.interaction, + state: 'new', + mediaType: MEDIA_CHANNEL.TELEPHONY, + }, + wrapUpRequired: false, + }, + }; + + const props: TaskListComponentProps = { + ...defaultProps, + taskList: {'extension-telephony-task': incomingTask}, + isBrowser: false, // Extension mode + }; + + const screen = await render(); + + // Verify only accept button (disabled, showing "Ringing...") + const acceptButton = screen.getByTestId('task:accept-button'); + expect(acceptButton).toHaveAttribute('data-color', 'join'); + expect(acceptButton).toHaveTextContent('Ringing...'); + expect(acceptButton).toHaveAttribute('data-disabled', 'true'); + + // Verify no decline button in extension mode + expect(screen.queryByTestId('task:decline-button')).not.toBeInTheDocument(); + + // Verify button container has only one button + const buttonContainer = screen.container.querySelector('.task-button-container') as HTMLElement; + expect(buttonContainer.children).toHaveLength(1); + }); + + it('should render digital incoming task with Accept button only', async () => { + extractTaskListItemDataSpy.mockReturnValue(mockTaskData.incoming.chat); + + const incomingChatTask = { + ...sampleTask, + data: { + ...sampleTask.data, + interactionId: 'incoming-chat-task', + interaction: { + ...sampleTask.data.interaction, + state: 'new', + mediaType: MEDIA_CHANNEL.CHAT, + }, + wrapUpRequired: false, + }, + }; + + const props: TaskListComponentProps = { + ...defaultProps, + taskList: {'incoming-chat-task': incomingChatTask}, + isBrowser: true, + }; + + const screen = await render(); + + // Verify only accept button for digital channels + const acceptButton = screen.getByTestId('task:accept-button'); + expect(acceptButton).toHaveAttribute('data-color', 'join'); + expect(acceptButton).toHaveTextContent('Accept'); + expect(acceptButton).toHaveAttribute('data-disabled', 'false'); + + // Verify no decline button for digital channels + expect(screen.queryByTestId('task:decline-button')).not.toBeInTheDocument(); + + // Verify button container has only one button + const buttonContainer = screen.container.querySelector('.task-button-container') as HTMLElement; + expect(buttonContainer.children).toHaveLength(1); + + // Verify digital incoming task uses specific class name for incoming digital tasks + const digitalTitle = screen.container.querySelector('.incoming-digital-task-title') as HTMLElement; + expect(digitalTitle).toHaveTextContent('Chat Customer'); + expect(digitalTitle).toHaveAttribute('aria-describedby', 'tooltip-incoming-chat-task'); + expect(digitalTitle).toHaveAttribute('id', 'tooltip-trigger-incoming-chat-task'); + expect(digitalTitle).toHaveAttribute('type', 'body-large-medium'); + + // Verify chat avatar + const chatAvatar = screen.container.querySelector('mdc-avatar') as HTMLElement; + expect(chatAvatar).toHaveClass('chat'); + expect(chatAvatar).toHaveAttribute('icon-name', 'chat-filled'); + expect(chatAvatar).toHaveAttribute('size', '32'); + + // Verify digital task tooltip + const digitalTooltip = screen.container.querySelector('mdc-tooltip') as HTMLElement; + expect(digitalTooltip).toBeInTheDocument(); + expect(digitalTooltip).toHaveAttribute('role', 'tooltip'); + expect(digitalTooltip).toHaveAttribute('id', 'tooltip-incoming-chat-task'); + expect(digitalTooltip).toHaveTextContent('Chat Customer'); + + // Verify task content for incoming digital tasks + const taskTextElements = screen.container.querySelectorAll('mdc-text.task-text'); + expect(taskTextElements[0]).toHaveTextContent('Chat Support'); // Queue name instead of "Active" + expect(taskTextElements[1]).toHaveTextContent('Time Left:'); // Incoming task timing + + // Verify time element + const timeElement = screen.container.querySelector('time') as HTMLElement; + expect(timeElement).toHaveTextContent('00:00'); + expect(timeElement).toHaveAttribute('datetime', '00:00'); + expect(timeElement).toHaveClass('task-text', 'task-text--secondary'); + }); + }); + + describe('Actions', () => { + const actionTask = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'action-task', + interaction: { + ...mockTask.data.interaction, + state: 'new', + mediaType: MEDIA_CHANNEL.TELEPHONY, + }, + wrapUpRequired: false, }, - isBrowser: true, - acceptTask: jest.fn(), - declineTask: jest.fn(), - onTaskSelect: jest.fn(), - logger: loggerMock, }; - render(); - expect(screen.getAllByTestId('ListItemBase')).toHaveLength(2); + beforeEach(() => { + extractTaskListItemDataSpy.mockReturnValue(mockTaskData.action.telephony); + }); + + it('should call acceptTask when accept button is clicked', async () => { + const taskList = {'action-task': actionTask}; + const props: TaskListComponentProps = { + ...defaultProps, + taskList, + }; + + const screen = await render(); + + const acceptButton = screen.getByTestId('task:accept-button'); + fireEvent.click(acceptButton); + + expect(mockAcceptTask).toHaveBeenCalledTimes(1); + expect(mockAcceptTask).toHaveBeenCalledWith(actionTask); + }); + + it('should call declineTask when decline button is clicked', async () => { + const taskList = {'action-task': actionTask}; + const props: TaskListComponentProps = { + ...defaultProps, + taskList, + }; + + const screen = await render(); + + const declineButton = screen.getByTestId('task:decline-button'); + fireEvent.click(declineButton); + + expect(mockDeclineTask).toHaveBeenCalledTimes(1); + expect(mockDeclineTask).toHaveBeenCalledWith(actionTask); + }); + + it('should call onTaskSelect when task is clicked', async () => { + // Create an ACTIVE task without buttons for this test + const activeTask = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'active-task', + interaction: { + ...mockTask.data.interaction, + state: 'active', // Active task, not incoming + mediaType: MEDIA_CHANNEL.TELEPHONY, + }, + }, + }; + + // Mock return value for ACTIVE task (no buttons) + extractTaskListItemDataSpy.mockReturnValue(mockTaskData.action.activeTask); + + const taskList = {'active-task': activeTask}; + const props: TaskListComponentProps = { + ...defaultProps, + taskList, + currentTask: null, + }; + + const screen = await render(); + + const taskElement = screen.getByRole('listitem'); + + // Verify this is an active task without buttons + const buttonContainer = screen.container.querySelector('.task-button-container') as HTMLElement; + expect(buttonContainer.children).toHaveLength(0); // No buttons for active task + + fireEvent.click(taskElement); + + expect(mockOnTaskSelect).toHaveBeenCalledTimes(1); + expect(mockOnTaskSelect).toHaveBeenCalledWith(activeTask); + }); + + it('should handle task selection between multiple tasks', async () => { + // Create second task as CHAT task with different mock data + const secondTask = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'unselected-chat-task', + interaction: { + ...mockTask.data.interaction, + state: 'active', + mediaType: MEDIA_CHANNEL.CHAT, + }, + }, + }; + + // Mock different return values for each task using common data + extractTaskListItemDataSpy + .mockReturnValueOnce(mockTaskData.selection.selectedTelephony) + .mockReturnValueOnce(mockTaskData.selection.unselectedChat); + + const taskList = { + 'action-task': actionTask, + 'unselected-chat-task': secondTask, + }; + + const {container} = await render( + + ); + + // Get both task elements + const taskElements = container.querySelectorAll('[role="listitem"]'); + const selectedTaskElement = taskElements[0]; // action-task (telephony) + const unselectedTaskElement = taskElements[1]; // unselected-chat-task (chat) + + // Verify initial selection state + expect(selectedTaskElement).toHaveClass('task-list-item--selected'); + expect(selectedTaskElement).toHaveAttribute('id', 'action-task'); + expect(unselectedTaskElement).not.toHaveClass('task-list-item--selected'); + expect(unselectedTaskElement).toHaveAttribute('id', 'unselected-chat-task'); + + // Verify visual differences for selected vs unselected tasks + // Selected task should have bold title + const selectedTitle = selectedTaskElement.querySelector('.task-title') as HTMLElement; + expect(selectedTitle).toHaveAttribute('type', 'body-large-bold'); + expect(selectedTitle).toHaveTextContent('1111111111'); // Phone number + + // Unselected task should have normal weight title + const unselectedTitle = unselectedTaskElement.querySelector('.task-digital-title') as HTMLElement; + expect(unselectedTitle).toHaveAttribute('type', 'body-large-medium'); + expect(unselectedTitle).toHaveTextContent('Unselected Chat Customer'); // Customer name + + // Verify different media type characteristics + const telephonyAvatar = selectedTaskElement.querySelector('mdc-avatar') as HTMLElement; + expect(telephonyAvatar).toHaveClass('telephony'); + expect(telephonyAvatar).toHaveAttribute('icon-name', 'handset-filled'); + + const chatAvatar = unselectedTaskElement.querySelector('mdc-avatar') as HTMLElement; + expect(chatAvatar).toHaveClass('chat'); + expect(chatAvatar).toHaveAttribute('icon-name', 'chat-filled'); + + // Digital tasks have tooltips, telephony doesn't + const chatTooltip = unselectedTaskElement.querySelector('mdc-tooltip'); + expect(chatTooltip).toBeInTheDocument(); + expect(chatTooltip).toHaveAttribute('role', 'tooltip'); + + const telephonyTooltip = selectedTaskElement.querySelector('mdc-tooltip'); + expect(telephonyTooltip).not.toBeInTheDocument(); + + // Verify selected task has buttons (incoming), unselected task doesn't (active) + const selectedButtonContainer = selectedTaskElement.querySelector('.task-button-container') as HTMLElement; + expect(selectedButtonContainer.children).toHaveLength(2); // Accept & Decline buttons + + const unselectedButtonContainer = unselectedTaskElement.querySelector('.task-button-container') as HTMLElement; + expect(unselectedButtonContainer.children).toHaveLength(0); // No buttons for active task + + // Test task interaction - clicking unselected task + fireEvent.click(unselectedTaskElement); + + // Verify onTaskSelect was called with the correct task + expect(mockOnTaskSelect).toHaveBeenCalledTimes(1); + expect(mockOnTaskSelect).toHaveBeenCalledWith(secondTask); + + // Re-verify selection state after click (Note: In this test, the selection state doesn't change + // because the parent component would handle the state update. This verifies current behavior) + expect(selectedTaskElement).toHaveClass('task-list-item--selected'); + expect(selectedTaskElement).toHaveAttribute('id', 'action-task'); + expect(unselectedTaskElement).not.toHaveClass('task-list-item--selected'); + expect(unselectedTaskElement).toHaveAttribute('id', 'unselected-chat-task'); + + // Verify utility function calls + expect(extractTaskListItemDataSpy).toHaveBeenCalledTimes(2); + const extractedData = extractTaskListItemDataSpy.mock.results; + expect(extractedData[0].value.mediaType).toBe(MEDIA_CHANNEL.TELEPHONY); + expect(extractedData[0].value.isIncomingTask).toBe(true); + expect(extractedData[1].value.mediaType).toBe(MEDIA_CHANNEL.CHAT); + expect(extractedData[1].value.isIncomingTask).toBe(false); + + // Additional verification: ensure task content reflects different states + const selectedTaskTexts = selectedTaskElement.querySelectorAll('mdc-text.task-text'); + expect(selectedTaskTexts[0]).toHaveTextContent('Selected Team'); // Queue name for incoming + expect(selectedTaskTexts[1]).toHaveTextContent('Time Left:'); // Incoming task timing + + const unselectedTaskTexts = unselectedTaskElement.querySelectorAll('mdc-text.task-text'); + expect(unselectedTaskTexts[0]).toHaveTextContent('Active'); // Status for active task + expect(unselectedTaskTexts[1]).toHaveTextContent('Handle Time:'); // Active task timing + }); }); }); diff --git a/packages/contact-center/cc-components/tests/components/task/TaskList/task-list.utils.tsx b/packages/contact-center/cc-components/tests/components/task/TaskList/task-list.utils.tsx new file mode 100644 index 000000000..f4ddc2905 --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/TaskList/task-list.utils.tsx @@ -0,0 +1,424 @@ +import { + extractTaskListItemData, + isTaskSelectable, + isCurrentTaskSelected, + isTaskListEmpty, + getTasksArray, + createTaskSelectHandler, +} from '../../../../src/components/task/TaskList/task-list.utils'; +import {MEDIA_CHANNEL, TaskListItemData} from '../../../../src/components/task/task.types'; +import {mockTask} from '@webex/test-fixtures'; + +describe('task-list.utils', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('extractTaskListItemData', () => { + describe('Active tasks', () => { + it('should extract correct data for active telephony task', () => { + const originalState = mockTask.data.interaction.state; + const originalCallAssociatedDetails = mockTask.data.interaction.callAssociatedDetails; + const originalWrapUpRequired = mockTask.data.wrapUpRequired; + const originalMediaType = mockTask.data.interaction.mediaType; + + mockTask.data.interaction.state = 'active'; + mockTask.data.interaction.callAssociatedDetails = { + ani: '1234567890', + customerName: 'John Doe', + virtualTeamName: 'Support Team', + ronaTimeout: '45', + }; + mockTask.data.wrapUpRequired = false; + mockTask.data.interaction.mediaType = MEDIA_CHANNEL.TELEPHONY; + + const result = extractTaskListItemData(mockTask, true); + + expect(result.ani).toBe('1234567890'); + expect(result.customerName).toBe('John Doe'); + expect(result.virtualTeamName).toBe('Support Team'); + expect(result.ronaTimeout).toBeNull(); // Active tasks don't show RONA timeout + expect(result.taskState).toBe('active'); + expect(result.isIncomingTask).toBe(false); + expect(result.mediaType).toBe(MEDIA_CHANNEL.TELEPHONY); + expect(result.isTelephony).toBe(true); + expect(result.isSocial).toBe(false); + expect(result.acceptText).toBeUndefined(); + expect(result.declineText).toBeUndefined(); + expect(result.title).toBe('1234567890'); // ANI for telephony + expect(result.disableAccept).toBe(false); + expect(result.displayState).toBe('active'); + + // Restore original values + mockTask.data.interaction.state = originalState; + mockTask.data.interaction.callAssociatedDetails = originalCallAssociatedDetails; + mockTask.data.wrapUpRequired = originalWrapUpRequired; + mockTask.data.interaction.mediaType = originalMediaType; + }); + + it('should extract correct data for active social media task', () => { + const originalState = mockTask.data.interaction.state; + const originalCallAssociatedDetails = mockTask.data.interaction.callAssociatedDetails; + const originalWrapUpRequired = mockTask.data.wrapUpRequired; + const originalMediaType = mockTask.data.interaction.mediaType; + const originalMediaChannel = mockTask.data.interaction.mediaChannel; + + mockTask.data.interaction.state = 'connected'; + mockTask.data.interaction.callAssociatedDetails = { + ani: '1234567890', + customerName: 'Alice Johnson', + virtualTeamName: 'Social Team', + }; + mockTask.data.wrapUpRequired = false; + mockTask.data.interaction.mediaType = MEDIA_CHANNEL.SOCIAL; + mockTask.data.interaction.mediaChannel = 'facebook'; + + const result = extractTaskListItemData(mockTask, true); + + expect(result.title).toBe('Alice Johnson'); // Customer name for social + expect(result.isSocial).toBe(true); + expect(result.isTelephony).toBe(false); + expect(result.displayState).toBe('connected'); + expect(result.isIncomingTask).toBe(false); + expect(result.ronaTimeout).toBeNull(); + + // Restore original values + mockTask.data.interaction.state = originalState; + mockTask.data.interaction.callAssociatedDetails = originalCallAssociatedDetails; + mockTask.data.wrapUpRequired = originalWrapUpRequired; + mockTask.data.interaction.mediaType = originalMediaType; + mockTask.data.interaction.mediaChannel = originalMediaChannel; + }); + }); + + describe('Incoming tasks', () => { + it('should extract correct data for incoming telephony task on browser', () => { + const originalState = mockTask.data.interaction.state; + const originalCallAssociatedDetails = mockTask.data.interaction.callAssociatedDetails; + const originalWrapUpRequired = mockTask.data.wrapUpRequired; + const originalMediaType = mockTask.data.interaction.mediaType; + + mockTask.data.interaction.state = 'new'; + mockTask.data.interaction.callAssociatedDetails = { + ani: '9876543210', + customerName: 'Jane Smith', + virtualTeamName: 'Sales Team', + ronaTimeout: '60', + }; + mockTask.data.wrapUpRequired = false; + mockTask.data.interaction.mediaType = MEDIA_CHANNEL.TELEPHONY; + + const result = extractTaskListItemData(mockTask, true); + + expect(result.ani).toBe('9876543210'); + expect(result.ronaTimeout).toBe(60); // Shows RONA timeout for incoming tasks + expect(result.taskState).toBe('new'); + expect(result.isIncomingTask).toBe(true); + expect(result.acceptText).toBe('Accept'); + expect(result.declineText).toBe('Decline'); + expect(result.disableAccept).toBe(false); + expect(result.displayState).toBe(''); // Empty for incoming tasks + + // Restore original values + mockTask.data.interaction.state = originalState; + mockTask.data.interaction.callAssociatedDetails = originalCallAssociatedDetails; + mockTask.data.wrapUpRequired = originalWrapUpRequired; + mockTask.data.interaction.mediaType = originalMediaType; + }); + + it('should extract correct data for incoming telephony task on non-browser', () => { + const originalState = mockTask.data.interaction.state; + const originalCallAssociatedDetails = mockTask.data.interaction.callAssociatedDetails; + const originalWrapUpRequired = mockTask.data.wrapUpRequired; + const originalMediaType = mockTask.data.interaction.mediaType; + + mockTask.data.interaction.state = 'new'; + mockTask.data.interaction.callAssociatedDetails = { + ani: '5555555555', + customerName: 'Mobile User', + virtualTeamName: 'Mobile Support', + }; + mockTask.data.wrapUpRequired = false; + mockTask.data.interaction.mediaType = MEDIA_CHANNEL.TELEPHONY; + + const result = extractTaskListItemData(mockTask, false); + + expect(result.acceptText).toBe('Ringing...'); + expect(result.declineText).toBeUndefined(); + expect(result.disableAccept).toBe(true); + expect(result.isIncomingTask).toBe(true); + + // Restore original values + mockTask.data.interaction.state = originalState; + mockTask.data.interaction.callAssociatedDetails = originalCallAssociatedDetails; + mockTask.data.wrapUpRequired = originalWrapUpRequired; + mockTask.data.interaction.mediaType = originalMediaType; + }); + + it('should extract correct data for incoming social media task', () => { + const originalState = mockTask.data.interaction.state; + const originalCallAssociatedDetails = mockTask.data.interaction.callAssociatedDetails; + const originalWrapUpRequired = mockTask.data.wrapUpRequired; + const originalMediaType = mockTask.data.interaction.mediaType; + + mockTask.data.interaction.state = 'new'; + mockTask.data.interaction.callAssociatedDetails = { + ani: '1234567890', + customerName: 'Social Customer', + virtualTeamName: 'Social Team', + }; + mockTask.data.wrapUpRequired = false; + mockTask.data.interaction.mediaType = MEDIA_CHANNEL.SOCIAL; + + const result = extractTaskListItemData(mockTask, true); + + expect(result.acceptText).toBe('Accept'); + expect(result.declineText).toBeUndefined(); // No decline for social + expect(result.isSocial).toBe(true); + expect(result.title).toBe('Social Customer'); + + // Restore original values + mockTask.data.interaction.state = originalState; + mockTask.data.interaction.callAssociatedDetails = originalCallAssociatedDetails; + mockTask.data.wrapUpRequired = originalWrapUpRequired; + mockTask.data.interaction.mediaType = originalMediaType; + }); + }); + }); + + describe('isTaskSelectable', () => { + it('should return false for the same task', () => { + const taskData = {isIncomingTask: false} as TaskListItemData; + const result = isTaskSelectable(mockTask, mockTask, taskData); + expect(result).toBe(false); + }); + + it('should return true for different non-incoming task', () => { + const differentTask = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'different-task-456', + }, + }; + + const currentTask = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'current-task-123', + }, + }; + + const taskData = {isIncomingTask: false} as TaskListItemData; + const result = isTaskSelectable(differentTask, currentTask, taskData); + expect(result).toBe(true); + }); + + it('should return false for incoming task without wrap up', () => { + const incomingTask = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'incoming-task-789', + wrapUpRequired: false, + }, + }; + + const currentTask = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'current-task-123', + }, + }; + + const taskData = {isIncomingTask: true} as TaskListItemData; + const result = isTaskSelectable(incomingTask, currentTask, taskData); + expect(result).toBe(false); + }); + + it('should return true when no current task is selected', () => { + const task = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'some-task-456', + }, + }; + + const taskData = {isIncomingTask: false} as TaskListItemData; + const result = isTaskSelectable(task, null, taskData); + expect(result).toBe(true); + }); + }); + + describe('isCurrentTaskSelected', () => { + it('should return true when task is currently selected', () => { + const task = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'test-123', + }, + }; + + const currentTask = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'test-123', + }, + }; + + const result = isCurrentTaskSelected(task, currentTask); + expect(result).toBe(true); + }); + + it('should return false when different task is selected', () => { + const task = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'test-123', + }, + }; + + const currentTask = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'different-456', + }, + }; + + const result = isCurrentTaskSelected(task, currentTask); + expect(result).toBe(false); + }); + + it('should return false when no task is selected', () => { + const task = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'test-123', + }, + }; + + const result = isCurrentTaskSelected(task, null); + expect(result).toBe(false); + }); + }); + + describe('isTaskListEmpty', () => { + it('should return true for null, undefined, or empty object task list', () => { + expect(isTaskListEmpty(null)).toBe(true); + expect(isTaskListEmpty(undefined)).toBe(true); + expect(isTaskListEmpty({})).toBe(true); + }); + + it('should return false for task list with tasks', () => { + const task1 = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'task-1', + }, + }; + + const taskList = { + 'task-1': task1, + }; + expect(isTaskListEmpty(taskList)).toBe(false); + }); + }); + + describe('getTasksArray', () => { + it('should convert task list object to array', () => { + const task1 = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'task-1', + }, + }; + + const taskList = { + 'task-1': task1, + }; + + const result = getTasksArray(taskList); + expect(result).toHaveLength(1); + expect(result).toContain(task1); + }); + + it('should return empty array for null, undefined, or empty object task list', () => { + expect(getTasksArray({})).toEqual([]); + expect(getTasksArray(null)).toEqual([]); + expect(getTasksArray(undefined)).toEqual([]); + }); + }); + + describe('createTaskSelectHandler', () => { + const mockOnTaskSelect = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should call onTaskSelect for selectable task', () => { + const task = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'task-1', + interaction: { + ...mockTask.data.interaction, + state: 'active', + }, + }, + }; + + const currentTask = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'task-2', + }, + }; + + const handler = createTaskSelectHandler(task, currentTask, mockOnTaskSelect); + handler(); + + expect(mockOnTaskSelect).toHaveBeenCalledWith(task); + }); + + it('should not call onTaskSelect for non-selectable task', () => { + const task = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'task-1', + interaction: { + ...mockTask.data.interaction, + state: 'new', + }, + wrapUpRequired: false, + }, + }; + + const currentTask = { + ...mockTask, + data: { + ...mockTask.data, + interactionId: 'task-2', + }, + }; + + const handler = createTaskSelectHandler(task, currentTask, mockOnTaskSelect); + handler(); + + expect(mockOnTaskSelect).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/contact-center/cc-components/tests/components/task/TaskTimer/task-timer.tsx b/packages/contact-center/cc-components/tests/components/task/TaskTimer/task-timer.tsx new file mode 100644 index 000000000..f78ee1dc7 --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/TaskTimer/task-timer.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import {render, screen, waitFor} from '@testing-library/react'; +import '@testing-library/jest-dom'; +import TaskTimer from '../../../../src/components/task/TaskTimer'; + +Object.defineProperty(global, 'Worker', { + writable: true, + value: class MockWorker { + constructor() {} + postMessage = jest.fn(); + addEventListener = jest.fn(); + removeEventListener = jest.fn(); + terminate = jest.fn(); + }, +}); + +Object.defineProperty(global, 'URL', { + writable: true, + value: { + createObjectURL: jest.fn(() => 'blob:mock-url'), + revokeObjectURL: jest.fn(), + }, +}); + +const mockDateNow = jest.spyOn(Date, 'now'); + +describe('TaskTimer Component', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.clearAllTimers(); + mockDateNow.mockReturnValue(1640995200000); // 2022-01-01 00:00:00 UTC + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + describe('Rendering', () => { + it('should render with proper semantic HTML structure and accessibility attributes', async () => { + const screen = await render(); + + await waitFor(() => { + const timeElement = screen.getByRole('time'); + + // Verify semantic HTML structure + expect(timeElement.tagName).toBe('TIME'); + expect(timeElement).toHaveClass('task-text', 'task-text--secondary'); + + // Verify accessibility attributes + expect(timeElement).toHaveAttribute('datetime', '00:00'); + expect(timeElement).toHaveTextContent('00:00'); + + // Verify worker initialization + expect(window.URL.createObjectURL).toHaveBeenCalled(); + }); + }); + + it('should handle prop combinations with correct DOM attributes and initial state', async () => { + const oneMinuteAgo = 1640995200000 - 60000; + const {rerender} = await render(); + + await waitFor(() => { + const timeElement = screen.getByRole('time'); + + // Verify DOM properties for elapsed time mode + expect(timeElement).toHaveAttribute('datetime'); + expect(timeElement).toHaveClass('task-text', 'task-text--secondary'); + expect(timeElement.textContent).toMatch(/^\d{2}:\d{2}$/); // MM:SS format + }); + + // Test countdown mode + rerender(); + + await waitFor(() => { + const timeElement = screen.getByRole('time'); + + // Verify DOM properties for countdown mode + expect(timeElement).toHaveAttribute('datetime'); + expect(timeElement).toHaveClass('task-text', 'task-text--secondary'); + expect(timeElement.textContent).toMatch(/^\d{2}:\d{2}$/); // MM:SS format + }); + + // Test edge case with undefined props + rerender(); + + await waitFor(() => { + const timeElement = screen.getByRole('time'); + + // Verify fallback state + expect(timeElement).toHaveTextContent('00:00'); + expect(timeElement).toHaveAttribute('datetime', '00:00'); + expect(timeElement).toHaveClass('task-text', 'task-text--secondary'); + }); + }); + }); +}); diff --git a/packages/contact-center/cc-components/tests/components/task/task/__snapshots__/task.snapshot.tsx.snap b/packages/contact-center/cc-components/tests/components/task/task/__snapshots__/task.snapshot.tsx.snap new file mode 100644 index 000000000..228618ae9 --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/task/__snapshots__/task.snapshot.tsx.snap @@ -0,0 +1,4690 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Task Component Snapshots Actions should handle accept task action correctly 1`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    + +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle accept task action correctly 2`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    + +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle accept task action correctly 3`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    + +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle accept task action correctly 4`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    + +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle accept task action correctly 5`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    + + +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle accept task action correctly 6`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    + + +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle decline task action correctly 1`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    + +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle decline task action correctly 2`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    + +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle decline task action correctly 3`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    + + +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle decline task action correctly 4`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    + + +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle decline task action correctly 5`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    + + +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle task selection action correctly 1`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle task selection action correctly 2`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle task selection action correctly 3`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    + + +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle task selection action correctly 4`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    + + +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle task selection action correctly 5`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    + + +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle task selection action correctly 6`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    + + +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle task selection action correctly 7`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    + + +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle task selection action correctly 8`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    + + +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle task selection action correctly 9`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle task selection action correctly 10`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle task selection action correctly 11`] = ` +
    +
  • +
    + +
    +
    +
    + + Chat Customer + + + Chat Customer + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle task selection action correctly 12`] = ` +
    +
  • +
    + +
    +
    +
    + + Chat Customer + + + Chat Customer + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle task selection action correctly 13`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle task selection action correctly 14`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle task selection action correctly 15`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Actions should handle task selection action correctly 16`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should handle default prop values and edge cases correctly: all-explicit-default-values 1`] = ` +
    +
  • +
    + +
    +
    +
    + + All Defaults Explicit + +
    +
    +
    +
    + +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should handle default prop values and edge cases correctly: explicitly-enabled-accept-button 1`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    + +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should handle default prop values and edge cases correctly: explicitly-not-incoming-task 1`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should handle default prop values and edge cases correctly: explicitly-unselected-task 1`] = ` +
    +
  • +
    + +
    +
    +
    + + Explicitly Unselected Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should handle default prop values and edge cases correctly: minimal-task-with-default-values 1`] = ` +
    +
  • +
    + +
    +
    +
    + + Minimal Task + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should handle default prop values and edge cases correctly: pure-minimal-task-props 1`] = ` +
    +
  • +
    + +
    +
    +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should handle default prop values and edge cases correctly: task-with-enabled-accept-button 1`] = ` +
    +
  • +
    + +
    +
    +
    + + Enabled Accept Task + +
    +
    +
    +
    + +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should render accept and decline buttons with correct states and properties 1`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    + + +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should render accept and decline buttons with correct states and properties 2`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    + +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should render accept and decline buttons with correct states and properties 3`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    + +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should render media type icons and brand visuals correctly for different channels: chat-task-with-avatar 1`] = ` +
    +
  • +
    + +
    +
    +
    + + Chat Customer Name + + + Chat Customer Name + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should render media type icons and brand visuals correctly for different channels: email-task-with-avatar 1`] = ` +
    +
  • +
    +
    +
    +
    + + Email Subject Line + + + Email Subject Line + + + Active + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should render media type icons and brand visuals correctly for different channels: facebook-task-with-brand-visual 1`] = ` +
    +
  • +
    +
    +
    +
    +
    +
    + + Facebook Customer + + + Facebook Customer + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should render media type icons and brand visuals correctly for different channels: telephony-task-with-avatar 1`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should render media type icons and brand visuals correctly for different channels: whatsapp-incoming-task-with-brand-visual 1`] = ` +
    +
  • +
    +
    + +
    +
    +
    +
    + + WhatsApp Contact + + + WhatsApp Contact + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should render state, queue, and time information correctly for different task scenarios 1`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should render state, queue, and time information correctly for different task scenarios 2`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Support Team + + + Time Left: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should render state, queue, and time information correctly for different task scenarios 3`] = ` +
    +
  • +
    +
    + +
    +
    +
    +
    + + Test Task + + + Test Task + + + WhatsApp Support + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should render titles and tooltips correctly based on media type and selection state: chat-task-with-title-and-tooltip 1`] = ` +
    +
  • +
    + +
    +
    +
    + + Live Chat with Customer Support + + + Live Chat with Customer Support + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should render titles and tooltips correctly based on media type and selection state: incoming-facebook-task-with-special-styling 1`] = ` +
    +
  • +
    +
    +
    +
    +
    +
    + + New Facebook Message from Customer + + + New Facebook Message from Customer + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should render titles and tooltips correctly based on media type and selection state: selected-email-task-with-bold-title 1`] = ` +
    +
  • +
    +
    +
    +
    + + Urgent: Customer Complaint Email + + + Urgent: Customer Complaint Email + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should render titles and tooltips correctly based on media type and selection state: selected-task-with-custom-styles 1`] = ` +
    +
  • +
    + +
    +
    +
    + + Test Task + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should render titles and tooltips correctly based on media type and selection state: selected-voice-task-with-bold-title 1`] = ` +
    +
  • +
    + +
    +
    +
    + + Important Customer Call + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should render titles and tooltips correctly based on media type and selection state: task-with-empty-string-title 1`] = ` +
    +
  • +
    + +
    +
    +
    + + Connected + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should render titles and tooltips correctly based on media type and selection state: task-without-title 1`] = ` +
    +
  • +
    + +
    +
    +
    + + Connected + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should render titles and tooltips correctly based on media type and selection state: telephony-task-with-simple-title 1`] = ` +
    +
  • +
    + +
    +
    +
    + + John Doe - Customer Call + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; + +exports[`Task Component Snapshots Rendering should render titles and tooltips correctly based on media type and selection state: unselected-email-task-with-medium-title 1`] = ` +
    +
  • +
    +
    +
    +
    + + Standard Customer Inquiry Email + + + Standard Customer Inquiry Email + + + Connected + + + Handle Time: + + + +
    +
    +
    +
    +
    +
  • +
    +`; diff --git a/packages/contact-center/cc-components/tests/components/task/task/task.snapshot.tsx b/packages/contact-center/cc-components/tests/components/task/task/task.snapshot.tsx new file mode 100644 index 000000000..f0dc67067 --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/task/task.snapshot.tsx @@ -0,0 +1,599 @@ +import React from 'react'; +import {render, fireEvent, getByTestId, getByRole} from '@testing-library/react'; +import '@testing-library/jest-dom'; +import Task, {TaskProps} from '../../../../src/components/task/Task'; +import {MEDIA_CHANNEL, TaskState} from '../../../../src/components/task/task.types'; +import * as taskUtils from '../../../../src/components/task/Task/task.utils'; + +Object.defineProperty(global, 'Worker', { + writable: true, + value: class MockWorker { + constructor() {} + postMessage = jest.fn(); + addEventListener = jest.fn(); + removeEventListener = jest.fn(); + terminate = jest.fn(); + }, +}); + +Object.defineProperty(global, 'URL', { + writable: true, + value: { + createObjectURL: jest.fn(() => 'blob:mock-url'), + revokeObjectURL: jest.fn(), + }, +}); + +describe('Task Component Snapshots', () => { + const mockAcceptTask = jest.fn(); + const mockDeclineTask = jest.fn(); + const mockOnTaskSelect = jest.fn(); + + // Default props using TaskProps interface + const defaultProps: TaskProps = { + interactionId: 'test-interaction-123', + title: 'Test Task', + state: TaskState.CONNECTED, + startTimeStamp: 1641234567890, + ronaTimeout: undefined, + selected: false, + isIncomingTask: false, + queue: undefined, + acceptTask: undefined, + declineTask: undefined, + onTaskSelect: undefined, + acceptText: undefined, + declineText: undefined, + disableAccept: false, + styles: undefined, + mediaType: MEDIA_CHANNEL.TELEPHONY, + mediaChannel: MEDIA_CHANNEL.TELEPHONY, + }; + + const extractTaskComponentDataSpy = jest.spyOn(taskUtils, 'extractTaskComponentData'); + const getTaskListItemClassesSpy = jest.spyOn(taskUtils, 'getTaskListItemClasses'); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + extractTaskComponentDataSpy.mockRestore(); + getTaskListItemClassesSpy.mockRestore(); + }); + + describe('Rendering', () => { + it('should render media type icons and brand visuals correctly for different channels', async () => { + // Test 1: Telephony task with avatar icon + const {container: telephonyContainer} = await render(); + expect(telephonyContainer).toMatchSnapshot('telephony-task-with-avatar'); + + // Test 2: Social media (Facebook) task with brand visual + const facebookProps = { + ...defaultProps, + mediaType: MEDIA_CHANNEL.SOCIAL, + mediaChannel: MEDIA_CHANNEL.FACEBOOK, + title: 'Facebook Customer', + }; + + const {container: facebookContainer} = await render(); + expect(facebookContainer).toMatchSnapshot('facebook-task-with-brand-visual'); + + // Test 3: Email task with avatar + const emailProps = { + ...defaultProps, + mediaType: MEDIA_CHANNEL.EMAIL, + mediaChannel: MEDIA_CHANNEL.EMAIL, + title: 'Email Subject Line', + state: TaskState.ACTIVE, + }; + + const {container: emailContainer} = await render(); + expect(emailContainer).toMatchSnapshot('email-task-with-avatar'); + + // Test 4: WhatsApp incoming task with brand visual + const whatsappProps = { + ...defaultProps, + mediaType: MEDIA_CHANNEL.SOCIAL, + mediaChannel: MEDIA_CHANNEL.WHATSAPP, + title: 'WhatsApp Contact', + isIncomingTask: true, + }; + + const {container: whatsappContainer} = await render(); + expect(whatsappContainer).toMatchSnapshot('whatsapp-incoming-task-with-brand-visual'); + + // Test 5: Chat task with avatar + const chatProps = { + ...defaultProps, + mediaType: MEDIA_CHANNEL.CHAT, + mediaChannel: MEDIA_CHANNEL.CHAT, + title: 'Chat Customer Name', + isIncomingTask: false, + }; + + const {container: chatContainer} = await render(); + expect(chatContainer).toMatchSnapshot('chat-task-with-avatar'); + }); + + it('should render titles and tooltips correctly based on media type and selection state', async () => { + // Test 1: Task without title should not render any title elements + const noTitleProps = { + ...defaultProps, + title: undefined, + }; + + const {container: noTitleContainer} = await render(); + expect(noTitleContainer).toMatchSnapshot('task-without-title'); + + // Test 2: Voice media (Telephony) should render simple title without tooltip + const telephonyWithTitleProps = { + ...defaultProps, + mediaType: MEDIA_CHANNEL.TELEPHONY, + mediaChannel: MEDIA_CHANNEL.TELEPHONY, + title: 'John Doe - Customer Call', + }; + + const {container: telephonyContainer} = await render(); + expect(telephonyContainer).toMatchSnapshot('telephony-task-with-simple-title'); + + // Test 3: Digital media (Chat) should render title with tooltip for user assistance + const chatWithTitleProps = { + ...defaultProps, + mediaType: MEDIA_CHANNEL.CHAT, + mediaChannel: MEDIA_CHANNEL.CHAT, + title: 'Live Chat with Customer Support', + isIncomingTask: false, + }; + + const {container: chatContainer} = await render(); + expect(chatContainer).toMatchSnapshot('chat-task-with-title-and-tooltip'); + + // Test 4: Selected digital task should display bold title for visual emphasis + const selectedEmailProps = { + ...defaultProps, + mediaType: MEDIA_CHANNEL.EMAIL, + mediaChannel: MEDIA_CHANNEL.EMAIL, + title: 'Urgent: Customer Complaint Email', + selected: true, + }; + + const {container: selectedContainer} = await render(); + expect(selectedContainer).toMatchSnapshot('selected-email-task-with-bold-title'); + + // Test 5: Unselected digital task should display medium weight title + const unselectedEmailProps = { + ...defaultProps, + mediaType: MEDIA_CHANNEL.EMAIL, + mediaChannel: MEDIA_CHANNEL.EMAIL, + title: 'Standard Customer Inquiry Email', + selected: false, + }; + + const {container: unselectedContainer} = await render(); + expect(unselectedContainer).toMatchSnapshot('unselected-email-task-with-medium-title'); + + // Test 6: Incoming digital task should use specialized styling and tooltip + const incomingFacebookProps = { + ...defaultProps, + mediaType: MEDIA_CHANNEL.SOCIAL, + mediaChannel: MEDIA_CHANNEL.FACEBOOK, + title: 'New Facebook Message from Customer', + isIncomingTask: true, + }; + + const {container: incomingContainer} = await render(); + expect(incomingContainer).toMatchSnapshot('incoming-facebook-task-with-special-styling'); + + // Test 7: Selected voice task should affect title weight + const selectedVoiceProps = { + ...defaultProps, + mediaType: MEDIA_CHANNEL.TELEPHONY, + mediaChannel: MEDIA_CHANNEL.TELEPHONY, + title: 'Important Customer Call', + selected: true, + }; + + const {container: selectedVoiceContainer} = await render(); + expect(selectedVoiceContainer).toMatchSnapshot('selected-voice-task-with-bold-title'); + + // Test 8: Selected task with custom styles + const selectedWithStylesProps = { + ...defaultProps, + selected: true, + styles: 'custom-task-style task-highlight', + }; + + const {container: styledContainer} = await render(); + expect(styledContainer).toMatchSnapshot('selected-task-with-custom-styles'); + + // Test 9: Empty string title should behave like undefined title + const emptyTitleProps = { + ...defaultProps, + title: '', + }; + + const {container: emptyTitleContainer} = await render(); + expect(emptyTitleContainer).toMatchSnapshot('task-with-empty-string-title'); + }); + + it('should render state, queue, and time information correctly for different task scenarios', async () => { + // Test 1: Active task with state and handle time + const activeProps = { + ...defaultProps, + state: TaskState.CONNECTED, + startTimeStamp: 1641234567890, + isIncomingTask: false, + }; + + const {container: activeContainer} = await render(); + expect(activeContainer).toMatchSnapshot(); + + // Test 2: Incoming task with queue and time left (RONA timeout) + const incomingProps = { + ...defaultProps, + isIncomingTask: true, + queue: 'Support Team', + ronaTimeout: 30, + state: TaskState.NEW, + startTimeStamp: 1641234567890, + }; + + const {container: incomingContainer} = await render(); + expect(incomingContainer).toMatchSnapshot(); + + // Test 3: WhatsApp incoming task with queue and handle time (no RONA) + const whatsappProps = { + ...defaultProps, + mediaType: MEDIA_CHANNEL.SOCIAL, + mediaChannel: MEDIA_CHANNEL.WHATSAPP, + isIncomingTask: true, + queue: 'WhatsApp Support', + ronaTimeout: undefined, // No RONA timeout + startTimeStamp: 1641234567890, + }; + + const {container: whatsappContainer} = await render(); + expect(whatsappContainer).toMatchSnapshot(); + }); + + it('should render accept and decline buttons with correct states and properties', async () => { + // Test 1: Task with both accept and decline buttons + const bothButtonsProps = { + ...defaultProps, + acceptText: 'Accept', + declineText: 'Decline', + acceptTask: mockAcceptTask, + declineTask: mockDeclineTask, + }; + + const {container: bothButtonsContainer} = await render(); + expect(bothButtonsContainer).toMatchSnapshot(); + + // Test 2: Task with disabled accept button + const disabledProps = { + ...defaultProps, + acceptText: 'Ringing...', + disableAccept: true, + acceptTask: mockAcceptTask, + }; + + const {container: disabledContainer} = await render(); + expect(disabledContainer).toMatchSnapshot(); + + // Test 3: Verify button container data attributes + const buttonDataProps = { + ...defaultProps, + acceptText: 'Accept Call', + acceptTask: mockAcceptTask, + }; + + const {container: buttonDataContainer} = await render(); + expect(buttonDataContainer).toMatchSnapshot(); + }); + + it('should handle default prop values and edge cases correctly', async () => { + // Test 1: Component with minimal props (tests default values) + const minimalProps = { + interactionId: 'test-minimal-123', + title: 'Minimal Task', + mediaType: MEDIA_CHANNEL.TELEPHONY, + mediaChannel: MEDIA_CHANNEL.TELEPHONY, + // Intentionally omitting: selected, isIncomingTask, disableAccept + // to test default values + }; + + const {container: minimalContainer} = await render(); + expect(minimalContainer).toMatchSnapshot('minimal-task-with-default-values'); + + // Test 2: Task with accept button using default disableAccept = false + const enabledAcceptProps = { + interactionId: 'test-enabled-456', + title: 'Enabled Accept Task', + mediaType: MEDIA_CHANNEL.TELEPHONY, + mediaChannel: MEDIA_CHANNEL.TELEPHONY, + acceptText: 'Accept Now', + acceptTask: mockAcceptTask, + // Intentionally omitting disableAccept to test default false value + }; + + const {container: enabledContainer} = await render(); + expect(enabledContainer).toMatchSnapshot('task-with-enabled-accept-button'); + + // Test 3: Explicitly test selected = false vs undefined + const explicitlyUnselectedProps = { + ...defaultProps, + selected: false, // Explicitly set to false + title: 'Explicitly Unselected Task', + }; + + const {container: explicitContainer} = await render(); + expect(explicitContainer).toMatchSnapshot('explicitly-unselected-task'); + + // Test 4: Explicitly test isIncomingTask = false vs undefined + const explicitlyNotIncomingProps = { + ...defaultProps, + isIncomingTask: false, // Explicitly set to false + state: TaskState.CONNECTED, + startTimeStamp: 1641234567890, + queue: 'Should Not Show Queue', // Queue provided but shouldn't show + }; + + const {container: notIncomingContainer} = await render(); + expect(notIncomingContainer).toMatchSnapshot('explicitly-not-incoming-task'); + + // Test 5: Explicitly test disableAccept = false vs undefined + const explicitlyEnabledProps = { + ...defaultProps, + acceptText: 'Join Meeting', + acceptTask: mockAcceptTask, + disableAccept: false, // Explicitly set to false + }; + + const {container: explicitEnabledContainer} = await render(); + expect(explicitEnabledContainer).toMatchSnapshot('explicitly-enabled-accept-button'); + + // Test 6: Component without any optional props (pure minimal) + const pureMinimalProps = { + interactionId: 'pure-minimal-789', + // Only required props - everything else should use defaults + }; + + const {container: pureContainer} = await render(); + expect(pureContainer).toMatchSnapshot('pure-minimal-task-props'); + + // Test 7: Edge case - all boolean props set to their default values explicitly + const allDefaultsExplicitProps = { + interactionId: 'all-defaults-explicit', + title: 'All Defaults Explicit', + selected: false, // Explicit default + isIncomingTask: false, // Explicit default + disableAccept: false, // Explicit default + acceptText: 'Accept', + acceptTask: mockAcceptTask, + mediaType: MEDIA_CHANNEL.TELEPHONY, + mediaChannel: MEDIA_CHANNEL.TELEPHONY, + }; + + const {container: allDefaultsContainer} = await render(); + expect(allDefaultsContainer).toMatchSnapshot('all-explicit-default-values'); + }); + }); + + describe('Actions', () => { + it('should handle accept task action correctly', async () => { + // Accept button click calls acceptTask callback + const acceptProps = { + ...defaultProps, + acceptText: 'Accept', + acceptTask: mockAcceptTask, + }; + + const {container: acceptContainer} = await render(); + expect(acceptContainer).toMatchSnapshot(); + + const acceptButton = getByTestId(acceptContainer, 'task:accept-button'); + fireEvent.click(acceptButton); + expect(acceptContainer).toMatchSnapshot(); + + // Disabled accept button does not trigger callback + const disabledAcceptProps = { + ...defaultProps, + acceptText: 'Ringing...', + disableAccept: true, + acceptTask: mockAcceptTask, + }; + + jest.clearAllMocks(); + + const {container: disabledContainer} = await render(); + expect(disabledContainer).toMatchSnapshot(); + + const disabledAcceptButton = getByTestId(disabledContainer, 'task:accept-button'); + fireEvent.click(disabledAcceptButton); + expect(disabledContainer).toMatchSnapshot(); + + // Accept action with both buttons - only accept should be called + const bothButtonsProps = { + ...defaultProps, + acceptText: 'Accept Call', + declineText: 'Decline', + acceptTask: mockAcceptTask, + declineTask: mockDeclineTask, + }; + + jest.clearAllMocks(); + + const {container: bothContainer} = await render(); + expect(bothContainer).toMatchSnapshot(); + + const acceptButtonBoth = getByTestId(bothContainer, 'task:accept-button'); + fireEvent.click(acceptButtonBoth); + expect(bothContainer).toMatchSnapshot(); + }); + + it('should handle decline task action correctly', async () => { + // Test 1: Decline button click calls declineTask callback + const declineProps = { + ...defaultProps, + declineText: 'Decline', + declineTask: mockDeclineTask, + }; + + const {container: declineContainer} = await render(); + expect(declineContainer).toMatchSnapshot(); + + const declineButton = getByTestId(declineContainer, 'task:decline-button'); + fireEvent.click(declineButton); + expect(declineContainer).toMatchSnapshot(); + + // Test 2: Decline action with both buttons - only decline should be called + const bothButtonsProps = { + ...defaultProps, + acceptText: 'Accept', + declineText: 'Decline Call', + acceptTask: mockAcceptTask, + declineTask: mockDeclineTask, + }; + + jest.clearAllMocks(); + + const {container: bothContainer} = await render(); + expect(bothContainer).toMatchSnapshot(); + + const declineButtonBoth = getByTestId(bothContainer, 'task:decline-button'); + fireEvent.click(declineButtonBoth); + expect(bothContainer).toMatchSnapshot(); + + // Test 3: Multiple decline clicks should call callback multiple times + jest.clearAllMocks(); + + fireEvent.click(declineButtonBoth); + fireEvent.click(declineButtonBoth); + expect(bothContainer).toMatchSnapshot(); + }); + + it('should handle task selection action correctly', async () => { + // Test 1: Task list item click calls onTaskSelect callback + const selectProps = { + ...defaultProps, + onTaskSelect: mockOnTaskSelect, + }; + + const {container: selectContainer} = await render(); + expect(selectContainer).toMatchSnapshot(); + + const listItem = getByRole(selectContainer, 'listitem'); + fireEvent.click(listItem); + expect(selectContainer).toMatchSnapshot(); + + // Test 2: Task selection with buttons - selection should work independently + const selectWithButtonsProps = { + ...defaultProps, + onTaskSelect: mockOnTaskSelect, + acceptText: 'Accept', + declineText: 'Decline', + acceptTask: mockAcceptTask, + declineTask: mockDeclineTask, + }; + + jest.clearAllMocks(); + + const {container: selectButtonsContainer} = await render(); + expect(selectButtonsContainer).toMatchSnapshot(); + + const listItemWithButtons = getByRole(selectButtonsContainer, 'listitem'); + + // Click on list item (not buttons) + fireEvent.click(listItemWithButtons); + expect(selectButtonsContainer).toMatchSnapshot(); + + // Test accept button with fresh render + jest.clearAllMocks(); + const {container: acceptTestContainer} = await render(); + expect(acceptTestContainer).toMatchSnapshot(); + + const acceptButton = getByTestId(acceptTestContainer, 'task:accept-button'); + fireEvent.click(acceptButton); + expect(acceptTestContainer).toMatchSnapshot(); + + // Test decline button with fresh render + jest.clearAllMocks(); + const {container: declineTestContainer} = await render(); + expect(declineTestContainer).toMatchSnapshot(); + + const declineButton = getByTestId(declineTestContainer, 'task:decline-button'); + fireEvent.click(declineButton); + expect(declineTestContainer).toMatchSnapshot(); + + // Test 3: Selected task maintains selection behavior + const selectedTaskProps = { + ...defaultProps, + selected: true, + onTaskSelect: mockOnTaskSelect, + }; + + jest.clearAllMocks(); + + const {container: selectedContainer} = await render(); + expect(selectedContainer).toMatchSnapshot(); + + const selectedListItem = getByRole(selectedContainer, 'listitem'); + fireEvent.click(selectedListItem); + expect(selectedContainer).toMatchSnapshot(); + + jest.clearAllMocks(); + + // Test 5: Task selection with different media types + const digitalTaskSelectProps = { + ...defaultProps, + mediaType: MEDIA_CHANNEL.CHAT, + mediaChannel: MEDIA_CHANNEL.CHAT, + title: 'Chat Customer', + onTaskSelect: mockOnTaskSelect, + }; + + jest.clearAllMocks(); + + const {container: digitalSelectContainer} = await render(); + expect(digitalSelectContainer).toMatchSnapshot(); + + const digitalListItem = getByRole(digitalSelectContainer, 'listitem'); + fireEvent.click(digitalListItem); + expect(digitalSelectContainer).toMatchSnapshot(); + + // Test 6: Task selection with custom styles + const styledTaskProps = { + ...defaultProps, + onTaskSelect: mockOnTaskSelect, + styles: 'custom-task-style', + selected: false, + }; + + jest.clearAllMocks(); + + const {container: styledContainer} = await render(); + expect(styledContainer).toMatchSnapshot(); + + const styledListItem = getByRole(styledContainer, 'listitem'); + fireEvent.click(styledListItem); + expect(styledContainer).toMatchSnapshot(); + + // Test 7: Task selection with incoming task + const incomingTaskProps = { + ...defaultProps, + isIncomingTask: true, + onTaskSelect: mockOnTaskSelect, + }; + + jest.clearAllMocks(); + + const {container: incomingContainer} = await render(); + expect(incomingContainer).toMatchSnapshot(); + + const incomingListItem = getByRole(incomingContainer, 'listitem'); + fireEvent.click(incomingListItem); + expect(incomingContainer).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/contact-center/cc-components/tests/components/task/task/task.tsx b/packages/contact-center/cc-components/tests/components/task/task/task.tsx new file mode 100644 index 000000000..382926581 --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/task/task.tsx @@ -0,0 +1,942 @@ +import React from 'react'; +import {render, fireEvent, within, getByTestId, getByRole, queryByTestId} from '@testing-library/react'; +import '@testing-library/jest-dom'; +import Task, {TaskProps} from '../../../../src/components/task/Task'; +import {MEDIA_CHANNEL, TaskState} from '../../../../src/components/task/task.types'; +import * as taskUtils from '../../../../src/components/task/Task/task.utils'; + +Object.defineProperty(global, 'Worker', { + writable: true, + value: class MockWorker { + constructor() {} + postMessage = jest.fn(); + addEventListener = jest.fn(); + removeEventListener = jest.fn(); + terminate = jest.fn(); + }, +}); + +Object.defineProperty(global, 'URL', { + writable: true, + value: { + createObjectURL: jest.fn(() => 'blob:mock-url'), + revokeObjectURL: jest.fn(), + }, +}); + +describe('Task Component', () => { + const mockAcceptTask = jest.fn(); + const mockDeclineTask = jest.fn(); + const mockOnTaskSelect = jest.fn(); + + // Default props using TaskProps interface + const defaultProps: TaskProps = { + interactionId: 'test-interaction-123', + title: 'Test Task', + state: TaskState.CONNECTED, + startTimeStamp: 1641234567890, + ronaTimeout: undefined, + selected: false, + isIncomingTask: false, + queue: undefined, + acceptTask: undefined, + declineTask: undefined, + onTaskSelect: undefined, + acceptText: undefined, + declineText: undefined, + disableAccept: false, + styles: undefined, + mediaType: MEDIA_CHANNEL.TELEPHONY, + mediaChannel: MEDIA_CHANNEL.TELEPHONY, + }; + + const extractTaskComponentDataSpy = jest.spyOn(taskUtils, 'extractTaskComponentData'); + const getTaskListItemClassesSpy = jest.spyOn(taskUtils, 'getTaskListItemClasses'); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + extractTaskComponentDataSpy.mockRestore(); + getTaskListItemClassesSpy.mockRestore(); + }); + + describe('Rendering', () => { + it('should render media type icons and brand visuals correctly for different channels', async () => { + // Test 1: Telephony task with avatar icon + const {container: telephonyContainer} = await render(); + + // Verify telephony avatar + const telephonyAvatar = telephonyContainer.querySelector('mdc-avatar'); + expect(telephonyAvatar).toBeInTheDocument(); + expect(telephonyAvatar).toHaveClass('telephony'); + expect(telephonyAvatar).toHaveAttribute('icon-name', 'handset-filled'); + expect(telephonyAvatar).toHaveAttribute('size', '32'); + + // Verify no tooltip for telephony + const telephonyTooltip = telephonyContainer.querySelector('mdc-tooltip'); + expect(telephonyTooltip).not.toBeInTheDocument(); + + // Test 2: Social media (Facebook) task with brand visual + const facebookProps = { + ...defaultProps, + mediaType: MEDIA_CHANNEL.SOCIAL, + mediaChannel: MEDIA_CHANNEL.FACEBOOK, + title: 'Facebook Customer', + }; + + const {container: facebookContainer} = await render(); + + // Verify brand visual background + const brandVisualBackground = facebookContainer.querySelector('.brand-visual-background'); + expect(brandVisualBackground).toBeInTheDocument(); + + // Verify Facebook brand visual + const facebookBrandVisual = facebookContainer.querySelector('mdc-brandvisual'); + expect(facebookBrandVisual).toBeInTheDocument(); + expect(facebookBrandVisual).toHaveClass('facebook'); + expect(facebookBrandVisual).toHaveAttribute('name', 'social-facebook-color'); + + // Verify no avatar for social media + const socialAvatar = facebookContainer.querySelector('mdc-avatar'); + expect(socialAvatar).not.toBeInTheDocument(); + + // Test 3: Email task with avatar + const emailProps = { + ...defaultProps, + mediaType: MEDIA_CHANNEL.EMAIL, + mediaChannel: MEDIA_CHANNEL.EMAIL, + title: 'Email Subject Line', + state: TaskState.ACTIVE, + }; + + const {container: emailContainer} = await render(); + + // Verify email avatar + const emailAvatar = emailContainer.querySelector('mdc-avatar'); + expect(emailAvatar).toBeInTheDocument(); + expect(emailAvatar).toHaveClass('email'); + expect(emailAvatar).toHaveAttribute('icon-name', 'email-filled'); + expect(emailAvatar).toHaveAttribute('size', '32'); + + // Test 4: WhatsApp incoming task with brand visual + const whatsappProps = { + ...defaultProps, + mediaType: MEDIA_CHANNEL.SOCIAL, + mediaChannel: MEDIA_CHANNEL.WHATSAPP, + title: 'WhatsApp Contact', + isIncomingTask: true, + }; + + const {container: whatsappContainer} = await render(); + + // Verify WhatsApp brand visual + const whatsappBrandVisual = whatsappContainer.querySelector('mdc-brandvisual'); + expect(whatsappBrandVisual).toBeInTheDocument(); + expect(whatsappBrandVisual).toHaveClass('whatsapp'); + expect(whatsappBrandVisual).toHaveAttribute('name', 'social-whatsapp-color'); + + // Test 5: Chat task with avatar + const chatProps = { + ...defaultProps, + mediaType: MEDIA_CHANNEL.CHAT, + mediaChannel: MEDIA_CHANNEL.CHAT, + title: 'Chat Customer Name', + isIncomingTask: false, + }; + + const {container: chatContainer} = await render(); + + // Verify chat avatar + const chatAvatar = chatContainer.querySelector('mdc-avatar'); + expect(chatAvatar).toBeInTheDocument(); + expect(chatAvatar).toHaveClass('chat'); + expect(chatAvatar).toHaveAttribute('icon-name', 'chat-filled'); + expect(chatAvatar).toHaveAttribute('size', '32'); + }); + + it('should render titles and tooltips correctly based on media type and selection state', async () => { + // Test 1: Task without title should not render any title elements + const noTitleProps = { + ...defaultProps, + title: undefined, + }; + + const {container: noTitleContainer} = await render(); + + // Verify no title elements are rendered when title is undefined + const noTitle = noTitleContainer.querySelector('.task-title'); + const noDigitalTitle = noTitleContainer.querySelector('.task-digital-title'); + const noIncomingTitle = noTitleContainer.querySelector('.incoming-digital-task-title'); + + expect(noTitle).not.toBeInTheDocument(); + expect(noDigitalTitle).not.toBeInTheDocument(); + expect(noIncomingTitle).not.toBeInTheDocument(); + + // Test 2: Voice media (Telephony) should render simple title without tooltip + const telephonyWithTitleProps = { + ...defaultProps, + mediaType: MEDIA_CHANNEL.TELEPHONY, + mediaChannel: MEDIA_CHANNEL.TELEPHONY, + title: 'John Doe - Customer Call', + }; + + const {container: telephonyContainer} = await render(); + + // Verify regular title is rendered for voice media without extra UI elements + const voiceTitle = telephonyContainer.querySelector('.task-title'); + expect(voiceTitle).toBeInTheDocument(); + expect(voiceTitle).toHaveTextContent('John Doe - Customer Call'); + expect(voiceTitle).not.toHaveAttribute('id'); // No tooltip trigger needed for voice + + // Verify tooltip is not rendered for voice media to keep UI clean + const noTooltip = telephonyContainer.querySelector('mdc-tooltip'); + expect(noTooltip).not.toBeInTheDocument(); + + // Test 3: Digital media (Chat) should render title with tooltip for user assistance + const chatWithTitleProps = { + ...defaultProps, + mediaType: MEDIA_CHANNEL.CHAT, + mediaChannel: MEDIA_CHANNEL.CHAT, + title: 'Live Chat with Customer Support', + isIncomingTask: false, + }; + + const {container: chatContainer} = await render(); + + // Verify digital title is rendered with proper accessibility attributes + const digitalTitle = chatContainer.querySelector('.task-digital-title'); + expect(digitalTitle).toBeInTheDocument(); + expect(digitalTitle).toHaveTextContent('Live Chat with Customer Support'); + expect(digitalTitle).toHaveAttribute('id', 'tooltip-trigger-test-interaction-123'); + + // Verify tooltip is rendered for non-voice media to show full title on hover + const tooltip = chatContainer.querySelector('mdc-tooltip'); + expect(tooltip).toBeInTheDocument(); + expect(tooltip).toBeInTheDocument(); + expect(tooltip).toHaveClass('task-tooltip'); + expect(tooltip).toHaveAttribute('id', 'tooltip-test-interaction-123'); + expect(tooltip).toHaveAttribute('triggerid', 'tooltip-trigger-test-interaction-123'); + expect(tooltip).toHaveTextContent('Live Chat with Customer Support'); + expect(tooltip).toHaveAttribute('append-to', ''); + expect(tooltip).toHaveAttribute('color', 'contrast'); + expect(tooltip).toHaveAttribute('delay', '0,0'); + expect(tooltip).toHaveAttribute('disable-aria-expanded', ''); + expect(tooltip).toHaveAttribute('flip', ''); + expect(tooltip).toHaveAttribute('hide-on-blur', ''); + expect(tooltip).toHaveAttribute('hide-on-escape', ''); + expect(tooltip).toHaveAttribute('offset', '4'); + expect(tooltip).toHaveAttribute('placement', 'top-start'); + expect(tooltip).toHaveAttribute('role', 'tooltip'); + expect(tooltip).toHaveAttribute('style', 'z-index: 1000;'); + expect(tooltip).toHaveAttribute('tooltip-type', 'description'); + expect(tooltip).toHaveAttribute('trigger', 'mouseenter focusin'); + expect(tooltip).toHaveAttribute('z-index', '1000'); + + // Test 4: Selected digital task should display bold title for visual emphasis + const selectedEmailProps = { + ...defaultProps, + mediaType: MEDIA_CHANNEL.EMAIL, + mediaChannel: MEDIA_CHANNEL.EMAIL, + title: 'Urgent: Customer Complaint Email', + selected: true, + }; + + const {container: selectedContainer} = await render(); + + // Verify selected task uses bold typography to indicate active state + const selectedTitle = selectedContainer.querySelector('.task-digital-title'); + expect(selectedTitle).toBeInTheDocument(); + expect(selectedTitle).toHaveAttribute('type', 'body-large-bold'); + expect(selectedTitle).toHaveAttribute('id', 'tooltip-trigger-test-interaction-123'); + expect(selectedTitle).toHaveAttribute('tagname', 'span'); + + // Test 5: Unselected digital task should display medium weight title + const unselectedEmailProps = { + ...defaultProps, + mediaType: MEDIA_CHANNEL.EMAIL, + mediaChannel: MEDIA_CHANNEL.EMAIL, + title: 'Standard Customer Inquiry Email', + selected: false, + }; + + const {container: unselectedContainer} = await render(); + // Verify unselected task uses medium typography for standard appearance + const unselectedTitle = unselectedContainer.querySelector('.task-digital-title'); + expect(unselectedTitle).toBeInTheDocument(); + expect(unselectedTitle).toHaveAttribute('type', 'body-large-medium'); + expect(unselectedTitle).toHaveAttribute('id', 'tooltip-trigger-test-interaction-123'); + expect(unselectedTitle).toHaveAttribute('tagname', 'span'); + + // Test 6: Incoming digital task should use specialized styling and tooltip + const incomingFacebookProps = { + ...defaultProps, + mediaType: MEDIA_CHANNEL.SOCIAL, + mediaChannel: MEDIA_CHANNEL.FACEBOOK, + title: 'New Facebook Message from Customer', + isIncomingTask: true, + }; + + const {container: incomingContainer} = await render(); + + // Verify incoming task uses special CSS class for visual distinction + const incomingTitle = incomingContainer.querySelector('.incoming-digital-task-title'); + expect(incomingTitle).toBeInTheDocument(); + expect(incomingTitle).toHaveTextContent('New Facebook Message from Customer'); + expect(incomingTitle).toHaveAttribute('id', 'tooltip-trigger-test-interaction-123'); + + // Verify tooltip is available for incoming tasks to show full context + const incomingTooltip = incomingContainer.querySelector('mdc-tooltip'); + expect(incomingTooltip).toBeInTheDocument(); + expect(incomingTooltip).toHaveTextContent('New Facebook Message from Customer'); + + // Test 7: Selected voice task should affect title weight + const selectedVoiceProps = { + ...defaultProps, + mediaType: MEDIA_CHANNEL.TELEPHONY, + mediaChannel: MEDIA_CHANNEL.TELEPHONY, + title: 'Important Customer Call', + selected: true, + }; + + const {container: selectedVoiceContainer} = await render(); + + // Verify selected voice task uses bold typography + const selectedVoiceTitle = selectedVoiceContainer.querySelector('.task-title'); + expect(selectedVoiceTitle).toHaveAttribute('type', 'body-large-bold'); + + // Test 8: Selected task with custom styles + const selectedWithStylesProps = { + ...defaultProps, + selected: true, + styles: 'custom-task-style task-highlight', + }; + + const {container: styledContainer} = await render(); + + // Verify selected task CSS classes + const selectedListItem = styledContainer.querySelector('[role="listitem"]'); + expect(selectedListItem).toHaveClass( + 'task-list-item', + 'task-list-item--selected', + 'custom-task-style', + 'task-highlight' + ); + + // Test 9: Empty string title should behave like undefined title + const emptyTitleProps = { + ...defaultProps, + title: '', + }; + + const {container: emptyTitleContainer} = await render(); + + // Verify empty title is treated as no title + const noEmptyTitle = emptyTitleContainer.querySelector('.task-title'); + const noEmptyDigitalTitle = emptyTitleContainer.querySelector('.task-digital-title'); + + expect(noEmptyTitle).not.toBeInTheDocument(); + expect(noEmptyDigitalTitle).not.toBeInTheDocument(); + }); + + it('should render state, queue, and time information correctly for different task scenarios', async () => { + // Test 1: Active task with state and handle time + const activeProps = { + ...defaultProps, + state: TaskState.CONNECTED, + startTimeStamp: 1641234567890, + isIncomingTask: false, + }; + + const {container: activeContainer} = await render(); + + // Verify state display + const stateText = getByTestId(activeContainer, 'test-interaction-123-state'); + expect(stateText).toHaveClass('md-text-wrapper', 'task-text'); + expect(stateText).toHaveAttribute('tagname', 'span'); + expect(stateText).toHaveAttribute('type', 'body-midsize-regular'); + expect(stateText).toHaveTextContent('Connected'); + + // Verify handle time display + const handleTime = getByTestId(activeContainer, 'test-interaction-123-handle-time'); + expect(handleTime).toHaveClass('md-text-wrapper', 'task-text'); + expect(handleTime).toHaveTextContent('Handle Time:'); + + const handleTimeElement = within(handleTime).getByRole('time'); + expect(handleTimeElement).toHaveClass('task-text', 'task-text--secondary'); + expect(handleTimeElement).toHaveAttribute('datetime', '00:00'); + expect(handleTimeElement).toHaveTextContent('00:00'); + + // Verify no queue display for active task using queryByTestId + const noQueue = queryByTestId(activeContainer, 'test-interaction-123-queue'); + expect(noQueue).not.toBeInTheDocument(); + + // Verify no time left display using queryByTestId + const noTimeLeft = queryByTestId(activeContainer, 'test-interaction-123-time-left'); + expect(noTimeLeft).not.toBeInTheDocument(); + + // Test 2: Incoming task with queue and time left (RONA timeout) + const incomingProps = { + ...defaultProps, + isIncomingTask: true, + queue: 'Support Team', + ronaTimeout: 30, + state: TaskState.NEW, + startTimeStamp: 1641234567890, + }; + + const {container: incomingContainer} = await render(); + + // Verify queue display + const queueText = getByTestId(incomingContainer, 'test-interaction-123-queue'); + expect(queueText).toHaveClass('md-text-wrapper', 'task-text'); + expect(queueText).toHaveAttribute('tagname', 'span'); + expect(queueText).toHaveAttribute('type', 'body-midsize-regular'); + expect(queueText).toHaveTextContent('Support Team'); + + // Verify time left display + const timeLeft = getByTestId(incomingContainer, 'test-interaction-123-time-left'); + expect(timeLeft).toHaveClass('md-text-wrapper', 'task-text'); + expect(timeLeft).toHaveTextContent('Time Left:'); + + const timeLeftElement = within(timeLeft).getByRole('time'); + expect(timeLeftElement).toHaveClass('task-text', 'task-text--secondary'); + expect(timeLeftElement).toHaveAttribute('datetime', '00:00'); + + // Verify no state display for incoming task + const noState = queryByTestId(incomingContainer, 'test-interaction-123-state'); + expect(noState).not.toBeInTheDocument(); + + // Verify no handle time display when RONA timeout exists + const noHandleTime = queryByTestId(incomingContainer, 'test-interaction-123-handle-time'); + expect(noHandleTime).not.toBeInTheDocument(); + + // Test 3: WhatsApp incoming task with queue and handle time (no RONA) + const whatsappProps = { + ...defaultProps, + mediaType: MEDIA_CHANNEL.SOCIAL, + mediaChannel: MEDIA_CHANNEL.WHATSAPP, + isIncomingTask: true, + queue: 'WhatsApp Support', + ronaTimeout: undefined, // No RONA timeout + startTimeStamp: 1641234567890, + }; + + const {container: whatsappContainer} = await render(); + + // Verify queue display + const whatsappQueue = getByTestId(whatsappContainer, 'test-interaction-123-queue'); + expect(whatsappQueue).toHaveTextContent('WhatsApp Support'); + + // Verify handle time display (since no RONA timeout) + const whatsappHandleTime = getByTestId(whatsappContainer, 'test-interaction-123-handle-time'); + expect(whatsappHandleTime).toHaveTextContent('Handle Time:'); + }); + + it('should render accept and decline buttons with correct states and properties', async () => { + // Test 1: Task with both accept and decline buttons + const bothButtonsProps = { + ...defaultProps, + acceptText: 'Accept', + declineText: 'Decline', + acceptTask: mockAcceptTask, + declineTask: mockDeclineTask, + }; + + const {container: bothButtonsContainer} = await render(); + + // Verify button container + const buttonContainer = bothButtonsContainer.querySelector('.task-button-container'); + expect(buttonContainer).toBeInTheDocument(); + + // Verify accept button + const acceptButton = getByTestId(bothButtonsContainer, 'task:accept-button'); + expect(acceptButton).toHaveClass('md-button-pill-wrapper', 'md-button-simple-wrapper'); + expect(acceptButton).toHaveAttribute('data-color', 'join'); + expect(acceptButton).toHaveAttribute('data-disabled', 'false'); + expect(acceptButton).toHaveAttribute('data-size', '40'); + expect(acceptButton).toHaveAttribute('type', 'button'); + expect(acceptButton).toHaveAttribute('tabindex', '-1'); + expect(acceptButton).not.toHaveAttribute('disabled'); + + const acceptButtonSpan = within(acceptButton).getByText('Accept'); + expect(acceptButtonSpan).toBeInTheDocument(); + + // Verify decline button + const declineButton = getByTestId(bothButtonsContainer, 'task:decline-button'); + expect(declineButton).toHaveClass('md-button-pill-wrapper', 'md-button-simple-wrapper'); + expect(declineButton).toHaveAttribute('data-color', 'cancel'); + expect(declineButton).toHaveAttribute('data-disabled', 'false'); + expect(declineButton).toHaveAttribute('data-size', '40'); + expect(declineButton).toHaveAttribute('type', 'button'); + expect(declineButton).toHaveAttribute('tabindex', '-1'); + + const declineButtonSpan = within(declineButton).getByText('Decline'); + expect(declineButtonSpan).toBeInTheDocument(); + + // Test 2: Task with disabled accept button + const disabledProps = { + ...defaultProps, + acceptText: 'Ringing...', + disableAccept: true, + acceptTask: mockAcceptTask, + }; + + const {container: disabledContainer} = await render(); + + // Verify disabled accept button + const disabledAcceptButton = getByTestId(disabledContainer, 'task:accept-button'); + expect(disabledAcceptButton).toHaveAttribute('data-color', 'join'); + expect(disabledAcceptButton).toHaveAttribute('data-disabled', 'true'); + expect(disabledAcceptButton).toHaveAttribute('disabled', ''); + + const disabledButtonSpan = within(disabledAcceptButton).getByText('Ringing...'); + expect(disabledButtonSpan).toBeInTheDocument(); + + // Verify no decline button when not provided + const noDeclineButton = queryByTestId(disabledContainer, 'task:decline-button'); + expect(noDeclineButton).not.toBeInTheDocument(); + + // Test 3: Verify button container data attributes + const buttonDataProps = { + ...defaultProps, + acceptText: 'Accept Call', + acceptTask: mockAcceptTask, + }; + + const {container: buttonDataContainer} = await render(); + + // Use getByTestId for type safety + const buttonWithData = getByTestId(buttonDataContainer, 'task:accept-button'); + expect(buttonWithData).toHaveAttribute('data-disabled-outline', 'false'); + expect(buttonWithData).toHaveAttribute('data-ghost', 'false'); + expect(buttonWithData).toHaveAttribute('data-grown', 'false'); + expect(buttonWithData).toHaveAttribute('data-inverted', 'false'); + expect(buttonWithData).toHaveAttribute('data-outline', 'false'); + expect(buttonWithData).toHaveAttribute('data-shallow-disabled', 'false'); + + // Verify button text + const buttonDataSpan = within(buttonWithData).getByText('Accept Call'); + expect(buttonDataSpan).toBeInTheDocument(); + }); + + it('should handle default prop values and edge cases correctly', async () => { + // Test 1: Component with minimal props (tests default values) + const minimalProps = { + interactionId: 'test-minimal-123', + title: 'Minimal Task', + mediaType: MEDIA_CHANNEL.TELEPHONY, + mediaChannel: MEDIA_CHANNEL.TELEPHONY, + // Intentionally omitting: selected, isIncomingTask, disableAccept + // to test default values + }; + + const {container: minimalContainer} = await render(); + + // Verify default selected = false is applied + const unselectedListItem = getByRole(minimalContainer, 'listitem'); + expect(unselectedListItem).toHaveClass('task-list-item'); + expect(unselectedListItem).not.toHaveClass('task-list-item--selected'); + + // Verify default isIncomingTask = false behavior (shows state, not queue) + const noQueue = queryByTestId(minimalContainer, 'test-minimal-123-queue'); + expect(noQueue).not.toBeInTheDocument(); + + // Test 2: Task with accept button using default disableAccept = false + const enabledAcceptProps = { + interactionId: 'test-enabled-456', + title: 'Enabled Accept Task', + mediaType: MEDIA_CHANNEL.TELEPHONY, + mediaChannel: MEDIA_CHANNEL.TELEPHONY, + acceptText: 'Accept Now', + acceptTask: mockAcceptTask, + // Intentionally omitting disableAccept to test default false value + }; + + const {container: enabledContainer} = await render(); + + // Verify default disableAccept = false is applied + const enabledAcceptButton = getByTestId(enabledContainer, 'task:accept-button'); + expect(enabledAcceptButton).toHaveAttribute('data-disabled', 'false'); + expect(enabledAcceptButton).not.toHaveAttribute('disabled'); + + // Verify button is clickable with default enabled state + fireEvent.click(enabledAcceptButton); + expect(mockAcceptTask).toHaveBeenCalledTimes(1); + + // Test 3: Explicitly test selected = false vs undefined + const explicitlyUnselectedProps = { + ...defaultProps, + selected: false, // Explicitly set to false + title: 'Explicitly Unselected Task', + }; + + const {container: explicitContainer} = await render(); + + const explicitListItem = getByRole(explicitContainer, 'listitem'); + expect(explicitListItem).not.toHaveClass('task-list-item--selected'); + + // Verify title uses medium weight for unselected state + const explicitTitle = explicitContainer.querySelector('.task-title'); + expect(explicitTitle).toHaveAttribute('type', 'body-large-medium'); + + // Test 4: Explicitly test isIncomingTask = false vs undefined + const explicitlyNotIncomingProps = { + ...defaultProps, + isIncomingTask: false, // Explicitly set to false + state: TaskState.CONNECTED, + startTimeStamp: 1641234567890, + queue: 'Should Not Show Queue', // Queue provided but shouldn't show + }; + + const {container: notIncomingContainer} = await render(); + + // Verify explicit isIncomingTask = false shows state, not queue + const stateElement = getByTestId(notIncomingContainer, 'test-interaction-123-state'); + expect(stateElement).toBeInTheDocument(); + expect(stateElement).toHaveTextContent('Connected'); + + const noQueueElement = queryByTestId(notIncomingContainer, 'test-interaction-123-queue'); + expect(noQueueElement).not.toBeInTheDocument(); + + // Test 5: Explicitly test disableAccept = false vs undefined + const explicitlyEnabledProps = { + ...defaultProps, + acceptText: 'Join Meeting', + acceptTask: mockAcceptTask, + disableAccept: false, // Explicitly set to false + }; + + jest.clearAllMocks(); + + const {container: explicitEnabledContainer} = await render(); + + const explicitEnabledButton = getByTestId(explicitEnabledContainer, 'task:accept-button'); + expect(explicitEnabledButton).toHaveAttribute('data-disabled', 'false'); + expect(explicitEnabledButton).not.toHaveAttribute('disabled'); + + // Verify explicit false allows button interaction + fireEvent.click(explicitEnabledButton); + expect(mockAcceptTask).toHaveBeenCalledTimes(1); + + // Test 6: Component without any optional props (pure minimal) + const pureMinimalProps = { + interactionId: 'pure-minimal-789', + // Only required props - everything else should use defaults + }; + + const {container: pureContainer} = await render(); + + // Verify component renders with all defaults + const pureListItem = getByRole(pureContainer, 'listitem'); + expect(pureListItem).toHaveClass('task-list-item'); + expect(pureListItem).not.toHaveClass('task-list-item--selected'); // selected = false default + expect(pureListItem).toHaveAttribute('id', 'pure-minimal-789'); + + // Verify empty button container (no accept/decline text provided) + const emptyButtonContainer = pureContainer.querySelector('.task-button-container'); + expect(emptyButtonContainer).toBeInTheDocument(); + expect(emptyButtonContainer).toBeEmptyDOMElement(); + + // Test 7: Edge case - all boolean props set to their default values explicitly + const allDefaultsExplicitProps = { + interactionId: 'all-defaults-explicit', + title: 'All Defaults Explicit', + selected: false, // Explicit default + isIncomingTask: false, // Explicit default + disableAccept: false, // Explicit default + acceptText: 'Accept', + acceptTask: mockAcceptTask, + mediaType: MEDIA_CHANNEL.TELEPHONY, + mediaChannel: MEDIA_CHANNEL.TELEPHONY, + }; + + jest.clearAllMocks(); + + const {container: allDefaultsContainer} = await render(); + + // Verify all explicit defaults work correctly + const allDefaultsListItem = getByRole(allDefaultsContainer, 'listitem'); + expect(allDefaultsListItem).not.toHaveClass('task-list-item--selected'); + + const allDefaultsButton = getByTestId(allDefaultsContainer, 'task:accept-button'); + expect(allDefaultsButton).not.toHaveAttribute('disabled'); + + fireEvent.click(allDefaultsButton); + expect(mockAcceptTask).toHaveBeenCalledTimes(1); + }); + }); + + describe('Actions', () => { + it('should handle accept task action correctly', async () => { + // Accept button click calls acceptTask callback + const acceptProps = { + ...defaultProps, + acceptText: 'Accept', + acceptTask: mockAcceptTask, + }; + + const {container: acceptContainer} = await render(); + + const acceptButton = getByTestId(acceptContainer, 'task:accept-button'); + expect(acceptButton).not.toHaveAttribute('disabled'); + + fireEvent.click(acceptButton); + + expect(mockAcceptTask).toHaveBeenCalledTimes(1); + expect(mockDeclineTask).not.toHaveBeenCalled(); + expect(mockOnTaskSelect).not.toHaveBeenCalled(); + + // Verify accept button text content + const acceptButtonSpan = within(acceptButton).getByText('Accept'); + expect(acceptButtonSpan).toBeInTheDocument(); + + // Disabled accept button does not trigger callback + const disabledAcceptProps = { + ...defaultProps, + acceptText: 'Ringing...', + disableAccept: true, + acceptTask: mockAcceptTask, + }; + + jest.clearAllMocks(); + + const {container: disabledContainer} = await render(); + + const disabledAcceptButton = getByTestId(disabledContainer, 'task:accept-button'); + expect(disabledAcceptButton).toHaveAttribute('disabled', ''); + expect(disabledAcceptButton).toHaveAttribute('data-disabled', 'true'); + + fireEvent.click(disabledAcceptButton); + + expect(mockAcceptTask).not.toHaveBeenCalled(); + expect(mockDeclineTask).not.toHaveBeenCalled(); + expect(mockOnTaskSelect).not.toHaveBeenCalled(); + + // Verify disabled button text content + const disabledButtonSpan = within(disabledAcceptButton).getByText('Ringing...'); + expect(disabledButtonSpan).toBeInTheDocument(); + + // Accept action with both buttons - only accept should be called + const bothButtonsProps = { + ...defaultProps, + acceptText: 'Accept Call', + declineText: 'Decline', + acceptTask: mockAcceptTask, + declineTask: mockDeclineTask, + }; + + jest.clearAllMocks(); + + const {container: bothContainer} = await render(); + + const acceptButtonBoth = getByTestId(bothContainer, 'task:accept-button'); + + fireEvent.click(acceptButtonBoth); + + expect(mockAcceptTask).toHaveBeenCalledTimes(1); + expect(mockDeclineTask).not.toHaveBeenCalled(); + expect(mockOnTaskSelect).not.toHaveBeenCalled(); + }); + + it('should handle decline task action correctly', async () => { + // Test 1: Decline button click calls declineTask callback + const declineProps = { + ...defaultProps, + declineText: 'Decline', + declineTask: mockDeclineTask, + }; + + const {container: declineContainer} = await render(); + + const declineButton = getByTestId(declineContainer, 'task:decline-button'); + expect(declineButton).not.toHaveAttribute('disabled'); + expect(declineButton).toHaveAttribute('data-color', 'cancel'); + + fireEvent.click(declineButton); + + expect(mockDeclineTask).toHaveBeenCalledTimes(1); + expect(mockAcceptTask).not.toHaveBeenCalled(); + expect(mockOnTaskSelect).not.toHaveBeenCalled(); + + // Verify decline button text content + const declineButtonSpan = within(declineButton).getByText('Decline'); + expect(declineButtonSpan).toBeInTheDocument(); + + // Test 2: Decline action with both buttons - only decline should be called + const bothButtonsProps = { + ...defaultProps, + acceptText: 'Accept', + declineText: 'Decline Call', + acceptTask: mockAcceptTask, + declineTask: mockDeclineTask, + }; + + jest.clearAllMocks(); + + const {container: bothContainer} = await render(); + + const declineButtonBoth = getByTestId(bothContainer, 'task:decline-button'); + + fireEvent.click(declineButtonBoth); + + expect(mockDeclineTask).toHaveBeenCalledTimes(1); + expect(mockAcceptTask).not.toHaveBeenCalled(); + expect(mockOnTaskSelect).not.toHaveBeenCalled(); + + // Test 3: Multiple decline clicks should call callback multiple times + jest.clearAllMocks(); + + fireEvent.click(declineButtonBoth); + fireEvent.click(declineButtonBoth); + + expect(mockDeclineTask).toHaveBeenCalledTimes(2); + expect(mockAcceptTask).not.toHaveBeenCalled(); + }); + + it('should handle task selection action correctly', async () => { + // Test 1: Task list item click calls onTaskSelect callback + const selectProps = { + ...defaultProps, + onTaskSelect: mockOnTaskSelect, + }; + + const {container: selectContainer} = await render(); + + const listItem = getByRole(selectContainer, 'listitem'); + expect(listItem).toHaveAttribute('id', 'test-interaction-123'); + expect(listItem).toHaveClass('task-list-item'); + + fireEvent.click(listItem); + + expect(mockOnTaskSelect).toHaveBeenCalledTimes(1); + expect(mockAcceptTask).not.toHaveBeenCalled(); + expect(mockDeclineTask).not.toHaveBeenCalled(); + + // Test 2: Task selection with buttons - selection should work independently + const selectWithButtonsProps = { + ...defaultProps, + onTaskSelect: mockOnTaskSelect, + acceptText: 'Accept', + declineText: 'Decline', + acceptTask: mockAcceptTask, + declineTask: mockDeclineTask, + }; + + jest.clearAllMocks(); + + const {container: selectButtonsContainer} = await render(); + + const listItemWithButtons = getByRole(selectButtonsContainer, 'listitem'); + + // Click on list item (not buttons) + fireEvent.click(listItemWithButtons); + + expect(mockOnTaskSelect).toHaveBeenCalledTimes(1); + expect(mockAcceptTask).not.toHaveBeenCalled(); + expect(mockDeclineTask).not.toHaveBeenCalled(); + + // Test accept button with fresh render + jest.clearAllMocks(); + const {container: acceptTestContainer} = await render(); + const acceptTestButton = getByTestId(acceptTestContainer, 'task:accept-button'); + + fireEvent.click(acceptTestButton); + expect(mockAcceptTask).toHaveBeenCalledTimes(1); + expect(mockOnTaskSelect).not.toHaveBeenCalled(); + + // Test decline button with fresh render + jest.clearAllMocks(); + const {container: declineTestContainer} = await render(); + const declineTestButton = getByTestId(declineTestContainer, 'task:decline-button'); + + fireEvent.click(declineTestButton); + expect(mockDeclineTask).toHaveBeenCalledTimes(1); + expect(mockOnTaskSelect).not.toHaveBeenCalled(); + + // Test 3: Selected task maintains selection behavior + const selectedTaskProps = { + ...defaultProps, + selected: true, + onTaskSelect: mockOnTaskSelect, + }; + + jest.clearAllMocks(); + + const {container: selectedContainer} = await render(); + + const selectedListItem = getByRole(selectedContainer, 'listitem'); + expect(selectedListItem).toHaveClass('task-list-item', 'task-list-item--selected'); + + fireEvent.click(selectedListItem); + + expect(mockOnTaskSelect).toHaveBeenCalledTimes(1); + + jest.clearAllMocks(); + + // Test 5: Task selection with different media types + const digitalTaskSelectProps = { + ...defaultProps, + mediaType: MEDIA_CHANNEL.CHAT, + mediaChannel: MEDIA_CHANNEL.CHAT, + title: 'Chat Customer', + onTaskSelect: mockOnTaskSelect, + }; + + jest.clearAllMocks(); + + const {container: digitalSelectContainer} = await render(); + + const digitalListItem = getByRole(digitalSelectContainer, 'listitem'); + fireEvent.click(digitalListItem); + + expect(mockOnTaskSelect).toHaveBeenCalledTimes(1); + + // Verify the task still renders with chat avatar + const chatAvatar = digitalSelectContainer.querySelector('mdc-avatar.chat'); + expect(chatAvatar).toBeInTheDocument(); + + // Test 6: Task selection with custom styles + const styledTaskProps = { + ...defaultProps, + onTaskSelect: mockOnTaskSelect, + styles: 'custom-task-style', + selected: false, + }; + + jest.clearAllMocks(); + + const {container: styledContainer} = await render(); + + const styledListItem = getByRole(styledContainer, 'listitem'); + expect(styledListItem).toHaveClass('task-list-item', 'custom-task-style'); + expect(styledListItem).not.toHaveClass('task-list-item--selected'); + + fireEvent.click(styledListItem); + + expect(mockOnTaskSelect).toHaveBeenCalledTimes(1); + + // Test 7: Task selection with incoming task + const incomingTaskProps = { + ...defaultProps, + isIncomingTask: true, + onTaskSelect: mockOnTaskSelect, + }; + + jest.clearAllMocks(); + + const {container: incomingContainer} = await render(); + + const incomingListItem = getByRole(incomingContainer, 'listitem'); + fireEvent.click(incomingListItem); + + expect(mockOnTaskSelect).toHaveBeenCalledTimes(1); + + // Test 10: List item accessibility attributes + expect(listItem).toHaveAttribute('role', 'listitem'); + expect(listItem).toHaveAttribute('tabindex', '0'); + expect(listItem).toHaveAttribute('data-interactive', 'true'); + }); + }); +}); diff --git a/packages/contact-center/cc-components/tests/components/task/task/task.utils.tsx b/packages/contact-center/cc-components/tests/components/task/task/task.utils.tsx new file mode 100644 index 000000000..60768634b --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/task/task.utils.tsx @@ -0,0 +1,353 @@ +import { + capitalizeFirstWord, + getTitleClassName, + createTooltipIds, + shouldShowState, + shouldShowQueue, + shouldShowHandleTime, + shouldShowTimeLeft, + getTaskListItemClasses, + extractTaskComponentData, +} from '../../../../src/components/task/Task/task.utils'; +import {MEDIA_CHANNEL} from '../../../../src/components/task/task.types'; +import * as utils from '../../../../src/utils'; + +describe('task.utils', () => { + // Spy on the getMediaTypeInfo function + let mockGetMediaTypeInfo: jest.SpyInstance; + + beforeAll(() => { + mockGetMediaTypeInfo = jest.spyOn(utils, 'getMediaTypeInfo'); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + mockGetMediaTypeInfo.mockRestore(); + }); + + describe('capitalizeFirstWord', () => { + it('should capitalize the first letter of a word', () => { + expect(capitalizeFirstWord('hello')).toBe('Hello'); + expect(capitalizeFirstWord('world')).toBe('World'); + }); + + it('should handle edge cases', () => { + // Empty strings + expect(capitalizeFirstWord('')).toBe(''); + // Single character + expect(capitalizeFirstWord('a')).toBe('A'); + // Already capitalized + expect(capitalizeFirstWord('Hello')).toBe('Hello'); + // Leading whitespace + expect(capitalizeFirstWord(' hello')).toBe('Hello'); + // Numbers and special characters + expect(capitalizeFirstWord('123abc')).toBe('123abc'); + expect(capitalizeFirstWord('!hello')).toBe('!hello'); + expect(capitalizeFirstWord(' hello world')).toBe('Hello world'); + }); + }); + + describe('getTitleClassName', () => { + it('should return correct class names for different task types', () => { + expect(getTitleClassName(true, true)).toBe('incoming-digital-task-title'); + expect(getTitleClassName(true, false)).toBe('task-digital-title'); + expect(getTitleClassName(false, true)).toBe('task-title'); + expect(getTitleClassName(false, false)).toBe('task-title'); + }); + }); + + describe('createTooltipIds', () => { + it('should create tooltip IDs with interaction ID', () => { + const result = createTooltipIds('test-123'); + expect(result).toEqual({ + tooltipTriggerId: 'tooltip-trigger-test-123', + tooltipId: 'tooltip-test-123', + }); + }); + }); + + describe('shouldShowState', () => { + it('should determine state visibility based on business rules', () => { + // Show state when active task (not incoming) + expect(shouldShowState('connected', false)).toBe(true); + // Don't show state for incoming tasks + expect(shouldShowState('new', true)).toBe(false); + // Don't show when no state + expect(shouldShowState(undefined, false)).toBe(false); + expect(shouldShowState('', false)).toBe(false); + }); + }); + + describe('shouldShowQueue', () => { + it('should determine queue visibility based on business rules', () => { + // Show queue only for incoming tasks + expect(shouldShowQueue('Support Team', true)).toBe(true); + expect(shouldShowQueue('Sales Team', true)).toBe(true); + // Don't show queue for active tasks + expect(shouldShowQueue('Support Team', false)).toBe(false); + // Don't show when no queue + expect(shouldShowQueue(undefined, true)).toBe(false); + expect(shouldShowQueue('', true)).toBe(false); + }); + }); + + describe('shouldShowHandleTime', () => { + it('should determine handle time visibility based on business rules', () => { + // Don't show without startTimeStamp + expect(shouldShowHandleTime(false, undefined, undefined)).toBe(false); + expect(shouldShowHandleTime(true, 30, undefined)).toBe(false); + // Show for incoming task without RONA timeout + expect(shouldShowHandleTime(true, undefined, 1641234567890)).toBe(true); + expect(shouldShowHandleTime(true, 0, 1641234567890)).toBe(true); + // Don't show for incoming task with RONA timeout + expect(shouldShowHandleTime(true, 30, 1641234567890)).toBe(false); + // Always show for active tasks with timestamp + expect(shouldShowHandleTime(false, undefined, 1641234567890)).toBe(true); + expect(shouldShowHandleTime(false, 30, 1641234567890)).toBe(true); + }); + }); + + describe('shouldShowTimeLeft', () => { + it('should determine time left visibility based on business rules', () => { + // Show only for incoming tasks with RONA timeout + expect(shouldShowTimeLeft(true, 30)).toBe(true); + expect(shouldShowTimeLeft(true, 60)).toBe(true); + // Don't show for active tasks + expect(shouldShowTimeLeft(false, 30)).toBe(false); + // Don't show without timeout + expect(shouldShowTimeLeft(true, undefined)).toBe(false); + expect(shouldShowTimeLeft(true, 0)).toBe(false); + // Handle edge cases + expect(shouldShowTimeLeft(undefined, 30)).toBe(false); + expect(shouldShowTimeLeft(undefined, undefined)).toBe(false); + }); + }); + + describe('getTaskListItemClasses', () => { + it('should build CSS classes correctly', () => { + expect(getTaskListItemClasses()).toBe('task-list-item'); + expect(getTaskListItemClasses(true)).toBe('task-list-item task-list-item--selected'); + expect(getTaskListItemClasses(false)).toBe('task-list-item'); + expect(getTaskListItemClasses(false, 'custom-class')).toBe('task-list-item custom-class'); + expect(getTaskListItemClasses(true, 'custom-class another-class')).toBe( + 'task-list-item task-list-item--selected custom-class another-class' + ); + }); + }); + + describe('extractTaskComponentData', () => { + beforeEach(() => { + // Ensure mock is cleared and ready for each test + mockGetMediaTypeInfo.mockClear(); + }); + + it('should extract data for telephony task correctly', () => { + // Mock telephony media type info to match actual implementation + mockGetMediaTypeInfo.mockReturnValue({ + labelName: 'Call', + iconName: 'handset-filled', + className: 'telephony', + isBrandVisual: false, + }); + + const input = { + mediaType: MEDIA_CHANNEL.TELEPHONY, + mediaChannel: MEDIA_CHANNEL.TELEPHONY, + isIncomingTask: false, + interactionId: 'test-123', + state: 'active', + queue: 'support team', + ronaTimeout: undefined, + startTimeStamp: 1641234567890, + }; + + const result = extractTaskComponentData(input); + + expect(result).toEqual({ + currentMediaType: { + labelName: 'Call', + iconName: 'handset-filled', + className: 'telephony', + isBrandVisual: false, + }, + isNonVoiceMedia: false, + tooltipTriggerId: 'tooltip-trigger-test-123', + tooltipId: 'tooltip-test-123', + titleClassName: 'task-title', + shouldShowState: true, + shouldShowQueue: false, + shouldShowHandleTime: true, + shouldShowTimeLeft: false, + capitalizedState: 'Active', + capitalizedQueue: 'Support team', + }); + + expect(mockGetMediaTypeInfo).toHaveBeenCalledTimes(1); + expect(mockGetMediaTypeInfo).toHaveBeenCalledWith(MEDIA_CHANNEL.TELEPHONY, MEDIA_CHANNEL.TELEPHONY); + }); + + it('should extract data for social media incoming task correctly', () => { + // Mock social media type info to match actual implementation + mockGetMediaTypeInfo.mockReturnValue({ + labelName: 'Chat', + iconName: 'chat-filled', + className: 'social', + isBrandVisual: false, + }); + + const input = { + mediaType: MEDIA_CHANNEL.SOCIAL, + mediaChannel: MEDIA_CHANNEL.SOCIAL, + isIncomingTask: true, + interactionId: 'social-456', + state: 'new', + queue: 'social support', + ronaTimeout: 30, + startTimeStamp: 1641234567890, + }; + + const result = extractTaskComponentData(input); + + expect(result).toEqual({ + currentMediaType: { + labelName: 'Chat', + iconName: 'chat-filled', + className: 'social', + isBrandVisual: false, + }, + isNonVoiceMedia: true, + tooltipTriggerId: 'tooltip-trigger-social-456', + tooltipId: 'tooltip-social-456', + titleClassName: 'incoming-digital-task-title', + shouldShowState: false, + shouldShowQueue: true, + shouldShowHandleTime: false, + shouldShowTimeLeft: true, + capitalizedState: 'New', + capitalizedQueue: 'Social support', + }); + + expect(mockGetMediaTypeInfo).toHaveBeenCalledTimes(1); + expect(mockGetMediaTypeInfo).toHaveBeenCalledWith(MEDIA_CHANNEL.SOCIAL, MEDIA_CHANNEL.SOCIAL); + }); + + it('should extract data for chat task correctly', () => { + // Mock chat media type info to match actual implementation + mockGetMediaTypeInfo.mockReturnValue({ + labelName: 'Chat', + iconName: 'chat-filled', + className: 'chat', + isBrandVisual: false, + }); + + const input = { + mediaType: MEDIA_CHANNEL.CHAT, + mediaChannel: MEDIA_CHANNEL.CHAT, + isIncomingTask: false, + interactionId: 'chat-789', + state: 'connected', + queue: 'chat team', + ronaTimeout: undefined, + startTimeStamp: 1641234567890, + }; + + const result = extractTaskComponentData(input); + + expect(result).toEqual({ + currentMediaType: { + labelName: 'Chat', + iconName: 'chat-filled', + className: 'chat', + isBrandVisual: false, + }, + isNonVoiceMedia: true, + tooltipTriggerId: 'tooltip-trigger-chat-789', + tooltipId: 'tooltip-chat-789', + titleClassName: 'task-digital-title', + shouldShowState: true, + shouldShowQueue: false, + shouldShowHandleTime: true, + shouldShowTimeLeft: false, + capitalizedState: 'Connected', + capitalizedQueue: 'Chat team', + }); + + expect(mockGetMediaTypeInfo).toHaveBeenCalledTimes(1); + expect(mockGetMediaTypeInfo).toHaveBeenCalledWith(MEDIA_CHANNEL.CHAT, MEDIA_CHANNEL.CHAT); + }); + + it('should handle default values and edge cases', () => { + // Mock default media type info to match actual implementation (defaults to telephony) + mockGetMediaTypeInfo.mockReturnValue({ + labelName: 'Call', + iconName: 'handset-filled', + className: 'telephony', + isBrandVisual: false, + }); + + const result = extractTaskComponentData({}); + + expect(result).toEqual({ + currentMediaType: { + labelName: 'Call', + iconName: 'handset-filled', + className: 'telephony', + isBrandVisual: false, + }, + isNonVoiceMedia: false, + tooltipTriggerId: 'tooltip-trigger-undefined', + tooltipId: 'tooltip-undefined', + titleClassName: 'task-title', + shouldShowState: false, + shouldShowQueue: false, + shouldShowHandleTime: false, + shouldShowTimeLeft: false, + capitalizedState: '', + capitalizedQueue: '', + }); + + expect(mockGetMediaTypeInfo).toHaveBeenCalledTimes(1); + expect(mockGetMediaTypeInfo).toHaveBeenCalledWith(undefined, undefined); + }); + + it('should determine voice vs non-voice media correctly', () => { + // Test voice media (telephony) + mockGetMediaTypeInfo.mockReturnValueOnce({ + labelName: 'Call', + iconName: 'handset-filled', + className: 'telephony', + isBrandVisual: false, + }); + + let result = extractTaskComponentData({ + mediaType: MEDIA_CHANNEL.TELEPHONY, + mediaChannel: MEDIA_CHANNEL.TELEPHONY, + }); + + expect(result.isNonVoiceMedia).toBe(false); + expect(mockGetMediaTypeInfo).toHaveBeenCalledWith(MEDIA_CHANNEL.TELEPHONY, MEDIA_CHANNEL.TELEPHONY); + + // Clear the mock for the next call + mockGetMediaTypeInfo.mockClear(); + + // Test non-voice media (email) + mockGetMediaTypeInfo.mockReturnValueOnce({ + labelName: 'Email', + iconName: 'email-filled', + className: 'email', + isBrandVisual: false, + }); + + result = extractTaskComponentData({ + mediaType: MEDIA_CHANNEL.EMAIL, + mediaChannel: MEDIA_CHANNEL.EMAIL, + }); + + expect(result.isNonVoiceMedia).toBe(true); + expect(mockGetMediaTypeInfo).toHaveBeenCalledWith(MEDIA_CHANNEL.EMAIL, MEDIA_CHANNEL.EMAIL); + }); + }); +}); diff --git a/packages/contact-center/station-login/tests/helper.ts b/packages/contact-center/station-login/tests/helper.ts index a67aed0a3..f0f30c491 100644 --- a/packages/contact-center/station-login/tests/helper.ts +++ b/packages/contact-center/station-login/tests/helper.ts @@ -513,7 +513,7 @@ describe('useStationLogin Hook', () => { }; act(() => { - onSpy.mock.calls[0][1](mockPayload); + (onSpy.mock.calls[0][1] as (payload: unknown) => void)(mockPayload); }); await waitFor(() => { @@ -523,7 +523,7 @@ describe('useStationLogin Hook', () => { expect(ccMock.on).toHaveBeenCalledWith(CC_EVENTS.AGENT_LOGOUT_SUCCESS, expect.any(Function)); act(() => { - onSpy.mock.calls[1][1]({}); + (onSpy.mock.calls[1][1] as (payload: unknown) => void)({}); }); await waitFor(() => { @@ -552,7 +552,7 @@ describe('useStationLogin Hook', () => { }; act(() => { - onSpy.mock.calls[0][1](mockPayload); + (onSpy.mock.calls[0][1] as (payload: unknown) => void)(mockPayload); }); await waitFor(() => { diff --git a/packages/contact-center/test-fixtures/src/incomingTaskFixtures.ts b/packages/contact-center/test-fixtures/src/incomingTaskFixtures.ts new file mode 100644 index 000000000..410185ab3 --- /dev/null +++ b/packages/contact-center/test-fixtures/src/incomingTaskFixtures.ts @@ -0,0 +1,64 @@ +import {MEDIA_CHANNEL} from '../../cc-components/src/components/task/task.types'; + +export const mockIncomingTaskData = { + webRTC: { + ani: '1234567890', + customerName: 'John Doe', + virtualTeamName: 'Support Team', + ronaTimeout: 30, + startTimeStamp: 1641234567890, + mediaType: MEDIA_CHANNEL.TELEPHONY, + mediaChannel: MEDIA_CHANNEL.TELEPHONY, + isTelephony: true, + isSocial: false, + acceptText: 'Accept', + declineText: 'Decline', + title: '1234567890', + disableAccept: false, + }, + extension: { + ani: '1234567890', + customerName: 'Mobile User', + virtualTeamName: 'Mobile Support', + ronaTimeout: 30, + startTimeStamp: 1641234567890, + mediaType: MEDIA_CHANNEL.TELEPHONY, + mediaChannel: MEDIA_CHANNEL.TELEPHONY, + isTelephony: true, + isSocial: false, + acceptText: 'Ringing...', + declineText: undefined, + title: '1234567890', + disableAccept: true, + }, + social: { + ani: 'social-user-123', + customerName: 'Social Customer', + virtualTeamName: 'Social Team', + ronaTimeout: 45, + startTimeStamp: 1641234567890, + mediaType: MEDIA_CHANNEL.SOCIAL, + mediaChannel: MEDIA_CHANNEL.FACEBOOK, + isTelephony: false, + isSocial: true, + acceptText: 'Accept', + declineText: undefined, + title: 'Social Customer', + disableAccept: false, + }, + chat: { + ani: 'chat-user-456', + customerName: 'Chat Customer', + virtualTeamName: 'Chat Support', + ronaTimeout: 60, + startTimeStamp: 1641234567890, + mediaType: MEDIA_CHANNEL.CHAT, + mediaChannel: MEDIA_CHANNEL.CHAT, + isTelephony: false, + isSocial: false, + acceptText: 'Accept', + declineText: undefined, + title: 'Chat Customer', + disableAccept: false, + }, +}; diff --git a/packages/contact-center/test-fixtures/src/index.ts b/packages/contact-center/test-fixtures/src/index.ts index 995b5bc6e..24e5c154b 100644 --- a/packages/contact-center/test-fixtures/src/index.ts +++ b/packages/contact-center/test-fixtures/src/index.ts @@ -1 +1,3 @@ export * from './fixtures'; +export * from './incomingTaskFixtures'; +export * from './taskListFixtures'; diff --git a/packages/contact-center/test-fixtures/src/taskListFixtures.ts b/packages/contact-center/test-fixtures/src/taskListFixtures.ts new file mode 100644 index 000000000..59cc5ad53 --- /dev/null +++ b/packages/contact-center/test-fixtures/src/taskListFixtures.ts @@ -0,0 +1,192 @@ +import {MEDIA_CHANNEL} from '../../cc-components/src/components/task/task.types'; + +export const mockTaskData = { + active: { + telephony: { + ani: '1234567890', + customerName: 'John Doe', + virtualTeamName: 'Support Team', + ronaTimeout: null, + taskState: 'active', + startTimeStamp: 1641234567890, + isIncomingTask: false, + mediaType: MEDIA_CHANNEL.TELEPHONY, + mediaChannel: MEDIA_CHANNEL.TELEPHONY, + isTelephony: true, + isSocial: false, + acceptText: undefined, + declineText: undefined, + title: '1234567890', + disableAccept: false, + displayState: 'active', + }, + chat: { + ani: 'chat-user-123', + customerName: 'Chat Customer', + virtualTeamName: 'Chat Team', + ronaTimeout: null, + taskState: 'active', + startTimeStamp: 1641234567890, + isIncomingTask: false, + mediaType: MEDIA_CHANNEL.CHAT, + mediaChannel: MEDIA_CHANNEL.CHAT, + isTelephony: false, + isSocial: false, + acceptText: undefined, + declineText: undefined, + title: 'Chat Customer', + disableAccept: false, + displayState: 'active', + }, + facebook: { + ani: 'facebook-user-456', + customerName: 'Facebook Customer', + virtualTeamName: 'Social Team', + ronaTimeout: null, + taskState: 'active', + startTimeStamp: 1641234567890, + isIncomingTask: false, + mediaType: MEDIA_CHANNEL.SOCIAL, + mediaChannel: MEDIA_CHANNEL.FACEBOOK, + isTelephony: false, + isSocial: true, + acceptText: undefined, + declineText: undefined, + title: 'Facebook Customer', + disableAccept: false, + displayState: 'active', + }, + }, + incoming: { + webrtcTelephony: { + ani: '1234567890', + customerName: 'WebRTC Customer', + virtualTeamName: 'Support', + ronaTimeout: 30, + taskState: 'new', + startTimeStamp: undefined, + isIncomingTask: true, + mediaType: MEDIA_CHANNEL.TELEPHONY, + mediaChannel: MEDIA_CHANNEL.TELEPHONY, + isTelephony: true, + isSocial: false, + acceptText: 'Accept', + declineText: 'Decline', + title: '1234567890', + disableAccept: false, + displayState: '', + }, + extensionTelephony: { + ani: '1234567890', + customerName: 'Extension Customer', + virtualTeamName: 'Support', + ronaTimeout: 30, + taskState: 'new', + startTimeStamp: undefined, + isIncomingTask: true, + mediaType: MEDIA_CHANNEL.TELEPHONY, + mediaChannel: MEDIA_CHANNEL.TELEPHONY, + isTelephony: true, + isSocial: false, + acceptText: 'Ringing...', + declineText: undefined, + title: '1234567890', + disableAccept: true, + displayState: '', + }, + chat: { + ani: 'chat-user-123', + customerName: 'Chat Customer', + virtualTeamName: 'Chat Support', + ronaTimeout: 30, + taskState: 'new', + startTimeStamp: undefined, + isIncomingTask: true, + mediaType: MEDIA_CHANNEL.CHAT, + mediaChannel: MEDIA_CHANNEL.CHAT, + isTelephony: false, + isSocial: false, + acceptText: 'Accept', + declineText: undefined, + title: 'Chat Customer', + disableAccept: false, + displayState: '', + }, + }, + action: { + telephony: { + ani: '1111111111', + customerName: 'Action Customer', + virtualTeamName: 'Action Team', + ronaTimeout: 30, + taskState: 'new', + startTimeStamp: undefined, + isIncomingTask: true, + mediaType: MEDIA_CHANNEL.TELEPHONY, + mediaChannel: MEDIA_CHANNEL.TELEPHONY, + isTelephony: true, + isSocial: false, + acceptText: 'Accept', + declineText: 'Decline', + title: '1111111111', + disableAccept: false, + displayState: '', + }, + activeTask: { + ani: '1111111111', + customerName: 'Active Customer', + virtualTeamName: 'Active Team', + ronaTimeout: null, + taskState: 'active', + startTimeStamp: 1641234567890, + isIncomingTask: false, + mediaType: MEDIA_CHANNEL.TELEPHONY, + mediaChannel: MEDIA_CHANNEL.TELEPHONY, + isTelephony: true, + isSocial: false, + acceptText: undefined, + declineText: undefined, + title: '1111111111', + disableAccept: false, + displayState: 'active', + }, + }, + selection: { + selectedTelephony: { + ani: '1111111111', + customerName: 'Selected Customer', + virtualTeamName: 'Selected Team', + ronaTimeout: 30, + taskState: 'new', + startTimeStamp: undefined, + isIncomingTask: true, + mediaType: MEDIA_CHANNEL.TELEPHONY, + mediaChannel: MEDIA_CHANNEL.TELEPHONY, + isTelephony: true, + isSocial: false, + acceptText: 'Accept', + declineText: 'Decline', + title: '1111111111', + disableAccept: false, + displayState: '', + }, + unselectedChat: { + ani: 'chat-user-456', + customerName: 'Unselected Chat Customer', + virtualTeamName: 'Chat Support Team', + ronaTimeout: null, + taskState: 'active', + startTimeStamp: 1641234567890, + isIncomingTask: false, + mediaType: MEDIA_CHANNEL.CHAT, + mediaChannel: MEDIA_CHANNEL.CHAT, + isTelephony: false, + isSocial: false, + acceptText: undefined, + declineText: undefined, + title: 'Unselected Chat Customer', + disableAccept: false, + displayState: 'active', + }, + }, +};