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 @@
-
-
+
+
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: {}
+
+
+
+
+
>
);
};
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');
+ });
+});