From 10ac9257dedba19979edbec945822cea1f12525a Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Fri, 31 Jan 2025 20:07:26 +0530 Subject: [PATCH 01/23] fix(store): direct-all-events-through-store --- packages/contact-center/store/src/index.ts | 2 +- packages/contact-center/store/src/store.ts | 8 +- .../contact-center/store/src/store.types.ts | 54 ++++++- .../store/src/storeEventsWrapper.ts | 135 ++++++++++++++++++ .../task/src/CallControl/index.tsx | 4 +- .../incoming-task.presentational.tsx | 5 +- .../task/src/IncomingTask/index.tsx | 5 +- .../task/src/TaskList/index.tsx | 4 +- packages/contact-center/task/src/helper.ts | 110 +------------- .../contact-center/task/src/task.types.ts | 15 +- .../cc/samples-cc-react-app/src/App.tsx | 84 +++++++++-- 11 files changed, 292 insertions(+), 134 deletions(-) create mode 100644 packages/contact-center/store/src/storeEventsWrapper.ts diff --git a/packages/contact-center/store/src/index.ts b/packages/contact-center/store/src/index.ts index 137f458b7..9861d6160 100644 --- a/packages/contact-center/store/src/index.ts +++ b/packages/contact-center/store/src/index.ts @@ -1,3 +1,3 @@ -import store from './store'; +import store from './storeEventsWrapper'; export * from './store.types'; export default store; diff --git a/packages/contact-center/store/src/store.ts b/packages/contact-center/store/src/store.ts index a34b4e6c4..1ba6e2ff8 100644 --- a/packages/contact-center/store/src/store.ts +++ b/packages/contact-center/store/src/store.ts @@ -10,6 +10,7 @@ import { IStore, ILogger, IWrapupCode, + TASK_EVENTS, } from './store.types'; import {ITask} from '@webex/plugin-cc'; @@ -23,9 +24,12 @@ class Store implements IStore { agentId: string = ''; selectedLoginOption: string = ''; wrapupCodes: IWrapupCode[] = []; + incomingTask: ITask = null; currentTask: ITask = null; isAgentLoggedIn = false; deviceType: string = ''; + taskList: ITask[] = []; + wrapupRequired: boolean; constructor() { makeAutoObservable(this, { @@ -107,5 +111,5 @@ class Store implements IStore { } } -const store = Store.getInstance(); -export default store; +// const store = Store.getInstance(); +export default Store; diff --git a/packages/contact-center/store/src/store.types.ts b/packages/contact-center/store/src/store.types.ts index a5a1fc907..79491334f 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -1,4 +1,5 @@ import {AgentLogin, IContactCenter, Profile, Team, LogContext} from '@webex/plugin-cc'; +import {ITask} from '@webex/plugin-cc'; type ILogger = { log: (message: string, context?: LogContext) => void; @@ -33,8 +34,37 @@ interface IStore { idleCodes: IdleCode[]; agentId: string; logger: ILogger; - registerCC(webex: WithWebex['webex']): Promise; + selectedLoginOption: string; + wrapupCodes: IWrapupCode[]; + currentTask: ITask; + incomingTask: ITask; + taskList: ITask[] + isAgentLoggedIn: boolean; + deviceType: string; init(params: InitParams): Promise; + setCurrentTask(task: any): void; + setSelectedLoginOption(option: string): void; + wrapupRequired: boolean; +} + +interface IStoreWrapper { + teams: Team[]; + loginOptions: string[]; + cc: IContactCenter; + idleCodes: IdleCode[]; + agentId: string; + logger: ILogger; + selectedLoginOption: string; + wrapupCodes: IWrapupCode[]; + currentTask: ITask; + incomingTask: ITask; + taskList: ITask[] + isAgentLoggedIn: boolean; + deviceType: string; + wrapupRequired: boolean; + init(params: InitParams): Promise; + setCurrentTask(task: any): void; + setSelectedLoginOption(option: string): void; } interface IWrapupCode { @@ -43,6 +73,21 @@ interface IWrapupCode { } +enum TASK_EVENTS { +TASK_INCOMING = 'task:incoming', +TASK_ASSIGNED = 'task:assigned', +TASK_MEDIA = 'task:media', +TASK_HOLD = 'task:hold', +TASK_UNHOLD = 'task:unhold', +TASK_CONSULT = 'task:consult', +TASK_CONSULT_END = 'task:consultEnd', +TASK_CONSULT_ACCEPT = 'task:consultAccepted', +TASK_PAUSE = 'task:pause', +TASK_RESUME = 'task:resume', +TASK_END = 'task:end', +TASK_WRAPUP = 'task:wrapup', +} // TODO: remove this once cc sdk exports this enum + export type { IContactCenter, Profile, @@ -53,5 +98,10 @@ export type { InitParams, IStore, ILogger, - IWrapupCode + IWrapupCode, + IStoreWrapper +} + +export { + TASK_EVENTS, } diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts new file mode 100644 index 000000000..b99e46e74 --- /dev/null +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -0,0 +1,135 @@ +// storeWrapper.ts + +import {IStoreWrapper, IStore, InitParams, TASK_EVENTS} from './store.types'; +import {ITask} from '@webex/plugin-cc'; +import Store from './store'; // Import your original store + +class StoreWrapper implements IStoreWrapper { + private store: IStore; + + constructor() { + this.store = Store.getInstance(); + } + + // Proxy all methods and properties of the original store + get teams() { + return this.store.teams; + } + get loginOptions() { + return this.store.loginOptions; + } + get cc() { + return this.store.cc; + } + get logger() { + return this.store.logger; + } + get idleCodes() { + return this.store.idleCodes; + } + get agentId() { + return this.store.agentId; + } + + get selectedLoginOption() { + return this.store.selectedLoginOption; + } + get wrapupCodes() { + return this.store.wrapupCodes; + } + get currentTask() { + return this.store.currentTask; + } + get isAgentLoggedIn() { + return this.store.isAgentLoggedIn; + } + get deviceType() { + return this.store.deviceType; + } + + get taskList() { + return this.store.taskList; + } + + get incomingTask() { + return this.store.incomingTask; + } + + get wrapupRequired() { + return this.store.wrapupRequired; + } + + setCurrentTask(task: any): void { + return this.store.setCurrentTask(task); + } + + setSelectedLoginOption(option: string): void { + return this.store.setSelectedLoginOption(option); + } + + init(options: InitParams): Promise { + console.log('Shreyas: Initializing store with options', options); + return this.store.init(options).then(() => { + console.log('Shreyas: Store initialized, setting up incoming task handler'); + this.setupIncomingTaskHandler(); + }); + } + + handleTaskRemoved = (taskId: string, wrapupRequired: boolean) => { + console.log('Shreyas: Handling task removal for taskId', taskId); + const taskToRemove = this.store.taskList.find((task) => task.data.interactionId === taskId); + this.store.wrapupRequired = wrapupRequired; + + if (taskToRemove) { + console.log('Shreyas: Task found, cleaning up listeners for taskId', taskId); + // Clean up listeners on the task + taskToRemove.off(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned(taskId)); + taskToRemove.off(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => + this.handleTaskRemoved(taskId, wrapupRequired) + ); + } + + this.store.taskList = this.store.taskList.filter((task) => task.data.interactionId !== taskId); + console.log('Shreyas: Task removed, updated task list', this.store.taskList); + }; + + handleTaskAssigned = (task: ITask) => () => { + this.store.incomingTask = null; + this.setCurrentTask(task); + }; + + handleIncomingTask = (task: ITask) => { + console.log('Shreyas: Handling incoming task', task); + this.store.incomingTask = task; + console.log('Shreyas: setted incomingTask in store'); + if (this.store.taskList.some((t) => t.data.interactionId === task.data.interactionId)) { + console.log('Shreyas: Task already present in the taskList', task.data.interactionId); + // Task already present in the taskList + return; + } + + // Attach event listeners to the task + console.log('Shreyas: Attaching event listeners to the task', task.data.interactionId); + task.on(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => + this.handleTaskRemoved(task.data.interactionId, wrapupRequired) + ); + task.on(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned(task)); + + this.store.taskList = [...this.store.taskList, task]; + console.log('Shreyas: Task added to task list', this.store.taskList); + }; + + setupIncomingTaskHandler() { + console.log('Shreyas: Setting up incoming task handler'); + const ccSDK = this.store.cc; + + ccSDK.on(TASK_EVENTS.TASK_INCOMING, (task) => { + console.log('Shreyas: Incoming task event received', task); + this.handleIncomingTask(task); + }); + } +} + +// Create and export a single instance of the wrapper +const storeWrapper = new StoreWrapper(); +export default storeWrapper; diff --git a/packages/contact-center/task/src/CallControl/index.tsx b/packages/contact-center/task/src/CallControl/index.tsx index aeda9372a..70561f11a 100644 --- a/packages/contact-center/task/src/CallControl/index.tsx +++ b/packages/contact-center/task/src/CallControl/index.tsx @@ -7,9 +7,9 @@ import {CallControlProps} from '../task.types'; import CallControlPresentational from './call-control.presentational'; const CallControlComponent: React.FunctionComponent = ({onHoldResume, onEnd, onWrapUp}) => { - const {logger, currentTask, wrapupCodes} = store; + const {logger, currentTask, wrapupCodes, wrapupRequired} = store; - const result = useCallControl({currentTask, onHoldResume, onEnd, onWrapUp, logger}); + const result = useCallControl({currentTask, onHoldResume, onEnd, onWrapUp, wrapupRequired, logger}); return ; }; diff --git a/packages/contact-center/task/src/IncomingTask/incoming-task.presentational.tsx b/packages/contact-center/task/src/IncomingTask/incoming-task.presentational.tsx index 228dc4388..8353d69e7 100644 --- a/packages/contact-center/task/src/IncomingTask/incoming-task.presentational.tsx +++ b/packages/contact-center/task/src/IncomingTask/incoming-task.presentational.tsx @@ -120,9 +120,8 @@ const styles: {[key: string]: React.CSSProperties} = { }; const IncomingTaskPresentational: React.FunctionComponent = (props) => { - const {incomingTask, accept, decline, isBrowser, isAnswered} = props; - - if (!incomingTask || isAnswered) { + const {incomingTask, accept, decline, isBrowser} = props; + if (!incomingTask) { return <>; // hidden component } diff --git a/packages/contact-center/task/src/IncomingTask/index.tsx b/packages/contact-center/task/src/IncomingTask/index.tsx index ff6d19eab..9bd8687e1 100644 --- a/packages/contact-center/task/src/IncomingTask/index.tsx +++ b/packages/contact-center/task/src/IncomingTask/index.tsx @@ -7,9 +7,8 @@ import IncomingTaskPresentational from './incoming-task.presentational'; import {IncomingTaskProps} from '../task.types'; const IncomingTaskComponent: React.FunctionComponent = ({onAccepted, onDeclined}) => { - const {cc, selectedLoginOption, logger} = store; - - const result = useIncomingTask({cc, onAccepted, onDeclined, selectedLoginOption, logger}); + const {cc, selectedLoginOption, incomingTask, logger} = store; + const result = useIncomingTask({cc, incomingTask, onAccepted, onDeclined, selectedLoginOption, logger}); const props = { ...result, diff --git a/packages/contact-center/task/src/TaskList/index.tsx b/packages/contact-center/task/src/TaskList/index.tsx index e87487b9a..8f0035429 100644 --- a/packages/contact-center/task/src/TaskList/index.tsx +++ b/packages/contact-center/task/src/TaskList/index.tsx @@ -6,9 +6,9 @@ import TaskListPresentational from './task-list.presentational'; import {useTaskList} from '../helper'; const TaskListComponent: React.FunctionComponent = () => { - const {cc, currentTask, selectedLoginOption, logger} = store; + const {cc, taskList, currentTask, selectedLoginOption, logger} = store; - const result = useTaskList({cc, selectedLoginOption, logger}); + const result = useTaskList({cc, selectedLoginOption, logger, taskList}); const props = { ...result, currentTask, diff --git a/packages/contact-center/task/src/helper.ts b/packages/contact-center/task/src/helper.ts index a30510692..d20402b8f 100644 --- a/packages/contact-center/task/src/helper.ts +++ b/packages/contact-center/task/src/helper.ts @@ -5,8 +5,7 @@ import {TASK_EVENTS, useCallControlProps, UseTaskListProps, UseTaskProps} from ' // Hook for managing the task list export const useTaskList = (props: UseTaskListProps) => { - const {cc, selectedLoginOption, onTaskAccepted, onTaskDeclined, logger} = props; - const [taskList, setTaskList] = useState([]); + const {selectedLoginOption, onTaskAccepted, onTaskDeclined, logger, taskList} = props; const isBrowser = selectedLoginOption === 'BROWSER'; const logError = (message: string, method: string) => { @@ -16,35 +15,6 @@ export const useTaskList = (props: UseTaskListProps) => { }); }; - const handleTaskRemoved = useCallback((taskId: string) => { - setTaskList((prev) => { - const taskToRemove = prev.find((task) => task.data.interactionId === taskId); - - if (taskToRemove) { - // Clean up listeners on the task - taskToRemove.off(TASK_EVENTS.TASK_END, () => handleTaskRemoved(taskId)); - } - - return prev.filter((task) => task.data.interactionId !== taskId); - }); - }, []); - - const handleIncomingTask = useCallback( - (task: ITask) => { - setTaskList((prev) => { - if (prev.some((t) => t.data.interactionId === task.data.interactionId)) { - return prev; - } - - // Attach event listeners to the task - task.on(TASK_EVENTS.TASK_END, () => handleTaskRemoved(task.data.interactionId)); - - return [...prev, task]; - }); - }, - [handleTaskRemoved] // Include handleTaskRemoved as a dependency - ); - const acceptTask = (task: ITask) => { const taskId = task?.data.interactionId; if (!taskId) return; @@ -52,7 +22,6 @@ export const useTaskList = (props: UseTaskListProps) => { task .accept(taskId) .then(() => { - store.setCurrentTask(task); onTaskAccepted && onTaskAccepted(task); }) .catch((error: Error) => { @@ -68,31 +37,18 @@ export const useTaskList = (props: UseTaskListProps) => { .decline(taskId) .then(() => { onTaskDeclined && onTaskDeclined(task); - store.setCurrentTask(null); + // store.setCurrentTask(null); }) .catch((error: Error) => { logError(`Error declining task: ${error}`, 'declineTask'); }); }; - useEffect(() => { - // Listen for incoming tasks globally - cc.on(TASK_EVENTS.TASK_INCOMING, handleIncomingTask); - - return () => { - cc.off(TASK_EVENTS.TASK_INCOMING, handleIncomingTask); - }; - }, [cc, handleIncomingTask]); - return {taskList, acceptTask, declineTask, isBrowser}; }; -// Hook for managing the current task export const useIncomingTask = (props: UseTaskProps) => { - const {cc, onAccepted, onDeclined, selectedLoginOption, logger} = props; - const [incomingTask, setIncomingTask] = useState(null); - const [isAnswered, setIsAnswered] = useState(false); - const [isEnded, setIsEnded] = useState(false); + const {cc, onAccepted, onDeclined, selectedLoginOption, incomingTask, logger} = props; const isBrowser = selectedLoginOption === 'BROWSER'; const logError = (message: string, method: string) => { @@ -102,41 +58,6 @@ export const useIncomingTask = (props: UseTaskProps) => { }); }; - const handleTaskAssigned = useCallback(() => { - // Task that are accepted using anything other than browser should be populated - // in the store only when we receive task assigned event - if (!isBrowser) store.setCurrentTask(incomingTask); - setIsAnswered(true); - }, [incomingTask]); - - const handleTaskEnded = useCallback(() => { - setIsEnded(true); - setIncomingTask(null); - }, []); - - const handleIncomingTask = useCallback((task: ITask) => { - setIncomingTask(task); - setIsAnswered(false); - setIsEnded(false); - }, []); - - useEffect(() => { - cc.on(TASK_EVENTS.TASK_INCOMING, handleIncomingTask); - - if (incomingTask) { - incomingTask.on(TASK_EVENTS.TASK_ASSIGNED, handleTaskAssigned); - incomingTask.on(TASK_EVENTS.TASK_END, handleTaskEnded); - } - - return () => { - cc.off(TASK_EVENTS.TASK_INCOMING, handleIncomingTask); - if (incomingTask) { - incomingTask.off(TASK_EVENTS.TASK_ASSIGNED, handleTaskAssigned); - incomingTask.off(TASK_EVENTS.TASK_END, handleTaskEnded); - } - }; - }, [cc, incomingTask, handleIncomingTask, handleTaskAssigned, handleTaskEnded]); - const accept = () => { const taskId = incomingTask?.data.interactionId; if (!taskId) return; @@ -144,9 +65,6 @@ export const useIncomingTask = (props: UseTaskProps) => { incomingTask .accept(taskId) .then(() => { - // Task that are accepted using BROWSER should be populated - // in the store when we accept the call - store.setCurrentTask(incomingTask); onAccepted && onAccepted(); }) .catch((error: Error) => { @@ -161,8 +79,6 @@ export const useIncomingTask = (props: UseTaskProps) => { incomingTask .decline(taskId) .then(() => { - setIncomingTask(null); - store.setCurrentTask(null); onDeclined && onDeclined(); }) .catch((error: Error) => { @@ -172,8 +88,6 @@ export const useIncomingTask = (props: UseTaskProps) => { return { incomingTask, - isAnswered, - isEnded, accept, decline, isBrowser, @@ -181,8 +95,7 @@ export const useIncomingTask = (props: UseTaskProps) => { }; export const useCallControl = (props: useCallControlProps) => { - const {currentTask, onHoldResume, onEnd, onWrapUp, logger} = props; - const [wrapupRequired, setWrapupRequired] = useState(false); + const {currentTask, onHoldResume, onEnd, onWrapUp, wrapupRequired, logger} = props; const audioRef = useRef(null); // Ref for the audio element const logError = (message: string, method: string) => { @@ -192,10 +105,6 @@ export const useCallControl = (props: useCallControlProps) => { }); }; - const handleTaskEnded = useCallback(({wrapupRequired}: {wrapupRequired: boolean}) => { - setWrapupRequired(wrapupRequired); - }, []); - const handleTaskMedia = useCallback( (track) => { if (audioRef.current) { @@ -207,14 +116,13 @@ export const useCallControl = (props: useCallControlProps) => { useEffect(() => { if (!currentTask) return; + // Call control only event for WebRTC calls currentTask.on(TASK_EVENTS.TASK_MEDIA, handleTaskMedia); - currentTask.on(TASK_EVENTS.TASK_END, handleTaskEnded); return () => { currentTask.off(TASK_EVENTS.TASK_MEDIA, handleTaskMedia); - currentTask.off(TASK_EVENTS.TASK_END, handleTaskEnded); }; - }, [currentTask, handleTaskEnded]); + }, [currentTask]); const toggleHold = (hold: boolean) => { if (hold) { @@ -237,10 +145,6 @@ export const useCallControl = (props: useCallControlProps) => { }; const toggleRecording = (pause: boolean) => { - const logLocation = { - module: 'widget-cc-task#helper.ts', - method: 'useCallControl#pauseResumeRecording', - }; if (pause) { currentTask.pauseRecording().catch((error: Error) => { logError(`Error pausing recording: ${error}`, 'toggleRecording'); @@ -267,8 +171,6 @@ export const useCallControl = (props: useCallControlProps) => { currentTask .wrapup({wrapUpReason: wrapUpReason, auxCodeId: auxCodeId}) .then(() => { - setWrapupRequired(false); - store.setCurrentTask(null); if (onWrapUp) onWrapUp(); }) .catch((error: Error) => { diff --git a/packages/contact-center/task/src/task.types.ts b/packages/contact-center/task/src/task.types.ts index 55fb1c2ad..9c0abb3f8 100644 --- a/packages/contact-center/task/src/task.types.ts +++ b/packages/contact-center/task/src/task.types.ts @@ -91,15 +91,15 @@ export interface TaskProps { logger: ILogger; } -export type UseTaskProps = Pick; -export type UseTaskListProps = Pick< +export type UseTaskProps = Pick< TaskProps, - 'cc' | 'selectedLoginOption' | 'onTaskAccepted' | 'onTaskDeclined' | 'logger' + 'cc' | 'incomingTask' | 'onAccepted' | 'onDeclined' | 'selectedLoginOption' | 'logger' >; -export type IncomingTaskPresentationalProps = Pick< +export type UseTaskListProps = Pick< TaskProps, - 'incomingTask' | 'isBrowser' | 'isAnswered' | 'isEnded' | 'accept' | 'decline' + 'cc' | 'taskList' | 'selectedLoginOption' | 'onTaskAccepted' | 'onTaskDeclined' | 'logger' >; +export type IncomingTaskPresentationalProps = Pick; export type IncomingTaskProps = Pick; export type TaskListProps = Pick; @@ -205,4 +205,7 @@ export type CallControlPresentationalProps = Pick< | 'wrapupCall' >; -export type useCallControlProps = Pick; +export type useCallControlProps = Pick< + ControlProps, + 'currentTask' | 'wrapupRequired' | 'onHoldResume' | 'onEnd' | 'onWrapUp' | 'logger' +>; diff --git a/widgets-samples/cc/samples-cc-react-app/src/App.tsx b/widgets-samples/cc/samples-cc-react-app/src/App.tsx index 3ddb7377f..25c5dd30e 100644 --- a/widgets-samples/cc/samples-cc-react-app/src/App.tsx +++ b/widgets-samples/cc/samples-cc-react-app/src/App.tsx @@ -3,6 +3,13 @@ import {StationLogin, UserState, IncomingTask, TaskList, CallControl, store} fro function App() { const [isSdkReady, setIsSdkReady] = useState(false); + const [selectedWidgets, setSelectedWidgets] = useState({ + stationLogin: false, + userState: false, + incomingTask: false, + taskList: false, + callControl: false, + }); const [accessToken, setAccessToken] = useState(''); const [isLoggedIn, setIsLoggedIn] = useState(false); @@ -51,6 +58,11 @@ function App() { console.log('onWrapup invoked'); }; + const handleCheckboxChange = (e) => { + const {name, checked} = e.target; + setSelectedWidgets((prev) => ({...prev, [name]: checked})); + }; + return ( <>

Contact Center widgets in a react app

@@ -72,15 +84,69 @@ function App() { {isSdkReady && ( <> - - {isLoggedIn && ( - <> - - - - - - )} +
+ + + + + +
+ + + )} + {isLoggedIn && ( + <> + {selectedWidgets.stationLogin && } + {selectedWidgets.userState && } + {selectedWidgets.incomingTask && } + {selectedWidgets.taskList && } + {selectedWidgets.callControl && } )} From 8322a1e719610025a563540c29a07f24674eecf2 Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Mon, 10 Feb 2025 15:02:40 +0530 Subject: [PATCH 02/23] fix(store): fix-wrapup-required-not-re-rendering-issue --- packages/contact-center/store/src/store.ts | 26 +++++++-- .../contact-center/store/src/store.types.ts | 32 ++++------- .../store/src/storeEventsWrapper.ts | 56 +++++++++---------- .../task/src/CallControl/index.tsx | 5 +- packages/contact-center/task/src/helper.ts | 5 +- .../contact-center/task/src/task.types.ts | 5 +- .../cc/samples-cc-react-app/src/App.tsx | 19 +++++-- 7 files changed, 75 insertions(+), 73 deletions(-) diff --git a/packages/contact-center/store/src/store.ts b/packages/contact-center/store/src/store.ts index 1ba6e2ff8..f6b9f1cef 100644 --- a/packages/contact-center/store/src/store.ts +++ b/packages/contact-center/store/src/store.ts @@ -10,7 +10,6 @@ import { IStore, ILogger, IWrapupCode, - TASK_EVENTS, } from './store.types'; import {ITask} from '@webex/plugin-cc'; @@ -29,19 +28,38 @@ class Store implements IStore { isAgentLoggedIn = false; deviceType: string = ''; taskList: ITask[] = []; - wrapupRequired: boolean; + wrapupRequired: boolean = false; constructor() { makeAutoObservable(this, { cc: observable.ref, currentTask: observable, // Make currentTask observable + incomingTask: observable, + taskList: observable, + wrapupRequired: observable, }); } + setWrapupRequired(value: boolean): void { + this.wrapupRequired = value; + } + setCurrentTask(task: ITask): void { this.currentTask = task; } + setIncomingTask(task: ITask): void { + this.incomingTask = task; + } + + setTaskList(taskList: ITask[]): void { + this.taskList = taskList; + } + + setSelectedLoginOption(option: string): void { + this.selectedLoginOption = option; + } + public static getInstance(): Store { if (!Store.instance) { console.log('Creating new store instance'); @@ -52,10 +70,6 @@ class Store implements IStore { return Store.instance; } - setSelectedLoginOption(option: string): void { - this.selectedLoginOption = option; - } - registerCC(webex: WithWebex['webex']): Promise { this.cc = webex.cc; this.logger = this.cc.LoggerProxy; diff --git a/packages/contact-center/store/src/store.types.ts b/packages/contact-center/store/src/store.types.ts index 79491334f..bce74d666 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -27,7 +27,7 @@ type IdleCode = { isDefault: boolean; } -interface IStore { +interface IStoreBase { teams: Team[]; loginOptions: string[]; cc: IContactCenter; @@ -38,33 +38,23 @@ interface IStore { wrapupCodes: IWrapupCode[]; currentTask: ITask; incomingTask: ITask; - taskList: ITask[] + taskList: ITask[]; isAgentLoggedIn: boolean; deviceType: string; + wrapupRequired: boolean; init(params: InitParams): Promise; - setCurrentTask(task: any): void; setSelectedLoginOption(option: string): void; - wrapupRequired: boolean; } -interface IStoreWrapper { - teams: Team[]; - loginOptions: string[]; - cc: IContactCenter; - idleCodes: IdleCode[]; - agentId: string; - logger: ILogger; - selectedLoginOption: string; - wrapupCodes: IWrapupCode[]; - currentTask: ITask; - incomingTask: ITask; - taskList: ITask[] - isAgentLoggedIn: boolean; - deviceType: string; - wrapupRequired: boolean; - init(params: InitParams): Promise; +interface IStore extends IStoreBase { setCurrentTask(task: any): void; - setSelectedLoginOption(option: string): void; + setWrapupRequired(value: boolean): void; + setTaskList(taskList: ITask[]): void; + setIncomingTask(task: ITask): void; +} + +interface IStoreWrapper extends IStoreBase { + handleTaskRemove(taskId: string): void; } interface IWrapupCode { diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index b99e46e74..4f152eef5 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -1,8 +1,7 @@ -// storeWrapper.ts - import {IStoreWrapper, IStore, InitParams, TASK_EVENTS} from './store.types'; import {ITask} from '@webex/plugin-cc'; -import Store from './store'; // Import your original store +import Store from './store'; +import {runInAction} from 'mobx'; class StoreWrapper implements IStoreWrapper { private store: IStore; @@ -59,72 +58,67 @@ class StoreWrapper implements IStoreWrapper { return this.store.wrapupRequired; } - setCurrentTask(task: any): void { - return this.store.setCurrentTask(task); - } - setSelectedLoginOption(option: string): void { return this.store.setSelectedLoginOption(option); } init(options: InitParams): Promise { - console.log('Shreyas: Initializing store with options', options); return this.store.init(options).then(() => { - console.log('Shreyas: Store initialized, setting up incoming task handler'); this.setupIncomingTaskHandler(); }); } - handleTaskRemoved = (taskId: string, wrapupRequired: boolean) => { - console.log('Shreyas: Handling task removal for taskId', taskId); + handleTaskRemove = (taskId: string) => { const taskToRemove = this.store.taskList.find((task) => task.data.interactionId === taskId); - this.store.wrapupRequired = wrapupRequired; - if (taskToRemove) { - console.log('Shreyas: Task found, cleaning up listeners for taskId', taskId); - // Clean up listeners on the task taskToRemove.off(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned(taskId)); taskToRemove.off(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => - this.handleTaskRemoved(taskId, wrapupRequired) + this.handleTaskEnd(taskId, wrapupRequired) ); } + const updateTaskList = this.store.taskList.filter((task) => task.data.interactionId !== taskId); + runInAction(() => { + this.store.setTaskList(updateTaskList); + this.store.setCurrentTask(null); + this.store.setWrapupRequired(false); + }); + }; - this.store.taskList = this.store.taskList.filter((task) => task.data.interactionId !== taskId); - console.log('Shreyas: Task removed, updated task list', this.store.taskList); + handleTaskEnd = (taskId: string, wrapupRequired: boolean) => { + // Task has been ended based on wrapupRequired we either set wrapupRequired or remove the task + if (wrapupRequired) { + this.store.setWrapupRequired(wrapupRequired); + } else { + this.handleTaskRemove(taskId); + } }; handleTaskAssigned = (task: ITask) => () => { - this.store.incomingTask = null; - this.setCurrentTask(task); + runInAction(() => { + this.store.setCurrentTask(task); + this.store.setIncomingTask(null); + }); }; handleIncomingTask = (task: ITask) => { - console.log('Shreyas: Handling incoming task', task); - this.store.incomingTask = task; - console.log('Shreyas: setted incomingTask in store'); + this.store.setIncomingTask(task); if (this.store.taskList.some((t) => t.data.interactionId === task.data.interactionId)) { - console.log('Shreyas: Task already present in the taskList', task.data.interactionId); // Task already present in the taskList return; } // Attach event listeners to the task - console.log('Shreyas: Attaching event listeners to the task', task.data.interactionId); task.on(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => - this.handleTaskRemoved(task.data.interactionId, wrapupRequired) + this.handleTaskEnd(task.data.interactionId, wrapupRequired) ); task.on(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned(task)); - - this.store.taskList = [...this.store.taskList, task]; - console.log('Shreyas: Task added to task list', this.store.taskList); + this.store.setTaskList([...this.store.taskList, task]); }; setupIncomingTaskHandler() { - console.log('Shreyas: Setting up incoming task handler'); const ccSDK = this.store.cc; ccSDK.on(TASK_EVENTS.TASK_INCOMING, (task) => { - console.log('Shreyas: Incoming task event received', task); this.handleIncomingTask(task); }); } diff --git a/packages/contact-center/task/src/CallControl/index.tsx b/packages/contact-center/task/src/CallControl/index.tsx index 70561f11a..398dce1da 100644 --- a/packages/contact-center/task/src/CallControl/index.tsx +++ b/packages/contact-center/task/src/CallControl/index.tsx @@ -8,10 +8,9 @@ import CallControlPresentational from './call-control.presentational'; const CallControlComponent: React.FunctionComponent = ({onHoldResume, onEnd, onWrapUp}) => { const {logger, currentTask, wrapupCodes, wrapupRequired} = store; + const result = useCallControl({currentTask, onHoldResume, onEnd, onWrapUp, logger}); - const result = useCallControl({currentTask, onHoldResume, onEnd, onWrapUp, wrapupRequired, logger}); - - return ; + return ; }; const CallControl = observer(CallControlComponent); diff --git a/packages/contact-center/task/src/helper.ts b/packages/contact-center/task/src/helper.ts index d20402b8f..18e83ddc8 100644 --- a/packages/contact-center/task/src/helper.ts +++ b/packages/contact-center/task/src/helper.ts @@ -1,6 +1,7 @@ import {useState, useEffect, useCallback, useRef} from 'react'; import {ITask} from '@webex/plugin-cc'; import store from '@webex/cc-store'; +import {runInAction} from 'mobx'; import {TASK_EVENTS, useCallControlProps, UseTaskListProps, UseTaskProps} from './task.types'; // Hook for managing the task list @@ -95,7 +96,7 @@ export const useIncomingTask = (props: UseTaskProps) => { }; export const useCallControl = (props: useCallControlProps) => { - const {currentTask, onHoldResume, onEnd, onWrapUp, wrapupRequired, logger} = props; + const {currentTask, onHoldResume, onEnd, onWrapUp, logger} = props; const audioRef = useRef(null); // Ref for the audio element const logError = (message: string, method: string) => { @@ -171,6 +172,7 @@ export const useCallControl = (props: useCallControlProps) => { currentTask .wrapup({wrapUpReason: wrapUpReason, auxCodeId: auxCodeId}) .then(() => { + store.handleTaskRemove(currentTask.data.interactionId); if (onWrapUp) onWrapUp(); }) .catch((error: Error) => { @@ -185,6 +187,5 @@ export const useCallControl = (props: useCallControlProps) => { toggleHold, toggleRecording, wrapupCall, - wrapupRequired, }; }; diff --git a/packages/contact-center/task/src/task.types.ts b/packages/contact-center/task/src/task.types.ts index 9c0abb3f8..76322bd4e 100644 --- a/packages/contact-center/task/src/task.types.ts +++ b/packages/contact-center/task/src/task.types.ts @@ -205,7 +205,4 @@ export type CallControlPresentationalProps = Pick< | 'wrapupCall' >; -export type useCallControlProps = Pick< - ControlProps, - 'currentTask' | 'wrapupRequired' | 'onHoldResume' | 'onEnd' | 'onWrapUp' | 'logger' ->; +export type useCallControlProps = Pick; diff --git a/widgets-samples/cc/samples-cc-react-app/src/App.tsx b/widgets-samples/cc/samples-cc-react-app/src/App.tsx index 25c5dd30e..151d00cbf 100644 --- a/widgets-samples/cc/samples-cc-react-app/src/App.tsx +++ b/widgets-samples/cc/samples-cc-react-app/src/App.tsx @@ -12,6 +12,7 @@ function App() { }); const [accessToken, setAccessToken] = useState(''); const [isLoggedIn, setIsLoggedIn] = useState(false); + const [showWidgets, setShowWidgets] = useState(false); const webexConfig = { fedramp: false, @@ -133,20 +134,26 @@ function App() { )} - {isLoggedIn && ( + {showWidgets && ( <> {selectedWidgets.stationLogin && } - {selectedWidgets.userState && } - {selectedWidgets.incomingTask && } - {selectedWidgets.taskList && } - {selectedWidgets.callControl && } + {isLoggedIn && ( + <> + {selectedWidgets.userState && } + {selectedWidgets.incomingTask && } + {selectedWidgets.taskList && } + {selectedWidgets.callControl && ( + + )} + + )} )} From ca6850a85c151c130e11ee564b07df6ef0637fe9 Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Tue, 11 Feb 2025 00:28:08 +0530 Subject: [PATCH 03/23] fix(store): add-unit-tests --- packages/contact-center/store/package.json | 2 +- packages/contact-center/store/tests/store.ts | 95 ++-- .../store/tests/storeEventsWrapper.ts | 296 ++++++++++++ packages/contact-center/task/src/helper.ts | 6 +- .../contact-center/task/src/task.types.ts | 6 +- packages/contact-center/task/tests/helper.ts | 434 +++++++++--------- 6 files changed, 576 insertions(+), 263 deletions(-) create mode 100644 packages/contact-center/store/tests/storeEventsWrapper.ts diff --git a/packages/contact-center/store/package.json b/packages/contact-center/store/package.json index ee72ea4dd..3569dd5f2 100644 --- a/packages/contact-center/store/package.json +++ b/packages/contact-center/store/package.json @@ -17,7 +17,7 @@ "build": "yarn run -T tsc", "build:src": "yarn run clean:dist && webpack", "build:watch": "webpack --watch", - "test:unit": "jest" + "test:unit": "jest --coverage" }, "dependencies": { "mobx": "6.13.5", diff --git a/packages/contact-center/store/tests/store.ts b/packages/contact-center/store/tests/store.ts index 5b96b1744..f2d53d5bc 100644 --- a/packages/contact-center/store/tests/store.ts +++ b/packages/contact-center/store/tests/store.ts @@ -27,11 +27,11 @@ jest.mock('webex', () => ({ describe('Store', () => { let mockWebex; + let storeInstance; beforeEach(() => { - // Reset teams and loginOptions before each test since store is a singleton - store.teams = []; - store.loginOptions = []; + // Reset store values before each test since store is a singleton + storeInstance = store.getInstance(); mockWebex = Webex.init(); jest.useFakeTimers(); // Use fake timers for testing setTimeout }); @@ -41,11 +41,26 @@ describe('Store', () => { }); it('should initialize with default values', () => { - expect(store.teams).toEqual([]); - expect(store.loginOptions).toEqual([]); - expect(store.isAgentLoggedIn).toBe(false); - expect(store.deviceType).toBe(''); - expect(makeAutoObservable).toHaveBeenCalledWith(store, {cc: expect.any(Function), currentTask: expect.any(Object)}); + expect(storeInstance.teams).toEqual([]); + expect(storeInstance.loginOptions).toEqual([]); + expect(storeInstance.idleCodes).toEqual([]); + expect(storeInstance.agentId).toBe(''); + expect(storeInstance.selectedLoginOption).toBe(''); + expect(storeInstance.wrapupCodes).toEqual([]); + expect(storeInstance.incomingTask).toBeNull(); + expect(storeInstance.currentTask).toBeNull(); + expect(storeInstance.isAgentLoggedIn).toBe(false); + expect(storeInstance.deviceType).toBe(''); + expect(storeInstance.taskList).toEqual([]); + expect(storeInstance.wrapupRequired).toBe(false); + + expect(makeAutoObservable).toHaveBeenCalledWith(storeInstance, { + cc: expect.any(Function), + currentTask: expect.any(Object), + incomingTask: expect.any(Object), + taskList: expect.any(Object), + wrapupRequired: expect.any(Object), + }); }); describe('registerCC', () => { @@ -60,14 +75,14 @@ describe('Store', () => { }; mockWebex.cc.register.mockResolvedValue(mockResponse); - await store.registerCC(mockWebex); + await storeInstance.registerCC(mockWebex); - expect(store.teams).toEqual(mockResponse.teams); - expect(store.loginOptions).toEqual(mockResponse.loginVoiceOptions); - expect(store.idleCodes).toEqual(mockResponse.idleCodes); - expect(store.agentId).toEqual(mockResponse.agentId); - expect(store.isAgentLoggedIn).toEqual(mockResponse.isAgentLoggedIn); - expect(store.deviceType).toEqual(mockResponse.deviceType); + expect(storeInstance.teams).toEqual(mockResponse.teams); + expect(storeInstance.loginOptions).toEqual(mockResponse.loginVoiceOptions); + expect(storeInstance.idleCodes).toEqual(mockResponse.idleCodes); + expect(storeInstance.agentId).toEqual(mockResponse.agentId); + expect(storeInstance.isAgentLoggedIn).toEqual(mockResponse.isAgentLoggedIn); + expect(storeInstance.deviceType).toEqual(mockResponse.deviceType); }); it('should log an error on failed register', async () => { @@ -75,13 +90,16 @@ describe('Store', () => { mockWebex.cc.register.mockRejectedValue(mockError); try { - await store.registerCC(mockWebex); + await storeInstance.registerCC(mockWebex); } catch (error) { expect(error).toEqual(mockError); - expect(store.logger.error).toHaveBeenCalledWith('Error registering contact center: Error: Register failed', { - method: 'registerCC', - module: 'cc-store#store.ts', - }); + expect(storeInstance.logger.error).toHaveBeenCalledWith( + 'Error registering contact center: Error: Register failed', + { + method: 'registerCC', + module: 'cc-store#store.ts', + } + ); } }); }); @@ -89,12 +107,12 @@ describe('Store', () => { describe('init', () => { it('should call registerCC if webex is in options', async () => { const initParams = {webex: mockWebex}; - jest.spyOn(store, 'registerCC').mockResolvedValue(); + jest.spyOn(storeInstance, 'registerCC').mockResolvedValue(); Webex.init.mockClear(); - await store.init(initParams); + await storeInstance.init(initParams); - expect(store.registerCC).toHaveBeenCalledWith(mockWebex); + expect(storeInstance.registerCC).toHaveBeenCalledWith(mockWebex); expect(Webex.init).not.toHaveBeenCalled(); }); @@ -103,15 +121,15 @@ describe('Store', () => { webexConfig: {anyConfig: true}, access_token: 'fake_token', }; - jest.spyOn(store, 'registerCC').mockResolvedValue(); + jest.spyOn(storeInstance, 'registerCC').mockResolvedValue(); - await store.init(initParams); + await storeInstance.init(initParams); expect(Webex.init).toHaveBeenCalledWith({ config: initParams.webexConfig, credentials: {access_token: initParams.access_token}, }); - expect(store.registerCC).toHaveBeenCalledWith(expect.any(Object)); + expect(storeInstance.registerCC).toHaveBeenCalledWith(expect.any(Object)); }); it('should reject the promise if registerCC fails in init method', async () => { @@ -120,9 +138,9 @@ describe('Store', () => { access_token: 'fake_token', }; - jest.spyOn(store, 'registerCC').mockRejectedValue(new Error('registerCC failed')); + jest.spyOn(storeInstance, 'registerCC').mockRejectedValue(new Error('registerCC failed')); - await expect(store.init(initParams)).rejects.toThrow('registerCC failed'); + await expect(storeInstance.init(initParams)).rejects.toThrow('registerCC failed'); }); it('should reject the promise if Webex SDK fails to initialize', async () => { @@ -133,13 +151,30 @@ describe('Store', () => { mockShouldCallback = false; - jest.spyOn(store, 'registerCC').mockResolvedValue(); + jest.spyOn(storeInstance, 'registerCC').mockResolvedValue(); - const initPromise = store.init(initParams); + const initPromise = storeInstance.init(initParams); jest.runAllTimers(); // Fast-forward the timers to simulate timeout await expect(initPromise).rejects.toThrow('Webex SDK failed to initialize'); }); + + it('should set states when called through setters', () => { + storeInstance.setIncomingTask('task1'); + expect(storeInstance.incomingTask).toBe('task1'); + + storeInstance.setCurrentTask('task2'); + expect(storeInstance.currentTask).toBe('task2'); + + storeInstance.setWrapupRequired(true); + expect(storeInstance.wrapupRequired).toBe(true); + + storeInstance.setTaskList(['task3']); + expect(storeInstance.taskList).toEqual(['task3']); + + storeInstance.setSelectedLoginOption('option1'); + expect(storeInstance.selectedLoginOption).toBe('option1'); + }); }); }); diff --git a/packages/contact-center/store/tests/storeEventsWrapper.ts b/packages/contact-center/store/tests/storeEventsWrapper.ts new file mode 100644 index 000000000..1f09b999d --- /dev/null +++ b/packages/contact-center/store/tests/storeEventsWrapper.ts @@ -0,0 +1,296 @@ +import {act, waitFor} from '@testing-library/react'; +import {TASK_EVENTS} from '../src/store.types'; +import storeWrapper from '../src/storeEventsWrapper'; +import {ITask} from '@webex/plugin-cc'; + +jest.mock('../src/store', () => ({ + getInstance: jest.fn().mockReturnValue({ + teams: 'mockTeams', + loginOptions: 'mockLoginOptions', + cc: { + on: jest.fn(), + off: jest.fn(), + }, + logger: 'mockLogger', + idleCodes: 'mockIdleCodes', + agentId: 'mockAgentId', + selectedLoginOption: 'mockSelectedLoginOption', + wrapupCodes: 'mockWrapupCodes', + currentTask: 'mockCurrentTask', + isAgentLoggedIn: 'mockIsAgentLoggedIn', + deviceType: 'mockDeviceType', + taskList: 'mockTaskList', + incomingTask: 'mockIncomingTask', + wrapupRequired: 'mockWrapupRequired', + setSelectedLoginOption: jest.fn(), + init: jest.fn().mockResolvedValue({}), + setIncomingTask: jest.fn(), + setCurrentTask: jest.fn(), + setTaskList: jest.fn(), + setWrapupRequired: jest.fn(), + }), +})); + +describe('storeEventsWrapper', () => { + describe('storeEventsWrapper Proxies', () => { + it('should proxy teams', () => { + expect(storeWrapper.teams).toBe('mockTeams'); + }); + + it('should proxy loginOptions', () => { + expect(storeWrapper.loginOptions).toBe('mockLoginOptions'); + }); + + it.skip('should proxy cc', () => { + expect(storeWrapper.cc).toBe({ + on: jest.fn(), + off: jest.fn(), + }); + }); + + it('should proxy logger', () => { + expect(storeWrapper.logger).toBe('mockLogger'); + }); + + it('should proxy idleCodes', () => { + expect(storeWrapper.idleCodes).toBe('mockIdleCodes'); + }); + + it('should proxy agentId', () => { + expect(storeWrapper.agentId).toBe('mockAgentId'); + }); + + it('should proxy selectedLoginOption', () => { + expect(storeWrapper.selectedLoginOption).toBe('mockSelectedLoginOption'); + }); + + it('should proxy wrapupCodes', () => { + expect(storeWrapper.wrapupCodes).toBe('mockWrapupCodes'); + }); + + it('should proxy currentTask', () => { + expect(storeWrapper.currentTask).toBe('mockCurrentTask'); + }); + + it('should proxy isAgentLoggedIn', () => { + expect(storeWrapper.isAgentLoggedIn).toBe('mockIsAgentLoggedIn'); + }); + + it('should proxy deviceType', () => { + expect(storeWrapper.deviceType).toBe('mockDeviceType'); + }); + + it('should proxy taskList', () => { + expect(storeWrapper.taskList).toBe('mockTaskList'); + }); + + it('should proxy incomingTask', () => { + expect(storeWrapper.incomingTask).toBe('mockIncomingTask'); + }); + + it('should proxy wrapupRequired', () => { + expect(storeWrapper.wrapupRequired).toBe('mockWrapupRequired'); + }); + }); + + describe('storeEventsWrapper', () => { + const mockTask: ITask = ({ + data: { + interactionId: 'interaction1', + }, + on: jest.fn(), + off: jest.fn(), + } as unknown) as ITask; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should initialize the store and set up incoming task handler', async () => { + const options = {someOption: 'value'}; + await storeWrapper.init(options); + + expect(storeWrapper['store'].init).toHaveBeenCalledWith(options); + expect(storeWrapper['store'].cc.on).toHaveBeenCalledWith(TASK_EVENTS.TASK_INCOMING, expect.any(Function)); + }); + + it('should handle incoming task', () => { + storeWrapper['store'].taskList = []; + storeWrapper.handleIncomingTask(mockTask); + + expect(storeWrapper['store'].setIncomingTask).toHaveBeenCalledWith(mockTask); + expect(mockTask.on).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, expect.any(Function)); + expect(mockTask.on).toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, expect.any(Function)); + expect(storeWrapper['store'].setTaskList).toHaveBeenCalledWith([mockTask]); + }); + + it('should handle task assignment', () => { + const handleTaskAssigned = storeWrapper.handleTaskAssigned(mockTask); + handleTaskAssigned(); + + expect(storeWrapper['store'].setCurrentTask).toHaveBeenCalledWith(mockTask); + expect(storeWrapper['store'].setIncomingTask).toHaveBeenCalledWith(null); + }); + + it('should handle task removal', () => { + storeWrapper['store'].taskList = [mockTask]; + storeWrapper.handleTaskRemove(mockTask.data.interactionId); + + expect(mockTask.off).toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, expect.any(Function)); + expect(mockTask.off).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, expect.any(Function)); + expect(storeWrapper['store'].setTaskList).toHaveBeenCalledWith([]); + expect(storeWrapper['store'].setCurrentTask).toHaveBeenCalledWith(null); + expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(false); + }); + + it('should handle task removal when no task is present', () => { + storeWrapper['store'].taskList = []; + storeWrapper.handleTaskRemove(mockTask.data.interactionId); + + expect(mockTask.off).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, expect.any(Function)); + expect(mockTask.off).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_END, expect.any(Function)); + expect(storeWrapper['store'].setTaskList).toHaveBeenCalledWith([]); + expect(storeWrapper['store'].setCurrentTask).toHaveBeenCalledWith(null); + expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(false); + }); + + it('should handle task end', () => { + storeWrapper.handleTaskEnd(mockTask.data.interactionId, true); + + expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(true); + + storeWrapper.handleTaskEnd(mockTask.data.interactionId, false); + + expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(false); + }); + + it('should set selected login option', () => { + const option = 'newLoginOption'; + storeWrapper.setSelectedLoginOption(option); + + expect(storeWrapper['store'].setSelectedLoginOption).toHaveBeenCalledWith(option); + }); + }); + + describe('storeEventsWrapper events reactions', () => { + const mockTask: ITask = ({ + data: { + interactionId: 'interaction1', + }, + on: jest.fn(), + off: jest.fn(), + } as unknown) as ITask; + + const options = {someOption: 'value'}; + + beforeEach(async () => { + jest.clearAllMocks(); + }); + + it('should initialize the store and set up incoming task handler', async () => { + await storeWrapper.init(options); + expect(storeWrapper['store'].init).toHaveBeenCalledWith(options); + expect(storeWrapper['store'].cc.on).toHaveBeenCalledWith(TASK_EVENTS.TASK_INCOMING, expect.any(Function)); + }); + + it('should handle task:incoming event ', async () => { + await storeWrapper.init(options); + storeWrapper['store'].taskList = []; + + act(() => { + storeWrapper['cc'].on.mock.calls[0][1](mockTask); + }); + + waitFor(() => { + expect(storeWrapper['store'].setIncomingTask).toHaveBeenCalledWith(mockTask); + expect(mockTask.on).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, expect.any(Function)); + expect(mockTask.on).toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, expect.any(Function)); + expect(storeWrapper['store'].setTaskList).toHaveBeenCalledWith([mockTask]); + }); + }); + + it('should handle task:end event with wrapupRequired', async () => { + await storeWrapper.init(options); + storeWrapper['store'].taskList = []; + + // Incoming task stage: a task has just reached the agent + act(() => { + storeWrapper['cc'].on.mock.calls[0][1](mockTask); + }); + + waitFor(() => { + expect(storeWrapper['store'].setIncomingTask).toHaveBeenCalledWith(mockTask); + expect(mockTask.on).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, expect.any(Function)); + expect(mockTask.on).toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, expect.any(Function)); + expect(storeWrapper['store'].setTaskList).toHaveBeenCalledWith([mockTask]); + }); + + // The call is answered and the task is assigned to the agent + act(() => { + mockTask.on.mock.calls[1][1](); + }); + + waitFor(() => { + // The task is assigned to the agent + expect(storeWrapper['store'].setCurrentTask).toHaveBeenCalledWith(mockTask); + expect(storeWrapper['store'].setIncomingTask).toHaveBeenCalledWith(null); + }); + + // Task end stage: the task is completed + act(() => { + storeWrapper['store'].taskList = [mockTask]; + mockTask.on.mock.calls[0][1]({wrapupRequired: true}); + }); + + waitFor(() => { + expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(true); + expect(mockTask.off).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, expect.any(Function)); + expect(mockTask.off).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_END, expect.any(Function)); + }); + }); + + it('should not add a duplicate task if the same task is present in store', () => { + storeWrapper['store'].taskList = [mockTask]; + storeWrapper.handleIncomingTask(mockTask); + + expect(storeWrapper['store'].setIncomingTask).toHaveBeenCalledWith(mockTask); + expect(mockTask.on).not.toHaveBeenCalledWith; + }); + + it('should handle task assignment', () => { + const handleTaskAssigned = storeWrapper.handleTaskAssigned(mockTask); + handleTaskAssigned(); + + expect(storeWrapper['store'].setCurrentTask).toHaveBeenCalledWith(mockTask); + expect(storeWrapper['store'].setIncomingTask).toHaveBeenCalledWith(null); + }); + + it('should handle task removal', () => { + storeWrapper['store'].taskList = [mockTask]; + storeWrapper.handleTaskRemove(mockTask.data.interactionId); + + expect(mockTask.off).toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, expect.any(Function)); + expect(mockTask.off).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, expect.any(Function)); + expect(storeWrapper['store'].setTaskList).toHaveBeenCalledWith([]); + expect(storeWrapper['store'].setCurrentTask).toHaveBeenCalledWith(null); + expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(false); + }); + + it('should handle task end', () => { + storeWrapper.handleTaskEnd(mockTask.data.interactionId, true); + + expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(true); + + storeWrapper.handleTaskEnd(mockTask.data.interactionId, false); + + expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(false); + }); + + it('should set selected login option', () => { + const option = 'newLoginOption'; + storeWrapper.setSelectedLoginOption(option); + + expect(storeWrapper['store'].setSelectedLoginOption).toHaveBeenCalledWith(option); + }); + }); +}); diff --git a/packages/contact-center/task/src/helper.ts b/packages/contact-center/task/src/helper.ts index 18e83ddc8..d2fde5a9d 100644 --- a/packages/contact-center/task/src/helper.ts +++ b/packages/contact-center/task/src/helper.ts @@ -1,7 +1,6 @@ -import {useState, useEffect, useCallback, useRef} from 'react'; +import {useEffect, useCallback, useRef} from 'react'; import {ITask} from '@webex/plugin-cc'; import store from '@webex/cc-store'; -import {runInAction} from 'mobx'; import {TASK_EVENTS, useCallControlProps, UseTaskListProps, UseTaskProps} from './task.types'; // Hook for managing the task list @@ -38,7 +37,6 @@ export const useTaskList = (props: UseTaskListProps) => { .decline(taskId) .then(() => { onTaskDeclined && onTaskDeclined(task); - // store.setCurrentTask(null); }) .catch((error: Error) => { logError(`Error declining task: ${error}`, 'declineTask'); @@ -172,8 +170,8 @@ export const useCallControl = (props: useCallControlProps) => { currentTask .wrapup({wrapUpReason: wrapUpReason, auxCodeId: auxCodeId}) .then(() => { - store.handleTaskRemove(currentTask.data.interactionId); if (onWrapUp) onWrapUp(); + store.handleTaskRemove(currentTask.data.interactionId); }) .catch((error: Error) => { logError(`Error wrapping up call: ${error}`, 'wrapupCall'); diff --git a/packages/contact-center/task/src/task.types.ts b/packages/contact-center/task/src/task.types.ts index 76322bd4e..4bb7bb0eb 100644 --- a/packages/contact-center/task/src/task.types.ts +++ b/packages/contact-center/task/src/task.types.ts @@ -138,17 +138,17 @@ export interface ControlProps { /** * Function to handle hold/resume actions. */ - onHoldResume: () => void; + onHoldResume?: () => void; /** * Function to handle ending the task. */ - onEnd: () => void; + onEnd?: () => void; /** * Function to handle wrapping up the task. */ - onWrapUp: () => void; + onWrapUp?: () => void; /** * Logger instance for logging purposes. diff --git a/packages/contact-center/task/tests/helper.ts b/packages/contact-center/task/tests/helper.ts index ab8948979..f716cd164 100644 --- a/packages/contact-center/task/tests/helper.ts +++ b/packages/contact-center/task/tests/helper.ts @@ -21,7 +21,7 @@ const taskMock = { const onAccepted = jest.fn(); const onDeclined = jest.fn(); -const onTaskAccepted = jest.fn(); +const onTaskAccepted = jest.fn().mockImplementation(() => {}); const onTaskDeclined = jest.fn(); const logger = { @@ -34,52 +34,79 @@ describe('useIncomingTask Hook', () => { logger.error.mockRestore(); }); - it('should register task events for the current task', async () => { + it('should call onAccepted if it is provided', async () => { const {result} = renderHook(() => - useIncomingTask({cc: ccMock, onAccepted, onDeclined, selectedLoginOption: 'BROWSER', logger}) + useIncomingTask({ + cc: ccMock, + incomingTask: taskMock, + onAccepted: onTaskAccepted, + onDeclined: onTaskDeclined, + selectedLoginOption: 'BROWSER', + logger, + }) ); act(() => { - ccMock.on.mock.calls[0][1](taskMock); + result.current.accept(); }); await waitFor(() => { - expect(taskMock.on).toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, expect.any(Function)); - expect(taskMock.on).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, expect.any(Function)); + expect(onTaskAccepted).toHaveBeenCalled(); }); // Ensure no errors are logged expect(logger.error).not.toHaveBeenCalled(); }); - it('should not call onAccepted if it is not provided', async () => { + it('should call onDeclined if it is provided', async () => { const {result} = renderHook(() => - useIncomingTask({cc: ccMock, onAccepted: null, onDeclined: null, selectedLoginOption: 'BROWSER', logger}) + useIncomingTask({ + cc: ccMock, + incomingTask: taskMock, + onAccepted: onTaskAccepted, + onDeclined: onTaskDeclined, + selectedLoginOption: 'BROWSER', + logger, + }) ); act(() => { - ccMock.on.mock.calls[0][1](taskMock); - }); - - act(() => { - result.current.accept(); + result.current.decline(); }); await waitFor(() => { - expect(onAccepted).not.toHaveBeenCalled(); + expect(onTaskDeclined).toHaveBeenCalled(); }); // Ensure no errors are logged expect(logger.error).not.toHaveBeenCalled(); }); - it('should not call onDeclined if it is not provided', async () => { + it('should return if there is no taskId for incoming task', async () => { + const noIdTask = { + data: {}, + accept: jest.fn(), + decline: jest.fn(), + on: jest.fn(), + off: jest.fn(), + }; const {result} = renderHook(() => - useIncomingTask({cc: ccMock, onAccepted: null, onDeclined: null, selectedLoginOption: 'BROWSER', logger}) + useIncomingTask({ + cc: ccMock, + incomingTask: noIdTask, + onAccepted: onTaskAccepted, + onDeclined: onTaskDeclined, + selectedLoginOption: 'BROWSER', + logger, + }) ); act(() => { - ccMock.on.mock.calls[0][1](taskMock); + result.current.accept(); + }); + + await waitFor(() => { + expect(onTaskAccepted).not.toHaveBeenCalled(); }); act(() => { @@ -87,27 +114,52 @@ describe('useIncomingTask Hook', () => { }); await waitFor(() => { - expect(onDeclined).not.toHaveBeenCalled(); + expect(onTaskDeclined).not.toHaveBeenCalled(); + }); + }); + + it('should not call onAccepted if it is not provided', async () => { + const {result} = renderHook(() => + useIncomingTask({ + cc: ccMock, + incomingTask: taskMock, + onAccepted: null, + onDeclined: null, + selectedLoginOption: 'BROWSER', + logger, + }) + ); + + act(() => { + result.current.accept(); + }); + + await waitFor(() => { + expect(onAccepted).not.toHaveBeenCalled(); }); // Ensure no errors are logged expect(logger.error).not.toHaveBeenCalled(); }); - it('should clean up task events on task change or unmount', async () => { - const {result, unmount} = renderHook(() => - useIncomingTask({cc: ccMock, onAccepted, onDeclined, selectedLoginOption: 'BROWSER', logger}) + it('should not call onDeclined if it is not provided', async () => { + const {result} = renderHook(() => + useIncomingTask({ + cc: ccMock, + incomingTask: taskMock, + onAccepted: null, + onDeclined: null, + selectedLoginOption: 'BROWSER', + logger, + }) ); act(() => { - ccMock.on.mock.calls[0][1](taskMock); + result.current.decline(); }); - unmount(); - await waitFor(() => { - expect(taskMock.off).toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, expect.any(Function)); - expect(ccMock.off).toHaveBeenCalledWith(TASK_EVENTS.TASK_INCOMING, expect.any(Function)); + expect(onDeclined).not.toHaveBeenCalled(); }); // Ensure no errors are logged @@ -122,13 +174,9 @@ describe('useIncomingTask Hook', () => { }; const {result} = renderHook(() => - useIncomingTask({cc: ccMock, onAccepted, selectedLoginOption: 'BROWSER', logger}) + useIncomingTask({cc: ccMock, incomingTask: failingTask, onAccepted, selectedLoginOption: 'BROWSER', logger}) ); - act(() => { - ccMock.on.mock.calls[0][1](failingTask); - }); - act(() => { result.current.accept(); }); @@ -153,13 +201,9 @@ describe('useIncomingTask Hook', () => { }; const {result} = renderHook(() => - useIncomingTask({cc: ccMock, onDeclined, selectedLoginOption: 'BROWSER', logger}) + useIncomingTask({cc: ccMock, incomingTask: failingTask, onDeclined, selectedLoginOption: 'BROWSER', logger}) ); - act(() => { - ccMock.on.mock.calls[0][1](failingTask); - }); - act(() => { result.current.decline(); }); @@ -178,13 +222,16 @@ describe('useIncomingTask Hook', () => { }); describe('useTaskList Hook', () => { + const mockTaskList = [taskMock, taskMock]; afterEach(() => { jest.clearAllMocks(); logger.error.mockRestore(); }); it('should call onTaskAccepted callback when provided', async () => { - const {result} = renderHook(() => useTaskList({cc: ccMock, selectedLoginOption: '', onTaskAccepted, logger})); + const {result} = renderHook(() => + useTaskList({cc: ccMock, selectedLoginOption: '', onTaskAccepted, logger, taskList: mockTaskList}) + ); act(() => { result.current.acceptTask(taskMock); @@ -198,8 +245,42 @@ describe('useTaskList Hook', () => { expect(logger.error).not.toHaveBeenCalled(); }); + it('should return if not task is passed while calling acceptTask', async () => { + // This test is purely to improve the coverage report, as the acceptTask function cannot be called without a task + const {result} = renderHook(() => + useTaskList({cc: ccMock, selectedLoginOption: '', onTaskAccepted, logger, taskList: mockTaskList}) + ); + + act(() => { + // @ts-ignore + result.current.acceptTask(); + }); + + await waitFor(() => { + expect(onTaskAccepted).not.toHaveBeenCalledWith(taskMock); + }); + }); + + it('should return if not task is passed while calling acceptTask', async () => { + // This test is purely to improve the coverage report, as the acceptTask function cannot be called without a task + const {result} = renderHook(() => + useTaskList({cc: ccMock, selectedLoginOption: '', onTaskDeclined, logger, taskList: mockTaskList}) + ); + + act(() => { + // @ts-ignore + result.current.declineTask(); + }); + + await waitFor(() => { + expect(onTaskDeclined).not.toHaveBeenCalledWith(taskMock); + }); + }); + it('should call onTaskDeclined callback when provided', async () => { - const {result} = renderHook(() => useTaskList({cc: ccMock, selectedLoginOption: '', onTaskDeclined, logger})); + const {result} = renderHook(() => + useTaskList({cc: ccMock, selectedLoginOption: '', onTaskDeclined, logger, taskList: mockTaskList}) + ); act(() => { result.current.declineTask(taskMock); @@ -221,13 +302,9 @@ describe('useTaskList Hook', () => { }; const {result} = renderHook(() => - useTaskList({cc: ccMock, onTaskAccepted, selectedLoginOption: 'BROWSER', logger}) + useTaskList({cc: ccMock, onTaskAccepted, selectedLoginOption: 'BROWSER', logger, taskList: mockTaskList}) ); - act(() => { - ccMock.on.mock.calls[0][1](failingTask); - }); - act(() => { result.current.acceptTask(failingTask); }); @@ -252,13 +329,9 @@ describe('useTaskList Hook', () => { }; const {result} = renderHook(() => - useTaskList({cc: ccMock, onTaskDeclined, selectedLoginOption: 'BROWSER', logger}) + useTaskList({cc: ccMock, onTaskDeclined, selectedLoginOption: 'BROWSER', logger, taskList: mockTaskList}) ); - act(() => { - ccMock.on.mock.calls[0][1](failingTask); - }); - act(() => { result.current.declineTask(failingTask); }); @@ -275,24 +348,16 @@ describe('useTaskList Hook', () => { }); }); - it('should add tasks to the list on TASK_INCOMING event', async () => { - const {result} = renderHook(() => useTaskList({cc: ccMock, logger, selectedLoginOption: ''})); - - act(() => { - ccMock.on.mock.calls[0][1](taskMock); - }); - - await waitFor(() => { - expect(result.current.taskList).toContain(taskMock); - }); - - // Ensure no errors are logged - expect(logger.error).not.toHaveBeenCalled(); - }); - it('should not call onTaskAccepted if it is not provided', async () => { const {result} = renderHook(() => - useTaskList({cc: ccMock, onTaskAccepted: null, onTaskDeclined: null, logger, selectedLoginOption: ''}) + useTaskList({ + cc: ccMock, + onTaskAccepted: null, + onTaskDeclined: null, + logger, + selectedLoginOption: '', + taskList: mockTaskList, + }) ); act(() => { @@ -309,7 +374,14 @@ describe('useTaskList Hook', () => { it('should not call onTaskDeclined if it is not provided', async () => { const {result} = renderHook(() => - useTaskList({cc: ccMock, onTaskAccepted: null, onTaskDeclined: null, logger, selectedLoginOption: ''}) + useTaskList({ + cc: ccMock, + onTaskAccepted: null, + onTaskDeclined: null, + logger, + selectedLoginOption: '', + taskList: mockTaskList, + }) ); act(() => { @@ -323,130 +395,13 @@ describe('useTaskList Hook', () => { // Ensure no errors are logged expect(logger.error).not.toHaveBeenCalled(); }); - - it('should remove a task from the list when it ends', async () => { - const {result} = renderHook(() => useTaskList({cc: ccMock, logger, selectedLoginOption: ''})); - - act(() => { - ccMock.on.mock.calls[0][1](taskMock); - }); - - act(() => { - taskMock.on.mock.calls.find((call) => call[0] === TASK_EVENTS.TASK_END)?.[1](); - }); - - await waitFor(() => { - expect(result.current.taskList).not.toContain(taskMock); - }); - - // Ensure no errors are logged - expect(logger.error).not.toHaveBeenCalled(); - }); - - it('should update an existing task in the list', async () => { - const {result} = renderHook(() => useTaskList({cc: ccMock, logger, selectedLoginOption: ''})); - - act(() => { - ccMock.on.mock.calls[0][1](taskMock); - }); - - const updatedTask = {...taskMock, data: {interactionId: 'interaction1', status: 'updated'}}; - act(() => { - taskMock.on.mock.calls.find((call) => call[0] === TASK_EVENTS.TASK_ASSIGNED)?.[1](updatedTask); - }); - - await waitFor(() => {}); - - // Ensure no errors are logged - expect(logger.error).not.toHaveBeenCalled(); - }); - - it('should deduplicate tasks by interactionId', async () => { - const {result} = renderHook(() => useTaskList({cc: ccMock, logger, selectedLoginOption: ''})); - - act(() => { - ccMock.on.mock.calls[0][1](taskMock); - ccMock.on.mock.calls[0][1](taskMock); - }); - - await waitFor(() => { - expect(result.current.taskList.length).toBe(1); - }); - - // Ensure no errors are logged - expect(logger.error).not.toHaveBeenCalled(); - }); - - describe('useIncomingTask Hook - Task Events', () => { - afterEach(() => { - jest.clearAllMocks(); - logger.error.mockRestore(); - }); - - it('should set isAnswered to true when task is assigned', async () => { - const {result} = renderHook(() => - useIncomingTask({ - cc: ccMock, - onAccepted, - onDeclined, - selectedLoginOption: 'BROWSER', - logger, - selectedLoginOption: '', - }) - ); - - // Simulate task being assigned - act(() => { - ccMock.on.mock.calls[0][1](taskMock); // Simulate incoming task - }); - - act(() => { - taskMock.on.mock.calls.find((call) => call[0] === TASK_EVENTS.TASK_ASSIGNED)?.[1](); // Trigger task assigned - }); - - await waitFor(() => { - expect(result.current.isAnswered).toBe(true); - }); - - // Ensure no errors are logged - expect(logger.error).not.toHaveBeenCalled(); - }); - - it('should set isEnded to true and clear currentTask when task ends', async () => { - const {result} = renderHook(() => - useIncomingTask({ - cc: ccMock, - onAccepted, - onDeclined, - selectedLoginOption: 'BROWSER', - logger, - selectedLoginOption: '', - }) - ); - - // Simulate task being assigned - act(() => { - ccMock.on.mock.calls[0][1](taskMock); // Simulate incoming task - }); - - // Simulate task ending - act(() => { - taskMock.on.mock.calls.find((call) => call[0] === TASK_EVENTS.TASK_END)?.[1](); // Trigger task end - }); - - await waitFor(() => { - expect(result.current.isEnded).toBe(true); - expect(result.current.incomingTask).toBeNull(); - }); - - // Ensure no errors are logged - expect(logger.error).not.toHaveBeenCalled(); - }); - }); }); describe('useCallControl', () => { const mockCurrentTask = { + data: { + interactionId: 'someMockInteractionId', + }, on: jest.fn(), off: jest.fn(), hold: jest.fn(() => Promise.resolve()), @@ -484,33 +439,35 @@ describe('useCallControl', () => { logger.error.mockRestore(); }); - it('should set up and clean up event listeners on currentTask', () => { - renderHook(() => + it('should not call any call backs if callbacks are not provided', async () => { + mockCurrentTask.hold.mockRejectedValueOnce(new Error('Hold error')); + + const {result} = renderHook(() => useCallControl({ currentTask: mockCurrentTask, - onHoldResume: mockOnHoldResume, - onEnd: mockOnEnd, - onWrapUp: mockOnWrapUp, logger: mockLogger, }) ); - expect(mockCurrentTask.on).toHaveBeenCalledWith('task:end', expect.any(Function)); + await act(async () => { + await result.current.toggleHold(true); + }); - // Cleanup on unmount - const {unmount} = renderHook(() => - useCallControl({ - currentTask: mockCurrentTask, - onHoldResume: mockOnHoldResume, - onEnd: mockOnEnd, - onWrapUp: mockOnWrapUp, - logger: mockLogger, - }) - ); + await act(async () => { + await result.current.toggleHold(false); + }); - unmount(); + await act(async () => { + await result.current.endCall(); + }); - expect(mockCurrentTask.off).toHaveBeenCalledWith('task:end', expect.any(Function)); + await act(async () => { + await result.current.wrapupCall('Wrap reason', '123'); + }); + + expect(mockOnHoldResume).not.toHaveBeenCalled(); + expect(mockOnEnd).not.toHaveBeenCalled(); + expect(mockOnWrapUp).not.toHaveBeenCalled(); }); it('should call holdResume with hold=true and handle success', async () => { @@ -571,7 +528,7 @@ describe('useCallControl', () => { expect(mockLogger.error).toHaveBeenCalledWith('Error holding call: Error: Hold error', expect.any(Object)); }); - it('should log an error if hold fails', async () => { + it('should log an error if resume fails', async () => { mockCurrentTask.resume.mockRejectedValueOnce(new Error('Resume error')); const {result} = renderHook(() => @@ -610,23 +567,6 @@ describe('useCallControl', () => { expect(mockOnEnd).toHaveBeenCalled(); }); - it('should update wrapupRequired on TASK_END event', async () => { - const {result} = renderHook(() => - useCallControl({ - currentTask: mockCurrentTask, - onHoldResume: mockOnHoldResume, - onEnd: mockOnEnd, - onWrapUp: mockOnWrapUp, - logger: mockLogger, - }) - ); - - await act(async () => { - await mockCurrentTask.on.mock.calls.find((call) => call[0] === TASK_EVENTS.TASK_END)?.[1]({wrapupRequired: true}); - }); - expect(result.current.wrapupRequired).toBe(true); - }); - it('should call endCall and handle failure', async () => { mockCurrentTask.end.mockRejectedValueOnce(new Error('End error')); const {result} = renderHook(() => @@ -644,10 +584,11 @@ describe('useCallControl', () => { }); expect(mockCurrentTask.end).toHaveBeenCalled(); + expect(mockOnEnd).not.toHaveBeenCalled(); expect(mockLogger.error).toHaveBeenCalledWith('Error ending call: Error: End error', expect.any(Object)); }); - it('should call wrapupCall and handle success', async () => { + it('should call wrapupCall ', async () => { const {result} = renderHook(() => useCallControl({ currentTask: mockCurrentTask, @@ -659,10 +600,10 @@ describe('useCallControl', () => { ); await act(async () => { - await result.current.wrapupCall('Wrap reason', 123); + await result.current.wrapupCall('Wrap reason', '123'); }); - expect(mockCurrentTask.wrapup).toHaveBeenCalledWith({wrapUpReason: 'Wrap reason', auxCodeId: 123}); + expect(mockCurrentTask.wrapup).toHaveBeenCalledWith({wrapUpReason: 'Wrap reason', auxCodeId: '123'}); expect(mockOnWrapUp).toHaveBeenCalled(); }); @@ -782,7 +723,7 @@ describe('useCallControl', () => { ); act(() => { - mockCurrentTask.on.mock.calls[0][1](mockAudio); + mockCurrentTask.on.mock.calls.find((call) => call[0] === TASK_EVENTS.TASK_MEDIA)?.[1](mockAudio); }); await waitFor(() => { @@ -897,4 +838,47 @@ describe('useCallControl', () => { // Ensure no errors are logged expect(logger.error).not.toHaveBeenCalled(); }); + + it('should not add media events if task is not available', async () => { + const mockAudioElement = {current: {srcObject: null}}; + jest.spyOn(React, 'useRef').mockReturnValue(mockAudioElement); + + renderHook(() => + useCallControl({ + currentTask: undefined, + onHoldResume: mockOnHoldResume, + onEnd: mockOnEnd, + onWrapUp: mockOnWrapUp, + logger: mockLogger, + }) + ); + // Ensure no event handler is set + expect(taskMock.on).not.toHaveBeenCalled(); + }); + + it('should test undefined audioRef.current branch', async () => { + // This test is to improve the coverage + const {result} = renderHook(() => + useCallControl({ + currentTask: mockCurrentTask, + onHoldResume: mockOnHoldResume, + onEnd: mockOnEnd, + onWrapUp: mockOnWrapUp, + logger: mockLogger, + }) + ); + + result.current.audioRef.current = undefined; + const mockTrack = new MediaStreamTrack(); + + act(() => { + const taskAssignedCallback = mockCurrentTask.on.mock.calls.find( + (call) => call[0] === TASK_EVENTS.TASK_MEDIA + )?.[1]; + + if (taskAssignedCallback) { + taskAssignedCallback(mockTrack); + } + }); + }); }); From cf1ed9945faca3d0325ea3e46d95343b73822a51 Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Tue, 11 Feb 2025 17:44:54 +0530 Subject: [PATCH 04/23] fix(store): move-multilogin-userstate-event-to-store --- .../contact-center/station-login/package.json | 2 +- .../station-login/src/helper.ts | 20 +- .../src/station-login/alert-modal.scss | 37 --- .../src/station-login/constants.ts | 2 - .../station-login/src/station-login/index.tsx | 6 +- .../station-login.presentational.tsx | 1 - .../station-login/station-login.style.scss | 38 +++ .../src/station-login/station-login.types.ts | 2 +- .../station-login/tests/helper.ts | 241 ++++++++++++++---- packages/contact-center/store/src/store.ts | 6 + .../contact-center/store/src/store.types.ts | 19 +- .../store/src/storeEventsWrapper.ts | 55 +++- packages/contact-center/store/tests/store.ts | 12 + .../store/tests/storeEventsWrapper.ts | 124 ++++++++- .../contact-center/user-state/package.json | 2 +- .../user-state/src/constants.ts | 1 - .../contact-center/user-state/src/helper.ts | 21 +- .../contact-center/user-state/tests/helper.ts | 61 +---- 18 files changed, 450 insertions(+), 200 deletions(-) delete mode 100644 packages/contact-center/station-login/src/station-login/alert-modal.scss delete mode 100644 packages/contact-center/user-state/src/constants.ts diff --git a/packages/contact-center/station-login/package.json b/packages/contact-center/station-login/package.json index c24705845..cae5f31ae 100644 --- a/packages/contact-center/station-login/package.json +++ b/packages/contact-center/station-login/package.json @@ -16,7 +16,7 @@ "build": "yarn run -T tsc", "build:src": "yarn run clean:dist && webpack", "build:watch": "webpack --watch", - "test:unit": "jest" + "test:unit": "jest --coverage" }, "dependencies": { "@webex/cc-store": "workspace:*", diff --git a/packages/contact-center/station-login/src/helper.ts b/packages/contact-center/station-login/src/helper.ts index 96d644606..950c3847d 100644 --- a/packages/contact-center/station-login/src/helper.ts +++ b/packages/contact-center/station-login/src/helper.ts @@ -2,7 +2,6 @@ import {useState, useEffect} from 'react'; import {StationLoginSuccess, StationLogoutSuccess} from '@webex/plugin-cc'; import {UseStationLoginProps} from './station-login/station-login.types'; import store from '@webex/cc-store'; // we need to import as we are losing the context of this in store -import {AGENT_MULTI_LOGIN} from './station-login/constants'; export const useStationLogin = (props: UseStationLoginProps) => { const cc = props.cc; @@ -16,21 +15,6 @@ export const useStationLogin = (props: UseStationLoginProps) => { const [loginSuccess, setLoginSuccess] = useState(); const [loginFailure, setLoginFailure] = useState(); const [logoutSuccess, setLogoutSuccess] = useState(); - const [showMultipleLoginAlert, setShowMultipleLoginAlert] = useState(false); - - useEffect(() => { - const handleMultiLoginCloseSession = (data) => { - if (data && typeof data === 'object' && data.type === 'AgentMultiLoginCloseSession') { - setShowMultipleLoginAlert(true); - } - }; - - cc.on(AGENT_MULTI_LOGIN, handleMultiLoginCloseSession); - - return () => { - cc.off(AGENT_MULTI_LOGIN, handleMultiLoginCloseSession); - }; - }, [cc]); useEffect(() => { setIsAgentLoggedIn(props.isAgentLoggedIn); @@ -38,7 +22,7 @@ export const useStationLogin = (props: UseStationLoginProps) => { const handleContinue = async () => { try { - setShowMultipleLoginAlert(false); + store.setShowMultipleLoginAlert(false); const profile = await cc.register(); if (profile.isAgentLoggedIn) { logger.log(`Agent Relogin Success`, { @@ -76,7 +60,6 @@ export const useStationLogin = (props: UseStationLoginProps) => { } }) .catch((error: Error) => { - logger.error(`Error logging in: ${error}`, { module: 'widget-station-login#helper.ts', method: 'login', @@ -120,7 +103,6 @@ export const useStationLogin = (props: UseStationLoginProps) => { loginSuccess, loginFailure, logoutSuccess, - showMultipleLoginAlert, isAgentLoggedIn, handleContinue, }; diff --git a/packages/contact-center/station-login/src/station-login/alert-modal.scss b/packages/contact-center/station-login/src/station-login/alert-modal.scss deleted file mode 100644 index 138eb3652..000000000 --- a/packages/contact-center/station-login/src/station-login/alert-modal.scss +++ /dev/null @@ -1,37 +0,0 @@ -.modal { - width: 400px; - border: none; - border-radius: 10px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); - padding: 20px; - text-align: center; - } - - .modal-content { - display: flex; - justify-content: flex-end; // Aligns the button to the right - } - - .modal::backdrop { - background: rgba(0, 0, 0, 0.5); - } - - h2 { - margin-top: 0; - } - - #ContinueButton { - background-color: #0078d4; - color: white; - border: none; - padding: 10px 20px; - border-radius: 5px; - cursor: pointer; - font-size: 16px; - cursor: pointer; - } - - #ContinueButton:hover { - background-color: #005a9e; - } - \ No newline at end of file diff --git a/packages/contact-center/station-login/src/station-login/constants.ts b/packages/contact-center/station-login/src/station-login/constants.ts index d42fff983..0705defb0 100644 --- a/packages/contact-center/station-login/src/station-login/constants.ts +++ b/packages/contact-center/station-login/src/station-login/constants.ts @@ -2,5 +2,3 @@ export const MULTIPLE_SIGN_IN_ALERT_MESSAGE = 'You are signed in to the Desktop in multiple application instances. Click Continue to proceed with the Desktop in this application instance. Else, close this window.'; export const MULTIPLE_SIGN_IN_ALERT_TITLE = 'Multiple Sign In Alert'; - -export const AGENT_MULTI_LOGIN = 'agent:multiLogin'; diff --git a/packages/contact-center/station-login/src/station-login/index.tsx b/packages/contact-center/station-login/src/station-login/index.tsx index c7f6c5235..12dd56544 100644 --- a/packages/contact-center/station-login/src/station-login/index.tsx +++ b/packages/contact-center/station-login/src/station-login/index.tsx @@ -7,15 +7,13 @@ import {useStationLogin} from '../helper'; import {StationLoginProps} from './station-login.types'; const StationLoginComponent: React.FunctionComponent = ({onLogin, onLogout}) => { - const {cc, teams, loginOptions, logger, deviceType, isAgentLoggedIn, handleContinue, showMultipleLoginAlert} = store; + const {cc, teams, loginOptions, logger, deviceType, isAgentLoggedIn, showMultipleLoginAlert} = store; const result = useStationLogin({ cc, onLogin, onLogout, logger, isAgentLoggedIn, - handleContinue, - showMultipleLoginAlert, }); const props = { @@ -24,7 +22,7 @@ const StationLoginComponent: React.FunctionComponent = ({onLo loginOptions, deviceType, }; - return ; + return ; }; const StationLogin = observer(StationLoginComponent); diff --git a/packages/contact-center/station-login/src/station-login/station-login.presentational.tsx b/packages/contact-center/station-login/src/station-login/station-login.presentational.tsx index 829f85833..5d6af87dc 100644 --- a/packages/contact-center/station-login/src/station-login/station-login.presentational.tsx +++ b/packages/contact-center/station-login/src/station-login/station-login.presentational.tsx @@ -2,7 +2,6 @@ import React, { useEffect, useRef} from 'react'; import { StationLoginPresentationalProps } from './station-login.types'; import './station-login.style.scss'; import { MULTIPLE_SIGN_IN_ALERT_MESSAGE, MULTIPLE_SIGN_IN_ALERT_TITLE } from './constants'; -import './alert-modal.scss'; const StationLoginPresentational: React.FunctionComponent = (props) => { const { name, teams, loginOptions, login, logout, relogin, setDeviceType, setDialNumber, setTeam, isAgentLoggedIn, deviceType, showMultipleLoginAlert, handleContinue} = props; // TODO: Use the loginSuccess, loginFailure, logoutSuccess props returned fromthe API response via helper file to reflect UI changes diff --git a/packages/contact-center/station-login/src/station-login/station-login.style.scss b/packages/contact-center/station-login/src/station-login/station-login.style.scss index feea0b874..d15784c36 100644 --- a/packages/contact-center/station-login/src/station-login/station-login.style.scss +++ b/packages/contact-center/station-login/src/station-login/station-login.style.scss @@ -53,3 +53,41 @@ border: 1px solid #ccc; border-radius: 4px; } + + +.modal { + width: 400px; + border: none; + border-radius: 10px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + padding: 20px; + text-align: center; +} + +.modal-content { + display: flex; + justify-content: flex-end; // Aligns the button to the right +} + +.modal::backdrop { + background: rgba(0, 0, 0, 0.5); +} + +h2 { + margin-top: 0; +} + +#ContinueButton { + background-color: #0078d4; + color: white; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + font-size: 16px; + cursor: pointer; +} + +#ContinueButton:hover { + background-color: #005a9e; +} diff --git a/packages/contact-center/station-login/src/station-login/station-login.types.ts b/packages/contact-center/station-login/src/station-login/station-login.types.ts index 5eb68b097..26c686c22 100644 --- a/packages/contact-center/station-login/src/station-login/station-login.types.ts +++ b/packages/contact-center/station-login/src/station-login/station-login.types.ts @@ -128,7 +128,7 @@ export type StationLoginPresentationalProps = Pick< export type UseStationLoginProps = Pick< IStationLoginProps, - 'cc' | 'onLogin' | 'onLogout' | 'logger' | 'isAgentLoggedIn' | 'handleContinue' | 'showMultipleLoginAlert' + 'cc' | 'onLogin' | 'onLogout' | 'logger' | 'isAgentLoggedIn' >; export type StationLoginProps = Pick; diff --git a/packages/contact-center/station-login/tests/helper.ts b/packages/contact-center/station-login/tests/helper.ts index 45e762389..8cd95656d 100644 --- a/packages/contact-center/station-login/tests/helper.ts +++ b/packages/contact-center/station-login/tests/helper.ts @@ -13,6 +13,7 @@ jest.mock('@webex/cc-store', () => { setSelectedLoginOption: jest.fn(), setCurrentState: jest.fn(), setLastStateChangeTimestamp: jest.fn(), + setShowMultipleLoginAlert: jest.fn(), }; }); @@ -50,28 +51,30 @@ describe('useStationLogin Hook', () => { }); it('should set loginSuccess on successful login', async () => { - const successResponse = {data: { - agentId: '6b310dff-569e-4ac7-b064-70f834ea56d8', - agentSessionId: 'c9c24ace-5170-4a9f-8bc2-2eeeff9d7c11', - auxCodeId: '00b4e8df-f7b0-460f-aacf-f1e635c87d4d', - deviceId: '1001', - deviceType: 'EXTENSION', - dn: '1001', - eventType: 'AgentDesktopMessage', - interactionIds: [], - lastIdleCodeChangeTimestamp: 1731997914706, - lastStateChangeTimestamp: 1731997914706, - orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a', - profileType: 'BLENDED', - roles: ['agent'], - siteId: 'd64e19c0-53a2-4ae0-ab7e-3ebc778b3dcd', - status: 'LoggedIn', - subStatus: 'Idle', - teamId: 'c789288e-39e3-40c9-8e66-62c6276f73de', - trackingId: 'f40915b9-07ed-4b6c-832d-e7f5e7af3b72', - type: 'AgentStationLoginSuccess', - voiceCount: 1, - }}; + const successResponse = { + data: { + agentId: '6b310dff-569e-4ac7-b064-70f834ea56d8', + agentSessionId: 'c9c24ace-5170-4a9f-8bc2-2eeeff9d7c11', + auxCodeId: '00b4e8df-f7b0-460f-aacf-f1e635c87d4d', + deviceId: '1001', + deviceType: 'EXTENSION', + dn: '1001', + eventType: 'AgentDesktopMessage', + interactionIds: [], + lastIdleCodeChangeTimestamp: 1731997914706, + lastStateChangeTimestamp: 1731997914706, + orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a', + profileType: 'BLENDED', + roles: ['agent'], + siteId: 'd64e19c0-53a2-4ae0-ab7e-3ebc778b3dcd', + status: 'LoggedIn', + subStatus: 'Idle', + teamId: 'c789288e-39e3-40c9-8e66-62c6276f73de', + trackingId: 'f40915b9-07ed-4b6c-832d-e7f5e7af3b72', + type: 'AgentStationLoginSuccess', + voiceCount: 1, + }, + }; ccMock.stationLogin.mockResolvedValue(successResponse); const setSelectedLoginOptionSpy = jest.spyOn(require('@webex/cc-store'), 'setSelectedLoginOption'); @@ -84,8 +87,6 @@ describe('useStationLogin Hook', () => { onLogout: logoutCb, logger, isAgentLoggedIn, - handleContinue: jest.fn(), - showMultipleLoginAlert: false, }) ); @@ -119,7 +120,6 @@ describe('useStationLogin Hook', () => { loginFailure: undefined, logoutSuccess: undefined, relogin: expect.any(Function), - showMultipleLoginAlert: false, handleContinue: expect.any(Function), }); @@ -131,6 +131,83 @@ describe('useStationLogin Hook', () => { }); }); + it('should set loginSuccess on successful login without auxCode and last state timestamp', async () => { + const successResponse = { + data: { + agentId: '6b310dff-569e-4ac7-b064-70f834ea56d8', + agentSessionId: 'c9c24ace-5170-4a9f-8bc2-2eeeff9d7c11', + }, + }; + + ccMock.stationLogin.mockResolvedValue(successResponse); + const setSelectedLoginOptionSpy = jest.spyOn(require('@webex/cc-store'), 'setSelectedLoginOption'); + const setSetCurrentStateSpy = jest.spyOn(require('@webex/cc-store'), 'setCurrentState'); + const setSetLastStateChangeTimestampSpy = jest.spyOn(require('@webex/cc-store'), 'setLastStateChangeTimestamp'); + const {result} = renderHook(() => + useStationLogin({ + cc: ccMock, + onLogin: loginCb, + onLogout: logoutCb, + logger, + isAgentLoggedIn, + }) + ); + + act(() => { + result.current.setDeviceType(loginParams.loginOption); + result.current.setDialNumber(loginParams.dialNumber); + result.current.setTeam(loginParams.teamId); + }); + + await act(async () => { + await result.current.login(); + }); + + await waitFor(async () => { + expect(setSelectedLoginOptionSpy).toHaveBeenCalledWith(loginParams.loginOption); + expect(setSetCurrentStateSpy).not.toHaveBeenCalledWith(successResponse.data.auxCodeId); + expect(setSetLastStateChangeTimestampSpy).not.toHaveBeenCalledWith( + new Date(successResponse.data.lastStateChangeTimestamp) + ); + }); + }); + + it('should set loginSuccess on successful login without onLogin callback', async () => { + const successResponse = { + data: { + agentId: '6b310dff-569e-4ac7-b064-70f834ea56d8', + agentSessionId: 'c9c24ace-5170-4a9f-8bc2-2eeeff9d7c11', + }, + }; + + ccMock.stationLogin.mockResolvedValue(successResponse); + const setSelectedLoginOptionSpy = jest.spyOn(require('@webex/cc-store'), 'setSelectedLoginOption'); + const setSetCurrentStateSpy = jest.spyOn(require('@webex/cc-store'), 'setCurrentState'); + const setSetLastStateChangeTimestampSpy = jest.spyOn(require('@webex/cc-store'), 'setLastStateChangeTimestamp'); + const {result} = renderHook(() => + useStationLogin({ + cc: ccMock, + onLogout: logoutCb, + logger, + isAgentLoggedIn, + }) + ); + + act(() => { + result.current.setDeviceType(loginParams.loginOption); + result.current.setDialNumber(loginParams.dialNumber); + result.current.setTeam(loginParams.teamId); + }); + + await act(async () => { + await result.current.login(); + }); + + await waitFor(async () => { + expect(loginCb).not.toHaveBeenCalledWith(); + }); + }); + it('should not call setSelectedLoginOptionSpy if login fails', async () => { const errorResponse = new Error('Login failed'); ccMock.stationLogin.mockRejectedValue(errorResponse); @@ -144,8 +221,6 @@ describe('useStationLogin Hook', () => { onLogout: logoutCb, logger, isAgentLoggedIn, - handleContinue: jest.fn(), - showMultipleLoginAlert: false, }) ); @@ -179,7 +254,6 @@ describe('useStationLogin Hook', () => { loginFailure: errorResponse, logoutSuccess: undefined, relogin: expect.any(Function), - showMultipleLoginAlert: false, handleContinue: expect.any(Function), }); @@ -196,8 +270,6 @@ describe('useStationLogin Hook', () => { onLogout: logoutCb, logger, isAgentLoggedIn, - handleContinue: jest.fn(), - showMultipleLoginAlert: false, }) ); @@ -222,8 +294,6 @@ describe('useStationLogin Hook', () => { onLogout: logoutCb, logger, isAgentLoggedIn, - handleContinue: jest.fn(), - showMultipleLoginAlert: false, }) ); @@ -258,6 +328,7 @@ describe('useStationLogin Hook', () => { loginFailure: errorResponse, logoutSuccess: undefined, relogin: expect.any(Function), + handleContinue: expect.any(Function), }); }); }); @@ -287,8 +358,6 @@ describe('useStationLogin Hook', () => { onLogout: logoutCb, logger, isAgentLoggedIn, - handleContinue: jest.fn(), - showMultipleLoginAlert: false, }) ); @@ -312,7 +381,6 @@ describe('useStationLogin Hook', () => { loginFailure: undefined, logoutSuccess: successResponse, relogin: expect.any(Function), - showMultipleLoginAlert: false, handleContinue: expect.any(Function), }); }); @@ -328,8 +396,6 @@ describe('useStationLogin Hook', () => { onLogout: logoutCb, logger, isAgentLoggedIn, - handleContinue: jest.fn(), - showMultipleLoginAlert: false, }) ); @@ -354,8 +420,6 @@ describe('useStationLogin Hook', () => { onLogin: loginCb, logger, isAgentLoggedIn, - handleContinue: jest.fn(), - showMultipleLoginAlert: false, }) ); @@ -378,8 +442,6 @@ describe('useStationLogin Hook', () => { onLogout: logoutCb, logger, isAgentLoggedIn, - handleContinue: jest.fn(), - showMultipleLoginAlert: false, }) ); @@ -393,7 +455,32 @@ describe('useStationLogin Hook', () => { }); }); - it('should handle AgentMultiLogin event', async () => { + it('should call relogin without login callback', async () => { + const setSelectedLoginOptionSpy = jest.spyOn(require('@webex/cc-store'), 'setSelectedLoginOption'); + + const {result} = renderHook(() => + useStationLogin({ + cc: ccMock, + onLogout: logoutCb, + logger, + isAgentLoggedIn, + }) + ); + + act(() => { + result.current.relogin(); + }); + + await waitFor(() => { + expect(setSelectedLoginOptionSpy).toHaveBeenCalled(); + expect(loginCb).not.toHaveBeenCalled(); + }); + }); + + it('should call handleContinue and set device type', async () => { + const setShowMultipleLoginAlertSpy = jest.spyOn(require('@webex/cc-store'), 'setShowMultipleLoginAlert'); + ccMock.register.mockResolvedValue({isAgentLoggedIn: true}); + const {result} = renderHook(() => useStationLogin({ cc: ccMock, @@ -401,18 +488,78 @@ describe('useStationLogin Hook', () => { onLogout: logoutCb, logger, isAgentLoggedIn, - handleContinue: jest.fn(), - showMultipleLoginAlert: false, }) ); - const event = new Event('AgentMultiLoginCloseSession'); act(() => { - ccMock.on.mock.calls[0][1](event); + result.current.handleContinue(); }); await waitFor(() => { - expect(result.current.showMultipleLoginAlert).toBe(true); + expect(setShowMultipleLoginAlertSpy).toHaveBeenCalledWith(false); + expect(ccMock.register).toHaveBeenCalled(); + expect(logger.log).toHaveBeenCalledWith('Agent Relogin Success', { + module: 'widget-station-login#station-login/helper.ts', + method: 'handleContinue', + }); + }); + }); + + it('should call handleContinue with agent not logged in', async () => { + const setShowMultipleLoginAlertSpy = jest.spyOn(require('@webex/cc-store'), 'setShowMultipleLoginAlert'); + ccMock.register.mockResolvedValue({isAgentLoggedIn: false}); + + const {result} = renderHook(() => + useStationLogin({ + cc: ccMock, + onLogin: loginCb, + onLogout: logoutCb, + logger, + isAgentLoggedIn, + }) + ); + + act(() => { + result.current.handleContinue(); + }); + + await waitFor(() => { + expect(setShowMultipleLoginAlertSpy).toHaveBeenCalledWith(false); + expect(ccMock.register).toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledWith('Agent Relogin Failed', { + module: 'widget-station-login#station-login/helper.ts', + method: 'handleContinue', + }); + }); + }); + + it('should call handleContinue and handle error', async () => { + const setShowMultipleLoginAlertSpy = jest.spyOn(require('@webex/cc-store'), 'setShowMultipleLoginAlert'); + ccMock.register.mockImplementation(() => { + throw Error('Relogin failed'); + }); + + const {result} = renderHook(() => + useStationLogin({ + cc: ccMock, + onLogin: loginCb, + onLogout: logoutCb, + logger, + isAgentLoggedIn, + }) + ); + + act(() => { + result.current.handleContinue(); + }); + + await waitFor(() => { + expect(setShowMultipleLoginAlertSpy).toHaveBeenCalledWith(false); + expect(ccMock.register).toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledWith('Error handling agent multi login continue: Error: Relogin failed', { + module: 'widget-station-login#station-login/index.tsx', + method: 'handleContinue', + }); }); }); }); diff --git a/packages/contact-center/store/src/store.ts b/packages/contact-center/store/src/store.ts index e77e77c8c..197c5ecbb 100644 --- a/packages/contact-center/store/src/store.ts +++ b/packages/contact-center/store/src/store.ts @@ -32,6 +32,8 @@ class Store implements IStore { wrapupRequired: boolean = false; currentState: string = ''; lastStateChangeTimestamp: Date = new Date(); + showMultipleLoginAlert: boolean = false; + constructor() { makeAutoObservable(this, { cc: observable.ref, @@ -70,6 +72,10 @@ class Store implements IStore { this.lastStateChangeTimestamp = timestamp; } + setShowMultipleLoginAlert(value: boolean): void { + this.showMultipleLoginAlert = value; + } + public static getInstance(): Store { if (!Store.instance) { console.log('Creating new store instance'); diff --git a/packages/contact-center/store/src/store.types.ts b/packages/contact-center/store/src/store.types.ts index bce74d666..cd4eb39da 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -42,8 +42,14 @@ interface IStoreBase { isAgentLoggedIn: boolean; deviceType: string; wrapupRequired: boolean; + currentState: string; + lastStateChangeTimestamp: Date; + showMultipleLoginAlert: boolean; init(params: InitParams): Promise; setSelectedLoginOption(option: string): void; + setCurrentState(state: string): void; + setLastStateChangeTimestamp(timestamp: Date): void; + setShowMultipleLoginAlert(value: boolean): void; } interface IStore extends IStoreBase { @@ -78,6 +84,13 @@ TASK_END = 'task:end', TASK_WRAPUP = 'task:wrapup', } // TODO: remove this once cc sdk exports this enum + +// Events that are received on the contact center SDK +enum CC_EVENTS{ + AGENT_MULTI_LOGIN = 'agent:multiLogin', + AGENT_STATE_CHANGE = 'agent:stateChange', +} + export type { IContactCenter, Profile, @@ -93,5 +106,9 @@ export type { } export { - TASK_EVENTS, + CC_EVENTS, + TASK_EVENTS } + + + diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index 4f152eef5..4f0f4e317 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -1,4 +1,4 @@ -import {IStoreWrapper, IStore, InitParams, TASK_EVENTS} from './store.types'; +import {IStoreWrapper, IStore, InitParams, TASK_EVENTS, CC_EVENTS} from './store.types'; import {ITask} from '@webex/plugin-cc'; import Store from './store'; import {runInAction} from 'mobx'; @@ -58,10 +58,34 @@ class StoreWrapper implements IStoreWrapper { return this.store.wrapupRequired; } + get currentState() { + return this.store.currentState; + } + + get lastStateChangeTimestamp() { + return this.store.lastStateChangeTimestamp; + } + + get showMultipleLoginAlert() { + return this.store.showMultipleLoginAlert; + } + + setShowMultipleLoginAlert(value: boolean): void { + return this.store.setShowMultipleLoginAlert(value); + } + setSelectedLoginOption(option: string): void { return this.store.setSelectedLoginOption(option); } + setCurrentState(state: string): void { + return this.store.setCurrentState(state); + } + + setLastStateChangeTimestamp(timestamp: Date): void { + return this.store.setLastStateChangeTimestamp(timestamp); + } + init(options: InitParams): Promise { return this.store.init(options).then(() => { this.setupIncomingTaskHandler(); @@ -72,9 +96,7 @@ class StoreWrapper implements IStoreWrapper { const taskToRemove = this.store.taskList.find((task) => task.data.interactionId === taskId); if (taskToRemove) { taskToRemove.off(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned(taskId)); - taskToRemove.off(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => - this.handleTaskEnd(taskId, wrapupRequired) - ); + taskToRemove.off(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => this.handleTaskEnd(taskId, wrapupRequired)); } const updateTaskList = this.store.taskList.filter((task) => task.data.interactionId !== taskId); runInAction(() => { @@ -115,12 +137,37 @@ class StoreWrapper implements IStoreWrapper { this.store.setTaskList([...this.store.taskList, task]); }; + handleStateChange = (data) => { + if (data && typeof data === 'object' && data.type === 'AgentStateChangeSuccess') { + const DEFAULT_CODE = '0'; // Default code when no aux code is present + this.store.setCurrentState(data.auxCodeId?.trim() !== '' ? data.auxCodeId : DEFAULT_CODE); + + const startTime = data.lastStateChangeTimestamp; + this.store.setLastStateChangeTimestamp(new Date(startTime)); + } + }; + + handleMultiLoginCloseSession = (data) => { + if (data && typeof data === 'object' && data.type === 'AgentMultiLoginCloseSession') { + this.store.setShowMultipleLoginAlert(true); + } + }; + setupIncomingTaskHandler() { const ccSDK = this.store.cc; ccSDK.on(TASK_EVENTS.TASK_INCOMING, (task) => { this.handleIncomingTask(task); }); + + ccSDK.on(CC_EVENTS.AGENT_STATE_CHANGE, this.handleStateChange); + ccSDK.on(CC_EVENTS.AGENT_MULTI_LOGIN, this.handleMultiLoginCloseSession); + + return () => { + ccSDK.off(TASK_EVENTS.TASK_INCOMING, this.handleIncomingTask); + ccSDK.off(CC_EVENTS.AGENT_STATE_CHANGE, this.handleStateChange); + ccSDK.off(CC_EVENTS.AGENT_MULTI_LOGIN, this.handleMultiLoginCloseSession); + }; } } diff --git a/packages/contact-center/store/tests/store.ts b/packages/contact-center/store/tests/store.ts index ab632489b..3878053b8 100644 --- a/packages/contact-center/store/tests/store.ts +++ b/packages/contact-center/store/tests/store.ts @@ -180,6 +180,18 @@ describe('Store', () => { storeInstance.setSelectedLoginOption('option1'); expect(storeInstance.selectedLoginOption).toBe('option1'); + + storeInstance.setCurrentTheme('theme1'); + expect(storeInstance.currentTheme).toBe('theme1'); + + storeInstance.setShowMultipleLoginAlert(true); + expect(storeInstance.showMultipleLoginAlert).toBe(true); + + storeInstance.setCurrentState('state1'); + expect(storeInstance.currentState).toBe('state1'); + + storeInstance.setLastStateChangeTimestamp(new Date()); + expect(storeInstance.lastStateChangeTimestamp).toEqual(expect.any(Date)); }); }); }); diff --git a/packages/contact-center/store/tests/storeEventsWrapper.ts b/packages/contact-center/store/tests/storeEventsWrapper.ts index 1f09b999d..5cc34ae7d 100644 --- a/packages/contact-center/store/tests/storeEventsWrapper.ts +++ b/packages/contact-center/store/tests/storeEventsWrapper.ts @@ -1,5 +1,5 @@ import {act, waitFor} from '@testing-library/react'; -import {TASK_EVENTS} from '../src/store.types'; +import {CC_EVENTS, TASK_EVENTS} from '../src/store.types'; import storeWrapper from '../src/storeEventsWrapper'; import {ITask} from '@webex/plugin-cc'; @@ -22,6 +22,12 @@ jest.mock('../src/store', () => ({ taskList: 'mockTaskList', incomingTask: 'mockIncomingTask', wrapupRequired: 'mockWrapupRequired', + currentState: 'mockCurrentState', + lastStateChangeTimestamp: 'mockLastStateChangeTimestamp', + showMultipleLoginAlert: 'mockShowMultipleLoginAlert', + setShowMultipleLoginAlert: jest.fn(), + setCurrentState: jest.fn(), + setLastStateChangeTimestamp: jest.fn(), setSelectedLoginOption: jest.fn(), init: jest.fn().mockResolvedValue({}), setIncomingTask: jest.fn(), @@ -41,13 +47,6 @@ describe('storeEventsWrapper', () => { expect(storeWrapper.loginOptions).toBe('mockLoginOptions'); }); - it.skip('should proxy cc', () => { - expect(storeWrapper.cc).toBe({ - on: jest.fn(), - off: jest.fn(), - }); - }); - it('should proxy logger', () => { expect(storeWrapper.logger).toBe('mockLogger'); }); @@ -91,6 +90,43 @@ describe('storeEventsWrapper', () => { it('should proxy wrapupRequired', () => { expect(storeWrapper.wrapupRequired).toBe('mockWrapupRequired'); }); + + it('should proxy currentState', () => { + expect(storeWrapper.currentState).toBe('mockCurrentState'); + }); + + it('should proxy lastStateChangeTimestamp', () => { + expect(storeWrapper.lastStateChangeTimestamp).toBe('mockLastStateChangeTimestamp'); + }); + + it('should proxy showMultipleLoginAlert', () => { + expect(storeWrapper.showMultipleLoginAlert).toBe('mockShowMultipleLoginAlert'); + + storeWrapper.setShowMultipleLoginAlert(true); + expect(storeWrapper['store'].setShowMultipleLoginAlert).toHaveBeenCalledWith(true); + }); + + it('should proxy setShowMultipleLoginAlert', () => { + expect(storeWrapper.setShowMultipleLoginAlert).toBeInstanceOf(Function); + + storeWrapper.setShowMultipleLoginAlert(true); + expect(storeWrapper['store'].setShowMultipleLoginAlert).toHaveBeenCalledWith(true); + }); + + it('should proxy setCurrentState', () => { + expect(storeWrapper.setCurrentState).toBeInstanceOf(Function); + + storeWrapper.setCurrentState('newState'); + expect(storeWrapper['store'].setCurrentState).toHaveBeenCalledWith('newState'); + }); + + it('should proxy setLastStateChangeTimestamp', () => { + expect(storeWrapper.setLastStateChangeTimestamp).toBeInstanceOf(Function); + + const timestamp = new Date(); + storeWrapper.setLastStateChangeTimestamp(timestamp); + expect(storeWrapper['store'].setLastStateChangeTimestamp).toHaveBeenCalledWith(timestamp); + }); }); describe('storeEventsWrapper', () => { @@ -187,10 +223,13 @@ describe('storeEventsWrapper', () => { jest.clearAllMocks(); }); - it('should initialize the store and set up incoming task handler', async () => { + it('should initialize the store and set up event handlers', async () => { await storeWrapper.init(options); expect(storeWrapper['store'].init).toHaveBeenCalledWith(options); + expect(storeWrapper['store'].cc.on).toHaveBeenCalledWith(TASK_EVENTS.TASK_INCOMING, expect.any(Function)); + expect(storeWrapper['store'].cc.on).toHaveBeenCalledWith(CC_EVENTS.AGENT_STATE_CHANGE, expect.any(Function)); + expect(storeWrapper['store'].cc.on).toHaveBeenCalledWith(CC_EVENTS.AGENT_MULTI_LOGIN, expect.any(Function)); }); it('should handle task:incoming event ', async () => { @@ -292,5 +331,72 @@ describe('storeEventsWrapper', () => { expect(storeWrapper['store'].setSelectedLoginOption).toHaveBeenCalledWith(option); }); + + it('should handle multilogin session modal with in correct data', async () => { + const options = {someOption: 'value'}; + await storeWrapper.init(options); + act(() => { + storeWrapper['store'].cc.on.mock.calls[2][1]({}); + }); + + expect(storeWrapper['store'].setShowMultipleLoginAlert).not.toHaveBeenCalledWith(true); + }); + + it('should handle multilogin session modal with correct data', async () => { + const options = {someOption: 'value'}; + await storeWrapper.init(options); + act(() => { + storeWrapper['store'].cc.on.mock.calls[2][1]({type: 'AgentMultiLoginCloseSession'}); + }); + + expect(storeWrapper['store'].setShowMultipleLoginAlert).toHaveBeenCalledWith(true); + }); + + it('should set selected login option', () => { + const option = 'newLoginOption'; + storeWrapper.setSelectedLoginOption(option); + + expect(storeWrapper['store'].setSelectedLoginOption).toHaveBeenCalledWith(option); + }); + + it('should handle state change event with incorrect data', async () => { + const options = {someOption: 'value'}; + await storeWrapper.init(options); + act(() => { + storeWrapper['store'].cc.on.mock.calls[1][1]({}); + }); + + expect(storeWrapper['store'].setCurrentState).not.toHaveBeenCalledWith(); + }); + + it('should handle state change event with correct data and emplty auxcodeId', async () => { + const options = {someOption: 'value'}; + await storeWrapper.init(options); + act(() => { + storeWrapper['store'].cc.on.mock.calls[1][1]({type: 'AgentStateChangeSuccess', auxCodeId: ''}); + }); + + expect(storeWrapper['store'].setCurrentState).toHaveBeenCalledWith('0'); + }); + + it('should handle state change event with correct data', async () => { + const options = {someOption: 'value'}; + await storeWrapper.init(options); + act(() => { + storeWrapper['store'].cc.on.mock.calls[1][1]({type: 'AgentStateChangeSuccess', auxCodeId: 'available'}); + }); + + expect(storeWrapper['store'].setCurrentState).toHaveBeenCalledWith('available'); + }); + + it('should return a function to remove event listeners on cc object', () => { + const removeListeners = storeWrapper.setupIncomingTaskHandler(); + + removeListeners(); + + expect(storeWrapper['cc'].off).toHaveBeenCalledWith(TASK_EVENTS.TASK_INCOMING, expect.any(Function)); + expect(storeWrapper['cc'].off).toHaveBeenCalledWith(CC_EVENTS.AGENT_STATE_CHANGE, expect.any(Function)); + expect(storeWrapper['cc'].off).toHaveBeenCalledWith(CC_EVENTS.AGENT_MULTI_LOGIN, expect.any(Function)); + }); }); }); diff --git a/packages/contact-center/user-state/package.json b/packages/contact-center/user-state/package.json index 23508fe65..ec6db3b66 100644 --- a/packages/contact-center/user-state/package.json +++ b/packages/contact-center/user-state/package.json @@ -16,7 +16,7 @@ "build": "yarn run -T tsc", "build:src": "yarn run clean:dist && webpack", "build:watch": "webpack --watch", - "test:unit": "jest" + "test:unit": "jest --coverage" }, "dependencies": { "@webex/cc-components": "workspace:*", diff --git a/packages/contact-center/user-state/src/constants.ts b/packages/contact-center/user-state/src/constants.ts deleted file mode 100644 index b9332cb21..000000000 --- a/packages/contact-center/user-state/src/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const AGENT_STATE_CHANGE = 'agent:stateChange'; diff --git a/packages/contact-center/user-state/src/helper.ts b/packages/contact-center/user-state/src/helper.ts index 18f2a094e..7270570f2 100644 --- a/packages/contact-center/user-state/src/helper.ts +++ b/packages/contact-center/user-state/src/helper.ts @@ -1,6 +1,5 @@ import {useState, useEffect, useRef} from 'react'; // TODO: Export & Import this AGENT_STATE_CHANGE constant from SDK -import {AGENT_STATE_CHANGE} from './constants'; import store from '@webex/cc-store'; export const useUserState = ({idleCodes, agentId, cc, currentState, lastStateChangeTimestamp}) => { const [isSettingAgentStatus, setIsSettingAgentStatus] = useState(false); @@ -36,23 +35,6 @@ export const useUserState = ({idleCodes, agentId, cc, currentState, lastStateCha workerRef.current.onmessage = (event) => { setElapsedTime(event.data); }; - - const handleStateChange = (data) => { - if (data && typeof data === 'object' && data.type === 'AgentStateChangeSuccess') { - const DEFAULT_CODE = '0'; // Default code when no aux code is present - store.setCurrentState(data.auxCodeId?.trim() !== '' ? data.auxCodeId : DEFAULT_CODE); - - const startTime = data.lastStateChangeTimestamp; - store.setLastStateChangeTimestamp(new Date(startTime)); - } - }; - - cc.on(AGENT_STATE_CHANGE, handleStateChange); - - return () => { - workerRef.current?.terminate(); - cc.off(AGENT_STATE_CHANGE, handleStateChange); - }; }, []); useEffect(() => { @@ -62,6 +44,9 @@ export const useUserState = ({idleCodes, agentId, cc, currentState, lastStateCha setElapsedTime(elapsed); workerRef.current.postMessage({type: 'reset', startTime: lastStateChangeTimestamp.getTime()}); } + return () => { + workerRef.current?.terminate(); + }; }, [lastStateChangeTimestamp]); const setAgentStatus = (selectedCode) => { diff --git a/packages/contact-center/user-state/tests/helper.ts b/packages/contact-center/user-state/tests/helper.ts index 6dcb667b6..86277de1b 100644 --- a/packages/contact-center/user-state/tests/helper.ts +++ b/packages/contact-center/user-state/tests/helper.ts @@ -51,8 +51,6 @@ describe('useUserState Hook', () => { elapsedTime: 0, currentState: '0', }); - - expect(mockCC.on).toHaveBeenCalledTimes(1); }); it('should increment elapsedTime every second', () => { @@ -103,50 +101,23 @@ describe('useUserState Hook', () => { }); }); - it('should handle errors from setAgentStatus and revert state', async () => { - mockCC.setAgentState.mockRejectedValueOnce(new Error('Error setting agent status')); + it('should handle setAgentStatus correctly and update state', async () => { + mockCC.setAgentState.mockResolvedValueOnce({data: {auxCodeId: '2', lastStateChangeTimestamp: new Date()}}); const {result} = renderHook(() => useUserState({idleCodes, agentId, cc: mockCC, currentState: '0', lastStateChangeTimestamp: new Date()}) ); - await act(async () => { - await result.current.setAgentStatus(idleCodes[1]); - }); - - await waitFor(() => { - expect(result.current.errorMessage).toBe('Error: Error setting agent status'); - }); - }); - - it('should handle agent state change events correctly', async () => { - const {result} = renderHook(() => - useUserState({idleCodes, agentId, cc: mockCC, currentState: 'auxCodeId', lastStateChangeTimestamp: new Date()}) - ); - - const handler = mockCC.on.mock.calls[0][1]; - act(() => { - handler({type: 'AgentStateChangeSuccess', auxCodeId: '123'}); + result.current.setAgentStatus(idleCodes[0]); }); await waitFor(() => { - expect(store.setCurrentState).toHaveBeenCalledWith('123'); + expect(store.setCurrentState).toHaveBeenCalledWith('2'); }); }); - it('should cleanup event listener on unmount', () => { - const {unmount} = renderHook(() => - useUserState({idleCodes, agentId, cc: mockCC, currentState: '0', lastStateChangeTimestamp: new Date()}) - ); - - unmount(); - expect(mockCC.off).toHaveBeenCalledTimes(1); - }); - - it('should update store with new current state when agent status changes', async () => { - mockCC.setAgentState.mockResolvedValueOnce({ - data: {auxCodeId: '2', lastStateChangeTimestamp: new Date().toISOString()}, - }); + it('should handle errors from setAgentStatus and revert state', async () => { + mockCC.setAgentState.mockRejectedValueOnce(new Error('Error setting agent status')); const {result} = renderHook(() => useUserState({idleCodes, agentId, cc: mockCC, currentState: '0', lastStateChangeTimestamp: new Date()}) ); @@ -156,25 +127,7 @@ describe('useUserState Hook', () => { }); await waitFor(() => { - expect(store.setCurrentState).toHaveBeenCalledWith('2'); - expect(store.setLastStateChangeTimestamp).toHaveBeenCalled(); - }); - }); - - it('should update store when agent state change event occurs', async () => { - const {result} = renderHook(() => - useUserState({idleCodes, agentId, cc: mockCC, currentState: '0', lastStateChangeTimestamp: new Date()}) - ); - - const handler = mockCC.on.mock.calls[0][1]; - - act(() => { - handler({type: 'AgentStateChangeSuccess', auxCodeId: '3', lastStateChangeTimestamp: new Date().toISOString()}); - }); - - await waitFor(() => { - expect(store.setCurrentState).toHaveBeenCalledWith('3'); - expect(store.setLastStateChangeTimestamp).toHaveBeenCalled(); + expect(result.current.errorMessage).toBe('Error: Error setting agent status'); }); }); }); From 7b8fbc0542b66b797676285810ec294ed01aad58 Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Tue, 11 Feb 2025 17:51:07 +0530 Subject: [PATCH 05/23] fix(store): remove-console-logs --- packages/contact-center/store/src/store.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/contact-center/store/src/store.ts b/packages/contact-center/store/src/store.ts index 197c5ecbb..6b691821c 100644 --- a/packages/contact-center/store/src/store.ts +++ b/packages/contact-center/store/src/store.ts @@ -147,5 +147,4 @@ class Store implements IStore { } } -// const store = Store.getInstance(); export default Store; From 35687477bd18bb1fde5ddb4dcb555ac9edc15a36 Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Tue, 11 Feb 2025 22:55:26 +0530 Subject: [PATCH 06/23] fix(store): remove-duplicate-properties --- .../station-login/src/helper.ts | 9 ++-- .../station-login/src/station-login/index.tsx | 3 +- .../station-login.presentational.tsx | 51 +++++++++++++------ .../src/station-login/station-login.types.ts | 2 +- .../station-login/tests/helper.ts | 45 ++++++++++------ .../tests/station-login/index.tsx | 6 +++ packages/contact-center/store/src/store.ts | 9 ++-- .../contact-center/store/src/store.types.ts | 6 ++- .../store/src/storeEventsWrapper.ts | 28 ++++++---- packages/contact-center/store/tests/store.ts | 5 +- .../store/tests/storeEventsWrapper.ts | 19 ++++--- .../task/src/IncomingTask/index.tsx | 4 +- .../task/src/TaskList/index.tsx | 4 +- packages/contact-center/task/src/helper.ts | 8 +-- .../contact-center/task/src/task.types.ts | 6 +-- .../task/tests/CallControl/index.tsx | 2 +- .../task/tests/IncomingTask/index.tsx | 4 +- .../task/tests/TaskList/index.tsx | 4 +- packages/contact-center/task/tests/helper.ts | 30 +++++------ 19 files changed, 150 insertions(+), 95 deletions(-) diff --git a/packages/contact-center/station-login/src/helper.ts b/packages/contact-center/station-login/src/helper.ts index 950c3847d..ec2fca4c5 100644 --- a/packages/contact-center/station-login/src/helper.ts +++ b/packages/contact-center/station-login/src/helper.ts @@ -10,7 +10,7 @@ export const useStationLogin = (props: UseStationLoginProps) => { const logger = props.logger; const [isAgentLoggedIn, setIsAgentLoggedIn] = useState(props.isAgentLoggedIn); const [dialNumber, setDialNumber] = useState(''); - const [deviceType, setDeviceType] = useState(''); + const [deviceType, setDeviceType] = useState(props.deviceType || ''); const [team, setTeam] = useState(''); const [loginSuccess, setLoginSuccess] = useState(); const [loginFailure, setLoginFailure] = useState(); @@ -48,7 +48,8 @@ export const useStationLogin = (props: UseStationLoginProps) => { .then((res: StationLoginSuccess) => { setLoginSuccess(res); setIsAgentLoggedIn(true); - store.setSelectedLoginOption(deviceType); + store.setDeviceType(deviceType); + store.setIsAgentLoggedIn(true); if (res.data.auxCodeId) { store.setCurrentState(res.data.auxCodeId); } @@ -73,6 +74,8 @@ export const useStationLogin = (props: UseStationLoginProps) => { .then((res: StationLogoutSuccess) => { setLogoutSuccess(res); setIsAgentLoggedIn(false); + store.setIsAgentLoggedIn(false); + store.setDeviceType(''); if (logoutCb) { logoutCb(); } @@ -86,7 +89,7 @@ export const useStationLogin = (props: UseStationLoginProps) => { }; function relogin() { - store.setSelectedLoginOption(deviceType); + store.setDeviceType(deviceType); if (loginCb) { loginCb(); } diff --git a/packages/contact-center/station-login/src/station-login/index.tsx b/packages/contact-center/station-login/src/station-login/index.tsx index 12dd56544..8411ec2e8 100644 --- a/packages/contact-center/station-login/src/station-login/index.tsx +++ b/packages/contact-center/station-login/src/station-login/index.tsx @@ -7,13 +7,14 @@ import {useStationLogin} from '../helper'; import {StationLoginProps} from './station-login.types'; const StationLoginComponent: React.FunctionComponent = ({onLogin, onLogout}) => { - const {cc, teams, loginOptions, logger, deviceType, isAgentLoggedIn, showMultipleLoginAlert} = store; + const {cc, teams, loginOptions, logger, isAgentLoggedIn, showMultipleLoginAlert, deviceType} = store; const result = useStationLogin({ cc, onLogin, onLogout, logger, isAgentLoggedIn, + deviceType, }); const props = { diff --git a/packages/contact-center/station-login/src/station-login/station-login.presentational.tsx b/packages/contact-center/station-login/src/station-login/station-login.presentational.tsx index 5d6af87dc..db3092d39 100644 --- a/packages/contact-center/station-login/src/station-login/station-login.presentational.tsx +++ b/packages/contact-center/station-login/src/station-login/station-login.presentational.tsx @@ -1,10 +1,24 @@ -import React, { useEffect, useRef} from 'react'; -import { StationLoginPresentationalProps } from './station-login.types'; +import React, {useEffect, useRef} from 'react'; +import {StationLoginPresentationalProps} from './station-login.types'; import './station-login.style.scss'; -import { MULTIPLE_SIGN_IN_ALERT_MESSAGE, MULTIPLE_SIGN_IN_ALERT_TITLE } from './constants'; +import {MULTIPLE_SIGN_IN_ALERT_MESSAGE, MULTIPLE_SIGN_IN_ALERT_TITLE} from './constants'; const StationLoginPresentational: React.FunctionComponent = (props) => { - const { name, teams, loginOptions, login, logout, relogin, setDeviceType, setDialNumber, setTeam, isAgentLoggedIn, deviceType, showMultipleLoginAlert, handleContinue} = props; // TODO: Use the loginSuccess, loginFailure, logoutSuccess props returned fromthe API response via helper file to reflect UI changes + const { + name, + teams, + loginOptions, + login, + logout, + relogin, + setDeviceType, + setDialNumber, + setTeam, + isAgentLoggedIn, + deviceType, + showMultipleLoginAlert, + handleContinue, + } = props; // TODO: Use the loginSuccess, loginFailure, logoutSuccess props returned fromthe API response via helper file to reflect UI changes const modalRef = useRef(null); useEffect(() => { @@ -52,7 +66,7 @@ const StationLoginPresentational: React.FunctionComponent { + const selectLoginOption = (event: {target: {value: string}}) => { const dialNumber = document.querySelector('#dialNumber') as HTMLInputElement; const deviceType = event.target.value; setDeviceType(deviceType); @@ -82,8 +96,10 @@ const StationLoginPresentational: React.FunctionComponent

{MULTIPLE_SIGN_IN_ALERT_TITLE}

{MULTIPLE_SIGN_IN_ALERT_MESSAGE}

-
- +
+
)} @@ -92,8 +108,8 @@ const StationLoginPresentational: React.FunctionComponent
Agent -
-
+
+
- - + {isAgentLoggedIn ? ( + + ) : ( + + )}
@@ -139,4 +158,4 @@ const StationLoginPresentational: React.FunctionComponent; export type StationLoginProps = Pick; diff --git a/packages/contact-center/station-login/tests/helper.ts b/packages/contact-center/station-login/tests/helper.ts index 8cd95656d..a4f6cd372 100644 --- a/packages/contact-center/station-login/tests/helper.ts +++ b/packages/contact-center/station-login/tests/helper.ts @@ -10,10 +10,11 @@ jest.mock('@webex/cc-store', () => { cc: {}, teams, loginOptions, - setSelectedLoginOption: jest.fn(), + setDeviceType: jest.fn(), setCurrentState: jest.fn(), setLastStateChangeTimestamp: jest.fn(), setShowMultipleLoginAlert: jest.fn(), + setIsAgentLoggedIn: jest.fn(), }; }); @@ -77,7 +78,7 @@ describe('useStationLogin Hook', () => { }; ccMock.stationLogin.mockResolvedValue(successResponse); - const setSelectedLoginOptionSpy = jest.spyOn(require('@webex/cc-store'), 'setSelectedLoginOption'); + const setDeviceTypeSpy = jest.spyOn(require('@webex/cc-store'), 'setDeviceType'); const setSetCurrentStateSpy = jest.spyOn(require('@webex/cc-store'), 'setCurrentState'); const setSetLastStateChangeTimestampSpy = jest.spyOn(require('@webex/cc-store'), 'setLastStateChangeTimestamp'); const {result} = renderHook(() => @@ -87,6 +88,7 @@ describe('useStationLogin Hook', () => { onLogout: logoutCb, logger, isAgentLoggedIn, + deviceType: 'BROWSER', }) ); @@ -106,7 +108,7 @@ describe('useStationLogin Hook', () => { loginOption: loginParams.loginOption, dialNumber: loginParams.dialNumber, }); - expect(loginCb).toHaveBeenCalledWith(); + // expect(loginCb).toHaveBeenCalledWith(); expect(result.current).toEqual({ name: 'StationLogin', @@ -123,7 +125,7 @@ describe('useStationLogin Hook', () => { handleContinue: expect.any(Function), }); - expect(setSelectedLoginOptionSpy).toHaveBeenCalledWith(loginParams.loginOption); + expect(setDeviceTypeSpy).toHaveBeenCalledWith(loginParams.loginOption); expect(setSetCurrentStateSpy).toHaveBeenCalledWith(successResponse.data.auxCodeId); expect(setSetLastStateChangeTimestampSpy).toHaveBeenCalledWith( new Date(successResponse.data.lastStateChangeTimestamp) @@ -136,11 +138,12 @@ describe('useStationLogin Hook', () => { data: { agentId: '6b310dff-569e-4ac7-b064-70f834ea56d8', agentSessionId: 'c9c24ace-5170-4a9f-8bc2-2eeeff9d7c11', + lastStateChangeTimestamp: 'mockDate', }, }; ccMock.stationLogin.mockResolvedValue(successResponse); - const setSelectedLoginOptionSpy = jest.spyOn(require('@webex/cc-store'), 'setSelectedLoginOption'); + const setDeviceTypeSpy = jest.spyOn(require('@webex/cc-store'), 'setDeviceType'); const setSetCurrentStateSpy = jest.spyOn(require('@webex/cc-store'), 'setCurrentState'); const setSetLastStateChangeTimestampSpy = jest.spyOn(require('@webex/cc-store'), 'setLastStateChangeTimestamp'); const {result} = renderHook(() => @@ -150,6 +153,7 @@ describe('useStationLogin Hook', () => { onLogout: logoutCb, logger, isAgentLoggedIn, + deviceType: '', }) ); @@ -164,7 +168,7 @@ describe('useStationLogin Hook', () => { }); await waitFor(async () => { - expect(setSelectedLoginOptionSpy).toHaveBeenCalledWith(loginParams.loginOption); + expect(setDeviceTypeSpy).toHaveBeenCalledWith(loginParams.loginOption); expect(setSetCurrentStateSpy).not.toHaveBeenCalledWith(successResponse.data.auxCodeId); expect(setSetLastStateChangeTimestampSpy).not.toHaveBeenCalledWith( new Date(successResponse.data.lastStateChangeTimestamp) @@ -181,15 +185,13 @@ describe('useStationLogin Hook', () => { }; ccMock.stationLogin.mockResolvedValue(successResponse); - const setSelectedLoginOptionSpy = jest.spyOn(require('@webex/cc-store'), 'setSelectedLoginOption'); - const setSetCurrentStateSpy = jest.spyOn(require('@webex/cc-store'), 'setCurrentState'); - const setSetLastStateChangeTimestampSpy = jest.spyOn(require('@webex/cc-store'), 'setLastStateChangeTimestamp'); const {result} = renderHook(() => useStationLogin({ cc: ccMock, onLogout: logoutCb, logger, isAgentLoggedIn, + deviceType: 'EXTENSION', }) ); @@ -208,10 +210,10 @@ describe('useStationLogin Hook', () => { }); }); - it('should not call setSelectedLoginOptionSpy if login fails', async () => { + it('should not call setDeviceType if login fails', async () => { const errorResponse = new Error('Login failed'); ccMock.stationLogin.mockRejectedValue(errorResponse); - const setSelectedLoginOptionSpy = jest.spyOn(require('@webex/cc-store'), 'setSelectedLoginOption'); + const setDeviceTypeSpy = jest.spyOn(require('@webex/cc-store'), 'setDeviceType'); loginCb.mockClear(); const {result} = renderHook(() => @@ -221,6 +223,7 @@ describe('useStationLogin Hook', () => { onLogout: logoutCb, logger, isAgentLoggedIn, + deviceType: 'EXTENSION', }) ); @@ -257,7 +260,7 @@ describe('useStationLogin Hook', () => { handleContinue: expect.any(Function), }); - expect(setSelectedLoginOptionSpy).not.toHaveBeenCalled(); + expect(setDeviceTypeSpy).not.toHaveBeenCalled(); }); }); @@ -270,6 +273,7 @@ describe('useStationLogin Hook', () => { onLogout: logoutCb, logger, isAgentLoggedIn, + deviceType: 'EXTENSION', }) ); @@ -294,6 +298,7 @@ describe('useStationLogin Hook', () => { onLogout: logoutCb, logger, isAgentLoggedIn, + deviceType: 'EXTENSION', }) ); @@ -358,6 +363,7 @@ describe('useStationLogin Hook', () => { onLogout: logoutCb, logger, isAgentLoggedIn, + deviceType: 'BROWSER', }) ); @@ -396,6 +402,7 @@ describe('useStationLogin Hook', () => { onLogout: logoutCb, logger, isAgentLoggedIn, + deviceType: 'EXTENSION', }) ); @@ -420,6 +427,7 @@ describe('useStationLogin Hook', () => { onLogin: loginCb, logger, isAgentLoggedIn, + deviceType: 'EXTENSION', }) ); @@ -433,7 +441,7 @@ describe('useStationLogin Hook', () => { }); it('should call relogin and set device type', async () => { - const setSelectedLoginOptionSpy = jest.spyOn(require('@webex/cc-store'), 'setSelectedLoginOption'); + const setDeviceTypeSpy = jest.spyOn(require('@webex/cc-store'), 'setDeviceType'); const {result} = renderHook(() => useStationLogin({ @@ -442,6 +450,7 @@ describe('useStationLogin Hook', () => { onLogout: logoutCb, logger, isAgentLoggedIn, + deviceType: 'EXTENSION', }) ); @@ -450,13 +459,13 @@ describe('useStationLogin Hook', () => { }); await waitFor(() => { - expect(setSelectedLoginOptionSpy).toHaveBeenCalled(); + expect(setDeviceTypeSpy).toHaveBeenCalled(); expect(loginCb).toHaveBeenCalled(); }); }); it('should call relogin without login callback', async () => { - const setSelectedLoginOptionSpy = jest.spyOn(require('@webex/cc-store'), 'setSelectedLoginOption'); + const setDeviceTypeSpy = jest.spyOn(require('@webex/cc-store'), 'setDeviceType'); const {result} = renderHook(() => useStationLogin({ @@ -464,6 +473,7 @@ describe('useStationLogin Hook', () => { onLogout: logoutCb, logger, isAgentLoggedIn, + deviceType: 'EXTENSION', }) ); @@ -472,7 +482,7 @@ describe('useStationLogin Hook', () => { }); await waitFor(() => { - expect(setSelectedLoginOptionSpy).toHaveBeenCalled(); + expect(setDeviceTypeSpy).toHaveBeenCalled(); expect(loginCb).not.toHaveBeenCalled(); }); }); @@ -488,6 +498,7 @@ describe('useStationLogin Hook', () => { onLogout: logoutCb, logger, isAgentLoggedIn, + deviceType: 'EXTENSION', }) ); @@ -516,6 +527,7 @@ describe('useStationLogin Hook', () => { onLogout: logoutCb, logger, isAgentLoggedIn, + deviceType: 'EXTENSION', }) ); @@ -546,6 +558,7 @@ describe('useStationLogin Hook', () => { onLogout: logoutCb, logger, isAgentLoggedIn, + deviceType: 'EXTENSION', }) ); diff --git a/packages/contact-center/station-login/tests/station-login/index.tsx b/packages/contact-center/station-login/tests/station-login/index.tsx index 59fbcf585..573637c2c 100644 --- a/packages/contact-center/station-login/tests/station-login/index.tsx +++ b/packages/contact-center/station-login/tests/station-login/index.tsx @@ -8,6 +8,7 @@ const teams = ['team123', 'team456']; const loginOptions = ['EXTENSION', 'AGENT_DN', 'BROWSER']; const deviceType = 'BROWSER'; +const logger = {}; // Mock the store import jest.mock('@webex/cc-store', () => { @@ -19,6 +20,8 @@ jest.mock('@webex/cc-store', () => { teams, loginOptions, deviceType, + logger, + isAgentLoggedIn: false, }; }); @@ -38,6 +41,9 @@ describe('StationLogin Component', () => { }, onLogin: loginCb, onLogout: logoutCb, + logger, + isAgentLoggedIn: false, + deviceType: 'BROWSER', }); const heading = screen.getByTestId('station-login-heading'); expect(heading).toHaveTextContent('StationLogin'); diff --git a/packages/contact-center/store/src/store.ts b/packages/contact-center/store/src/store.ts index 6b691821c..cd292bb24 100644 --- a/packages/contact-center/store/src/store.ts +++ b/packages/contact-center/store/src/store.ts @@ -21,7 +21,6 @@ class Store implements IStore { logger: ILogger; idleCodes: IdleCode[] = []; agentId: string = ''; - selectedLoginOption: string = ''; currentTheme: string = 'LIGHT'; wrapupCodes: IWrapupCode[] = []; incomingTask: ITask = null; @@ -60,8 +59,8 @@ class Store implements IStore { this.taskList = taskList; } - setSelectedLoginOption(option: string): void { - this.selectedLoginOption = option; + setDeviceType(option: string): void { + this.deviceType = option; } setCurrentState(state: string): void { @@ -76,6 +75,10 @@ class Store implements IStore { this.showMultipleLoginAlert = value; } + setIsAgentLoggedIn(value: boolean): void { + this.isAgentLoggedIn = value; + } + public static getInstance(): Store { if (!Store.instance) { console.log('Creating new store instance'); diff --git a/packages/contact-center/store/src/store.types.ts b/packages/contact-center/store/src/store.types.ts index cd4eb39da..16923f3a8 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -34,7 +34,6 @@ interface IStoreBase { idleCodes: IdleCode[]; agentId: string; logger: ILogger; - selectedLoginOption: string; wrapupCodes: IWrapupCode[]; currentTask: ITask; incomingTask: ITask; @@ -45,11 +44,14 @@ interface IStoreBase { currentState: string; lastStateChangeTimestamp: Date; showMultipleLoginAlert: boolean; + currentTheme: string; init(params: InitParams): Promise; - setSelectedLoginOption(option: string): void; + setDeviceType(option: string): void; setCurrentState(state: string): void; setLastStateChangeTimestamp(timestamp: Date): void; setShowMultipleLoginAlert(value: boolean): void; + setCurrentTheme(theme: string): void; + setIsAgentLoggedIn(value: boolean): void; } interface IStore extends IStoreBase { diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index 4f0f4e317..ea0d5ff3e 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -30,8 +30,8 @@ class StoreWrapper implements IStoreWrapper { return this.store.agentId; } - get selectedLoginOption() { - return this.store.selectedLoginOption; + get deviceType() { + return this.store.deviceType; } get wrapupCodes() { return this.store.wrapupCodes; @@ -42,10 +42,6 @@ class StoreWrapper implements IStoreWrapper { get isAgentLoggedIn() { return this.store.isAgentLoggedIn; } - get deviceType() { - return this.store.deviceType; - } - get taskList() { return this.store.taskList; } @@ -70,12 +66,20 @@ class StoreWrapper implements IStoreWrapper { return this.store.showMultipleLoginAlert; } + get currentTheme() { + return this.store.currentTheme; + } + + setCurrentTheme(theme: string): void { + return this.store.setCurrentTheme(theme); + } + setShowMultipleLoginAlert(value: boolean): void { return this.store.setShowMultipleLoginAlert(value); } - setSelectedLoginOption(option: string): void { - return this.store.setSelectedLoginOption(option); + setDeviceType(option: string): void { + return this.store.setDeviceType(option); } setCurrentState(state: string): void { @@ -86,6 +90,10 @@ class StoreWrapper implements IStoreWrapper { return this.store.setLastStateChangeTimestamp(timestamp); } + setIsAgentLoggedIn(value: boolean): void { + return this.store.setIsAgentLoggedIn(value); + } + init(options: InitParams): Promise { return this.store.init(options).then(() => { this.setupIncomingTaskHandler(); @@ -96,7 +104,9 @@ class StoreWrapper implements IStoreWrapper { const taskToRemove = this.store.taskList.find((task) => task.data.interactionId === taskId); if (taskToRemove) { taskToRemove.off(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned(taskId)); - taskToRemove.off(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => this.handleTaskEnd(taskId, wrapupRequired)); + taskToRemove.off(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => + this.handleTaskEnd(taskId, wrapupRequired) + ); } const updateTaskList = this.store.taskList.filter((task) => task.data.interactionId !== taskId); runInAction(() => { diff --git a/packages/contact-center/store/tests/store.ts b/packages/contact-center/store/tests/store.ts index 3878053b8..fc420b59a 100644 --- a/packages/contact-center/store/tests/store.ts +++ b/packages/contact-center/store/tests/store.ts @@ -45,7 +45,6 @@ describe('Store', () => { expect(storeInstance.loginOptions).toEqual([]); expect(storeInstance.idleCodes).toEqual([]); expect(storeInstance.agentId).toBe(''); - expect(storeInstance.selectedLoginOption).toBe(''); expect(storeInstance.wrapupCodes).toEqual([]); expect(storeInstance.incomingTask).toBeNull(); expect(storeInstance.currentTask).toBeNull(); @@ -178,8 +177,8 @@ describe('Store', () => { storeInstance.setTaskList(['task3']); expect(storeInstance.taskList).toEqual(['task3']); - storeInstance.setSelectedLoginOption('option1'); - expect(storeInstance.selectedLoginOption).toBe('option1'); + storeInstance.setDeviceType('option1'); + expect(storeInstance.deviceType).toBe('option1'); storeInstance.setCurrentTheme('theme1'); expect(storeInstance.currentTheme).toBe('theme1'); diff --git a/packages/contact-center/store/tests/storeEventsWrapper.ts b/packages/contact-center/store/tests/storeEventsWrapper.ts index 5cc34ae7d..3429eb8ca 100644 --- a/packages/contact-center/store/tests/storeEventsWrapper.ts +++ b/packages/contact-center/store/tests/storeEventsWrapper.ts @@ -14,7 +14,6 @@ jest.mock('../src/store', () => ({ logger: 'mockLogger', idleCodes: 'mockIdleCodes', agentId: 'mockAgentId', - selectedLoginOption: 'mockSelectedLoginOption', wrapupCodes: 'mockWrapupCodes', currentTask: 'mockCurrentTask', isAgentLoggedIn: 'mockIsAgentLoggedIn', @@ -28,7 +27,7 @@ jest.mock('../src/store', () => ({ setShowMultipleLoginAlert: jest.fn(), setCurrentState: jest.fn(), setLastStateChangeTimestamp: jest.fn(), - setSelectedLoginOption: jest.fn(), + setDeviceType: jest.fn(), init: jest.fn().mockResolvedValue({}), setIncomingTask: jest.fn(), setCurrentTask: jest.fn(), @@ -59,8 +58,8 @@ describe('storeEventsWrapper', () => { expect(storeWrapper.agentId).toBe('mockAgentId'); }); - it('should proxy selectedLoginOption', () => { - expect(storeWrapper.selectedLoginOption).toBe('mockSelectedLoginOption'); + it('should proxy deviceType', () => { + expect(storeWrapper.deviceType).toBe('mockDeviceType'); }); it('should proxy wrapupCodes', () => { @@ -202,9 +201,9 @@ describe('storeEventsWrapper', () => { it('should set selected login option', () => { const option = 'newLoginOption'; - storeWrapper.setSelectedLoginOption(option); + storeWrapper.setDeviceType(option); - expect(storeWrapper['store'].setSelectedLoginOption).toHaveBeenCalledWith(option); + expect(storeWrapper['store'].setDeviceType).toHaveBeenCalledWith(option); }); }); @@ -327,9 +326,9 @@ describe('storeEventsWrapper', () => { it('should set selected login option', () => { const option = 'newLoginOption'; - storeWrapper.setSelectedLoginOption(option); + storeWrapper.setDeviceType(option); - expect(storeWrapper['store'].setSelectedLoginOption).toHaveBeenCalledWith(option); + expect(storeWrapper['store'].setDeviceType).toHaveBeenCalledWith(option); }); it('should handle multilogin session modal with in correct data', async () => { @@ -354,9 +353,9 @@ describe('storeEventsWrapper', () => { it('should set selected login option', () => { const option = 'newLoginOption'; - storeWrapper.setSelectedLoginOption(option); + storeWrapper.setDeviceType(option); - expect(storeWrapper['store'].setSelectedLoginOption).toHaveBeenCalledWith(option); + expect(storeWrapper['store'].setDeviceType).toHaveBeenCalledWith(option); }); it('should handle state change event with incorrect data', async () => { diff --git a/packages/contact-center/task/src/IncomingTask/index.tsx b/packages/contact-center/task/src/IncomingTask/index.tsx index 9bd8687e1..9cf0116e9 100644 --- a/packages/contact-center/task/src/IncomingTask/index.tsx +++ b/packages/contact-center/task/src/IncomingTask/index.tsx @@ -7,8 +7,8 @@ import IncomingTaskPresentational from './incoming-task.presentational'; import {IncomingTaskProps} from '../task.types'; const IncomingTaskComponent: React.FunctionComponent = ({onAccepted, onDeclined}) => { - const {cc, selectedLoginOption, incomingTask, logger} = store; - const result = useIncomingTask({cc, incomingTask, onAccepted, onDeclined, selectedLoginOption, logger}); + const {cc, deviceType, incomingTask, logger} = store; + const result = useIncomingTask({cc, incomingTask, onAccepted, onDeclined, deviceType, logger}); const props = { ...result, diff --git a/packages/contact-center/task/src/TaskList/index.tsx b/packages/contact-center/task/src/TaskList/index.tsx index 8f0035429..a023e008f 100644 --- a/packages/contact-center/task/src/TaskList/index.tsx +++ b/packages/contact-center/task/src/TaskList/index.tsx @@ -6,9 +6,9 @@ import TaskListPresentational from './task-list.presentational'; import {useTaskList} from '../helper'; const TaskListComponent: React.FunctionComponent = () => { - const {cc, taskList, currentTask, selectedLoginOption, logger} = store; + const {cc, taskList, currentTask, deviceType, logger} = store; - const result = useTaskList({cc, selectedLoginOption, logger, taskList}); + const result = useTaskList({cc, deviceType, logger, taskList}); const props = { ...result, currentTask, diff --git a/packages/contact-center/task/src/helper.ts b/packages/contact-center/task/src/helper.ts index d2fde5a9d..2229246c9 100644 --- a/packages/contact-center/task/src/helper.ts +++ b/packages/contact-center/task/src/helper.ts @@ -5,8 +5,8 @@ import {TASK_EVENTS, useCallControlProps, UseTaskListProps, UseTaskProps} from ' // Hook for managing the task list export const useTaskList = (props: UseTaskListProps) => { - const {selectedLoginOption, onTaskAccepted, onTaskDeclined, logger, taskList} = props; - const isBrowser = selectedLoginOption === 'BROWSER'; + const {deviceType, onTaskAccepted, onTaskDeclined, logger, taskList} = props; + const isBrowser = deviceType === 'BROWSER'; const logError = (message: string, method: string) => { logger.error(message, { @@ -47,8 +47,8 @@ export const useTaskList = (props: UseTaskListProps) => { }; export const useIncomingTask = (props: UseTaskProps) => { - const {cc, onAccepted, onDeclined, selectedLoginOption, incomingTask, logger} = props; - const isBrowser = selectedLoginOption === 'BROWSER'; + const {cc, onAccepted, onDeclined, deviceType, incomingTask, logger} = props; + const isBrowser = deviceType === 'BROWSER'; const logError = (message: string, method: string) => { logger.error(message, { diff --git a/packages/contact-center/task/src/task.types.ts b/packages/contact-center/task/src/task.types.ts index 4bb7bb0eb..578865e30 100644 --- a/packages/contact-center/task/src/task.types.ts +++ b/packages/contact-center/task/src/task.types.ts @@ -78,7 +78,7 @@ export interface TaskProps { /** * Selected login option */ - selectedLoginOption: string; + deviceType: string; /** * List of tasks @@ -93,11 +93,11 @@ export interface TaskProps { export type UseTaskProps = Pick< TaskProps, - 'cc' | 'incomingTask' | 'onAccepted' | 'onDeclined' | 'selectedLoginOption' | 'logger' + 'cc' | 'incomingTask' | 'onAccepted' | 'onDeclined' | 'deviceType' | 'logger' >; export type UseTaskListProps = Pick< TaskProps, - 'cc' | 'taskList' | 'selectedLoginOption' | 'onTaskAccepted' | 'onTaskDeclined' | 'logger' + 'cc' | 'taskList' | 'deviceType' | 'onTaskAccepted' | 'onTaskDeclined' | 'logger' >; export type IncomingTaskPresentationalProps = Pick; export type IncomingTaskProps = Pick; diff --git a/packages/contact-center/task/tests/CallControl/index.tsx b/packages/contact-center/task/tests/CallControl/index.tsx index 7f467d65b..70f455a87 100644 --- a/packages/contact-center/task/tests/CallControl/index.tsx +++ b/packages/contact-center/task/tests/CallControl/index.tsx @@ -8,7 +8,7 @@ import '@testing-library/jest-dom'; // Mock the store jest.mock('@webex/cc-store', () => ({ cc: {}, - selectedLoginOption: 'BROWSER', + deviceType: 'BROWSER', wrapupCodes: [], logger: {}, currentTask: { diff --git a/packages/contact-center/task/tests/IncomingTask/index.tsx b/packages/contact-center/task/tests/IncomingTask/index.tsx index ff71c9972..fdf6bbda6 100644 --- a/packages/contact-center/task/tests/IncomingTask/index.tsx +++ b/packages/contact-center/task/tests/IncomingTask/index.tsx @@ -8,7 +8,7 @@ import '@testing-library/jest-dom'; // Mock the store jest.mock('@webex/cc-store', () => ({ cc: {}, - selectedLoginOption: 'BROWSER', + deviceType: 'BROWSER', })); const onAcceptedCb = jest.fn(); @@ -35,7 +35,7 @@ describe('IncomingTask Component', () => { // Assert that the useIncomingTask hook is called with the correct arguments expect(useIncomingTaskSpy).toHaveBeenCalledWith({ cc: store.cc, - selectedLoginOption: store.selectedLoginOption, + deviceType: store.deviceType, onAccepted: onAcceptedCb, onDeclined: onDeclinedCb, }); diff --git a/packages/contact-center/task/tests/TaskList/index.tsx b/packages/contact-center/task/tests/TaskList/index.tsx index 4fe02fa7a..19afcedb0 100644 --- a/packages/contact-center/task/tests/TaskList/index.tsx +++ b/packages/contact-center/task/tests/TaskList/index.tsx @@ -14,7 +14,7 @@ jest.mock('../../src/TaskList/task-list.presentational', () => { // Mock `@webex/cc-store`. jest.mock('@webex/cc-store', () => ({ cc: {}, - selectedLoginOption: 'BROWSER', + deviceType: 'BROWSER', onAccepted: jest.fn(), onDeclined: jest.fn(), })); @@ -49,6 +49,6 @@ describe('TaskList Component', () => { expect(TaskListPresentational).toHaveBeenCalledWith({taskList: taskListMock}, {}); // Verify that `useTaskList` is called with the correct arguments. - expect(helper.useTaskList).toHaveBeenCalledWith({cc: store.cc, selectedLoginOption: 'BROWSER'}); + expect(helper.useTaskList).toHaveBeenCalledWith({cc: store.cc, deviceType: 'BROWSER'}); }); }); diff --git a/packages/contact-center/task/tests/helper.ts b/packages/contact-center/task/tests/helper.ts index f716cd164..3f514bc7e 100644 --- a/packages/contact-center/task/tests/helper.ts +++ b/packages/contact-center/task/tests/helper.ts @@ -41,7 +41,7 @@ describe('useIncomingTask Hook', () => { incomingTask: taskMock, onAccepted: onTaskAccepted, onDeclined: onTaskDeclined, - selectedLoginOption: 'BROWSER', + deviceType: 'BROWSER', logger, }) ); @@ -65,7 +65,7 @@ describe('useIncomingTask Hook', () => { incomingTask: taskMock, onAccepted: onTaskAccepted, onDeclined: onTaskDeclined, - selectedLoginOption: 'BROWSER', + deviceType: 'BROWSER', logger, }) ); @@ -96,7 +96,7 @@ describe('useIncomingTask Hook', () => { incomingTask: noIdTask, onAccepted: onTaskAccepted, onDeclined: onTaskDeclined, - selectedLoginOption: 'BROWSER', + deviceType: 'BROWSER', logger, }) ); @@ -125,7 +125,7 @@ describe('useIncomingTask Hook', () => { incomingTask: taskMock, onAccepted: null, onDeclined: null, - selectedLoginOption: 'BROWSER', + deviceType: 'BROWSER', logger, }) ); @@ -149,7 +149,7 @@ describe('useIncomingTask Hook', () => { incomingTask: taskMock, onAccepted: null, onDeclined: null, - selectedLoginOption: 'BROWSER', + deviceType: 'BROWSER', logger, }) ); @@ -174,7 +174,7 @@ describe('useIncomingTask Hook', () => { }; const {result} = renderHook(() => - useIncomingTask({cc: ccMock, incomingTask: failingTask, onAccepted, selectedLoginOption: 'BROWSER', logger}) + useIncomingTask({cc: ccMock, incomingTask: failingTask, onAccepted, deviceType: 'BROWSER', logger}) ); act(() => { @@ -201,7 +201,7 @@ describe('useIncomingTask Hook', () => { }; const {result} = renderHook(() => - useIncomingTask({cc: ccMock, incomingTask: failingTask, onDeclined, selectedLoginOption: 'BROWSER', logger}) + useIncomingTask({cc: ccMock, incomingTask: failingTask, onDeclined, deviceType: 'BROWSER', logger}) ); act(() => { @@ -230,7 +230,7 @@ describe('useTaskList Hook', () => { it('should call onTaskAccepted callback when provided', async () => { const {result} = renderHook(() => - useTaskList({cc: ccMock, selectedLoginOption: '', onTaskAccepted, logger, taskList: mockTaskList}) + useTaskList({cc: ccMock, deviceType: '', onTaskAccepted, logger, taskList: mockTaskList}) ); act(() => { @@ -248,7 +248,7 @@ describe('useTaskList Hook', () => { it('should return if not task is passed while calling acceptTask', async () => { // This test is purely to improve the coverage report, as the acceptTask function cannot be called without a task const {result} = renderHook(() => - useTaskList({cc: ccMock, selectedLoginOption: '', onTaskAccepted, logger, taskList: mockTaskList}) + useTaskList({cc: ccMock, deviceType: '', onTaskAccepted, logger, taskList: mockTaskList}) ); act(() => { @@ -264,7 +264,7 @@ describe('useTaskList Hook', () => { it('should return if not task is passed while calling acceptTask', async () => { // This test is purely to improve the coverage report, as the acceptTask function cannot be called without a task const {result} = renderHook(() => - useTaskList({cc: ccMock, selectedLoginOption: '', onTaskDeclined, logger, taskList: mockTaskList}) + useTaskList({cc: ccMock, deviceType: '', onTaskDeclined, logger, taskList: mockTaskList}) ); act(() => { @@ -279,7 +279,7 @@ describe('useTaskList Hook', () => { it('should call onTaskDeclined callback when provided', async () => { const {result} = renderHook(() => - useTaskList({cc: ccMock, selectedLoginOption: '', onTaskDeclined, logger, taskList: mockTaskList}) + useTaskList({cc: ccMock, deviceType: '', onTaskDeclined, logger, taskList: mockTaskList}) ); act(() => { @@ -302,7 +302,7 @@ describe('useTaskList Hook', () => { }; const {result} = renderHook(() => - useTaskList({cc: ccMock, onTaskAccepted, selectedLoginOption: 'BROWSER', logger, taskList: mockTaskList}) + useTaskList({cc: ccMock, onTaskAccepted, deviceType: 'BROWSER', logger, taskList: mockTaskList}) ); act(() => { @@ -329,7 +329,7 @@ describe('useTaskList Hook', () => { }; const {result} = renderHook(() => - useTaskList({cc: ccMock, onTaskDeclined, selectedLoginOption: 'BROWSER', logger, taskList: mockTaskList}) + useTaskList({cc: ccMock, onTaskDeclined, deviceType: 'BROWSER', logger, taskList: mockTaskList}) ); act(() => { @@ -355,7 +355,7 @@ describe('useTaskList Hook', () => { onTaskAccepted: null, onTaskDeclined: null, logger, - selectedLoginOption: '', + deviceType: '', taskList: mockTaskList, }) ); @@ -379,7 +379,7 @@ describe('useTaskList Hook', () => { onTaskAccepted: null, onTaskDeclined: null, logger, - selectedLoginOption: '', + deviceType: '', taskList: mockTaskList, }) ); From 3751062063af395e26f1cd64332fe3509837a179 Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Wed, 12 Feb 2025 11:55:30 +0530 Subject: [PATCH 07/23] fix(store): fix-timer-issue-call-rona-issue --- packages/contact-center/store/src/store.ts | 2 ++ .../contact-center/store/src/store.types.ts | 1 + .../store/src/storeEventsWrapper.ts | 33 +++++++++++++------ .../contact-center/user-state/src/helper.ts | 26 +++++++++++---- .../cc/samples-cc-react-app/src/App.tsx | 2 +- 5 files changed, 46 insertions(+), 18 deletions(-) diff --git a/packages/contact-center/store/src/store.ts b/packages/contact-center/store/src/store.ts index cd292bb24..7012d98e9 100644 --- a/packages/contact-center/store/src/store.ts +++ b/packages/contact-center/store/src/store.ts @@ -40,6 +40,8 @@ class Store implements IStore { incomingTask: observable, taskList: observable, wrapupRequired: observable, + setLastStateChangeTimestamp: observable, + currentState: observable, }); } diff --git a/packages/contact-center/store/src/store.types.ts b/packages/contact-center/store/src/store.types.ts index 16923f3a8..93d0f99b6 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -84,6 +84,7 @@ TASK_PAUSE = 'task:pause', TASK_RESUME = 'task:resume', TASK_END = 'task:end', TASK_WRAPUP = 'task:wrapup', +TASK_REJECT= 'task:rejected', } // TODO: remove this once cc sdk exports this enum diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index ea0d5ff3e..a3b97e2c7 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -101,28 +101,34 @@ class StoreWrapper implements IStoreWrapper { } handleTaskRemove = (taskId: string) => { + // Remove the task from the taskList const taskToRemove = this.store.taskList.find((task) => task.data.interactionId === taskId); if (taskToRemove) { taskToRemove.off(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned(taskId)); taskToRemove.off(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => this.handleTaskEnd(taskId, wrapupRequired) ); + taskToRemove.off(TASK_EVENTS.TASK_REJECT, () => this.handleTaskRemove(taskId)); } const updateTaskList = this.store.taskList.filter((task) => task.data.interactionId !== taskId); + runInAction(() => { this.store.setTaskList(updateTaskList); - this.store.setCurrentTask(null); this.store.setWrapupRequired(false); + + // Remove the task from currentTask or incomingTask if it is the same task + if (this.store.currentTask?.data.interactionId === taskId) { + this.store.setCurrentTask(null); + } + + if (this.store.incomingTask?.data.interactionId === taskId) { + this.store.setIncomingTask(null); + } }); }; handleTaskEnd = (taskId: string, wrapupRequired: boolean) => { - // Task has been ended based on wrapupRequired we either set wrapupRequired or remove the task - if (wrapupRequired) { - this.store.setWrapupRequired(wrapupRequired); - } else { - this.handleTaskRemove(taskId); - } + this.store.setWrapupRequired(wrapupRequired); }; handleTaskAssigned = (task: ITask) => () => { @@ -140,10 +146,17 @@ class StoreWrapper implements IStoreWrapper { } // Attach event listeners to the task - task.on(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => - this.handleTaskEnd(task.data.interactionId, wrapupRequired) - ); + task.on(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => { + this.handleTaskEnd(task.data.interactionId, wrapupRequired); + }); + + // When we receive TASK_ASSIGNED the task was accepted by the agent and we need wrap up task.on(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned(task)); + + // When we receive TASK_REJECT sdk changes the agent status + // When we receive TASK_REJECT that means the task was not accepted by the agent and we wont need wrap up + task.on(TASK_EVENTS.TASK_REJECT, () => this.handleTaskRemove(task.data.interactionId)); + this.store.setTaskList([...this.store.taskList, task]); }; diff --git a/packages/contact-center/user-state/src/helper.ts b/packages/contact-center/user-state/src/helper.ts index 7270570f2..2148f7c62 100644 --- a/packages/contact-center/user-state/src/helper.ts +++ b/packages/contact-center/user-state/src/helper.ts @@ -31,23 +31,35 @@ export const useUserState = ({idleCodes, agentId, cc, currentState, lastStateCha const blob = new Blob([workerScript], {type: 'application/javascript'}); const workerUrl = URL.createObjectURL(blob); workerRef.current = new Worker(workerUrl); - workerRef.current.postMessage({type: 'start', startTime: Date.now()}); workerRef.current.onmessage = (event) => { + workerRef.current.postMessage({type: 'start', startTime: Date.now()}); setElapsedTime(event.data); }; }, []); useEffect(() => { - if (workerRef.current && lastStateChangeTimestamp) { - const timeNow = new Date(); - const elapsed = Math.floor(Math.abs(timeNow.getTime() - lastStateChangeTimestamp.getTime()) / 1000); - setElapsedTime(elapsed); - workerRef.current.postMessage({type: 'reset', startTime: lastStateChangeTimestamp.getTime()}); + if (workerRef.current) { + workerRef.current.terminate(); + const blob = new Blob([workerScript], {type: 'application/javascript'}); + const workerUrl = URL.createObjectURL(blob); + workerRef.current = new Worker(workerUrl); + workerRef.current.onmessage = (event) => { + setElapsedTime(event.data); + }; + if (lastStateChangeTimestamp) { + const timeNow = new Date(); + const elapsed = Math.floor(Math.abs(timeNow.getTime() - lastStateChangeTimestamp.getTime()) / 1000); + setElapsedTime(elapsed); + workerRef.current.postMessage({type: 'reset', startTime: lastStateChangeTimestamp.getTime()}); + } else { + workerRef.current.postMessage({type: 'start', startTime: Date.now()}); + } } + return () => { workerRef.current?.terminate(); }; - }, [lastStateChangeTimestamp]); + }, [currentState]); const setAgentStatus = (selectedCode) => { const {auxCodeId, state} = { diff --git a/widgets-samples/cc/samples-cc-react-app/src/App.tsx b/widgets-samples/cc/samples-cc-react-app/src/App.tsx index b59745db6..c31684b9d 100644 --- a/widgets-samples/cc/samples-cc-react-app/src/App.tsx +++ b/widgets-samples/cc/samples-cc-react-app/src/App.tsx @@ -164,7 +164,7 @@ function App() { {showWidgets && ( <> {selectedWidgets.stationLogin && } - {isLoggedIn && ( + {store.isAgentLoggedIn && ( <> {selectedWidgets.userState && } {selectedWidgets.incomingTask && } From 6dfd0c899aca3bbd98431497086c80a95e1268ee Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Wed, 12 Feb 2025 14:08:20 +0530 Subject: [PATCH 08/23] fix(store): add-tests --- packages/contact-center/store/src/store.ts | 1 - .../store/src/storeEventsWrapper.ts | 4 +- packages/contact-center/store/tests/store.ts | 4 ++ .../store/tests/storeEventsWrapper.ts | 38 +++++++++++++++++-- .../contact-center/user-state/tests/helper.ts | 17 --------- 5 files changed, 40 insertions(+), 24 deletions(-) diff --git a/packages/contact-center/store/src/store.ts b/packages/contact-center/store/src/store.ts index 7012d98e9..480af4e9b 100644 --- a/packages/contact-center/store/src/store.ts +++ b/packages/contact-center/store/src/store.ts @@ -40,7 +40,6 @@ class Store implements IStore { incomingTask: observable, taskList: observable, wrapupRequired: observable, - setLastStateChangeTimestamp: observable, currentState: observable, }); } diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index a3b97e2c7..16a0f750d 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -105,9 +105,7 @@ class StoreWrapper implements IStoreWrapper { const taskToRemove = this.store.taskList.find((task) => task.data.interactionId === taskId); if (taskToRemove) { taskToRemove.off(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned(taskId)); - taskToRemove.off(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => - this.handleTaskEnd(taskId, wrapupRequired) - ); + taskToRemove.off(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => this.handleTaskEnd(taskId, wrapupRequired)); taskToRemove.off(TASK_EVENTS.TASK_REJECT, () => this.handleTaskRemove(taskId)); } const updateTaskList = this.store.taskList.filter((task) => task.data.interactionId !== taskId); diff --git a/packages/contact-center/store/tests/store.ts b/packages/contact-center/store/tests/store.ts index fc420b59a..e94f541bb 100644 --- a/packages/contact-center/store/tests/store.ts +++ b/packages/contact-center/store/tests/store.ts @@ -55,6 +55,7 @@ describe('Store', () => { expect(makeAutoObservable).toHaveBeenCalledWith(storeInstance, { cc: expect.any(Function), + currentState: expect.any(Object), currentTask: expect.any(Object), incomingTask: expect.any(Object), taskList: expect.any(Object), @@ -191,6 +192,9 @@ describe('Store', () => { storeInstance.setLastStateChangeTimestamp(new Date()); expect(storeInstance.lastStateChangeTimestamp).toEqual(expect.any(Date)); + + storeInstance.setIsAgentLoggedIn(true); + expect(storeInstance.isAgentLoggedIn).toBe(true); }); }); }); diff --git a/packages/contact-center/store/tests/storeEventsWrapper.ts b/packages/contact-center/store/tests/storeEventsWrapper.ts index 3429eb8ca..e764a1ce5 100644 --- a/packages/contact-center/store/tests/storeEventsWrapper.ts +++ b/packages/contact-center/store/tests/storeEventsWrapper.ts @@ -16,7 +16,7 @@ jest.mock('../src/store', () => ({ agentId: 'mockAgentId', wrapupCodes: 'mockWrapupCodes', currentTask: 'mockCurrentTask', - isAgentLoggedIn: 'mockIsAgentLoggedIn', + isAgentLoggedIn: false, deviceType: 'mockDeviceType', taskList: 'mockTaskList', incomingTask: 'mockIncomingTask', @@ -24,6 +24,7 @@ jest.mock('../src/store', () => ({ currentState: 'mockCurrentState', lastStateChangeTimestamp: 'mockLastStateChangeTimestamp', showMultipleLoginAlert: 'mockShowMultipleLoginAlert', + currentTheme: 'mockCurrentTheme', setShowMultipleLoginAlert: jest.fn(), setCurrentState: jest.fn(), setLastStateChangeTimestamp: jest.fn(), @@ -33,6 +34,8 @@ jest.mock('../src/store', () => ({ setCurrentTask: jest.fn(), setTaskList: jest.fn(), setWrapupRequired: jest.fn(), + setCurrentTheme: jest.fn(), + setIsAgentLoggedIn: jest.fn(), }), })); @@ -71,7 +74,7 @@ describe('storeEventsWrapper', () => { }); it('should proxy isAgentLoggedIn', () => { - expect(storeWrapper.isAgentLoggedIn).toBe('mockIsAgentLoggedIn'); + expect(storeWrapper.isAgentLoggedIn).toBe(false); }); it('should proxy deviceType', () => { @@ -126,6 +129,24 @@ describe('storeEventsWrapper', () => { storeWrapper.setLastStateChangeTimestamp(timestamp); expect(storeWrapper['store'].setLastStateChangeTimestamp).toHaveBeenCalledWith(timestamp); }); + + it('should proxy currentTheme', () => { + expect(storeWrapper.currentTheme).toBe('mockCurrentTheme'); + }); + + it('should proxy setCurrentTheme', () => { + expect(storeWrapper.setCurrentTheme).toBeInstanceOf(Function); + + storeWrapper.setCurrentTheme('newTheme'); + expect(storeWrapper['store'].setCurrentTheme).toHaveBeenCalledWith('newTheme'); + }); + + it('should proxy setIsAgentLoggedIn', () => { + expect(storeWrapper.setIsAgentLoggedIn).toBeInstanceOf(Function); + + storeWrapper.setIsAgentLoggedIn(false); + expect(storeWrapper['store'].isAgentLoggedIn).toBe(false); + }); }); describe('storeEventsWrapper', () => { @@ -169,23 +190,31 @@ describe('storeEventsWrapper', () => { it('should handle task removal', () => { storeWrapper['store'].taskList = [mockTask]; + storeWrapper['store'].currentTask = mockTask; + storeWrapper['store'].incomingTask = mockTask; storeWrapper.handleTaskRemove(mockTask.data.interactionId); expect(mockTask.off).toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, expect.any(Function)); expect(mockTask.off).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, expect.any(Function)); + expect(storeWrapper['store'].setTaskList).toHaveBeenCalledWith([]); expect(storeWrapper['store'].setCurrentTask).toHaveBeenCalledWith(null); + expect(storeWrapper['store'].setIncomingTask).toHaveBeenCalledWith(null); + expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(false); }); it('should handle task removal when no task is present', () => { storeWrapper['store'].taskList = []; + storeWrapper['store'].currentTask = null; + storeWrapper['store'].incomingTask = null; storeWrapper.handleTaskRemove(mockTask.data.interactionId); expect(mockTask.off).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, expect.any(Function)); expect(mockTask.off).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_END, expect.any(Function)); expect(storeWrapper['store'].setTaskList).toHaveBeenCalledWith([]); - expect(storeWrapper['store'].setCurrentTask).toHaveBeenCalledWith(null); + expect(storeWrapper['store'].setCurrentTask).not.toHaveBeenCalledWith(null); + expect(storeWrapper['store'].setIncomingTask).not.toHaveBeenCalledWith(null); expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(false); }); @@ -305,12 +334,15 @@ describe('storeEventsWrapper', () => { it('should handle task removal', () => { storeWrapper['store'].taskList = [mockTask]; + storeWrapper['store'].currentTask = mockTask; + storeWrapper['store'].incomingTask = mockTask; storeWrapper.handleTaskRemove(mockTask.data.interactionId); expect(mockTask.off).toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, expect.any(Function)); expect(mockTask.off).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, expect.any(Function)); expect(storeWrapper['store'].setTaskList).toHaveBeenCalledWith([]); expect(storeWrapper['store'].setCurrentTask).toHaveBeenCalledWith(null); + expect(storeWrapper['store'].setIncomingTask).toHaveBeenCalledWith(null); expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(false); }); diff --git a/packages/contact-center/user-state/tests/helper.ts b/packages/contact-center/user-state/tests/helper.ts index 86277de1b..7412a76d8 100644 --- a/packages/contact-center/user-state/tests/helper.ts +++ b/packages/contact-center/user-state/tests/helper.ts @@ -69,23 +69,6 @@ describe('useUserState Hook', () => { expect(result.current.elapsedTime).toBe(3); }); - it('should reset elapsedTime when lastStateChangeTimestamp changes', async () => { - const newTimestamp = new Date(); - const {result, rerender} = renderHook( - ({timestamp}) => - useUserState({idleCodes, agentId, cc: mockCC, currentState: '0', lastStateChangeTimestamp: timestamp}), - {initialProps: {timestamp: new Date(Date.now() - 5000)}} - ); - - expect(result.current.elapsedTime).toBe(5); - - rerender({timestamp: newTimestamp}); - - await waitFor(() => { - expect(result.current.elapsedTime).toBe(0); - }); - }); - it('should handle setAgentStatus correctly and update state', async () => { mockCC.setAgentState.mockResolvedValueOnce({data: {auxCodeId: '2', lastStateChangeTimestamp: new Date()}}); const {result} = renderHook(() => From 102712c06ae01ab0a3cb9be8d183410ef38054b6 Mon Sep 17 00:00:00 2001 From: Rajesh Kumar Date: Wed, 12 Feb 2025 20:13:12 +0530 Subject: [PATCH 09/23] feat(cc): add task hydrate --- .../contact-center/store/src/store.types.ts | 1 + .../store/src/storeEventsWrapper.ts | 27 ++++++++++++++--- .../store/tests/storeEventsWrapper.ts | 30 +++++++++++++++++++ .../call-control.presentational.tsx | 16 +++++++++- .../call-control.presentational.tsx | 29 ++++++++++++++++++ 5 files changed, 98 insertions(+), 5 deletions(-) diff --git a/packages/contact-center/store/src/store.types.ts b/packages/contact-center/store/src/store.types.ts index 93d0f99b6..e710f0aa4 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -85,6 +85,7 @@ TASK_RESUME = 'task:resume', TASK_END = 'task:end', TASK_WRAPUP = 'task:wrapup', TASK_REJECT= 'task:rejected', +TASK_HYDRATE = 'task:hydrate', } // TODO: remove this once cc sdk exports this enum diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index 16a0f750d..da686178d 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -105,7 +105,9 @@ class StoreWrapper implements IStoreWrapper { const taskToRemove = this.store.taskList.find((task) => task.data.interactionId === taskId); if (taskToRemove) { taskToRemove.off(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned(taskId)); - taskToRemove.off(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => this.handleTaskEnd(taskId, wrapupRequired)); + taskToRemove.off(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => + this.handleTaskEnd(taskId, wrapupRequired) + ); taskToRemove.off(TASK_EVENTS.TASK_REJECT, () => this.handleTaskRemove(taskId)); } const updateTaskList = this.store.taskList.filter((task) => task.data.interactionId !== taskId); @@ -174,20 +176,37 @@ class StoreWrapper implements IStoreWrapper { } }; + handleTaskHydrate = (task: ITask) => { + this.store.setCurrentTask(task); + this.store.setTaskList([...this.store.taskList, task]); + + const {interaction, agentId} = task.data; + const {state, isTerminated, participants} = interaction; + + // Update call control states + if (isTerminated) { + // wrapup + const wrapupRequired = state === 'wrapUp' && !participants[agentId].isWrappedUp; + this.store.setWrapupRequired(wrapupRequired); + + return; + } + }; + setupIncomingTaskHandler() { const ccSDK = this.store.cc; - ccSDK.on(TASK_EVENTS.TASK_INCOMING, (task) => { - this.handleIncomingTask(task); - }); + ccSDK.on(TASK_EVENTS.TASK_INCOMING, this.handleIncomingTask); ccSDK.on(CC_EVENTS.AGENT_STATE_CHANGE, this.handleStateChange); ccSDK.on(CC_EVENTS.AGENT_MULTI_LOGIN, this.handleMultiLoginCloseSession); + ccSDK.on(TASK_EVENTS.TASK_HYDRATE, this.handleTaskHydrate); return () => { ccSDK.off(TASK_EVENTS.TASK_INCOMING, this.handleIncomingTask); ccSDK.off(CC_EVENTS.AGENT_STATE_CHANGE, this.handleStateChange); ccSDK.off(CC_EVENTS.AGENT_MULTI_LOGIN, this.handleMultiLoginCloseSession); + ccSDK.off(TASK_EVENTS.TASK_HYDRATE, this.handleTaskHydrate); }; } } diff --git a/packages/contact-center/store/tests/storeEventsWrapper.ts b/packages/contact-center/store/tests/storeEventsWrapper.ts index e764a1ce5..25b7e6fa8 100644 --- a/packages/contact-center/store/tests/storeEventsWrapper.ts +++ b/packages/contact-center/store/tests/storeEventsWrapper.ts @@ -420,12 +420,42 @@ describe('storeEventsWrapper', () => { expect(storeWrapper['store'].setCurrentState).toHaveBeenCalledWith('available'); }); + it('should handle hydrating the store with correct data', async () => { + const options = {someOption: 'value'}; + await storeWrapper.init(options); + storeWrapper['store'].taskList = []; + + const mockTask = { + data: { + interaction: { + isTerminated: true, + state: 'wrapUp', + participants: { + agent1: { + isWrappedUp: false, + }, + }, + }, + agentId: 'agent1', + }, + }; + + act(() => { + storeWrapper['store'].cc.on.mock.calls[3][1](mockTask); + }); + + expect(storeWrapper['store'].setCurrentTask).toHaveBeenCalledWith(mockTask); + expect(storeWrapper['store'].setTaskList).toHaveBeenCalledWith([mockTask]); + expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(true); + }); + it('should return a function to remove event listeners on cc object', () => { const removeListeners = storeWrapper.setupIncomingTaskHandler(); removeListeners(); expect(storeWrapper['cc'].off).toHaveBeenCalledWith(TASK_EVENTS.TASK_INCOMING, expect.any(Function)); + expect(storeWrapper['cc'].off).toHaveBeenCalledWith(TASK_EVENTS.TASK_HYDRATE, expect.any(Function)); expect(storeWrapper['cc'].off).toHaveBeenCalledWith(CC_EVENTS.AGENT_STATE_CHANGE, expect.any(Function)); expect(storeWrapper['cc'].off).toHaveBeenCalledWith(CC_EVENTS.AGENT_MULTI_LOGIN, expect.any(Function)); }); diff --git a/packages/contact-center/task/src/CallControl/call-control.presentational.tsx b/packages/contact-center/task/src/CallControl/call-control.presentational.tsx index 328272161..2589ad5b9 100644 --- a/packages/contact-center/task/src/CallControl/call-control.presentational.tsx +++ b/packages/contact-center/task/src/CallControl/call-control.presentational.tsx @@ -1,4 +1,4 @@ -import React, {useState} from 'react'; +import React, {useEffect, useState} from 'react'; import {WrapupCodes} from '@webex/cc-store'; import {CallControlPresentationalProps} from '../task.types'; @@ -12,6 +12,20 @@ function CallControlPresentational(props: CallControlPresentationalProps) { const {currentTask, audioRef, toggleHold, toggleRecording, endCall, wrapupCall, wrapupCodes, wrapupRequired} = props; + useEffect(() => { + if (!currentTask || !currentTask.data || !currentTask.data.interaction) return; + + const {interaction, mediaResourceId} = currentTask.data; + const {media, callProcessingDetails} = interaction; + const isHold = media && media[mediaResourceId] && media[mediaResourceId].isHold; + setIsHeld(isHold); + + if (callProcessingDetails) { + const {isPaused} = callProcessingDetails; + setIsRecording(!isPaused); + } + }, [currentTask]); + const handletoggleHold = () => { toggleHold(!isHeld); setIsHeld(!isHeld); diff --git a/packages/contact-center/task/tests/CallControl/call-control.presentational.tsx b/packages/contact-center/task/tests/CallControl/call-control.presentational.tsx index 6e0c4831a..c3d576bab 100644 --- a/packages/contact-center/task/tests/CallControl/call-control.presentational.tsx +++ b/packages/contact-center/task/tests/CallControl/call-control.presentational.tsx @@ -107,4 +107,33 @@ describe('CallControlPresentational', () => { const wrapupButton = screen.getByText('Wrap Up'); expect(wrapupButton).not.toBeDisabled(); }); + + it('sets the isHeld and isRecording state correctly based on store data', () => { + const mockTask = { + data: { + interaction: { + media: { + mediaResourceId1: { + isHold: true, + }, + }, + callProcessingDetails: { + isPaused: false, + }, + }, + mediaResourceId: 'mediaResourceId1', + agentId: 'agent1', + wrapupRequired: false, + }, + }; + + const propsWithCurrentTask = {...defaultProps, currentTask: mockTask}; + render(); + + const holdButton = screen.getByText('Resume'); + expect(holdButton).not.toBeDisabled(); + + const pauseButton = screen.getByText('Pause Recording'); + expect(pauseButton).not.toBeDisabled(); + }); }); From d5a80349e5799d9f82b1eebc3712aa7fd3ead9ea Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Wed, 12 Feb 2025 20:31:59 +0530 Subject: [PATCH 10/23] fix(store): task-end-workaround-wrapuprequired --- .../store/src/storeEventsWrapper.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index 16a0f750d..4e94261f7 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -105,7 +105,9 @@ class StoreWrapper implements IStoreWrapper { const taskToRemove = this.store.taskList.find((task) => task.data.interactionId === taskId); if (taskToRemove) { taskToRemove.off(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned(taskId)); - taskToRemove.off(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => this.handleTaskEnd(taskId, wrapupRequired)); + taskToRemove.off(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => + this.handleTaskEnd(taskToRemove, wrapupRequired) + ); taskToRemove.off(TASK_EVENTS.TASK_REJECT, () => this.handleTaskRemove(taskId)); } const updateTaskList = this.store.taskList.filter((task) => task.data.interactionId !== taskId); @@ -125,8 +127,14 @@ class StoreWrapper implements IStoreWrapper { }); }; - handleTaskEnd = (taskId: string, wrapupRequired: boolean) => { - this.store.setWrapupRequired(wrapupRequired); + handleTaskEnd = (task: ITask, wrapupRequired: boolean) => { + // TODO: SDK needs to send only 1 event on end : https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-615785 + if (task.data.interaction.state === 'connected') { + this.store.setWrapupRequired(true); + return; + } else if (task.data.interaction.state !== 'connected' && this.store.wrapupRequired !== true) { + this.handleTaskRemove(task.data.interactionId); + } }; handleTaskAssigned = (task: ITask) => () => { @@ -145,7 +153,7 @@ class StoreWrapper implements IStoreWrapper { // Attach event listeners to the task task.on(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => { - this.handleTaskEnd(task.data.interactionId, wrapupRequired); + this.handleTaskEnd(task, wrapupRequired); }); // When we receive TASK_ASSIGNED the task was accepted by the agent and we need wrap up From dcfc0dcead636e9f1b0d910a511f4dc921dc4fd9 Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Wed, 12 Feb 2025 20:39:52 +0530 Subject: [PATCH 11/23] fix(store): add-tests --- .../store/tests/storeEventsWrapper.ts | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/packages/contact-center/store/tests/storeEventsWrapper.ts b/packages/contact-center/store/tests/storeEventsWrapper.ts index e764a1ce5..9bd68287e 100644 --- a/packages/contact-center/store/tests/storeEventsWrapper.ts +++ b/packages/contact-center/store/tests/storeEventsWrapper.ts @@ -153,6 +153,9 @@ describe('storeEventsWrapper', () => { const mockTask: ITask = ({ data: { interactionId: 'interaction1', + interaction: { + state: 'connected', + }, }, on: jest.fn(), off: jest.fn(), @@ -219,13 +222,13 @@ describe('storeEventsWrapper', () => { }); it('should handle task end', () => { - storeWrapper.handleTaskEnd(mockTask.data.interactionId, true); + storeWrapper.handleTaskEnd(mockTask, true); expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(true); - storeWrapper.handleTaskEnd(mockTask.data.interactionId, false); + storeWrapper.handleTaskEnd(mockTask, false); - expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(false); + expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(true); }); it('should set selected login option', () => { @@ -240,6 +243,9 @@ describe('storeEventsWrapper', () => { const mockTask: ITask = ({ data: { interactionId: 'interaction1', + interaction: { + state: 'connected', + }, }, on: jest.fn(), off: jest.fn(), @@ -347,11 +353,28 @@ describe('storeEventsWrapper', () => { }); it('should handle task end', () => { - storeWrapper.handleTaskEnd(mockTask.data.interactionId, true); + storeWrapper.handleTaskEnd(mockTask, true); + + expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(true); + + storeWrapper.handleTaskEnd(mockTask, false); expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(true); + }); + + it('should handle task end when call is not connected', () => { + mockTask.data.interaction.state = 'new'; + storeWrapper['store'].wrapupRequired = false; + storeWrapper.handleTaskEnd(mockTask, true); + + expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(false); + + storeWrapper.handleTaskEnd(mockTask, false); + + expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(false); - storeWrapper.handleTaskEnd(mockTask.data.interactionId, false); + storeWrapper['store'].wrapupRequired = true; + storeWrapper.handleTaskEnd(mockTask, true); expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(false); }); From c90a6524244789d61a441429be7e3427fb7af2ac Mon Sep 17 00:00:00 2001 From: Rajesh Kumar Date: Wed, 12 Feb 2025 21:12:46 +0530 Subject: [PATCH 12/23] disable end button on hold --- .../task/src/CallControl/call-control.presentational.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contact-center/task/src/CallControl/call-control.presentational.tsx b/packages/contact-center/task/src/CallControl/call-control.presentational.tsx index 2589ad5b9..14ec3ac79 100644 --- a/packages/contact-center/task/src/CallControl/call-control.presentational.tsx +++ b/packages/contact-center/task/src/CallControl/call-control.presentational.tsx @@ -65,7 +65,7 @@ function CallControlPresentational(props: CallControlPresentationalProps) { -
From 3eb367cf620b40331693987e400c1d24b0bb2cd9 Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Fri, 14 Feb 2025 09:29:47 +0530 Subject: [PATCH 13/23] fix(store): event-listener --- packages/contact-center/store/package.json | 2 +- packages/contact-center/store/src/store.ts | 4 ++- .../contact-center/store/src/store.types.ts | 2 +- .../store/src/storeEventsWrapper.ts | 22 ++++++++---- .../contact-center/store/src/taskHandler.ts | 0 packages/contact-center/task/package.json | 2 +- .../cc/samples-cc-react-app/src/App.tsx | 3 ++ yarn.lock | 34 +++++++++---------- 8 files changed, 41 insertions(+), 28 deletions(-) create mode 100644 packages/contact-center/store/src/taskHandler.ts diff --git a/packages/contact-center/store/package.json b/packages/contact-center/store/package.json index 3cd560c76..4b85ce913 100644 --- a/packages/contact-center/store/package.json +++ b/packages/contact-center/store/package.json @@ -22,7 +22,7 @@ "dependencies": { "mobx": "6.13.5", "typescript": "5.6.3", - "webex": "3.7.0-wxcc.12" + "webex": "3.7.0-wxcc.15" }, "devDependencies": { "@babel/core": "7.25.2", diff --git a/packages/contact-center/store/src/store.ts b/packages/contact-center/store/src/store.ts index 480af4e9b..6fba72d1f 100644 --- a/packages/contact-center/store/src/store.ts +++ b/packages/contact-center/store/src/store.ts @@ -10,6 +10,7 @@ import { IStore, ILogger, IWrapupCode, + TASK_EVENTS, } from './store.types'; import {ITask} from '@webex/plugin-cc'; @@ -119,7 +120,7 @@ class Store implements IStore { }); } - init(options: InitParams): Promise { + init(options: InitParams, hydrateCallback): Promise { if ('webex' in options) { // If devs decide to go with webex, they will have to listen to the ready event before calling init // This has to be documented @@ -138,6 +139,7 @@ class Store implements IStore { }); webex.once('ready', () => { + hydrateCallback(webex.cc); clearTimeout(timer); this.registerCC(webex) .then(() => { diff --git a/packages/contact-center/store/src/store.types.ts b/packages/contact-center/store/src/store.types.ts index e710f0aa4..7669f01d2 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -45,7 +45,7 @@ interface IStoreBase { lastStateChangeTimestamp: Date; showMultipleLoginAlert: boolean; currentTheme: string; - init(params: InitParams): Promise; + init(params: InitParams,callback:any): Promise; setDeviceType(option: string): void; setCurrentState(state: string): void; setLastStateChangeTimestamp(timestamp: Date): void; diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index 9cb897930..2864cd987 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -95,9 +95,7 @@ class StoreWrapper implements IStoreWrapper { } init(options: InitParams): Promise { - return this.store.init(options).then(() => { - this.setupIncomingTaskHandler(); - }); + return this.store.init(options, this.setupIncomingTaskHandler); } handleTaskRemove = (taskId: string) => { @@ -183,6 +181,17 @@ class StoreWrapper implements IStoreWrapper { }; handleTaskHydrate = (task: ITask) => { + task.on(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => { + this.handleTaskEnd(task, wrapupRequired); + }); + + // When we receive TASK_ASSIGNED the task was accepted by the agent and we need wrap up + task.on(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned(task)); + + // When we receive TASK_REJECT sdk changes the agent status + // When we receive TASK_REJECT that means the task was not accepted by the agent and we wont need wrap up + task.on(TASK_EVENTS.TASK_REJECT, () => this.handleTaskRemove(task.data.interactionId)); + this.store.setCurrentTask(task); this.store.setTaskList([...this.store.taskList, task]); @@ -199,9 +208,8 @@ class StoreWrapper implements IStoreWrapper { } }; - setupIncomingTaskHandler() { - const ccSDK = this.store.cc; - + setupIncomingTaskHandler = (ccSDK: any) => { + console.log('Shreyas: Setting up incoming task handler'); ccSDK.on(TASK_EVENTS.TASK_INCOMING, this.handleIncomingTask); ccSDK.on(CC_EVENTS.AGENT_STATE_CHANGE, this.handleStateChange); @@ -214,7 +222,7 @@ class StoreWrapper implements IStoreWrapper { ccSDK.off(CC_EVENTS.AGENT_MULTI_LOGIN, this.handleMultiLoginCloseSession); ccSDK.off(TASK_EVENTS.TASK_HYDRATE, this.handleTaskHydrate); }; - } + }; } // Create and export a single instance of the wrapper diff --git a/packages/contact-center/store/src/taskHandler.ts b/packages/contact-center/store/src/taskHandler.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/contact-center/task/package.json b/packages/contact-center/task/package.json index a2cb12b91..e2fdd9c16 100644 --- a/packages/contact-center/task/package.json +++ b/packages/contact-center/task/package.json @@ -21,7 +21,7 @@ "dependencies": { "@webex/cc-store": "workspace:*", "mobx-react-lite": "^4.1.0", - "webex": "3.7.0-wxcc.12" + "webex": "3.7.0-wxcc.15" }, "devDependencies": { "@babel/core": "7.25.2", diff --git a/widgets-samples/cc/samples-cc-react-app/src/App.tsx b/widgets-samples/cc/samples-cc-react-app/src/App.tsx index c31684b9d..e9f5fa0c4 100644 --- a/widgets-samples/cc/samples-cc-react-app/src/App.tsx +++ b/widgets-samples/cc/samples-cc-react-app/src/App.tsx @@ -22,6 +22,9 @@ function App() { logger: { level: 'log', }, + cc: { + allowMultiLogin: true, + }, }; const onLogin = () => { diff --git a/yarn.lock b/yarn.lock index 73b854507..b2a585bdb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5677,9 +5677,9 @@ __metadata: languageName: node linkType: hard -"@webex/calling@npm:3.6.0-wxcc.1": - version: 3.6.0-wxcc.1 - resolution: "@webex/calling@npm:3.6.0-wxcc.1" +"@webex/calling@npm:3.7.0-wxcc.1": + version: 3.7.0-wxcc.1 + resolution: "@webex/calling@npm:3.7.0-wxcc.1" dependencies: "@types/platform": "npm:1.3.4" "@webex/internal-media-core": "npm:2.11.1" @@ -5690,7 +5690,7 @@ __metadata: platform: "npm:1.3.6" uuid: "npm:8.3.2" xstate: "npm:4.30.6" - checksum: 10c0/e553d2b376c9c691a2cabb6fbbd4b8e3507506916c81a77b0b4d39f91a222f28216c1ab60edbfc60fbfc74dc701df1ea93f4fec113203ab17e0e79359b19dd8e + checksum: 10c0/9d623cb31697a9976c8f8a4f62bab82d959f78f31b31746bc0a8e4c5cd6645352eae4234565f2028ecf6ce1a476e17d700c37cafcb8096ae091e12a94c865f0c languageName: node linkType: hard @@ -5778,7 +5778,7 @@ __metadata: mobx: "npm:6.13.5" ts-loader: "npm:9.5.1" typescript: "npm:5.6.3" - webex: "npm:3.7.0-wxcc.12" + webex: "npm:3.7.0-wxcc.15" webpack: "npm:5.94.0" webpack-cli: "npm:5.1.4" webpack-merge: "npm:6.0.1" @@ -5807,7 +5807,7 @@ __metadata: mobx-react-lite: "npm:^4.1.0" ts-loader: "npm:9.5.1" typescript: "npm:5.6.3" - webex: "npm:3.7.0-wxcc.12" + webex: "npm:3.7.0-wxcc.15" webpack: "npm:5.94.0" webpack-cli: "npm:5.1.4" webpack-merge: "npm:6.0.1" @@ -7103,17 +7103,17 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-cc@npm:3.5.0-wxcc.18": - version: 3.5.0-wxcc.18 - resolution: "@webex/plugin-cc@npm:3.5.0-wxcc.18" +"@webex/plugin-cc@npm:3.5.0-wxcc.21": + version: 3.5.0-wxcc.21 + resolution: "@webex/plugin-cc@npm:3.5.0-wxcc.21" dependencies: "@types/platform": "npm:1.3.4" - "@webex/calling": "npm:3.6.0-wxcc.1" + "@webex/calling": "npm:3.7.0-wxcc.1" "@webex/internal-plugin-mercury": "npm:3.5.0-wxcc.1" "@webex/webex-core": "npm:3.5.0-wxcc.1" buffer: "npm:6.0.3" jest-html-reporters: "npm:3.0.11" - checksum: 10c0/13b115ad2593d272a41932e2423a502f423240ce4c0c1040327dfef1f7eb12abd49b36916228f6953be0bfe7c966c353d66eb0b5978ea05b1b2a7a57c74aa091 + checksum: 10c0/9f9c71b3084b1e1f1101004abc4b8b0bf245a9b288081a3e1abdcdcbe6660e6abb55dd253d8fe5faf3545554a35385cb9f0811f62b066af14aeafd98ccdada6a languageName: node linkType: hard @@ -28078,13 +28078,13 @@ __metadata: languageName: node linkType: hard -"webex@npm:3.7.0-wxcc.12": - version: 3.7.0-wxcc.12 - resolution: "webex@npm:3.7.0-wxcc.12" +"webex@npm:3.7.0-wxcc.15": + version: 3.7.0-wxcc.15 + resolution: "webex@npm:3.7.0-wxcc.15" dependencies: "@babel/polyfill": "npm:^7.12.1" "@babel/runtime-corejs2": "npm:^7.14.8" - "@webex/calling": "npm:3.6.0-wxcc.1" + "@webex/calling": "npm:3.7.0-wxcc.1" "@webex/common": "npm:3.5.0-wxcc.1" "@webex/internal-plugin-calendar": "npm:3.5.0-wxcc.1" "@webex/internal-plugin-device": "npm:3.5.0-wxcc.1" @@ -28094,7 +28094,7 @@ __metadata: "@webex/internal-plugin-voicea": "npm:3.5.0-wxcc.1" "@webex/plugin-attachment-actions": "npm:3.5.0-wxcc.1" "@webex/plugin-authorization": "npm:3.5.0-wxcc.1" - "@webex/plugin-cc": "npm:3.5.0-wxcc.18" + "@webex/plugin-cc": "npm:3.5.0-wxcc.21" "@webex/plugin-device-manager": "npm:3.5.0-wxcc.1" "@webex/plugin-logger": "npm:3.5.0-wxcc.1" "@webex/plugin-meetings": "npm:3.5.0-wxcc.1" @@ -28108,7 +28108,7 @@ __metadata: "@webex/storage-adapter-local-storage": "npm:3.5.0-wxcc.1" "@webex/webex-core": "npm:3.5.0-wxcc.1" lodash: "npm:^4.17.21" - checksum: 10c0/2c12d3df8f49bf276860dde9a7ca0f7dd0aa65ddbf9acf30918ac8638fc556adbecf918a3c2781927d0b42e9259b7fad7e1f36c47fdfd97778c7e20c158a9fcd + checksum: 10c0/0fb9690a694746d9bd4cb8e9ed48083a3cedeeb91e1befe58aa607ab78a01b7c5cfb822647ed3382fca33318771547892ab218fa534c65ce17f0793838e2e883 languageName: node linkType: hard From 50fb2f5aee1d244b85eb0ff03e9c9fe8dbadc231 Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Fri, 14 Feb 2025 10:12:31 +0530 Subject: [PATCH 14/23] fix(store): move-setters-to-wrapper --- .../station-login.presentational.tsx | 16 ++-- .../station-login/tests/helper.ts | 2 +- packages/contact-center/store/src/store.ts | 40 --------- .../contact-center/store/src/store.types.ts | 23 +++-- .../store/src/storeEventsWrapper.ts | 84 ++++++++++++------- 5 files changed, 72 insertions(+), 93 deletions(-) diff --git a/packages/contact-center/station-login/src/station-login/station-login.presentational.tsx b/packages/contact-center/station-login/src/station-login/station-login.presentational.tsx index db3092d39..45a1a9b9c 100644 --- a/packages/contact-center/station-login/src/station-login/station-login.presentational.tsx +++ b/packages/contact-center/station-login/src/station-login/station-login.presentational.tsx @@ -1,7 +1,7 @@ -import React, {useEffect, useRef} from 'react'; -import {StationLoginPresentationalProps} from './station-login.types'; +import React, { useEffect, useRef} from 'react'; +import { StationLoginPresentationalProps } from './station-login.types'; import './station-login.style.scss'; -import {MULTIPLE_SIGN_IN_ALERT_MESSAGE, MULTIPLE_SIGN_IN_ALERT_TITLE} from './constants'; +import { MULTIPLE_SIGN_IN_ALERT_MESSAGE, MULTIPLE_SIGN_IN_ALERT_TITLE } from './constants'; const StationLoginPresentational: React.FunctionComponent = (props) => { const { @@ -66,7 +66,8 @@ const StationLoginPresentational: React.FunctionComponent { + const selectLoginOption = (event: { target: { value: string } }) => + { const dialNumber = document.querySelector('#dialNumber') as HTMLInputElement; const deviceType = event.target.value; setDeviceType(deviceType); @@ -91,7 +92,7 @@ const StationLoginPresentational: React.FunctionComponent + <> {showMultipleLoginAlert && (

{MULTIPLE_SIGN_IN_ALERT_TITLE}

@@ -154,8 +155,7 @@ const StationLoginPresentational: React.FunctionComponent
-
+ ); }; - -export default StationLoginPresentational; +export default StationLoginPresentational; \ No newline at end of file diff --git a/packages/contact-center/station-login/tests/helper.ts b/packages/contact-center/station-login/tests/helper.ts index a4f6cd372..97016bee5 100644 --- a/packages/contact-center/station-login/tests/helper.ts +++ b/packages/contact-center/station-login/tests/helper.ts @@ -108,7 +108,7 @@ describe('useStationLogin Hook', () => { loginOption: loginParams.loginOption, dialNumber: loginParams.dialNumber, }); - // expect(loginCb).toHaveBeenCalledWith(); + expect(loginCb).toHaveBeenCalledWith(); expect(result.current).toEqual({ name: 'StationLogin', diff --git a/packages/contact-center/store/src/store.ts b/packages/contact-center/store/src/store.ts index 480af4e9b..104c6a3a6 100644 --- a/packages/contact-center/store/src/store.ts +++ b/packages/contact-center/store/src/store.ts @@ -44,42 +44,6 @@ class Store implements IStore { }); } - setWrapupRequired(value: boolean): void { - this.wrapupRequired = value; - } - - setCurrentTask(task: ITask): void { - this.currentTask = task; - } - - setIncomingTask(task: ITask): void { - this.incomingTask = task; - } - - setTaskList(taskList: ITask[]): void { - this.taskList = taskList; - } - - setDeviceType(option: string): void { - this.deviceType = option; - } - - setCurrentState(state: string): void { - this.currentState = state; - } - - setLastStateChangeTimestamp(timestamp: Date): void { - this.lastStateChangeTimestamp = timestamp; - } - - setShowMultipleLoginAlert(value: boolean): void { - this.showMultipleLoginAlert = value; - } - - setIsAgentLoggedIn(value: boolean): void { - this.isAgentLoggedIn = value; - } - public static getInstance(): Store { if (!Store.instance) { console.log('Creating new store instance'); @@ -90,10 +54,6 @@ class Store implements IStore { return Store.instance; } - setCurrentTheme(theme: string): void { - this.currentTheme = theme; - } - registerCC(webex: WithWebex['webex']): Promise { this.cc = webex.cc; this.logger = this.cc.LoggerProxy; diff --git a/packages/contact-center/store/src/store.types.ts b/packages/contact-center/store/src/store.types.ts index 93d0f99b6..bbe3a4802 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -27,7 +27,7 @@ type IdleCode = { isDefault: boolean; } -interface IStoreBase { +interface IStore { teams: Team[]; loginOptions: string[]; cc: IContactCenter; @@ -46,23 +46,22 @@ interface IStoreBase { showMultipleLoginAlert: boolean; currentTheme: string; init(params: InitParams): Promise; - setDeviceType(option: string): void; - setCurrentState(state: string): void; - setLastStateChangeTimestamp(timestamp: Date): void; - setShowMultipleLoginAlert(value: boolean): void; - setCurrentTheme(theme: string): void; - setIsAgentLoggedIn(value: boolean): void; } -interface IStore extends IStoreBase { + +interface IStoreWrapper extends IStore{ + store: IStore; setCurrentTask(task: any): void; setWrapupRequired(value: boolean): void; setTaskList(taskList: ITask[]): void; setIncomingTask(task: ITask): void; -} - -interface IStoreWrapper extends IStoreBase { - handleTaskRemove(taskId: string): void; + setDeviceType(option: string): void; + setCurrentState(state: string): void; + setLastStateChangeTimestamp(timestamp: Date): void; + setShowMultipleLoginAlert(value: boolean): void; + setCurrentTheme(theme: string): void; + setIsAgentLoggedIn(value: boolean): void; + setWrapupCodes(wrapupCodes: IWrapupCode[]): void; } interface IWrapupCode { diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index 4e94261f7..fd0ef8691 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -1,10 +1,10 @@ -import {IStoreWrapper, IStore, InitParams, TASK_EVENTS, CC_EVENTS} from './store.types'; +import {IStoreWrapper, IStore, InitParams, TASK_EVENTS, CC_EVENTS, IWrapupCode} from './store.types'; import {ITask} from '@webex/plugin-cc'; import Store from './store'; import {runInAction} from 'mobx'; class StoreWrapper implements IStoreWrapper { - private store: IStore; + store: IStore; constructor() { this.store = Store.getInstance(); @@ -70,29 +70,49 @@ class StoreWrapper implements IStoreWrapper { return this.store.currentTheme; } - setCurrentTheme(theme: string): void { - return this.store.setCurrentTheme(theme); - } + setCurrentTheme = (theme: string): void => { + this.store.currentTheme = theme; + }; - setShowMultipleLoginAlert(value: boolean): void { - return this.store.setShowMultipleLoginAlert(value); - } + setShowMultipleLoginAlert = (value: boolean): void => { + this.store.showMultipleLoginAlert = value; + }; - setDeviceType(option: string): void { - return this.store.setDeviceType(option); - } + setDeviceType = (option: string): void => { + this.store.deviceType = option; + }; - setCurrentState(state: string): void { - return this.store.setCurrentState(state); - } + setCurrentState = (state: string): void => { + this.store.currentState = state; + }; - setLastStateChangeTimestamp(timestamp: Date): void { - return this.store.setLastStateChangeTimestamp(timestamp); - } + setLastStateChangeTimestamp = (timestamp: Date): void => { + this.store.lastStateChangeTimestamp = timestamp; + }; - setIsAgentLoggedIn(value: boolean): void { - return this.store.setIsAgentLoggedIn(value); - } + setIsAgentLoggedIn = (value: boolean): void => { + this.store.isAgentLoggedIn = value; + }; + + setWrapupRequired = (value: boolean): void => { + this.store.wrapupRequired = value; + }; + + setCurrentTask = (task: ITask): void => { + this.store.currentTask = task; + }; + + setIncomingTask = (task: ITask): void => { + this.store.incomingTask = task; + }; + + setTaskList = (taskList: ITask[]): void => { + this.store.taskList = taskList; + }; + + setWrapupCodes = (wrapupCodes: IWrapupCode[]): void => { + this.store.wrapupCodes = wrapupCodes; + }; init(options: InitParams): Promise { return this.store.init(options).then(() => { @@ -113,16 +133,16 @@ class StoreWrapper implements IStoreWrapper { const updateTaskList = this.store.taskList.filter((task) => task.data.interactionId !== taskId); runInAction(() => { - this.store.setTaskList(updateTaskList); - this.store.setWrapupRequired(false); + this.setTaskList(updateTaskList); + this.setWrapupRequired(false); // Remove the task from currentTask or incomingTask if it is the same task if (this.store.currentTask?.data.interactionId === taskId) { - this.store.setCurrentTask(null); + this.setCurrentTask(null); } if (this.store.incomingTask?.data.interactionId === taskId) { - this.store.setIncomingTask(null); + this.setIncomingTask(null); } }); }; @@ -130,7 +150,7 @@ class StoreWrapper implements IStoreWrapper { handleTaskEnd = (task: ITask, wrapupRequired: boolean) => { // TODO: SDK needs to send only 1 event on end : https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-615785 if (task.data.interaction.state === 'connected') { - this.store.setWrapupRequired(true); + this.setWrapupRequired(true); return; } else if (task.data.interaction.state !== 'connected' && this.store.wrapupRequired !== true) { this.handleTaskRemove(task.data.interactionId); @@ -139,13 +159,13 @@ class StoreWrapper implements IStoreWrapper { handleTaskAssigned = (task: ITask) => () => { runInAction(() => { - this.store.setCurrentTask(task); - this.store.setIncomingTask(null); + this.setCurrentTask(task); + this.setIncomingTask(null); }); }; handleIncomingTask = (task: ITask) => { - this.store.setIncomingTask(task); + this.setIncomingTask(task); if (this.store.taskList.some((t) => t.data.interactionId === task.data.interactionId)) { // Task already present in the taskList return; @@ -163,22 +183,22 @@ class StoreWrapper implements IStoreWrapper { // When we receive TASK_REJECT that means the task was not accepted by the agent and we wont need wrap up task.on(TASK_EVENTS.TASK_REJECT, () => this.handleTaskRemove(task.data.interactionId)); - this.store.setTaskList([...this.store.taskList, task]); + this.setTaskList([...this.store.taskList, task]); }; handleStateChange = (data) => { if (data && typeof data === 'object' && data.type === 'AgentStateChangeSuccess') { const DEFAULT_CODE = '0'; // Default code when no aux code is present - this.store.setCurrentState(data.auxCodeId?.trim() !== '' ? data.auxCodeId : DEFAULT_CODE); + this.setCurrentState(data.auxCodeId?.trim() !== '' ? data.auxCodeId : DEFAULT_CODE); const startTime = data.lastStateChangeTimestamp; - this.store.setLastStateChangeTimestamp(new Date(startTime)); + this.setLastStateChangeTimestamp(new Date(startTime)); } }; handleMultiLoginCloseSession = (data) => { if (data && typeof data === 'object' && data.type === 'AgentMultiLoginCloseSession') { - this.store.setShowMultipleLoginAlert(true); + this.setShowMultipleLoginAlert(true); } }; From bf2269cb2758df38bfe1e77e541fd8320bfaaa7a Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Fri, 14 Feb 2025 10:20:57 +0530 Subject: [PATCH 15/23] fix(styles): remove-unecessary-formatting --- .../station-login.presentational.tsx | 10 +-- .../cc/samples-cc-react-app/src/App.tsx | 74 +++++++++---------- 2 files changed, 39 insertions(+), 45 deletions(-) diff --git a/packages/contact-center/station-login/src/station-login/station-login.presentational.tsx b/packages/contact-center/station-login/src/station-login/station-login.presentational.tsx index 45a1a9b9c..64a706ddf 100644 --- a/packages/contact-center/station-login/src/station-login/station-login.presentational.tsx +++ b/packages/contact-center/station-login/src/station-login/station-login.presentational.tsx @@ -97,10 +97,8 @@ const StationLoginPresentational: React.FunctionComponent

{MULTIPLE_SIGN_IN_ALERT_TITLE}

{MULTIPLE_SIGN_IN_ALERT_MESSAGE}

-
- +
+
)} @@ -109,8 +107,8 @@ const StationLoginPresentational: React.FunctionComponent
Agent -
-
+
+
- - -

Contact Center widgets in a react app

- setAccessToken(e.target.value)} - /> -
- { - setCurrentTheme(themeCheckboxRef.current.checked ? 'DARK' : 'LIGHT'); - store.setCurrentTheme(themeCheckboxRef.current.checked ? 'DARK' : 'LIGHT'); - }} - />{' '} - Dark Theme -
- - {isSdkReady && ( + +

Contact Center widgets in a react app

+ setAccessToken(e.target.value)} + /> +
+ { + setCurrentTheme(themeCheckboxRef.current.checked ? 'DARK' : 'LIGHT'); + store.setCurrentTheme(themeCheckboxRef.current.checked ? 'DARK' : 'LIGHT'); + }} + /> Dark Theme +
+ + {isSdkReady && ( <>
); } From e15e7e69d75a2cd5933edb23818c18e74a506cf6 Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Fri, 14 Feb 2025 11:16:32 +0530 Subject: [PATCH 16/23] fix(store): change-event-name --- packages/contact-center/store/src/store.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contact-center/store/src/store.ts b/packages/contact-center/store/src/store.ts index 6fba72d1f..4e94cf680 100644 --- a/packages/contact-center/store/src/store.ts +++ b/packages/contact-center/store/src/store.ts @@ -120,7 +120,7 @@ class Store implements IStore { }); } - init(options: InitParams, hydrateCallback): Promise { + init(options: InitParams, setupEventListeners): Promise { if ('webex' in options) { // If devs decide to go with webex, they will have to listen to the ready event before calling init // This has to be documented @@ -139,7 +139,7 @@ class Store implements IStore { }); webex.once('ready', () => { - hydrateCallback(webex.cc); + setupEventListeners(webex.cc); clearTimeout(timer); this.registerCC(webex) .then(() => { From fae8bb06959aa6df658df4c466beabf5b38ba683 Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Fri, 14 Feb 2025 11:29:50 +0530 Subject: [PATCH 17/23] fix(store): add-UTs --- packages/contact-center/store/tests/store.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/contact-center/store/tests/store.ts b/packages/contact-center/store/tests/store.ts index e94f541bb..ce6562368 100644 --- a/packages/contact-center/store/tests/store.ts +++ b/packages/contact-center/store/tests/store.ts @@ -110,12 +110,25 @@ describe('Store', () => { }); describe('init', () => { + it('should call eventListenerCallback ', async () => { + const eventListenerCallback = jest.fn(); + const initParams = {webex: mockWebex}; + jest.spyOn(storeInstance, 'registerCC').mockResolvedValue(); + Webex.init.mockClear(); + + await storeInstance.init(initParams, eventListenerCallback); + + expect(eventListenerCallback).toHaveBeenCalled(); + expect(storeInstance.registerCC).toHaveBeenCalledWith(mockWebex); + expect(Webex.init).not.toHaveBeenCalled(); + }); + it('should call registerCC if webex is in options', async () => { const initParams = {webex: mockWebex}; jest.spyOn(storeInstance, 'registerCC').mockResolvedValue(); Webex.init.mockClear(); - await storeInstance.init(initParams); + await storeInstance.init(initParams, jest.fn()); expect(storeInstance.registerCC).toHaveBeenCalledWith(mockWebex); expect(Webex.init).not.toHaveBeenCalled(); @@ -128,7 +141,7 @@ describe('Store', () => { }; jest.spyOn(storeInstance, 'registerCC').mockResolvedValue(); - await storeInstance.init(initParams); + await storeInstance.init(initParams, jest.fn()); expect(Webex.init).toHaveBeenCalledWith({ config: initParams.webexConfig, @@ -145,7 +158,7 @@ describe('Store', () => { jest.spyOn(storeInstance, 'registerCC').mockRejectedValue(new Error('registerCC failed')); - await expect(storeInstance.init(initParams)).rejects.toThrow('registerCC failed'); + await expect(storeInstance.init(initParams, jest.fn())).rejects.toThrow('registerCC failed'); }); it('should reject the promise if Webex SDK fails to initialize', async () => { From 5694f48ba45ff6c13e1e93ace7e00fb34c8fcc5e Mon Sep 17 00:00:00 2001 From: Rajesh Kumar Date: Fri, 14 Feb 2025 16:02:39 +0530 Subject: [PATCH 18/23] test(cc-store): fix failing UTs --- .../store/tests/storeEventsWrapper.ts | 56 ++++++++++++++----- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/packages/contact-center/store/tests/storeEventsWrapper.ts b/packages/contact-center/store/tests/storeEventsWrapper.ts index 180cddfb7..f9123881f 100644 --- a/packages/contact-center/store/tests/storeEventsWrapper.ts +++ b/packages/contact-center/store/tests/storeEventsWrapper.ts @@ -169,8 +169,7 @@ describe('storeEventsWrapper', () => { const options = {someOption: 'value'}; await storeWrapper.init(options); - expect(storeWrapper['store'].init).toHaveBeenCalledWith(options); - expect(storeWrapper['store'].cc.on).toHaveBeenCalledWith(TASK_EVENTS.TASK_INCOMING, expect.any(Function)); + expect(storeWrapper['store'].init).toHaveBeenCalledWith(options, expect.any(Function)); }); it('should handle incoming task', () => { @@ -258,20 +257,27 @@ describe('storeEventsWrapper', () => { }); it('should initialize the store and set up event handlers', async () => { + const cc = storeWrapper['store'].cc; + storeWrapper['store'].init = jest.fn().mockReturnValue(storeWrapper.setupIncomingTaskHandler(cc)); + await storeWrapper.init(options); - expect(storeWrapper['store'].init).toHaveBeenCalledWith(options); - expect(storeWrapper['store'].cc.on).toHaveBeenCalledWith(TASK_EVENTS.TASK_INCOMING, expect.any(Function)); - expect(storeWrapper['store'].cc.on).toHaveBeenCalledWith(CC_EVENTS.AGENT_STATE_CHANGE, expect.any(Function)); - expect(storeWrapper['store'].cc.on).toHaveBeenCalledWith(CC_EVENTS.AGENT_MULTI_LOGIN, expect.any(Function)); + expect(storeWrapper['store'].init).toHaveBeenCalledWith(options, expect.any(Function)); + + expect(cc.on).toHaveBeenCalledWith(TASK_EVENTS.TASK_INCOMING, expect.any(Function)); + expect(cc.on).toHaveBeenCalledWith(CC_EVENTS.AGENT_STATE_CHANGE, expect.any(Function)); + expect(cc.on).toHaveBeenCalledWith(CC_EVENTS.AGENT_MULTI_LOGIN, expect.any(Function)); }); it('should handle task:incoming event ', async () => { + const cc = storeWrapper['store'].cc; + storeWrapper['store'].init = jest.fn().mockReturnValue(storeWrapper.setupIncomingTaskHandler(cc)); + await storeWrapper.init(options); storeWrapper['store'].taskList = []; act(() => { - storeWrapper['cc'].on.mock.calls[0][1](mockTask); + storeWrapper['store'].cc.on.mock.calls[0][1](mockTask); }); waitFor(() => { @@ -283,6 +289,9 @@ describe('storeEventsWrapper', () => { }); it('should handle task:end event with wrapupRequired', async () => { + const cc = storeWrapper['store'].cc; + storeWrapper['store'].init = jest.fn().mockReturnValue(storeWrapper.setupIncomingTaskHandler(cc)); + await storeWrapper.init(options); storeWrapper['store'].taskList = []; @@ -387,16 +396,23 @@ describe('storeEventsWrapper', () => { }); it('should handle multilogin session modal with in correct data', async () => { + const cc = storeWrapper['store'].cc; + storeWrapper['store'].init = jest.fn().mockReturnValue(storeWrapper.setupIncomingTaskHandler(cc)); + const options = {someOption: 'value'}; await storeWrapper.init(options); + act(() => { - storeWrapper['store'].cc.on.mock.calls[2][1]({}); + cc.on.mock.calls[2][1]({}); }); expect(storeWrapper['store'].setShowMultipleLoginAlert).not.toHaveBeenCalledWith(true); }); it('should handle multilogin session modal with correct data', async () => { + const cc = storeWrapper['store'].cc; + storeWrapper['store'].init = jest.fn().mockReturnValue(storeWrapper.setupIncomingTaskHandler(cc)); + const options = {someOption: 'value'}; await storeWrapper.init(options); act(() => { @@ -414,36 +430,48 @@ describe('storeEventsWrapper', () => { }); it('should handle state change event with incorrect data', async () => { + const cc = storeWrapper['store'].cc; + storeWrapper['store'].init = jest.fn().mockReturnValue(storeWrapper.setupIncomingTaskHandler(cc)); + const options = {someOption: 'value'}; await storeWrapper.init(options); act(() => { - storeWrapper['store'].cc.on.mock.calls[1][1]({}); + cc.on.mock.calls[1][1]({}); }); expect(storeWrapper['store'].setCurrentState).not.toHaveBeenCalledWith(); }); it('should handle state change event with correct data and emplty auxcodeId', async () => { + const cc = storeWrapper['store'].cc; + storeWrapper['store'].init = jest.fn().mockReturnValue(storeWrapper.setupIncomingTaskHandler(cc)); + const options = {someOption: 'value'}; await storeWrapper.init(options); act(() => { - storeWrapper['store'].cc.on.mock.calls[1][1]({type: 'AgentStateChangeSuccess', auxCodeId: ''}); + cc.on.mock.calls[1][1]({type: 'AgentStateChangeSuccess', auxCodeId: ''}); }); expect(storeWrapper['store'].setCurrentState).toHaveBeenCalledWith('0'); }); it('should handle state change event with correct data', async () => { + const cc = storeWrapper['store'].cc; + storeWrapper['store'].init = jest.fn().mockReturnValue(storeWrapper.setupIncomingTaskHandler(cc)); + const options = {someOption: 'value'}; await storeWrapper.init(options); act(() => { - storeWrapper['store'].cc.on.mock.calls[1][1]({type: 'AgentStateChangeSuccess', auxCodeId: 'available'}); + cc.on.mock.calls[1][1]({type: 'AgentStateChangeSuccess', auxCodeId: 'available'}); }); expect(storeWrapper['store'].setCurrentState).toHaveBeenCalledWith('available'); }); it('should handle hydrating the store with correct data', async () => { + const cc = storeWrapper['store'].cc; + storeWrapper['store'].init = jest.fn().mockReturnValue(storeWrapper.setupIncomingTaskHandler(cc)); + const options = {someOption: 'value'}; await storeWrapper.init(options); storeWrapper['store'].taskList = []; @@ -461,10 +489,11 @@ describe('storeEventsWrapper', () => { }, agentId: 'agent1', }, + on: jest.fn(), }; act(() => { - storeWrapper['store'].cc.on.mock.calls[3][1](mockTask); + cc.on.mock.calls[3][1](mockTask); }); expect(storeWrapper['store'].setCurrentTask).toHaveBeenCalledWith(mockTask); @@ -473,7 +502,8 @@ describe('storeEventsWrapper', () => { }); it('should return a function to remove event listeners on cc object', () => { - const removeListeners = storeWrapper.setupIncomingTaskHandler(); + const cc = storeWrapper['store'].cc; + const removeListeners = storeWrapper.setupIncomingTaskHandler(cc); removeListeners(); From aabf6dfbdfd0b21b6fb900b5caaaaf4284159c70 Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Fri, 14 Feb 2025 17:40:23 +0530 Subject: [PATCH 19/23] fix(store): fix-unit-tests --- packages/contact-center/store/src/store.ts | 1 + .../store/src/storeEventsWrapper.ts | 15 +- packages/contact-center/store/tests/store.ts | 33 +-- .../store/tests/storeEventsWrapper.ts | 198 +++++++++++++----- 4 files changed, 154 insertions(+), 93 deletions(-) diff --git a/packages/contact-center/store/src/store.ts b/packages/contact-center/store/src/store.ts index 53e2c2dbd..e75154a70 100644 --- a/packages/contact-center/store/src/store.ts +++ b/packages/contact-center/store/src/store.ts @@ -84,6 +84,7 @@ class Store implements IStore { if ('webex' in options) { // If devs decide to go with webex, they will have to listen to the ready event before calling init // This has to be documented + setupEventListeners(options.webex.cc); return this.registerCC(options.webex); } return new Promise((resolve, reject) => { diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index bc0015bf8..8e4cad07f 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -123,9 +123,7 @@ class StoreWrapper implements IStoreWrapper { const taskToRemove = this.store.taskList.find((task) => task.data.interactionId === taskId); if (taskToRemove) { taskToRemove.off(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned(taskId)); - taskToRemove.off(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => - this.handleTaskEnd(taskToRemove, wrapupRequired) - ); + taskToRemove.off(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => this.handleTaskEnd(taskToRemove, wrapupRequired)); taskToRemove.off(TASK_EVENTS.TASK_REJECT, () => this.handleTaskRemove(taskId)); } const updateTaskList = this.store.taskList.filter((task) => task.data.interactionId !== taskId); @@ -201,9 +199,7 @@ class StoreWrapper implements IStoreWrapper { }; handleTaskHydrate = (task: ITask) => { - task.on(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => { - this.handleTaskEnd(task, wrapupRequired); - }); + task.on(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => {this.handleTaskEnd(task, wrapupRequired);}); // When we receive TASK_ASSIGNED the task was accepted by the agent and we need wrap up task.on(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned(task)); @@ -212,8 +208,8 @@ class StoreWrapper implements IStoreWrapper { // When we receive TASK_REJECT that means the task was not accepted by the agent and we wont need wrap up task.on(TASK_EVENTS.TASK_REJECT, () => this.handleTaskRemove(task.data.interactionId)); - this.store.setCurrentTask(task); - this.store.setTaskList([...this.store.taskList, task]); + this.setCurrentTask(task); + this.setTaskList([...this.store.taskList, task]); const {interaction, agentId} = task.data; const {state, isTerminated, participants} = interaction; @@ -222,14 +218,13 @@ class StoreWrapper implements IStoreWrapper { if (isTerminated) { // wrapup const wrapupRequired = state === 'wrapUp' && !participants[agentId].isWrappedUp; - this.store.setWrapupRequired(wrapupRequired); + this.setWrapupRequired(wrapupRequired); return; } }; setupIncomingTaskHandler = (ccSDK: any) => { - console.log('Shreyas: Setting up incoming task handler'); ccSDK.on(TASK_EVENTS.TASK_INCOMING, this.handleIncomingTask); ccSDK.on(CC_EVENTS.AGENT_STATE_CHANGE, this.handleStateChange); diff --git a/packages/contact-center/store/tests/store.ts b/packages/contact-center/store/tests/store.ts index ce6562368..d6ff9b745 100644 --- a/packages/contact-center/store/tests/store.ts +++ b/packages/contact-center/store/tests/store.ts @@ -113,6 +113,7 @@ describe('Store', () => { it('should call eventListenerCallback ', async () => { const eventListenerCallback = jest.fn(); const initParams = {webex: mockWebex}; + jest.spyOn(storeInstance, 'registerCC').mockResolvedValue(); Webex.init.mockClear(); @@ -177,37 +178,5 @@ describe('Store', () => { await expect(initPromise).rejects.toThrow('Webex SDK failed to initialize'); }); - - it('should set states when called through setters', () => { - storeInstance.setIncomingTask('task1'); - expect(storeInstance.incomingTask).toBe('task1'); - - storeInstance.setCurrentTask('task2'); - expect(storeInstance.currentTask).toBe('task2'); - - storeInstance.setWrapupRequired(true); - expect(storeInstance.wrapupRequired).toBe(true); - - storeInstance.setTaskList(['task3']); - expect(storeInstance.taskList).toEqual(['task3']); - - storeInstance.setDeviceType('option1'); - expect(storeInstance.deviceType).toBe('option1'); - - storeInstance.setCurrentTheme('theme1'); - expect(storeInstance.currentTheme).toBe('theme1'); - - storeInstance.setShowMultipleLoginAlert(true); - expect(storeInstance.showMultipleLoginAlert).toBe(true); - - storeInstance.setCurrentState('state1'); - expect(storeInstance.currentState).toBe('state1'); - - storeInstance.setLastStateChangeTimestamp(new Date()); - expect(storeInstance.lastStateChangeTimestamp).toEqual(expect.any(Date)); - - storeInstance.setIsAgentLoggedIn(true); - expect(storeInstance.isAgentLoggedIn).toBe(true); - }); }); }); diff --git a/packages/contact-center/store/tests/storeEventsWrapper.ts b/packages/contact-center/store/tests/storeEventsWrapper.ts index f9123881f..ad5496796 100644 --- a/packages/contact-center/store/tests/storeEventsWrapper.ts +++ b/packages/contact-center/store/tests/storeEventsWrapper.ts @@ -105,48 +105,63 @@ describe('storeEventsWrapper', () => { expect(storeWrapper.showMultipleLoginAlert).toBe('mockShowMultipleLoginAlert'); storeWrapper.setShowMultipleLoginAlert(true); - expect(storeWrapper['store'].setShowMultipleLoginAlert).toHaveBeenCalledWith(true); + expect(storeWrapper['store'].showMultipleLoginAlert).toBe(true); }); - it('should proxy setShowMultipleLoginAlert', () => { + it('should setShowMultipleLoginAlert', () => { expect(storeWrapper.setShowMultipleLoginAlert).toBeInstanceOf(Function); storeWrapper.setShowMultipleLoginAlert(true); - expect(storeWrapper['store'].setShowMultipleLoginAlert).toHaveBeenCalledWith(true); + expect(storeWrapper['store'].showMultipleLoginAlert).toBe(true); }); - it('should proxy setCurrentState', () => { + it('should setCurrentState', () => { expect(storeWrapper.setCurrentState).toBeInstanceOf(Function); storeWrapper.setCurrentState('newState'); - expect(storeWrapper['store'].setCurrentState).toHaveBeenCalledWith('newState'); + expect(storeWrapper['store'].currentState).toBe('newState'); }); - it('should proxy setLastStateChangeTimestamp', () => { + it('should setLastStateChangeTimestamp', () => { expect(storeWrapper.setLastStateChangeTimestamp).toBeInstanceOf(Function); const timestamp = new Date(); storeWrapper.setLastStateChangeTimestamp(timestamp); - expect(storeWrapper['store'].setLastStateChangeTimestamp).toHaveBeenCalledWith(timestamp); + expect(storeWrapper['store'].lastStateChangeTimestamp).toBe(timestamp); }); - it('should proxy currentTheme', () => { + it('should currentTheme', () => { expect(storeWrapper.currentTheme).toBe('mockCurrentTheme'); }); - it('should proxy setCurrentTheme', () => { + it('should setCurrentTheme', () => { expect(storeWrapper.setCurrentTheme).toBeInstanceOf(Function); storeWrapper.setCurrentTheme('newTheme'); - expect(storeWrapper['store'].setCurrentTheme).toHaveBeenCalledWith('newTheme'); + expect(storeWrapper['store'].currentTheme).toBe('newTheme'); }); - it('should proxy setIsAgentLoggedIn', () => { + it('should setIsAgentLoggedIn', () => { expect(storeWrapper.setIsAgentLoggedIn).toBeInstanceOf(Function); storeWrapper.setIsAgentLoggedIn(false); expect(storeWrapper['store'].isAgentLoggedIn).toBe(false); }); + + it('should setIsAgentLoggedIn', () => { + expect(storeWrapper.setIsAgentLoggedIn).toBeInstanceOf(Function); + + storeWrapper.setIsAgentLoggedIn(false); + expect(storeWrapper['store'].isAgentLoggedIn).toBe(false); + }); + + it('should setWrapupCodes', () => { + const mockCodes = [{code1: 'code1'}, {code2: 'code2'}]; + expect(storeWrapper.setWrapupCodes).toBeInstanceOf(Function); + + storeWrapper.setWrapupCodes(mockCodes); + expect(storeWrapper['store'].wrapupCodes).toBe(mockCodes); + }); }); describe('storeEventsWrapper', () => { @@ -173,37 +188,49 @@ describe('storeEventsWrapper', () => { }); it('should handle incoming task', () => { + const setIncomingTaskSpy = jest.spyOn(storeWrapper, 'setIncomingTask'); + const setTaskListSpy = jest.spyOn(storeWrapper, 'setTaskList'); + storeWrapper['store'].taskList = []; storeWrapper.handleIncomingTask(mockTask); - expect(storeWrapper['store'].setIncomingTask).toHaveBeenCalledWith(mockTask); + expect(setIncomingTaskSpy).toHaveBeenCalledWith(mockTask); expect(mockTask.on).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, expect.any(Function)); expect(mockTask.on).toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, expect.any(Function)); - expect(storeWrapper['store'].setTaskList).toHaveBeenCalledWith([mockTask]); + expect(setTaskListSpy).toHaveBeenCalledWith([mockTask]); }); it('should handle task assignment', () => { + const setCurrentTaskSpy = jest.spyOn(storeWrapper, 'setCurrentTask'); + const setIncomingTaskSpy = jest.spyOn(storeWrapper, 'setIncomingTask'); + + // Why is this, this way? const handleTaskAssigned = storeWrapper.handleTaskAssigned(mockTask); handleTaskAssigned(); - expect(storeWrapper['store'].setCurrentTask).toHaveBeenCalledWith(mockTask); - expect(storeWrapper['store'].setIncomingTask).toHaveBeenCalledWith(null); + expect(setCurrentTaskSpy).toHaveBeenCalledWith(mockTask); + expect(setIncomingTaskSpy).toHaveBeenCalledWith(null); }); it('should handle task removal', () => { + const setTaskListSpy = jest.spyOn(storeWrapper, 'setTaskList'); + const setCurrentTaskSpy = jest.spyOn(storeWrapper, 'setCurrentTask'); + const setIncomingTaskSpy = jest.spyOn(storeWrapper, 'setIncomingTask'); + const setWrapupRequiredSpy = jest.spyOn(storeWrapper, 'setWrapupRequired'); + storeWrapper['store'].taskList = [mockTask]; storeWrapper['store'].currentTask = mockTask; storeWrapper['store'].incomingTask = mockTask; + storeWrapper.handleTaskRemove(mockTask.data.interactionId); expect(mockTask.off).toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, expect.any(Function)); expect(mockTask.off).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, expect.any(Function)); - expect(storeWrapper['store'].setTaskList).toHaveBeenCalledWith([]); - expect(storeWrapper['store'].setCurrentTask).toHaveBeenCalledWith(null); - expect(storeWrapper['store'].setIncomingTask).toHaveBeenCalledWith(null); - - expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(false); + expect(setTaskListSpy).toHaveBeenCalledWith([]); + expect(setCurrentTaskSpy).toHaveBeenCalledWith(null); + expect(setIncomingTaskSpy).toHaveBeenCalledWith(null); + expect(setWrapupRequiredSpy).toHaveBeenCalledWith(false); }); it('should handle task removal when no task is present', () => { @@ -214,27 +241,36 @@ describe('storeEventsWrapper', () => { expect(mockTask.off).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, expect.any(Function)); expect(mockTask.off).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_END, expect.any(Function)); - expect(storeWrapper['store'].setTaskList).toHaveBeenCalledWith([]); - expect(storeWrapper['store'].setCurrentTask).not.toHaveBeenCalledWith(null); - expect(storeWrapper['store'].setIncomingTask).not.toHaveBeenCalledWith(null); - expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(false); + const setTaskListSpy = jest.spyOn(storeWrapper, 'setTaskList'); + const setCurrentTaskSpy = jest.spyOn(storeWrapper, 'setCurrentTask'); + const setIncomingTaskSpy = jest.spyOn(storeWrapper, 'setIncomingTask'); + const setWrapupRequiredSpy = jest.spyOn(storeWrapper, 'setWrapupRequired'); + + storeWrapper.handleTaskRemove(mockTask.data.interactionId); + + expect(setTaskListSpy).toHaveBeenCalledWith([]); + expect(setCurrentTaskSpy).not.toHaveBeenCalledWith(null); + expect(setIncomingTaskSpy).not.toHaveBeenCalledWith(null); + expect(setWrapupRequiredSpy).toHaveBeenCalledWith(false); }); it('should handle task end', () => { - storeWrapper.handleTaskEnd(mockTask, true); + const setWrapupRequiredSpy = jest.spyOn(storeWrapper, 'setWrapupRequired'); - expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(true); + storeWrapper.handleTaskEnd(mockTask, true); + expect(setWrapupRequiredSpy).toHaveBeenCalledWith(true); storeWrapper.handleTaskEnd(mockTask, false); - - expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(true); + expect(setWrapupRequiredSpy).toHaveBeenCalledWith(true); }); it('should set selected login option', () => { + const setDeviceTypeSpy = jest.spyOn(storeWrapper, 'setDeviceType'); const option = 'newLoginOption'; + storeWrapper.setDeviceType(option); - expect(storeWrapper['store'].setDeviceType).toHaveBeenCalledWith(option); + expect(setDeviceTypeSpy).toHaveBeenCalledWith(option); }); }); @@ -332,22 +368,31 @@ describe('storeEventsWrapper', () => { }); it('should not add a duplicate task if the same task is present in store', () => { + const setIncomingTaskSpy = jest.spyOn(storeWrapper, 'setIncomingTask'); storeWrapper['store'].taskList = [mockTask]; storeWrapper.handleIncomingTask(mockTask); - expect(storeWrapper['store'].setIncomingTask).toHaveBeenCalledWith(mockTask); + expect(setIncomingTaskSpy).toHaveBeenCalledWith(mockTask); expect(mockTask.on).not.toHaveBeenCalledWith; }); it('should handle task assignment', () => { + jest.spyOn(storeWrapper, 'setCurrentTask'); + jest.spyOn(storeWrapper, 'setIncomingTask'); + const handleTaskAssigned = storeWrapper.handleTaskAssigned(mockTask); handleTaskAssigned(); - expect(storeWrapper['store'].setCurrentTask).toHaveBeenCalledWith(mockTask); - expect(storeWrapper['store'].setIncomingTask).toHaveBeenCalledWith(null); + expect(storeWrapper.setCurrentTask).toHaveBeenCalledWith(mockTask); + expect(storeWrapper.setIncomingTask).toHaveBeenCalledWith(null); }); it('should handle task removal', () => { + const setTaskListSpy = jest.spyOn(storeWrapper, 'setTaskList'); + const setCurrentTaskSpy = jest.spyOn(storeWrapper, 'setCurrentTask'); + const setIncomingTaskSpy = jest.spyOn(storeWrapper, 'setIncomingTask'); + const setWrapupRequiredSpy = jest.spyOn(storeWrapper, 'setWrapupRequired'); + storeWrapper['store'].taskList = [mockTask]; storeWrapper['store'].currentTask = mockTask; storeWrapper['store'].incomingTask = mockTask; @@ -355,47 +400,52 @@ describe('storeEventsWrapper', () => { expect(mockTask.off).toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, expect.any(Function)); expect(mockTask.off).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, expect.any(Function)); - expect(storeWrapper['store'].setTaskList).toHaveBeenCalledWith([]); - expect(storeWrapper['store'].setCurrentTask).toHaveBeenCalledWith(null); - expect(storeWrapper['store'].setIncomingTask).toHaveBeenCalledWith(null); - expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(false); + + expect(setTaskListSpy).toHaveBeenCalledWith([]); + expect(setCurrentTaskSpy).toHaveBeenCalledWith(null); + expect(setIncomingTaskSpy).toHaveBeenCalledWith(null); + expect(setWrapupRequiredSpy).toHaveBeenCalledWith(false); }); it('should handle task end', () => { + jest.spyOn(storeWrapper, 'setWrapupRequired'); storeWrapper.handleTaskEnd(mockTask, true); - expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(true); + expect(storeWrapper.setWrapupRequired).toHaveBeenCalledWith(true); storeWrapper.handleTaskEnd(mockTask, false); - expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(true); + expect(storeWrapper.setWrapupRequired).toHaveBeenCalledWith(true); }); it('should handle task end when call is not connected', () => { + jest.spyOn(storeWrapper, 'setWrapupRequired'); mockTask.data.interaction.state = 'new'; storeWrapper['store'].wrapupRequired = false; storeWrapper.handleTaskEnd(mockTask, true); - expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(false); + expect(storeWrapper.setWrapupRequired).toHaveBeenCalledWith(false); storeWrapper.handleTaskEnd(mockTask, false); - expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(false); + expect(storeWrapper.setWrapupRequired).toHaveBeenCalledWith(false); storeWrapper['store'].wrapupRequired = true; storeWrapper.handleTaskEnd(mockTask, true); - expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(false); + expect(storeWrapper.setWrapupRequired).toHaveBeenCalledWith(false); }); it('should set selected login option', () => { + jest.spyOn(storeWrapper, 'setDeviceType'); const option = 'newLoginOption'; storeWrapper.setDeviceType(option); - expect(storeWrapper['store'].setDeviceType).toHaveBeenCalledWith(option); + expect(storeWrapper.setDeviceType).toHaveBeenCalledWith(option); }); it('should handle multilogin session modal with in correct data', async () => { + jest.spyOn(storeWrapper, 'setShowMultipleLoginAlert'); const cc = storeWrapper['store'].cc; storeWrapper['store'].init = jest.fn().mockReturnValue(storeWrapper.setupIncomingTaskHandler(cc)); @@ -406,12 +456,13 @@ describe('storeEventsWrapper', () => { cc.on.mock.calls[2][1]({}); }); - expect(storeWrapper['store'].setShowMultipleLoginAlert).not.toHaveBeenCalledWith(true); + expect(storeWrapper.setShowMultipleLoginAlert).not.toHaveBeenCalledWith(true); }); it('should handle multilogin session modal with correct data', async () => { const cc = storeWrapper['store'].cc; storeWrapper['store'].init = jest.fn().mockReturnValue(storeWrapper.setupIncomingTaskHandler(cc)); + jest.spyOn(storeWrapper, 'setShowMultipleLoginAlert'); const options = {someOption: 'value'}; await storeWrapper.init(options); @@ -419,19 +470,21 @@ describe('storeEventsWrapper', () => { storeWrapper['store'].cc.on.mock.calls[2][1]({type: 'AgentMultiLoginCloseSession'}); }); - expect(storeWrapper['store'].setShowMultipleLoginAlert).toHaveBeenCalledWith(true); + expect(storeWrapper.setShowMultipleLoginAlert).toHaveBeenCalledWith(true); }); it('should set selected login option', () => { const option = 'newLoginOption'; + jest.spyOn(storeWrapper, 'setDeviceType'); storeWrapper.setDeviceType(option); - expect(storeWrapper['store'].setDeviceType).toHaveBeenCalledWith(option); + expect(storeWrapper.setDeviceType).toHaveBeenCalledWith(option); }); it('should handle state change event with incorrect data', async () => { const cc = storeWrapper['store'].cc; storeWrapper['store'].init = jest.fn().mockReturnValue(storeWrapper.setupIncomingTaskHandler(cc)); + jest.spyOn(storeWrapper, 'setCurrentState'); const options = {someOption: 'value'}; await storeWrapper.init(options); @@ -439,12 +492,13 @@ describe('storeEventsWrapper', () => { cc.on.mock.calls[1][1]({}); }); - expect(storeWrapper['store'].setCurrentState).not.toHaveBeenCalledWith(); + expect(storeWrapper.setCurrentState).not.toHaveBeenCalledWith(); }); it('should handle state change event with correct data and emplty auxcodeId', async () => { const cc = storeWrapper['store'].cc; storeWrapper['store'].init = jest.fn().mockReturnValue(storeWrapper.setupIncomingTaskHandler(cc)); + jest.spyOn(storeWrapper, 'setCurrentState'); const options = {someOption: 'value'}; await storeWrapper.init(options); @@ -452,12 +506,13 @@ describe('storeEventsWrapper', () => { cc.on.mock.calls[1][1]({type: 'AgentStateChangeSuccess', auxCodeId: ''}); }); - expect(storeWrapper['store'].setCurrentState).toHaveBeenCalledWith('0'); + expect(storeWrapper.setCurrentState).toHaveBeenCalledWith('0'); }); it('should handle state change event with correct data', async () => { const cc = storeWrapper['store'].cc; storeWrapper['store'].init = jest.fn().mockReturnValue(storeWrapper.setupIncomingTaskHandler(cc)); + jest.spyOn(storeWrapper, 'setCurrentState'); const options = {someOption: 'value'}; await storeWrapper.init(options); @@ -465,10 +520,14 @@ describe('storeEventsWrapper', () => { cc.on.mock.calls[1][1]({type: 'AgentStateChangeSuccess', auxCodeId: 'available'}); }); - expect(storeWrapper['store'].setCurrentState).toHaveBeenCalledWith('available'); + expect(storeWrapper.setCurrentState).toHaveBeenCalledWith('available'); }); it('should handle hydrating the store with correct data', async () => { + const setCurrentTaskSpy = jest.spyOn(storeWrapper, 'setCurrentTask'); + const setTaskListSpy = jest.spyOn(storeWrapper, 'setTaskList'); + const setWrapupRequiredSpy = jest.spyOn(storeWrapper, 'setWrapupRequired'); + const cc = storeWrapper['store'].cc; storeWrapper['store'].init = jest.fn().mockReturnValue(storeWrapper.setupIncomingTaskHandler(cc)); @@ -496,9 +555,46 @@ describe('storeEventsWrapper', () => { cc.on.mock.calls[3][1](mockTask); }); - expect(storeWrapper['store'].setCurrentTask).toHaveBeenCalledWith(mockTask); - expect(storeWrapper['store'].setTaskList).toHaveBeenCalledWith([mockTask]); - expect(storeWrapper['store'].setWrapupRequired).toHaveBeenCalledWith(true); + expect(setCurrentTaskSpy).toHaveBeenCalledWith(mockTask); + expect(setTaskListSpy).toHaveBeenCalledWith([mockTask]); + expect(setWrapupRequiredSpy).toHaveBeenCalledWith(true); + }); + + it('should handle hydrating the store with correct data', async () => { + const setCurrentTaskSpy = jest.spyOn(storeWrapper, 'setCurrentTask'); + const setTaskListSpy = jest.spyOn(storeWrapper, 'setTaskList'); + const setWrapupRequiredSpy = jest.spyOn(storeWrapper, 'setWrapupRequired'); + + const cc = storeWrapper['store'].cc; + storeWrapper['store'].init = jest.fn().mockReturnValue(storeWrapper.setupIncomingTaskHandler(cc)); + + const options = {someOption: 'value'}; + await storeWrapper.init(options); + storeWrapper['store'].taskList = []; + + const mockTask = { + data: { + interaction: { + isTerminated: false, + state: 'wrapUp', + participants: { + agent1: { + isWrappedUp: false, + }, + }, + }, + agentId: 'agent1', + }, + on: jest.fn(), + }; + + act(() => { + cc.on.mock.calls[3][1](mockTask); + }); + + expect(setCurrentTaskSpy).toHaveBeenCalledWith(mockTask); + expect(setTaskListSpy).toHaveBeenCalledWith([mockTask]); + expect(setWrapupRequiredSpy).not.toHaveBeenCalledWith(); }); it('should return a function to remove event listeners on cc object', () => { From 7ea2c7d8f5835a7c2893a5cfb73cf4aaa3faf431 Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Fri, 14 Feb 2025 18:43:19 +0530 Subject: [PATCH 20/23] fix(samples): add-wc-samples-changes --- .../cc/samples-cc-react-app/src/App.tsx | 2 +- widgets-samples/cc/samples-cc-wc-app/app.js | 172 ++++++++++++------ .../cc/samples-cc-wc-app/index.html | 18 ++ 3 files changed, 133 insertions(+), 59 deletions(-) diff --git a/widgets-samples/cc/samples-cc-react-app/src/App.tsx b/widgets-samples/cc/samples-cc-react-app/src/App.tsx index b6c4be581..f0f46012e 100644 --- a/widgets-samples/cc/samples-cc-react-app/src/App.tsx +++ b/widgets-samples/cc/samples-cc-react-app/src/App.tsx @@ -163,7 +163,7 @@ function App() { {showWidgets && ( <> {selectedWidgets.stationLogin && } - {store.isAgentLoggedIn && ( + {store.isAgentLoggedIn && isLoggedIn && ( <> {selectedWidgets.userState && } {selectedWidgets.incomingTask && } diff --git a/widgets-samples/cc/samples-cc-wc-app/app.js b/widgets-samples/cc/samples-cc-wc-app/app.js index e98267e66..a0c836bd2 100644 --- a/widgets-samples/cc/samples-cc-wc-app/app.js +++ b/widgets-samples/cc/samples-cc-wc-app/app.js @@ -6,87 +6,143 @@ const ccUserState = document.createElement('widget-cc-user-state'); const ccIncomingTask = document.createElement('widget-cc-incoming-task'); const ccTaskList = document.createElement('widget-cc-task-list'); const ccCallControl = document.createElement('widget-cc-call-control'); -const themeProviderElem = document.getElementById('theme-provider-elem'); +const themeProviderElem = document.getElementById('theme-provider-elem'); +const widgetSelection = document.getElementById('widget-selection'); +const stationLoginCheckbox = document.getElementById('stationLoginCheckbox'); +const userStateCheckbox = document.getElementById('userStateCheckbox'); +const incomingTaskCheckbox = document.getElementById('incomingTaskCheckbox'); +const taskListCheckbox = document.getElementById('taskListCheckbox'); +const callControlCheckbox = document.getElementById('callControlCheckbox'); themeElem.addEventListener('change', () => { - store.setCurrentTheme(themeElem.checked ? 'DARK' : 'LIGHT'); - themeProviderElem.setAttribute('themeclass', themeElem.checked ? 'mds-theme-stable-darkWebex' : 'mds-theme-stable-lightWebex'); + store.setCurrentTheme(themeElem.checked ? 'DARK' : 'LIGHT'); + themeProviderElem.setAttribute( + 'themeclass', + themeElem.checked ? 'mds-theme-stable-darkWebex' : 'mds-theme-stable-lightWebex' + ); }); if (!ccStationLogin && !ccUserState) { - console.error('Failed to find the required elements'); + console.error('Failed to find the required elements'); } -function switchButtonState(){ - const buttonElem = document.querySelector('button'); - buttonElem.disabled = accessTokenElem.value.trim() === ''; +function switchButtonState() { + const buttonElem = document.querySelector('button'); + buttonElem.disabled = accessTokenElem.value.trim() === ''; } -function initWidgets(){ - const webexConfig = { - fedramp: false, - logger: { - level: 'log' - }, - } - store.init({ - webexConfig, - access_token: accessTokenElem.value.trim() - }).then(() => { - ccStationLogin.onLogin = loginSuccess; - ccStationLogin.onLogout = logoutSuccess; - ccIncomingTask.onAccepted = onAccepted; - ccIncomingTask.onDeclined = onDeclined; - ccTaskList.onTaskAccepted = onTaskAccepted; - ccTaskList.onTaskDeclined = onTaskDeclined; - ccCallControl.onHoldResume = onHoldResume; - ccCallControl.onEnd = onEnd; - ccCallControl.onWrapup = onWrapup; - ccStationLogin.classList.remove('disabled'); - }).catch((error) => { - console.error('Failed to initialize widgets:', error); +function initWidgets() { + const webexConfig = { + fedramp: false, + logger: { + level: 'log', + }, + }; + store + .init({ + webexConfig, + access_token: accessTokenElem.value.trim(), + }) + .then(() => { + ccStationLogin.onLogin = loginSuccess; + ccStationLogin.onLogout = logoutSuccess; + ccIncomingTask.onAccepted = onAccepted; + ccIncomingTask.onDeclined = onDeclined; + ccTaskList.onTaskAccepted = onTaskAccepted; + ccTaskList.onTaskDeclined = onTaskDeclined; + ccCallControl.onHoldResume = onHoldResume; + ccCallControl.onEnd = onEnd; + ccCallControl.onWrapup = onWrapup; + widgetSelection.classList.remove('disabled'); + }) + .catch((error) => { + console.error('Failed to initialize widgets:', error); }); } -function loginSuccess(){ - console.log('Agent login has been succesful'); +function submitWidgets() { + if (stationLoginCheckbox.checked) { + ccStationLogin.classList.remove('disabled'); + widgetsContainer.appendChild(ccStationLogin); + } else { + ccStationLogin.classList.add('disabled'); + } + + if (userStateCheckbox.checked) { + ccUserState.classList.remove('disabled'); + } else { + ccUserState.classList.add('disabled'); + } + + if (incomingTaskCheckbox.checked) { + ccIncomingTask.classList.remove('disabled'); + } else { + ccIncomingTask.classList.add('disabled'); + } + if (taskListCheckbox.checked) { + ccTaskList.classList.remove('disabled'); + } else { + ccTaskList.classList.add('disabled'); + } + if (callControlCheckbox.checked) { + ccCallControl.classList.remove('disabled'); + } else { + ccCallControl.classList.add('disabled'); + } +} + +function loginSuccess() { + console.log('Agent login has been succesful'); + if (userStateCheckbox.checked) { ccUserState.classList.remove('disabled'); widgetsContainer.appendChild(ccUserState); + } + if (incomingTaskCheckbox.checked) { + ccIncomingTask.classList.remove('disabled'); widgetsContainer.appendChild(ccIncomingTask); + } + if (taskListCheckbox.checked) { + ccTaskList.classList.remove('disabled'); widgetsContainer.appendChild(ccTaskList); + } + if (callControlCheckbox.checked) { + ccCallControl.classList.remove('disabled'); widgetsContainer.appendChild(ccCallControl); + } } -function logoutSuccess(){ - console.log('Agent logout has been succesful'); - ccUserState.classList.add('disabled'); +function logoutSuccess() { + console.log('Agent logout has been succesful'); + ccUserState.classList.add('disabled'); + ccIncomingTask.classList.add('disabled'); + ccTaskList.classList.add('disabled'); + ccCallControl.classList.add('disabled'); } -function onAccepted(){ - console.log('onAccepted Invoked'); -}; +function onAccepted() { + console.log('onAccepted Invoked'); +} -function onDeclined(){ - console.log('onDeclined invoked'); -}; +function onDeclined() { + console.log('onDeclined invoked'); +} -function onTaskAccepted(){ - console.log('onTaskAccepted invoked'); -}; +function onTaskAccepted() { + console.log('onTaskAccepted invoked'); +} -function onTaskDeclined(){ - console.log('onTaskDeclined invoked'); -}; +function onTaskDeclined() { + console.log('onTaskDeclined invoked'); +} function onHoldResume() { - console.log('onHoldResume invoked'); - } - - function onEnd() { - console.log('onEnd invoked'); - } - - function onWrapup() { - console.log('onWrapUp invoked'); - } - \ No newline at end of file + console.log('onHoldResume invoked'); +} + +function onEnd() { + console.log('onEnd invoked'); +} + +function onWrapup() { + console.log('onWrapUp invoked'); +} diff --git a/widgets-samples/cc/samples-cc-wc-app/index.html b/widgets-samples/cc/samples-cc-wc-app/index.html index da3fe9e24..65a112091 100644 --- a/widgets-samples/cc/samples-cc-wc-app/index.html +++ b/widgets-samples/cc/samples-cc-wc-app/index.html @@ -34,6 +34,24 @@

Contact Center widgets as web-component

+
+ + + + + + +
From cf22511e860ce0b300605e669291adc85ba8e8a0 Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Fri, 14 Feb 2025 19:47:28 +0530 Subject: [PATCH 21/23] fix(store): review-comments --- packages/contact-center/store/src/taskHandler.ts | 0 .../task/src/CallControl/call-control.presentational.tsx | 3 ++- packages/contact-center/user-state/src/helper.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) delete mode 100644 packages/contact-center/store/src/taskHandler.ts diff --git a/packages/contact-center/store/src/taskHandler.ts b/packages/contact-center/store/src/taskHandler.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/contact-center/task/src/CallControl/call-control.presentational.tsx b/packages/contact-center/task/src/CallControl/call-control.presentational.tsx index 14ec3ac79..8b1b84033 100644 --- a/packages/contact-center/task/src/CallControl/call-control.presentational.tsx +++ b/packages/contact-center/task/src/CallControl/call-control.presentational.tsx @@ -39,7 +39,8 @@ function CallControlPresentational(props: CallControlPresentationalProps) { const handleWrapupCall = () => { if (selectedWrapupReason && selectedWrapupId) { wrapupCall(selectedWrapupReason, selectedWrapupId); - setSelectedWrapupReason(''); + setSelectedWrapupReason(null); + setSelectedWrapupId(null); } }; diff --git a/packages/contact-center/user-state/src/helper.ts b/packages/contact-center/user-state/src/helper.ts index 2148f7c62..5ee0e3fce 100644 --- a/packages/contact-center/user-state/src/helper.ts +++ b/packages/contact-center/user-state/src/helper.ts @@ -31,8 +31,8 @@ export const useUserState = ({idleCodes, agentId, cc, currentState, lastStateCha const blob = new Blob([workerScript], {type: 'application/javascript'}); const workerUrl = URL.createObjectURL(blob); workerRef.current = new Worker(workerUrl); + workerRef.current.postMessage({type: 'start', startTime: Date.now()}); workerRef.current.onmessage = (event) => { - workerRef.current.postMessage({type: 'start', startTime: Date.now()}); setElapsedTime(event.data); }; }, []); From 671d8d432bea134d01847f70eb8787b54d64026c Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Mon, 17 Feb 2025 14:22:20 +0530 Subject: [PATCH 22/23] fix(store): conflict-resolution --- .../station-login.presentational.tsx | 20 +-- .../contact-center/store/src/store.types.ts | 1 + .../store/src/storeEventsWrapper.ts | 19 ++- .../cc/samples-cc-react-app/src/App.tsx | 120 +++++++++--------- widgets-samples/cc/samples-cc-wc-app/app.js | 50 +++----- .../cc/samples-cc-wc-app/index.html | 50 +++----- 6 files changed, 119 insertions(+), 141 deletions(-) diff --git a/packages/contact-center/station-login/src/station-login/station-login.presentational.tsx b/packages/contact-center/station-login/src/station-login/station-login.presentational.tsx index 9bee3d312..6c7fe8ea4 100644 --- a/packages/contact-center/station-login/src/station-login/station-login.presentational.tsx +++ b/packages/contact-center/station-login/src/station-login/station-login.presentational.tsx @@ -1,7 +1,7 @@ -import React, { useEffect, useRef} from 'react'; -import { StationLoginPresentationalProps } from './station-login.types'; +import React, {useEffect, useRef} from 'react'; +import {StationLoginPresentationalProps} from './station-login.types'; import './station-login.style.scss'; -import { MULTIPLE_SIGN_IN_ALERT_MESSAGE, MULTIPLE_SIGN_IN_ALERT_TITLE } from './constants'; +import {MULTIPLE_SIGN_IN_ALERT_MESSAGE, MULTIPLE_SIGN_IN_ALERT_TITLE} from './constants'; const StationLoginPresentational: React.FunctionComponent = (props) => { const { @@ -46,6 +46,9 @@ const StationLoginPresentational: React.FunctionComponent - { - const selectLoginOption = (event: { target: { value: string } }) => - { + const selectLoginOption = (event: {target: {value: string}}) => { const dialNumber = document.querySelector('#dialNumber') as HTMLInputElement; const deviceType = event.target.value; setDeviceType(deviceType); @@ -99,8 +99,10 @@ const StationLoginPresentational: React.FunctionComponent

{MULTIPLE_SIGN_IN_ALERT_TITLE}

{MULTIPLE_SIGN_IN_ALERT_MESSAGE}

-
- +
+
)} diff --git a/packages/contact-center/store/src/store.types.ts b/packages/contact-center/store/src/store.types.ts index fe78cbcc7..f8ebf1dad 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -46,6 +46,7 @@ interface IStore { showMultipleLoginAlert: boolean; currentTheme: string; init(params: InitParams,callback:any): Promise; + registerCC(webex?: WithWebex['webex']): Promise; } diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index 8e4cad07f..d09bba8a7 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -1,4 +1,4 @@ -import {IStoreWrapper, IStore, InitParams, TASK_EVENTS, CC_EVENTS, IWrapupCode} from './store.types'; +import {IStoreWrapper, IStore, InitParams, TASK_EVENTS, CC_EVENTS, IWrapupCode, WithWebex} from './store.types'; import {ITask} from '@webex/plugin-cc'; import Store from './store'; import {runInAction} from 'mobx'; @@ -118,12 +118,18 @@ class StoreWrapper implements IStoreWrapper { return this.store.init(options, this.setupIncomingTaskHandler); } + registerCC = (webex?: WithWebex['webex']) => { + return this.store.registerCC(webex); + }; + handleTaskRemove = (taskId: string) => { // Remove the task from the taskList const taskToRemove = this.store.taskList.find((task) => task.data.interactionId === taskId); if (taskToRemove) { taskToRemove.off(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned(taskId)); - taskToRemove.off(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => this.handleTaskEnd(taskToRemove, wrapupRequired)); + taskToRemove.off(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => + this.handleTaskEnd(taskToRemove, wrapupRequired) + ); taskToRemove.off(TASK_EVENTS.TASK_REJECT, () => this.handleTaskRemove(taskId)); } const updateTaskList = this.store.taskList.filter((task) => task.data.interactionId !== taskId); @@ -199,7 +205,9 @@ class StoreWrapper implements IStoreWrapper { }; handleTaskHydrate = (task: ITask) => { - task.on(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => {this.handleTaskEnd(task, wrapupRequired);}); + task.on(TASK_EVENTS.TASK_END, ({wrapupRequired}: {wrapupRequired: boolean}) => { + this.handleTaskEnd(task, wrapupRequired); + }); // When we receive TASK_ASSIGNED the task was accepted by the agent and we need wrap up task.on(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned(task)); @@ -208,8 +216,11 @@ class StoreWrapper implements IStoreWrapper { // When we receive TASK_REJECT that means the task was not accepted by the agent and we wont need wrap up task.on(TASK_EVENTS.TASK_REJECT, () => this.handleTaskRemove(task.data.interactionId)); + if (!this.store.taskList.some((t) => t.data.interactionId === task.data.interactionId)) { + this.setTaskList([...this.store.taskList, task]); + } + this.setCurrentTask(task); - this.setTaskList([...this.store.taskList, task]); const {interaction, agentId} = task.data; const {state, isTerminated, participants} = interaction; diff --git a/widgets-samples/cc/samples-cc-react-app/src/App.tsx b/widgets-samples/cc/samples-cc-react-app/src/App.tsx index 984cbe67d..fd3020592 100644 --- a/widgets-samples/cc/samples-cc-react-app/src/App.tsx +++ b/widgets-samples/cc/samples-cc-react-app/src/App.tsx @@ -15,7 +15,6 @@ function App() { const [isLoggedIn, setIsLoggedIn] = useState(false); const themeCheckboxRef = useRef(null); const [currentTheme, setCurrentTheme] = useState(store.currentTheme); - const [showWidgets, setShowWidgets] = useState(false); const [isMultiLoginEnabled, setIsMultiLoginEnabled] = useState(false); const webexConfig = { @@ -93,6 +92,55 @@ function App() { onChange={(e) => setAccessToken(e.target.value)} />
+ <> +
+ + + + + +
+ {' '} Dark Theme
+
+ Note: The "Enable Multi Login" option must be set before initializing the SDK. Changes to + this setting after SDK initialization will not take effect. Please ensure you configure this option before + clicking the "Init Widgets" button. +
+ +
{isSdkReady && ( - <> -
- - - - - -
- - - )} - {showWidgets && ( <> {selectedWidgets.stationLogin && } - {store.isAgentLoggedIn && isLoggedIn && ( + {store.isAgentLoggedIn && ( <> {selectedWidgets.userState && } {selectedWidgets.incomingTask && } diff --git a/widgets-samples/cc/samples-cc-wc-app/app.js b/widgets-samples/cc/samples-cc-wc-app/app.js index 43cf00090..127304142 100644 --- a/widgets-samples/cc/samples-cc-wc-app/app.js +++ b/widgets-samples/cc/samples-cc-wc-app/app.js @@ -2,18 +2,21 @@ const accessTokenElem = document.getElementById('access_token_elem'); const themeElem = document.getElementById('theme'); const widgetsContainer = document.getElementById('widgets-container'); -const ccStationLogin = document.getElementById('cc-station-login'); + +const ccStationLogin = document.createElement('widget-cc-station-login'); const ccUserState = document.createElement('widget-cc-user-state'); const ccIncomingTask = document.createElement('widget-cc-incoming-task'); const ccTaskList = document.createElement('widget-cc-task-list'); const ccCallControl = document.createElement('widget-cc-call-control'); + const themeProviderElem = document.getElementById('theme-provider-elem'); -const widgetSelection = document.getElementById('widget-selection'); + const stationLoginCheckbox = document.getElementById('stationLoginCheckbox'); const userStateCheckbox = document.getElementById('userStateCheckbox'); const incomingTaskCheckbox = document.getElementById('incomingTaskCheckbox'); const taskListCheckbox = document.getElementById('taskListCheckbox'); const callControlCheckbox = document.getElementById('callControlCheckbox'); + let isMultiLoginEnabled = false; themeElem.addEventListener('change', () => { @@ -63,46 +66,23 @@ function initWidgets() { ccCallControl.onHoldResume = onHoldResume; ccCallControl.onEnd = onEnd; ccCallControl.onWrapup = onWrapup; - widgetSelection.classList.remove('disabled'); + + if (stationLoginCheckbox.checked) { + ccStationLogin.classList.remove('disabled'); + widgetsContainer.appendChild(ccStationLogin); + } + if (store.isAgentLoggedIn) { + loginSuccess(); + } else { + console.error('Agent is not logged in! Station Login Widget is required'); + } }) .catch((error) => { console.error('Failed to initialize widgets:', error); }); } -function submitWidgets() { - if (stationLoginCheckbox.checked) { - ccStationLogin.classList.remove('disabled'); - widgetsContainer.appendChild(ccStationLogin); - } else { - ccStationLogin.classList.add('disabled'); - } - - if (userStateCheckbox.checked) { - ccUserState.classList.remove('disabled'); - } else { - ccUserState.classList.add('disabled'); - } - - if (incomingTaskCheckbox.checked) { - ccIncomingTask.classList.remove('disabled'); - } else { - ccIncomingTask.classList.add('disabled'); - } - if (taskListCheckbox.checked) { - ccTaskList.classList.remove('disabled'); - } else { - ccTaskList.classList.add('disabled'); - } - if (callControlCheckbox.checked) { - ccCallControl.classList.remove('disabled'); - } else { - ccCallControl.classList.add('disabled'); - } -} - function loginSuccess() { - console.log('Agent login has been succesful'); if (userStateCheckbox.checked) { ccUserState.classList.remove('disabled'); widgetsContainer.appendChild(ccUserState); diff --git a/widgets-samples/cc/samples-cc-wc-app/index.html b/widgets-samples/cc/samples-cc-wc-app/index.html index 00bc5a7bc..1c8d4cfaa 100644 --- a/widgets-samples/cc/samples-cc-wc-app/index.html +++ b/widgets-samples/cc/samples-cc-wc-app/index.html @@ -23,50 +23,34 @@

Contact Center widgets as web-component

spellcheck="false" autocapitalize="off" /> +
+ + + + + +

- Dark Theme + Dark Theme
- Note: The "Enable Multi Login" option must be set before initializing the SDK. Changes - to this setting after SDK initialization will not take effect. Please ensure you configure this option - before clicking the "Init Widgets" button. + Note: The "Enable Multi Login" option must be set before initializing the SDK. Changes to + this setting after SDK initialization will not take effect. Please ensure you configure this option before + clicking the "Init Widgets" button.

Enable Multi Login + /> + Enable Multi Login
-
- -
-
- - - - - - -
+
From f93e60e5762412b13dcee9639550e780a9813772 Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Mon, 17 Feb 2025 14:48:11 +0530 Subject: [PATCH 23/23] fix(station-login): fix-tests-after-conflicts --- .../station-login/tests/helper.ts | 16 +++++----- .../station-login.presentational.tsx | 30 ------------------- 2 files changed, 9 insertions(+), 37 deletions(-) diff --git a/packages/contact-center/station-login/tests/helper.ts b/packages/contact-center/station-login/tests/helper.ts index 97016bee5..bf088f9ba 100644 --- a/packages/contact-center/station-login/tests/helper.ts +++ b/packages/contact-center/station-login/tests/helper.ts @@ -10,6 +10,7 @@ jest.mock('@webex/cc-store', () => { cc: {}, teams, loginOptions, + registerCC: jest.fn(), setDeviceType: jest.fn(), setCurrentState: jest.fn(), setLastStateChangeTimestamp: jest.fn(), @@ -24,7 +25,6 @@ const ccMock = { stationLogout: jest.fn(), on: jest.fn(), off: jest.fn(), - register: jest.fn(), }; // Sample login parameters @@ -489,7 +489,8 @@ describe('useStationLogin Hook', () => { it('should call handleContinue and set device type', async () => { const setShowMultipleLoginAlertSpy = jest.spyOn(require('@webex/cc-store'), 'setShowMultipleLoginAlert'); - ccMock.register.mockResolvedValue({isAgentLoggedIn: true}); + require('@webex/cc-store').isAgentLoggedIn = true; + const registerCCSpy = jest.spyOn(require('@webex/cc-store'), 'registerCC'); const {result} = renderHook(() => useStationLogin({ @@ -508,7 +509,7 @@ describe('useStationLogin Hook', () => { await waitFor(() => { expect(setShowMultipleLoginAlertSpy).toHaveBeenCalledWith(false); - expect(ccMock.register).toHaveBeenCalled(); + expect(registerCCSpy).toHaveBeenCalled(); expect(logger.log).toHaveBeenCalledWith('Agent Relogin Success', { module: 'widget-station-login#station-login/helper.ts', method: 'handleContinue', @@ -517,8 +518,9 @@ describe('useStationLogin Hook', () => { }); it('should call handleContinue with agent not logged in', async () => { + require('@webex/cc-store').isAgentLoggedIn = false; const setShowMultipleLoginAlertSpy = jest.spyOn(require('@webex/cc-store'), 'setShowMultipleLoginAlert'); - ccMock.register.mockResolvedValue({isAgentLoggedIn: false}); + const registerCCSpy = jest.spyOn(require('@webex/cc-store'), 'registerCC'); const {result} = renderHook(() => useStationLogin({ @@ -537,7 +539,7 @@ describe('useStationLogin Hook', () => { await waitFor(() => { expect(setShowMultipleLoginAlertSpy).toHaveBeenCalledWith(false); - expect(ccMock.register).toHaveBeenCalled(); + expect(registerCCSpy).toHaveBeenCalled(); expect(logger.error).toHaveBeenCalledWith('Agent Relogin Failed', { module: 'widget-station-login#station-login/helper.ts', method: 'handleContinue', @@ -547,7 +549,7 @@ describe('useStationLogin Hook', () => { it('should call handleContinue and handle error', async () => { const setShowMultipleLoginAlertSpy = jest.spyOn(require('@webex/cc-store'), 'setShowMultipleLoginAlert'); - ccMock.register.mockImplementation(() => { + const registerCCSpy = jest.spyOn(require('@webex/cc-store'), 'registerCC').mockImplementation(() => { throw Error('Relogin failed'); }); @@ -568,7 +570,7 @@ describe('useStationLogin Hook', () => { await waitFor(() => { expect(setShowMultipleLoginAlertSpy).toHaveBeenCalledWith(false); - expect(ccMock.register).toHaveBeenCalled(); + expect(registerCCSpy).toHaveBeenCalled(); expect(logger.error).toHaveBeenCalledWith('Error handling agent multi login continue: Error: Relogin failed', { module: 'widget-station-login#station-login/index.tsx', method: 'handleContinue', diff --git a/packages/contact-center/station-login/tests/station-login/station-login.presentational.tsx b/packages/contact-center/station-login/tests/station-login/station-login.presentational.tsx index ec7d95f8b..3f2a0880c 100644 --- a/packages/contact-center/station-login/tests/station-login/station-login.presentational.tsx +++ b/packages/contact-center/station-login/tests/station-login/station-login.presentational.tsx @@ -34,36 +34,6 @@ describe('StationLoginPresentational', () => { expect(heading).toHaveTextContent('StationLogin'); }); - it('calls setDeviceType and relogin on relogin', () => { - const setDeviceType = jest.fn(); - const reloginMock = jest.fn(); - const handleContinueMock = jest.fn(); - const modalRef = React.createRef(); - const props = { - name: 'StationLogin', - login: jest.fn(), - logout: jest.fn(), - loginSuccess: undefined, - loginFailure: undefined, - logoutSuccess: undefined, - teams: ['team123'], - loginOptions: ['EXTENSION', 'AGENT_DN', 'BROWSER'], - setDeviceType, - setDialNumber: jest.fn(), - setTeam: jest.fn(), - isAgentLoggedIn: true, - deviceType: 'EXTENSION', - relogin: reloginMock, - handleContinue: handleContinueMock, - modalRef, - showMultipleLoginAlert: false, - }; - render(); - - expect(setDeviceType).toHaveBeenCalledWith('EXTENSION'); - expect(reloginMock).toHaveBeenCalled(); - }); - it('calls handleContinue function and closes the dialog when Continue button is clicked', () => { const handleContinueMock = jest.fn(); const modalRef = React.createRef();