diff --git a/docs/react-samples/src/App.tsx b/docs/react-samples/src/App.tsx index fbdc81389..416865daa 100644 --- a/docs/react-samples/src/App.tsx +++ b/docs/react-samples/src/App.tsx @@ -6,6 +6,7 @@ import {UserState} from '@webex/cc-user-state'; function App() { const [isSdkReady, setIsSdkReady] = useState(false); const [accessToken, setAccessToken] = useState(""); + const [isLoggedIn, setIsLoggedIn] = useState(false); const webexConfig = { fedramp: false, @@ -16,10 +17,12 @@ function App() { const onLogin = () => { console.log('Agent login has been succesful'); + setIsLoggedIn(true); } const onLogout = () => { console.log('Agent logout has been succesful'); + setIsLoggedIn(false); } return ( @@ -39,12 +42,13 @@ function App() { }); }} >Init Widgets - {/* write code to check if sdk is ready and load components */} { isSdkReady && ( <> - + { + isLoggedIn && + } ) } diff --git a/docs/web-component-samples/app.js b/docs/web-component-samples/app.js index 66bbd47fa..24ff1caca 100644 --- a/docs/web-component-samples/app.js +++ b/docs/web-component-samples/app.js @@ -1,6 +1,11 @@ -const widgetsContainer = document.getElementById('widgets-container'); const accessTokenElem = document.getElementById('access_token_elem'); +const widgetsContainer = document.getElementById('widgets-container'); const ccStationLogin = document.getElementById('cc-station-login'); +const ccUserState = document.createElement('widget-cc-user-state'); + +if (!ccStationLogin && !ccUserState) { + console.error('Failed to find the required elements'); +} function switchButtonState(){ const buttonElem = document.querySelector('button'); @@ -20,7 +25,7 @@ function initWidgets(){ }).then(() => { ccStationLogin.onLogin = loginSuccess; ccStationLogin.onLogout = logoutSuccess; - widgetsContainer.classList.remove('disabled'); + ccStationLogin.classList.remove('disabled'); }).catch((error) => { console.error('Failed to initialize widgets:', error); }); @@ -28,8 +33,11 @@ function initWidgets(){ function loginSuccess(){ console.log('Agent login has been succesful'); + ccUserState.classList.remove('disabled'); + widgetsContainer.appendChild(ccUserState); } function logoutSuccess(){ console.log('Agent logout has been succesful'); + ccUserState.classList.add('disabled'); } \ No newline at end of file diff --git a/docs/web-component-samples/index.html b/docs/web-component-samples/index.html index fcc56a2a5..137c03ed3 100644 --- a/docs/web-component-samples/index.html +++ b/docs/web-component-samples/index.html @@ -22,9 +22,8 @@

Contact Center widgets as web-component

autocapitalize="off" /> -
- - +
+
diff --git a/packages/contact-center/station-login/src/helper.ts b/packages/contact-center/station-login/src/helper.ts index b5af9fddf..54572129b 100644 --- a/packages/contact-center/station-login/src/helper.ts +++ b/packages/contact-center/station-login/src/helper.ts @@ -17,7 +17,9 @@ export const useStationLogin = (props: UseStationLoginProps) => { cc.stationLogin({teamId: team, loginOption: deviceType, dialNumber: dialNumber}) .then((res: StationLoginSuccess) => { setLoginSuccess(res); - loginCb(); + if(loginCb){ + loginCb(); + } }).catch((error: Error) => { console.error(error); setLoginFailure(error); @@ -28,8 +30,10 @@ export const useStationLogin = (props: UseStationLoginProps) => { cc.stationLogout({logoutReason: 'User requested logout'}) .then((res: StationLogoutSuccess) => { setLogoutSuccess(res); - logoutCb(); - }).catch((error: any) => { + if(logoutCb){ + logoutCb(); + } + }).catch((error: Error) => { console.error(error); }); }; 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 928762c0e..94cb813f0 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 @@ -51,12 +51,12 @@ export interface IStationLoginProps { /** * Callback function to be invoked once the agent login is successful */ - onLogin: () => void; + onLogin?: () => void; /** * Callback function to be invoked once the agent login is successful */ - onLogout: () => void; + onLogout?: () => void; /** * Handler to set device type diff --git a/packages/contact-center/station-login/tests/helper.ts b/packages/contact-center/station-login/tests/helper.ts index 5bf46173e..501b724b1 100644 --- a/packages/contact-center/station-login/tests/helper.ts +++ b/packages/contact-center/station-login/tests/helper.ts @@ -1,4 +1,4 @@ -import {renderHook, act} from '@testing-library/react-hooks'; +import {renderHook, act, waitFor} from '@testing-library/react'; import {useStationLogin} from '../src/helper'; // Mock webex instance @@ -18,6 +18,10 @@ const loginCb = jest.fn(); const logoutCb = jest.fn(); describe('useStationLogin Hook', () => { + + afterEach(() => { + jest.clearAllMocks(); + }); it('should set loginSuccess on successful login', async () => { const successResponse = { @@ -45,7 +49,7 @@ describe('useStationLogin Hook', () => { ccMock.stationLogin.mockResolvedValue(successResponse); - const { result, waitForNextUpdate } = renderHook(() => + const { result } = renderHook(() => useStationLogin({cc: ccMock, onLogin: loginCb, onLogout: logoutCb}) ); @@ -57,25 +61,42 @@ describe('useStationLogin Hook', () => { result.current.login(); }); - await waitForNextUpdate(); - - expect(ccMock.stationLogin).toHaveBeenCalledWith({ - teamId: loginParams.teamId, - loginOption: loginParams.loginOption, - dialNumber: loginParams.dialNumber, + waitFor(() => { + expect(ccMock.stationLogin).toHaveBeenCalledWith({ + teamId: loginParams.teamId, + loginOption: loginParams.loginOption, + dialNumber: loginParams.dialNumber, + }); + expect(loginCb).toHaveBeenCalledWith(); + + expect(result.current).toEqual({ + name: 'StationLogin', + setDeviceType: expect.any(Function), + setDialNumber: expect.any(Function), + setTeam: expect.any(Function), + login: expect.any(Function), + logout: expect.any(Function), + loginSuccess: successResponse, + loginFailure: undefined, + logoutSuccess: undefined + }); }); - expect(loginCb).toHaveBeenCalledWith(); - - expect(result.current).toEqual({ - name: 'StationLogin', - setDeviceType: expect.any(Function), - setDialNumber: expect.any(Function), - setTeam: expect.any(Function), - login: expect.any(Function), - logout: expect.any(Function), - loginSuccess: successResponse, - loginFailure: undefined, - logoutSuccess: undefined + }); + + it('should not call login callback if not present', async () => { + + ccMock.stationLogin.mockResolvedValue({}); + + const { result } = renderHook(() => + useStationLogin({cc: ccMock, onLogout: logoutCb}) + ); + + act(() => { + result.current.login(); + }); + + waitFor(() => { + expect(loginCb).not.toHaveBeenCalled(); }); }); @@ -84,7 +105,7 @@ describe('useStationLogin Hook', () => { ccMock.stationLogin.mockRejectedValue(errorResponse); loginCb.mockClear(); - const { result, waitForNextUpdate } = renderHook(() => + const { result } = renderHook(() => useStationLogin({cc: ccMock, onLogin: loginCb, onLogout: logoutCb}) ); @@ -96,26 +117,26 @@ describe('useStationLogin Hook', () => { result.current.login(); }); - await waitForNextUpdate(); - - expect(ccMock.stationLogin).toHaveBeenCalledWith({ - teamId: loginParams.teamId, - loginOption: loginParams.loginOption, - dialNumber: loginParams.dialNumber, - }); - - expect(loginCb).not.toHaveBeenCalledWith(); - - expect(result.current).toEqual({ - name: 'StationLogin', - setDeviceType: expect.any(Function), - setDialNumber: expect.any(Function), - setTeam: expect.any(Function), - login: expect.any(Function), - logout: expect.any(Function), - loginSuccess: undefined, - loginFailure: errorResponse, - logoutSuccess: undefined + waitFor(() => { + expect(ccMock.stationLogin).toHaveBeenCalledWith({ + teamId: loginParams.teamId, + loginOption: loginParams.loginOption, + dialNumber: loginParams.dialNumber, + }); + + expect(loginCb).not.toHaveBeenCalledWith(); + + expect(result.current).toEqual({ + name: 'StationLogin', + setDeviceType: expect.any(Function), + setDialNumber: expect.any(Function), + setTeam: expect.any(Function), + login: expect.any(Function), + logout: expect.any(Function), + loginSuccess: undefined, + loginFailure: errorResponse, + logoutSuccess: undefined + }); }); }); @@ -137,7 +158,7 @@ describe('useStationLogin Hook', () => { ccMock.stationLogout.mockResolvedValue(successResponse); - const {result, waitForNextUpdate} = renderHook(() => + const {result} = renderHook(() => useStationLogin({cc: ccMock, onLogin: loginCb, onLogout: logoutCb}) ); @@ -145,22 +166,38 @@ describe('useStationLogin Hook', () => { result.current.logout(); }); - await waitForNextUpdate(); + waitFor(() => { + expect(ccMock.stationLogout).toHaveBeenCalledWith({logoutReason: 'User requested logout'}); + expect(logoutCb).toHaveBeenCalledWith(); + + + expect(result.current).toEqual({ + name: 'StationLogin', + setDeviceType: expect.any(Function), + setDialNumber: expect.any(Function), + setTeam: expect.any(Function), + login: expect.any(Function), + logout: expect.any(Function), + loginSuccess: undefined, + loginFailure: undefined, + logoutSuccess: successResponse + }); + }); + }); - expect(ccMock.stationLogout).toHaveBeenCalledWith({logoutReason: 'User requested logout'}); - expect(logoutCb).toHaveBeenCalledWith(); + it('should not call logout callback if not present', async () => { + ccMock.stationLogout.mockResolvedValue({}); + const {result} = renderHook(() => + useStationLogin({cc: ccMock, onLogin: loginCb}) + ); + + act(() => { + result.current.logout(); + }); - expect(result.current).toEqual({ - name: 'StationLogin', - setDeviceType: expect.any(Function), - setDialNumber: expect.any(Function), - setTeam: expect.any(Function), - login: expect.any(Function), - logout: expect.any(Function), - loginSuccess: undefined, - loginFailure: undefined, - logoutSuccess: successResponse + waitFor(() => { + expect(logoutCb).not.toHaveBeenCalled(); }); }); }) diff --git a/packages/contact-center/store/package.json b/packages/contact-center/store/package.json index 7114c201f..2927ab43c 100644 --- a/packages/contact-center/store/package.json +++ b/packages/contact-center/store/package.json @@ -46,8 +46,6 @@ }, "jest": { "testEnvironment": "jsdom", - "//": "We can remove this when we have tests", - "passWithNoTests": true, "testMatch": [ "**/tests/**/*.ts", "**/tests/**/*.tsx" diff --git a/packages/contact-center/store/src/store.ts b/packages/contact-center/store/src/store.ts index 0a514cd77..f654a122b 100644 --- a/packages/contact-center/store/src/store.ts +++ b/packages/contact-center/store/src/store.ts @@ -1,11 +1,11 @@ import {makeAutoObservable, observable} from 'mobx'; import Webex from 'webex'; import { - AgentLogin, IContactCenter, Profile, Team, WithWebex, + IdleCode, InitParams, IStore } from './store.types'; @@ -14,6 +14,8 @@ class Store implements IStore { teams: Team[] = []; loginOptions: string[] = []; cc: IContactCenter; + idleCodes: IdleCode[] = []; + agentId: string = ''; constructor() { makeAutoObservable(this, {cc: observable.ref}); @@ -24,6 +26,8 @@ class Store implements IStore { return this.cc.register().then((response: Profile) => { this.teams = response.teams; this.loginOptions = response.loginVoiceOptions; + this.idleCodes = response.idleCodes; + this.agentId = response.agentId; }).catch((error) => { console.error('Error registering contact center', error); return Promise.reject(error); diff --git a/packages/contact-center/store/src/store.types.ts b/packages/contact-center/store/src/store.types.ts index bb5770845..f5f3d31e9 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -11,10 +11,19 @@ interface WithWebexConfig { type InitParams = WithWebex | WithWebexConfig; +type IdleCode = { + name: string; + id: string; + isSystem: boolean; + isDefault: boolean; +} + interface IStore { teams: Team[]; loginOptions: string[]; cc: IContactCenter; + idleCodes: IdleCode[]; + agentId: string; registerCC(webex: WithWebex['webex']): Promise; init(params: InitParams): Promise; @@ -26,6 +35,7 @@ export type { Team, AgentLogin, WithWebex, + IdleCode, InitParams, IStore } \ No newline at end of file diff --git a/packages/contact-center/store/tests/store.ts b/packages/contact-center/store/tests/store.ts index f853c0f1f..d0bf75dbf 100644 --- a/packages/contact-center/store/tests/store.ts +++ b/packages/contact-center/store/tests/store.ts @@ -44,10 +44,12 @@ describe('Store', () => { }); describe('registerCC', () => { - it('should set teams and loginOptions on successful register', async () => { + it('should initialise store values on successful register', async () => { const mockResponse = { teams: [{ id: 'team1', name: 'Team 1' }], - loginVoiceOptions: ['option1', 'option2'] + loginVoiceOptions: ['option1', 'option2'], + idleCodes: [{ id: 'code1', name: 'Code 1', isSystem: false, isDefault: false }], + agentId: 'agent1' }; mockWebex.cc.register.mockResolvedValue(mockResponse); @@ -55,6 +57,8 @@ describe('Store', () => { expect(store.teams).toEqual(mockResponse.teams); expect(store.loginOptions).toEqual(mockResponse.loginVoiceOptions); + expect(store.idleCodes).toEqual(mockResponse.idleCodes); + expect(store.agentId).toEqual(mockResponse.agentId); }); it('should log an error on failed register', async () => { diff --git a/packages/contact-center/user-state/package.json b/packages/contact-center/user-state/package.json index 786a0e740..0ba2617ba 100644 --- a/packages/contact-center/user-state/package.json +++ b/packages/contact-center/user-state/package.json @@ -46,8 +46,11 @@ }, "jest": { "testEnvironment": "jsdom", - "//": "We can remove this when we have tests", - "passWithNoTests": true + "testMatch": [ + "**/tests/**/*.ts", + "**/tests/**/*.tsx" + ], + "verbose": true }, "stableVersion": "1.28.0-ccwidgets.1" } diff --git a/packages/contact-center/user-state/src/helper.ts b/packages/contact-center/user-state/src/helper.ts index 32d6b89c7..1b5e9fd83 100644 --- a/packages/contact-center/user-state/src/helper.ts +++ b/packages/contact-center/user-state/src/helper.ts @@ -1,11 +1,56 @@ -export const useUserState = () => { +import {useState, useEffect} from "react"; +export const useUserState = ({idleCodes, agentId, cc}) => { - const handleAgentStatus = (event: { target: { value: string; }; }) => { - }; + const [isSettingAgentStatus, setIsSettingAgentStatus] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + + const [elapsedTime, setElapsedTime] = useState(0); + const [currentState, setCurrentState] = useState({}); + + useEffect(() => { + // Reset the timer whenever the component mounts or the state changes + setElapsedTime(0); + const timer = setInterval(() => { + setElapsedTime(prevTime => prevTime + 1); + }, 1000); + + // Cleanup the timer on component unmount + return () => clearInterval(timer); + }, []); - const setAgentStatus = () => { + const setAgentStatus = (selectedCode) => { + const { + auxCodeId, + state + } = { + auxCodeId: selectedCode.id, + state: selectedCode.name + } + setIsSettingAgentStatus(true); + let oldState = { + ...currentState + }; + setCurrentState(selectedCode); + const chosenState = state === 'Available' ? 'Available' : 'Idle'; + cc.setAgentState({state: chosenState, auxCodeId, agentId, lastStateChangeReason: state}).then((response) => { + setErrorMessage(''); + setElapsedTime(0); + }).catch(error => { + setCurrentState(oldState); + setErrorMessage(error.toString()); + }).finally(() => { + setIsSettingAgentStatus(false); + }); }; - return {name: 'UserState', handleAgentStatus, setAgentStatus}; + return { + idleCodes, + setAgentStatus, + isSettingAgentStatus, + errorMessage, + elapsedTime, + currentState, + setCurrentState + } }; diff --git a/packages/contact-center/user-state/src/user-state/index.tsx b/packages/contact-center/user-state/src/user-state/index.tsx index e5d3300f3..704680f5f 100644 --- a/packages/contact-center/user-state/src/user-state/index.tsx +++ b/packages/contact-center/user-state/src/user-state/index.tsx @@ -5,13 +5,15 @@ import r2wc from '@r2wc/react-to-web-component'; import {useUserState} from '../helper'; import UserStatePresentational from './user-state.presentational'; +import {IUserState} from './use-state.types'; const UserState: React.FunctionComponent = observer(() => { - const {} = store; - const result = useUserState(); - const props = { - ...result, - }; + const {cc, idleCodes, agentId} = store; + const props: IUserState = useUserState({ + idleCodes, + agentId, + cc + }); return ; }); diff --git a/packages/contact-center/user-state/src/user-state/styles-module.scss b/packages/contact-center/user-state/src/user-state/styles-module.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/contact-center/user-state/src/user-state/use-state.types.ts b/packages/contact-center/user-state/src/user-state/use-state.types.ts index fd5f8a7fc..1d9708ae9 100644 --- a/packages/contact-center/user-state/src/user-state/use-state.types.ts +++ b/packages/contact-center/user-state/src/user-state/use-state.types.ts @@ -1,19 +1,49 @@ +import { IdleCode } from '@webex/cc-store'; + /** * Interface representing the state of a user. */ export interface IUserState { /** - * The name of the user. + * The list of idle codes. + */ + idleCodes: IdleCode[]; + + /** + * Function to set the agent + * status. + * @param status The status to set. + * @param status.auxCodeId The aux code id. + * @param status.state The state to set. + * @returns void + */ + setAgentStatus: (status: { auxCodeId: string; state: string }) => void; + + /** + * Boolean indicating if the agent status is being set. + */ + isSettingAgentStatus: boolean; + + /** + * The error message to display + */ + errorMessage: string; + + /** + * The duration of the current user state */ - name: string; + elapsedTime: number; /** - * Handler for agent state changes + * The idle code of the current user state */ - handleAgentStatus: (event) => void; + currentState: IdleCode; /** - * Setter for agent state + * Function to set the current state + * of the user. + * @param state The state to set. + * @returns void */ - setAgentStatus: () => void + setCurrentState: (state: IdleCode) => void; } diff --git a/packages/contact-center/user-state/src/user-state/user-state.presentational.tsx b/packages/contact-center/user-state/src/user-state/user-state.presentational.tsx index 915f1b930..1c7174ed7 100644 --- a/packages/contact-center/user-state/src/user-state/user-state.presentational.tsx +++ b/packages/contact-center/user-state/src/user-state/user-state.presentational.tsx @@ -1,14 +1,123 @@ -import React from 'react'; +import React, {CSSProperties, useMemo, useRef} from 'react'; import {IUserState} from './use-state.types'; +const getStyles = (isSettingAgentStatus: boolean): Record => ({ + box: { + backgroundColor: '#ffffff', + borderRadius: '8px', + boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)', + padding: '20px', + maxWidth: '800px', + margin: '0 auto' + }, + + sectionBox: { + padding: '10px', + border: '1px solid #ddd', + borderRadius: '8px' + }, + + fieldset: { + border: '1px solid #ccc', + borderRadius: '5px', + padding: '10px', + marginBottom: '20px', + position: 'relative' + } as CSSProperties, + + legendBox: { + fontWeight: 'bold', + color: '#0052bf' + }, + + btn: { + padding: '10px 20px', + backgroundColor: '#0052bf', + color: 'white', + border: 'none', + borderRadius: '4px', + cursor: 'pointer', + transition: 'background-color 0.3s', + marginRight: '8px' + }, + + select: { + width: '100%', + padding: '8px', + marginTop: '8px', + marginBottom: '12px', + border: '1px solid #ccc', + borderRadius: '4px' + }, + + input: { + width: '97%', + padding: '8px', + marginTop: '8px', + marginBottom: '12px', + border: '1px solid #ccc', + borderRadius: '4px' + }, + + elapsedTime: { + position: 'absolute', + right: '30px', + top: '25px', + color: isSettingAgentStatus ? 'grey' : 'black' + } as CSSProperties +}); + const UserStatePresentational: React.FunctionComponent = (props) => { - const {handleAgentStatus, setAgentStatus} = props; + const {idleCodes,setAgentStatus,isSettingAgentStatus, errorMessage, elapsedTime, currentState} = props; + + const styles = useMemo(() => getStyles(isSettingAgentStatus), [isSettingAgentStatus]); + const selectRef = useRef(null); + + const formatTime = (time: number): string => { + const hours = Math.floor(time / 3600); + const minutes = Math.floor((time % 3600) / 60); + const seconds = time % 60; + return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + }; return ( <> -

{props.name}

-

User State: {}

+
+
+
+ Agent State +
+ +
{formatTime(elapsedTime)}
+ { + errorMessage &&
{errorMessage}
+ } +
+
+
); }; diff --git a/packages/contact-center/user-state/tests/helper.ts b/packages/contact-center/user-state/tests/helper.ts new file mode 100644 index 000000000..69e4209fa --- /dev/null +++ b/packages/contact-center/user-state/tests/helper.ts @@ -0,0 +1,96 @@ +import { renderHook, act, waitFor } from '@testing-library/react'; +import { useUserState } from '../src/helper'; + +describe('useUserState Hook', () => { + const mockCC = { + setAgentState: jest.fn() + }; + + const idleCodes = [ + { id: '1', name: 'Idle Code 1', isSystem: false }, + { id: '2', name: 'Available', isSystem: false } + ]; + + const agentId = 'agent123'; + + beforeEach(() => { + jest.useFakeTimers(); + mockCC.setAgentState.mockReset(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should initialize with default values', () => { + const { result } = renderHook(() => useUserState({ idleCodes, agentId, cc: mockCC })); + + expect(result.current).toMatchObject({ + isSettingAgentStatus: false, + errorMessage: '', + elapsedTime: 0, + currentState: {} + }); + }); + + it('should increment elapsedTime every second', () => { + const { result } = renderHook(() => useUserState({ idleCodes, agentId, cc: mockCC })); + + act(() => { + jest.advanceTimersByTime(3000); + }); + + expect(result.current.elapsedTime).toBe(3); + }); + + it('should reset elapsedTime when agent status is set', async () => { + mockCC.setAgentState.mockResolvedValueOnce({}); + const { result } = renderHook(() => useUserState({ idleCodes, agentId, cc: mockCC })); + + act(() => { + result.current.setAgentStatus(idleCodes[1]); + jest.advanceTimersByTime(3000); + }); + + await waitFor(() => { + expect(result.current.elapsedTime).toBe(3); + }); + }); + + it('should handle setAgentStatus correctly and update current state', async () => { + mockCC.setAgentState.mockResolvedValueOnce({}); + const { result } = renderHook(() => useUserState({ idleCodes, agentId, cc: mockCC })); + + act(() => { + result.current.setAgentStatus(idleCodes[1]); + }); + + expect(result.current.isSettingAgentStatus).toBe(true); + + await waitFor(() => { + expect(result.current).toMatchObject({ + isSettingAgentStatus: false, + errorMessage: '', + currentState: idleCodes[1] + }); + }); + }); + + it('should handle errors from setAgentStatus and revert state', async () => { + const errorMsg = 'Error setting agent status'; + mockCC.setAgentState.mockRejectedValueOnce(new Error(errorMsg)); + const { result } = renderHook(() => useUserState({ idleCodes, agentId, cc: mockCC })); + + act(() => { + result.current.setAgentStatus(idleCodes[1]); + }); + + await waitFor(() => { + expect(result.current).toMatchObject({ + isSettingAgentStatus: false, + errorMessage: `Error: ${errorMsg}`, + currentState: {} + }); + }); + }); +}); diff --git a/packages/contact-center/user-state/tests/user-state/index.tsx b/packages/contact-center/user-state/tests/user-state/index.tsx new file mode 100644 index 000000000..9eb323152 --- /dev/null +++ b/packages/contact-center/user-state/tests/user-state/index.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import {render, screen} from '@testing-library/react'; +import {UserState} from '../../src'; +import * as helper from '../../src/helper'; +import '@testing-library/jest-dom'; + +// Mock the store import +jest.mock('@webex/cc-store', () => {return { + cc: {}, + idleCodes: [], + agentId: 'testAgentId' +}}); + +describe('UserState Component', () => { + it('renders UserStatePresentational with correct props', () => { + const useUserStateSpy = jest.spyOn(helper, 'useUserState'); + + render(); + + expect(useUserStateSpy).toHaveBeenCalledWith({cc: {}, idleCodes: [], agentId: 'testAgentId'}); + const heading = screen.getByTestId('user-state-title'); + expect(heading).toHaveTextContent('Agent State'); + }); +}); \ No newline at end of file diff --git a/packages/contact-center/user-state/tests/user-state/user-state.presentational.tsx b/packages/contact-center/user-state/tests/user-state/user-state.presentational.tsx new file mode 100644 index 000000000..c17517aa6 --- /dev/null +++ b/packages/contact-center/user-state/tests/user-state/user-state.presentational.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import UserStatePresentational from '../../src/user-state/user-state.presentational'; + +describe('UserStatePresentational Component', () => { + const mockSetAgentStatus = jest.fn(); + const mockSetCurrentState = jest.fn(); + const defaultProps = { + idleCodes: [ + { id: '1', name: 'Idle Code 1', isSystem: false }, + { id: '2', name: 'Idle Code 2', isSystem: true }, + { id: '3', name: 'Idle Code 3', isSystem: false } + ], + setAgentStatus: mockSetAgentStatus, + isSettingAgentStatus: false, + errorMessage: '', + elapsedTime: 3661, // 1 hour, 1 minute, 1 second + currentState: { id: '1' }, + setCurrentState: mockSetCurrentState + }; + + it('should render the component with correct elements', () => { + render(); + expect(screen.getByTestId('user-state-title')).toHaveTextContent('Agent State'); + expect(screen.getByRole('combobox')).toBeInTheDocument(); + expect(screen.getByText('01:01:01')).toBeInTheDocument(); + }); + + it('should render only non-system idle codes in the dropdown', () => { + render(); + const options = screen.getAllByRole('option'); + expect(options).toHaveLength(2); + expect(options[0]).toHaveTextContent('Idle Code 1'); + expect(options[1]).toHaveTextContent('Idle Code 3'); + }); + + it('should call setAgentStatus with correct code when an idle code is selected', () => { + render(); + fireEvent.change(screen.getByRole('combobox'), { target: { value: '3' } }); + expect(mockSetAgentStatus).toHaveBeenCalledWith({ id: '3', name: 'Idle Code 3', isSystem: false }); + }); + + it('should display an error message if provided', () => { + render(); + expect(screen.getByText('Error message')).toBeInTheDocument(); + expect(screen.getByText('Error message')).toHaveStyle('color: red'); + }); + + it('should disable the select box when isSettingAgentStatus is true', () => { + render(); + expect(screen.getByRole('combobox')).toBeDisabled(); + }); + + it('should render elapsed time in correct color based on isSettingAgentStatus', () => { + const { rerender } = render(); + expect(screen.getByText('01:01:01')).toHaveStyle('color: black'); + + rerender(); + expect(screen.getByText('01:01:01')).toHaveStyle('color: grey'); + }); +});