From 136e5504231467a221593c537339ca8dde638c13 Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Mon, 7 Jul 2025 11:49:05 +0530 Subject: [PATCH 01/12] fix(ui-metrics): @webex/ui-metrics package --- .../contact-center/cc-components/package.json | 2 +- .../contact-center/cc-widgets/package.json | 2 +- .../contact-center/station-login/package.json | 3 +- .../station-login/src/station-login/index.tsx | 7 +- packages/contact-center/store/package.json | 2 +- packages/contact-center/task/package.json | 3 +- .../task/src/CallControl/index.tsx | 5 +- .../task/src/CallControlCAD/index.tsx | 4 +- .../task/src/IncomingTask/index.tsx | 4 +- .../task/src/OutdialCall/index.tsx | 4 +- .../task/src/TaskList/index.tsx | 4 +- .../contact-center/ui-metrics/package.json | 35 +++++++++ .../contact-center/ui-metrics/src/index.ts | 6 ++ .../ui-metrics/src/metricsLogger.ts | 72 +++++++++++++++++++ .../ui-metrics/src/withMetrics.tsx | 47 ++++++++++++ .../contact-center/ui-metrics/tsconfig.json | 11 +++ .../ui-metrics/webpack.config.js | 12 ++++ .../contact-center/user-state/package.json | 3 +- .../user-state/src/user-state/index.tsx | 4 +- .../cc/samples-cc-react-app/webpack.config.js | 1 + yarn.lock | 24 +++++++ 21 files changed, 242 insertions(+), 13 deletions(-) create mode 100644 packages/contact-center/ui-metrics/package.json create mode 100644 packages/contact-center/ui-metrics/src/index.ts create mode 100644 packages/contact-center/ui-metrics/src/metricsLogger.ts create mode 100644 packages/contact-center/ui-metrics/src/withMetrics.tsx create mode 100644 packages/contact-center/ui-metrics/tsconfig.json create mode 100644 packages/contact-center/ui-metrics/webpack.config.js diff --git a/packages/contact-center/cc-components/package.json b/packages/contact-center/cc-components/package.json index eab9d347e..0755dc3e5 100644 --- a/packages/contact-center/cc-components/package.json +++ b/packages/contact-center/cc-components/package.json @@ -65,4 +65,4 @@ "react": ">=18.3.1", "react-dom": ">=18.3.1" } -} \ No newline at end of file +} diff --git a/packages/contact-center/cc-widgets/package.json b/packages/contact-center/cc-widgets/package.json index 6d8971840..02a603aa3 100644 --- a/packages/contact-center/cc-widgets/package.json +++ b/packages/contact-center/cc-widgets/package.json @@ -86,4 +86,4 @@ "^.+\\.(css|less|scss)$": "babel-jest" } } -} \ No newline at end of file +} diff --git a/packages/contact-center/station-login/package.json b/packages/contact-center/station-login/package.json index fe3211825..1b46870a1 100644 --- a/packages/contact-center/station-login/package.json +++ b/packages/contact-center/station-login/package.json @@ -22,6 +22,7 @@ "dependencies": { "@webex/cc-components": "workspace:*", "@webex/cc-store": "workspace:*", + "@webex/ui-metrics": "workspace:*", "mobx-react-lite": "^4.1.0" }, "devDependencies": { @@ -63,4 +64,4 @@ "react": ">=18.3.1", "react-dom": ">=18.3.1" } -} \ No newline at end of file +} 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 98293bef4..cd7a60bf2 100644 --- a/packages/contact-center/station-login/src/station-login/index.tsx +++ b/packages/contact-center/station-login/src/station-login/index.tsx @@ -5,6 +5,7 @@ import {observer} from 'mobx-react-lite'; import {StationLoginComponent} from '@webex/cc-components'; import {useStationLogin} from '../helper'; import {StationLoginProps} from './station-login.types'; +import {withMetrics} from '@webex/ui-metrics'; const StationLogin: React.FunctionComponent = observer( ({onLogin, onLogout, onCCSignOut, profileMode, onSaveStart, onSaveEnd, doStationLogout}) => { @@ -22,6 +23,7 @@ const StationLogin: React.FunctionComponent = observer( teamId, setTeamId, } = store; + const result = useStationLogin({ cc, onLogin, @@ -38,6 +40,7 @@ const StationLogin: React.FunctionComponent = observer( }); const dialNumberRegex = cc?.agentConfig?.regexUS; + const props = { ...result, setDeviceType, @@ -54,7 +57,9 @@ const StationLogin: React.FunctionComponent = observer( logger, profileMode, }; - return ; + + const StationLoginWithMetrics = withMetrics(StationLoginComponent, 'StationLogin'); + return ; } ); diff --git a/packages/contact-center/store/package.json b/packages/contact-center/store/package.json index 01e7b3731..0934a1a67 100644 --- a/packages/contact-center/store/package.json +++ b/packages/contact-center/store/package.json @@ -57,4 +57,4 @@ "webpack-cli": "5.1.4", "webpack-merge": "6.0.1" } -} \ No newline at end of file +} diff --git a/packages/contact-center/task/package.json b/packages/contact-center/task/package.json index 07c4583ad..c52cfce8b 100644 --- a/packages/contact-center/task/package.json +++ b/packages/contact-center/task/package.json @@ -22,6 +22,7 @@ "dependencies": { "@webex/cc-components": "workspace:*", "@webex/cc-store": "workspace:*", + "@webex/ui-metrics": "workspace:*", "mobx-react-lite": "^4.1.0" }, "devDependencies": { @@ -62,4 +63,4 @@ "react": ">=18.3.1", "react-dom": ">=18.3.1" } -} \ No newline at end of file +} diff --git a/packages/contact-center/task/src/CallControl/index.tsx b/packages/contact-center/task/src/CallControl/index.tsx index 82929a599..992faac03 100644 --- a/packages/contact-center/task/src/CallControl/index.tsx +++ b/packages/contact-center/task/src/CallControl/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import {observer} from 'mobx-react-lite'; +import {withMetrics} from '@webex/ui-metrics'; import store from '@webex/cc-store'; import {useCallControl} from '../helper'; import {CallControlProps} from '../task.types'; @@ -45,7 +46,9 @@ const CallControl: React.FunctionComponent = observer( allowConsultToQueue, logger, }; - return ; + + const CallControlWithMetrics = withMetrics(CallControlComponent, 'CallControl'); + return ; } ); diff --git a/packages/contact-center/task/src/CallControlCAD/index.tsx b/packages/contact-center/task/src/CallControlCAD/index.tsx index 29d395a1f..7c0f6f0a3 100644 --- a/packages/contact-center/task/src/CallControlCAD/index.tsx +++ b/packages/contact-center/task/src/CallControlCAD/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import {observer} from 'mobx-react-lite'; +import {withMetrics} from '@webex/ui-metrics'; import store from '@webex/cc-store'; import {useCallControl} from '../helper'; import {CallControlProps} from '../task.types'; @@ -47,7 +48,8 @@ const CallControlCAD: React.FunctionComponent = observer( logger, }; - return ; + const CallControlCADWithMetrics = withMetrics(CallControlCADComponent, 'CallControlCAD'); + return ; } ); diff --git a/packages/contact-center/task/src/IncomingTask/index.tsx b/packages/contact-center/task/src/IncomingTask/index.tsx index 6dfe5d4db..7d3aa7338 100644 --- a/packages/contact-center/task/src/IncomingTask/index.tsx +++ b/packages/contact-center/task/src/IncomingTask/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import {observer} from 'mobx-react-lite'; +import {withMetrics} from '@webex/ui-metrics'; import store from '@webex/cc-store'; import {useIncomingTask} from '../helper'; import {IncomingTaskComponent} from '@webex/cc-components'; @@ -15,7 +16,8 @@ const IncomingTask: React.FunctionComponent = observer(({inco logger, }; - return ; + const IncomingTaskWithMetrics = withMetrics(IncomingTaskComponent, 'IncomingTask'); + return ; }); export {IncomingTask}; diff --git a/packages/contact-center/task/src/OutdialCall/index.tsx b/packages/contact-center/task/src/OutdialCall/index.tsx index aa6eff781..5d8506f16 100644 --- a/packages/contact-center/task/src/OutdialCall/index.tsx +++ b/packages/contact-center/task/src/OutdialCall/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import store from '@webex/cc-store'; import {observer} from 'mobx-react-lite'; +import {withMetrics} from '@webex/ui-metrics'; import {OutdialCallComponent} from '@webex/cc-components'; import {useOutdialCall} from '../helper'; @@ -12,7 +13,8 @@ const OutdialCall: React.FunctionComponent = observer(() => { ...result, }; - return ; + const OutdialCallWithMetrics = withMetrics(OutdialCallComponent, 'OutdialCall'); + return ; }); export {OutdialCall}; diff --git a/packages/contact-center/task/src/TaskList/index.tsx b/packages/contact-center/task/src/TaskList/index.tsx index 6d3ed878d..b7f22655f 100644 --- a/packages/contact-center/task/src/TaskList/index.tsx +++ b/packages/contact-center/task/src/TaskList/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import store from '@webex/cc-store'; import {observer} from 'mobx-react-lite'; +import {withMetrics} from '@webex/ui-metrics'; import {TaskListComponent} from '@webex/cc-components'; import {useTaskList} from '../helper'; @@ -17,7 +18,8 @@ const TaskList: React.FunctionComponent = observer( logger, }; - return ; + const TaskListWithMetrics = withMetrics(TaskListComponent, 'TaskList'); + return ; } ); diff --git a/packages/contact-center/ui-metrics/package.json b/packages/contact-center/ui-metrics/package.json new file mode 100644 index 000000000..391eeb1d8 --- /dev/null +++ b/packages/contact-center/ui-metrics/package.json @@ -0,0 +1,35 @@ +{ + "name": "@webex/ui-metrics", + "version": "1.0.0", + "description": "UI metrics tracking for Webex widgets", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build:src": "webpack --mode=development", + "clean": "rm -rf dist", + "clean:dist": "rm -rf dist", + "test:unit": "jest", + "test:styles": "echo 'No styles to test'" + }, + "dependencies": { + "uuid": "^9.0.0" + }, + "devDependencies": { + "@types/uuid": "^9.0.0", + "typescript": "^5.6.3", + "webpack": "^5.96.1", + "webpack-cli": "^5.1.4" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "keywords": [ + "webex", + "widgets", + "metrics", + "analytics", + "ui-tracking" + ], + "author": "devsupport@webex.com", + "license": "MIT" +} diff --git a/packages/contact-center/ui-metrics/src/index.ts b/packages/contact-center/ui-metrics/src/index.ts new file mode 100644 index 000000000..dcf8a1914 --- /dev/null +++ b/packages/contact-center/ui-metrics/src/index.ts @@ -0,0 +1,6 @@ +import withMetrics from './withMetrics'; +import {WidgetMetrics} from './metricsLogger'; +import {logMetrics, havePropsChanged, sanitizePropsForLogging} from './metricsLogger'; + +export {logMetrics, havePropsChanged, sanitizePropsForLogging, withMetrics}; +export type {WidgetMetrics}; diff --git a/packages/contact-center/ui-metrics/src/metricsLogger.ts b/packages/contact-center/ui-metrics/src/metricsLogger.ts new file mode 100644 index 000000000..6a683ba82 --- /dev/null +++ b/packages/contact-center/ui-metrics/src/metricsLogger.ts @@ -0,0 +1,72 @@ +export type WidgetMetrics = { + widgetName: string; + event: + | 'WIDGET_LOADED' + | 'PROP_RECEIVED' + | 'INTERACTION' + | 'ERROR' + | 'WIDGET_INITIALIZED' + | 'WIDGET_PROPS_RECEIVED' + | 'WIDGET_RENDER_START' + | 'WIDGET_RENDER_COMPLETE' + | 'WIDGET_UNMOUNTED' + | 'WIDGET_PROP_UPDATED'; + props?: Record; + timestamp: number; + additionalContext?: Record; +}; + +export const logMetrics = (metric: WidgetMetrics) => { + console.log('[WIDGET_METRICS]', metric, null, 2); + // Optional: send to Amplitude or a log endpoint +}; + +export function havePropsChanged(prev: any, next: any): boolean { + if (prev === next) return false; + + // Do shallow comparison first + if (typeof prev !== typeof next) return true; + if (!prev || !next) return prev !== next; + + const prevKeys = Object.keys(prev); + const nextKeys = Object.keys(next); + + if (prevKeys.length !== nextKeys.length) return true; + + // Check if any primitive values changed + for (const key of prevKeys) { + const prevVal = prev[key]; + const nextVal = next[key]; + + if (prevVal === nextVal) continue; + if (typeof prevVal !== 'object' || prevVal === null) return true; + if (typeof nextVal !== 'object' || nextVal === null) return true; + } + + // Fall back to deep comparison only for objects that passed shallow check + try { + return JSON.stringify(prev) !== JSON.stringify(next); + } catch { + return true; // fallback to log if circular structure + } +} + +export function sanitizePropsForLogging(obj: Record, depth = 0): Record { + if (depth > 2) return {}; // prevent deep nesting issues + + const result: Record = {}; + + for (const key in obj) { + const value = obj[key]; + + if (typeof value === 'function') { + result[key] = '[Function]'; + } else if (value && typeof value === 'object') { + result[key] = sanitizePropsForLogging(value, depth + 1); + } else { + result[key] = value; + } + } + + return result; +} diff --git a/packages/contact-center/ui-metrics/src/withMetrics.tsx b/packages/contact-center/ui-metrics/src/withMetrics.tsx new file mode 100644 index 000000000..320c624d2 --- /dev/null +++ b/packages/contact-center/ui-metrics/src/withMetrics.tsx @@ -0,0 +1,47 @@ +import React, {useEffect, useRef} from 'react'; +import {havePropsChanged, logMetrics} from './metricsLogger'; + +export default function withMetrics

(Component: any, widgetName: string) { + return React.memo( + (props: P) => { + const previousProps = useRef

(); + + // Handle mount and unmount events + useEffect(() => { + logMetrics({ + widgetName, + event: 'WIDGET_INITIALIZED', + props, + timestamp: Date.now(), + }); + + return () => { + logMetrics({ + widgetName, + event: 'WIDGET_UNMOUNTED', + timestamp: Date.now(), + }); + }; + }, []); + + // Handle prop updates + useEffect(() => { + if (previousProps.current && havePropsChanged(previousProps.current, props)) { + logMetrics({ + widgetName, + event: 'WIDGET_PROP_UPDATED', + props, + timestamp: Date.now(), + }); + } + previousProps.current = props; + }); + + return ; + }, + (prevProps, nextProps) => { + const hasChanged = havePropsChanged(prevProps, nextProps); + return !hasChanged; + } + ); +} diff --git a/packages/contact-center/ui-metrics/tsconfig.json b/packages/contact-center/ui-metrics/tsconfig.json new file mode 100644 index 000000000..50b68ce6a --- /dev/null +++ b/packages/contact-center/ui-metrics/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.json", + "include": [ + "./src" + ], + "compilerOptions": { + "outDir": "./dist", + "declaration": true, + "declarationDir": "./dist/types" + }, +} \ No newline at end of file diff --git a/packages/contact-center/ui-metrics/webpack.config.js b/packages/contact-center/ui-metrics/webpack.config.js new file mode 100644 index 000000000..247cf2783 --- /dev/null +++ b/packages/contact-center/ui-metrics/webpack.config.js @@ -0,0 +1,12 @@ +const {merge} = require('webpack-merge'); +const path = require('path'); + +const baseConfig = require('../../../webpack.config'); + +module.exports = merge(baseConfig, { + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'index.js', // Set the output filename to index.js + libraryTarget: 'commonjs2', + }, +}); diff --git a/packages/contact-center/user-state/package.json b/packages/contact-center/user-state/package.json index cc4f46cdc..23b86ce1e 100644 --- a/packages/contact-center/user-state/package.json +++ b/packages/contact-center/user-state/package.json @@ -22,6 +22,7 @@ "dependencies": { "@webex/cc-components": "workspace:*", "@webex/cc-store": "workspace:*", + "@webex/ui-metrics": "workspace:*", "mobx-react-lite": "^4.1.0", "typescript": "5.6.3" }, @@ -61,4 +62,4 @@ "react": ">=18.3.1", "react-dom": ">=18.3.1" } -} \ No newline at end of file +} 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 6e03ad675..046597202 100644 --- a/packages/contact-center/user-state/src/user-state/index.tsx +++ b/packages/contact-center/user-state/src/user-state/index.tsx @@ -5,6 +5,7 @@ import {observer} from 'mobx-react-lite'; import {useUserState} from '../helper'; import {UserStateComponent, IUserState} from '@webex/cc-components'; import {IUserStateProps} from '../user-state.types'; +import {withMetrics} from '@webex/ui-metrics'; const UserState: React.FunctionComponent = observer(({onStateChange}) => { const { @@ -35,7 +36,8 @@ const UserState: React.FunctionComponent = observer(({onStateCh logger, }; - return ; + const UserStateWithMetrics = withMetrics(UserStateComponent, 'UserState'); + return ; }); export {UserState}; diff --git a/widgets-samples/cc/samples-cc-react-app/webpack.config.js b/widgets-samples/cc/samples-cc-react-app/webpack.config.js index cfd17aaf2..ef43552bc 100644 --- a/widgets-samples/cc/samples-cc-react-app/webpack.config.js +++ b/widgets-samples/cc/samples-cc-react-app/webpack.config.js @@ -17,6 +17,7 @@ module.exports = merge(baseConfig, { '@webex/cc-user-state': path.resolve(__dirname, '../../../packages/contact-center/user-state/src'), '@webex/cc-task': path.resolve(__dirname, '../../../packages/contact-center/task/src'), '@webex/cc-components': path.resolve(__dirname, '../../../packages/contact-center/cc-components/src'), + '@webex/ui-metrics': path.resolve(__dirname, '../../../packages/contact-center/ui-metrics/src'), }, }, module: { diff --git a/yarn.lock b/yarn.lock index f84429f86..5ebbc7357 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8516,6 +8516,13 @@ __metadata: languageName: node linkType: hard +"@types/uuid@npm:^9.0.0": + version: 9.0.8 + resolution: "@types/uuid@npm:9.0.8" + checksum: 10c0/b411b93054cb1d4361919579ef3508a1f12bf15b5fdd97337d3d351bece6c921b52b6daeef89b62340fd73fd60da407878432a1af777f40648cbe53a01723489 + languageName: node + linkType: hard + "@types/validator@npm:^13.1.3": version: 13.12.2 resolution: "@types/validator@npm:13.12.2" @@ -9446,6 +9453,7 @@ __metadata: "@types/react-test-renderer": "npm:18" "@webex/cc-components": "workspace:*" "@webex/cc-store": "workspace:*" + "@webex/ui-metrics": "workspace:*" babel-jest: "npm:29.7.0" babel-loader: "npm:9.2.1" eslint: "npm:^9.20.1" @@ -9531,6 +9539,7 @@ __metadata: "@types/react-test-renderer": "npm:18" "@webex/cc-components": "workspace:*" "@webex/cc-store": "workspace:*" + "@webex/ui-metrics": "workspace:*" babel-jest: "npm:29.7.0" babel-loader: "npm:9.2.1" eslint: "npm:^9.20.1" @@ -9576,6 +9585,7 @@ __metadata: "@types/react-test-renderer": "npm:18" "@webex/cc-components": "workspace:*" "@webex/cc-store": "workspace:*" + "@webex/ui-metrics": "workspace:*" babel-jest: "npm:29.7.0" babel-loader: "npm:9.2.1" eslint: "npm:^9.20.1" @@ -12295,6 +12305,20 @@ __metadata: languageName: node linkType: hard +"@webex/ui-metrics@workspace:*, @webex/ui-metrics@workspace:packages/contact-center/ui-metrics": + version: 0.0.0-use.local + resolution: "@webex/ui-metrics@workspace:packages/contact-center/ui-metrics" + dependencies: + "@types/uuid": "npm:^9.0.0" + typescript: "npm:^5.6.3" + uuid: "npm:^9.0.0" + webpack: "npm:^5.96.1" + webpack-cli: "npm:^5.1.4" + peerDependencies: + react: ">=16.8.0" + languageName: unknown + linkType: soft + "@webex/web-capabilities@npm:^1.1.0, @webex/web-capabilities@npm:^1.4.0, @webex/web-capabilities@npm:^1.4.1": version: 1.4.1 resolution: "@webex/web-capabilities@npm:1.4.1" From 032fd092fb48c5242bc93642cd6254f51c8717f8 Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Mon, 21 Jul 2025 11:28:41 +0530 Subject: [PATCH 02/12] fix(ui-metrics): metrics rerendering issue --- .../contact-center/cc-components/package.json | 3 +- .../components/StationLogin/station-login.tsx | 4 +- .../src/components/UserState/user-state.tsx | 4 +- .../task/CallControl/call-control.tsx | 4 +- .../task/CallControlCAD/call-control-cad.tsx | 4 +- .../task/IncomingTask/incoming-task.tsx | 4 +- .../task/OutdialCall/outdial-call.tsx | 4 +- .../components/task/TaskList/task-list.tsx | 4 +- .../contact-center/station-login/package.json | 1 - .../station-login/src/station-login/index.tsx | 4 +- packages/contact-center/task/package.json | 1 - .../task/src/CallControl/index.tsx | 4 +- .../task/src/CallControlCAD/index.tsx | 4 +- .../task/src/IncomingTask/index.tsx | 4 +- .../task/src/OutdialCall/index.tsx | 4 +- .../task/src/TaskList/index.tsx | 4 +- .../contact-center/ui-metrics/package.json | 1 + .../contact-center/ui-metrics/src/index.ts | 3 +- .../ui-metrics/src/metricsLogger.ts | 39 ++++--------------- .../ui-metrics/src/withMetrics.tsx | 22 +---------- .../contact-center/user-state/package.json | 1 - .../user-state/src/user-state/index.tsx | 4 +- webpack.config.js | 15 ++++--- yarn.lock | 5 +-- 24 files changed, 52 insertions(+), 95 deletions(-) diff --git a/packages/contact-center/cc-components/package.json b/packages/contact-center/cc-components/package.json index e3d114fbc..eea2fee23 100644 --- a/packages/contact-center/cc-components/package.json +++ b/packages/contact-center/cc-components/package.json @@ -26,7 +26,8 @@ "dependencies": { "@momentum-ui/illustrations": "^1.24.0", "@r2wc/react-to-web-component": "2.0.3", - "@webex/cc-store": "workspace:*" + "@webex/cc-store": "workspace:*", + "@webex/ui-metrics": "workspace:*" }, "devDependencies": { "@babel/core": "7.25.2", diff --git a/packages/contact-center/cc-components/src/components/StationLogin/station-login.tsx b/packages/contact-center/cc-components/src/components/StationLogin/station-login.tsx index e0ff392fe..adbc2b353 100644 --- a/packages/contact-center/cc-components/src/components/StationLogin/station-login.tsx +++ b/packages/contact-center/cc-components/src/components/StationLogin/station-login.tsx @@ -16,6 +16,7 @@ import { updateDialNumberLabel, handleCCSignoutKeyDown, } from './station-login.utils'; +import {withMetrics} from '@webex/ui-metrics'; const StationLoginComponent: React.FunctionComponent = (props) => { const { @@ -343,4 +344,5 @@ const StationLoginComponent: React.FunctionComponent ); }; -export default StationLoginComponent; +const StationLoginComponentWithMetrics = withMetrics(StationLoginComponent, 'StationLogin'); +export default StationLoginComponentWithMetrics; diff --git a/packages/contact-center/cc-components/src/components/UserState/user-state.tsx b/packages/contact-center/cc-components/src/components/UserState/user-state.tsx index 83ce3d9b3..74eb13a1d 100644 --- a/packages/contact-center/cc-components/src/components/UserState/user-state.tsx +++ b/packages/contact-center/cc-components/src/components/UserState/user-state.tsx @@ -17,6 +17,7 @@ import { getSelectedKey, buildDropdownItems, } from './user-state.utils'; +import {withMetrics} from '@webex/ui-metrics'; const UserStateComponent: React.FunctionComponent = (props) => { const { @@ -103,4 +104,5 @@ const UserStateComponent: React.FunctionComponent = (props) => { ); }; -export default UserStateComponent; +const UserStateComponentWithMetrics = withMetrics(UserStateComponent, 'UserState'); +export default UserStateComponentWithMetrics; diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx index b8afefe27..22c202db5 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx @@ -22,6 +22,7 @@ import { SELECT, SUBMIT_WRAP_UP, } from '../constants'; +import {withMetrics} from '@webex/ui-metrics'; function CallControlComponent(props: CallControlComponentProps) { const [selectedWrapupReason, setSelectedWrapupReason] = useState(null); @@ -399,4 +400,5 @@ function CallControlComponent(props: CallControlComponentProps) { ); } -export default CallControlComponent; +const CallControlComponentWithMetrics = withMetrics(CallControlComponent, 'CallControl'); +export default CallControlComponentWithMetrics; diff --git a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx index fdacfd384..df9551578 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx @@ -19,6 +19,7 @@ import { CUSTOMER_NAME, RONA, } from '../constants'; +import {withMetrics} from '@webex/ui-metrics'; const CallControlCADComponent: React.FC = (props) => { const { @@ -214,4 +215,5 @@ const CallControlCADComponent: React.FC = (props) => ); }; -export default CallControlCADComponent; +const CallControlCADComponentWithMetrics = withMetrics(CallControlCADComponent, 'CallControlCAD'); +export default CallControlCADComponentWithMetrics; diff --git a/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.tsx b/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.tsx index c71e36f60..76053c044 100644 --- a/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.tsx +++ b/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.tsx @@ -1,6 +1,7 @@ import React from 'react'; import {IncomingTaskComponentProps, MEDIA_CHANNEL} from '../task.types'; import Task from '../Task'; +import {withMetrics} from '@webex/ui-metrics'; const IncomingTaskComponent: React.FunctionComponent = (props) => { const {incomingTask, isBrowser, accept, reject, logger} = props; @@ -64,4 +65,5 @@ const IncomingTaskComponent: React.FunctionComponent ); }; -export default IncomingTaskComponent; +const IncomingTaskComponentWithMetrics = withMetrics(IncomingTaskComponent, 'IncomingTask'); +export default IncomingTaskComponentWithMetrics; diff --git a/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.tsx b/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.tsx index e75abd183..c74486dcb 100644 --- a/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.tsx +++ b/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.tsx @@ -1,6 +1,7 @@ import React, {useState} from 'react'; import {OutdialCallComponentProps} from '../task.types'; import './outdial-call.style.scss'; +import {withMetrics} from '@webex/ui-metrics'; const OutdialCallComponent: React.FunctionComponent = (props) => { const {startOutdial} = props; @@ -49,4 +50,5 @@ const OutdialCallComponent: React.FunctionComponent = ); }; -export default OutdialCallComponent; +const OutdialCallComponentWithMetrics = withMetrics(OutdialCallComponent, 'OutdialCall'); +export default OutdialCallComponentWithMetrics; diff --git a/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx b/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx index da77d11a2..03f242e01 100644 --- a/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx +++ b/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx @@ -2,6 +2,7 @@ import React from 'react'; import {TaskListComponentProps, MEDIA_CHANNEL} from '../task.types'; import Task from '../Task'; import './styles.scss'; +import {withMetrics} from '@webex/ui-metrics'; const TaskListComponent: React.FunctionComponent = (props) => { const {currentTask, taskList, acceptTask, declineTask, isBrowser, onTaskSelect, logger} = props; @@ -74,4 +75,5 @@ const TaskListComponent: React.FunctionComponent = (prop ); }; -export default TaskListComponent; +const TaskListComponentWithMetrics = withMetrics(TaskListComponent, 'TaskList'); +export default TaskListComponentWithMetrics; diff --git a/packages/contact-center/station-login/package.json b/packages/contact-center/station-login/package.json index 4e667aca5..a3eb9d378 100644 --- a/packages/contact-center/station-login/package.json +++ b/packages/contact-center/station-login/package.json @@ -22,7 +22,6 @@ "dependencies": { "@webex/cc-components": "workspace:*", "@webex/cc-store": "workspace:*", - "@webex/ui-metrics": "workspace:*", "mobx-react-lite": "^4.1.0" }, "devDependencies": { 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 cd7a60bf2..5ce1067da 100644 --- a/packages/contact-center/station-login/src/station-login/index.tsx +++ b/packages/contact-center/station-login/src/station-login/index.tsx @@ -5,7 +5,6 @@ import {observer} from 'mobx-react-lite'; import {StationLoginComponent} from '@webex/cc-components'; import {useStationLogin} from '../helper'; import {StationLoginProps} from './station-login.types'; -import {withMetrics} from '@webex/ui-metrics'; const StationLogin: React.FunctionComponent = observer( ({onLogin, onLogout, onCCSignOut, profileMode, onSaveStart, onSaveEnd, doStationLogout}) => { @@ -58,8 +57,7 @@ const StationLogin: React.FunctionComponent = observer( profileMode, }; - const StationLoginWithMetrics = withMetrics(StationLoginComponent, 'StationLogin'); - return ; + return ; } ); diff --git a/packages/contact-center/task/package.json b/packages/contact-center/task/package.json index 19e24713d..01037139f 100644 --- a/packages/contact-center/task/package.json +++ b/packages/contact-center/task/package.json @@ -22,7 +22,6 @@ "dependencies": { "@webex/cc-components": "workspace:*", "@webex/cc-store": "workspace:*", - "@webex/ui-metrics": "workspace:*", "mobx-react-lite": "^4.1.0" }, "devDependencies": { diff --git a/packages/contact-center/task/src/CallControl/index.tsx b/packages/contact-center/task/src/CallControl/index.tsx index 992faac03..ba78f948f 100644 --- a/packages/contact-center/task/src/CallControl/index.tsx +++ b/packages/contact-center/task/src/CallControl/index.tsx @@ -1,7 +1,6 @@ import React from 'react'; import {observer} from 'mobx-react-lite'; -import {withMetrics} from '@webex/ui-metrics'; import store from '@webex/cc-store'; import {useCallControl} from '../helper'; import {CallControlProps} from '../task.types'; @@ -47,8 +46,7 @@ const CallControl: React.FunctionComponent = observer( logger, }; - const CallControlWithMetrics = withMetrics(CallControlComponent, 'CallControl'); - return ; + return ; } ); diff --git a/packages/contact-center/task/src/CallControlCAD/index.tsx b/packages/contact-center/task/src/CallControlCAD/index.tsx index 7c0f6f0a3..29d395a1f 100644 --- a/packages/contact-center/task/src/CallControlCAD/index.tsx +++ b/packages/contact-center/task/src/CallControlCAD/index.tsx @@ -1,7 +1,6 @@ import React from 'react'; import {observer} from 'mobx-react-lite'; -import {withMetrics} from '@webex/ui-metrics'; import store from '@webex/cc-store'; import {useCallControl} from '../helper'; import {CallControlProps} from '../task.types'; @@ -48,8 +47,7 @@ const CallControlCAD: React.FunctionComponent = observer( logger, }; - const CallControlCADWithMetrics = withMetrics(CallControlCADComponent, 'CallControlCAD'); - return ; + return ; } ); diff --git a/packages/contact-center/task/src/IncomingTask/index.tsx b/packages/contact-center/task/src/IncomingTask/index.tsx index 7d3aa7338..6dfe5d4db 100644 --- a/packages/contact-center/task/src/IncomingTask/index.tsx +++ b/packages/contact-center/task/src/IncomingTask/index.tsx @@ -1,7 +1,6 @@ import React from 'react'; import {observer} from 'mobx-react-lite'; -import {withMetrics} from '@webex/ui-metrics'; import store from '@webex/cc-store'; import {useIncomingTask} from '../helper'; import {IncomingTaskComponent} from '@webex/cc-components'; @@ -16,8 +15,7 @@ const IncomingTask: React.FunctionComponent = observer(({inco logger, }; - const IncomingTaskWithMetrics = withMetrics(IncomingTaskComponent, 'IncomingTask'); - return ; + return ; }); export {IncomingTask}; diff --git a/packages/contact-center/task/src/OutdialCall/index.tsx b/packages/contact-center/task/src/OutdialCall/index.tsx index 5d8506f16..aa6eff781 100644 --- a/packages/contact-center/task/src/OutdialCall/index.tsx +++ b/packages/contact-center/task/src/OutdialCall/index.tsx @@ -1,7 +1,6 @@ import React from 'react'; import store from '@webex/cc-store'; import {observer} from 'mobx-react-lite'; -import {withMetrics} from '@webex/ui-metrics'; import {OutdialCallComponent} from '@webex/cc-components'; import {useOutdialCall} from '../helper'; @@ -13,8 +12,7 @@ const OutdialCall: React.FunctionComponent = observer(() => { ...result, }; - const OutdialCallWithMetrics = withMetrics(OutdialCallComponent, 'OutdialCall'); - return ; + return ; }); export {OutdialCall}; diff --git a/packages/contact-center/task/src/TaskList/index.tsx b/packages/contact-center/task/src/TaskList/index.tsx index b7f22655f..6d3ed878d 100644 --- a/packages/contact-center/task/src/TaskList/index.tsx +++ b/packages/contact-center/task/src/TaskList/index.tsx @@ -1,7 +1,6 @@ import React from 'react'; import store from '@webex/cc-store'; import {observer} from 'mobx-react-lite'; -import {withMetrics} from '@webex/ui-metrics'; import {TaskListComponent} from '@webex/cc-components'; import {useTaskList} from '../helper'; @@ -18,8 +17,7 @@ const TaskList: React.FunctionComponent = observer( logger, }; - const TaskListWithMetrics = withMetrics(TaskListComponent, 'TaskList'); - return ; + return ; } ); diff --git a/packages/contact-center/ui-metrics/package.json b/packages/contact-center/ui-metrics/package.json index 391eeb1d8..c172d51b7 100644 --- a/packages/contact-center/ui-metrics/package.json +++ b/packages/contact-center/ui-metrics/package.json @@ -12,6 +12,7 @@ "test:styles": "echo 'No styles to test'" }, "dependencies": { + "@webex/cc-store": "workspace:*", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/packages/contact-center/ui-metrics/src/index.ts b/packages/contact-center/ui-metrics/src/index.ts index dcf8a1914..522b06d0b 100644 --- a/packages/contact-center/ui-metrics/src/index.ts +++ b/packages/contact-center/ui-metrics/src/index.ts @@ -1,6 +1,5 @@ import withMetrics from './withMetrics'; import {WidgetMetrics} from './metricsLogger'; -import {logMetrics, havePropsChanged, sanitizePropsForLogging} from './metricsLogger'; -export {logMetrics, havePropsChanged, sanitizePropsForLogging, withMetrics}; +export {withMetrics}; export type {WidgetMetrics}; diff --git a/packages/contact-center/ui-metrics/src/metricsLogger.ts b/packages/contact-center/ui-metrics/src/metricsLogger.ts index 6a683ba82..36a0ead8f 100644 --- a/packages/contact-center/ui-metrics/src/metricsLogger.ts +++ b/packages/contact-center/ui-metrics/src/metricsLogger.ts @@ -1,23 +1,18 @@ +import store from '@webex/cc-store'; + export type WidgetMetrics = { widgetName: string; - event: - | 'WIDGET_LOADED' - | 'PROP_RECEIVED' - | 'INTERACTION' - | 'ERROR' - | 'WIDGET_INITIALIZED' - | 'WIDGET_PROPS_RECEIVED' - | 'WIDGET_RENDER_START' - | 'WIDGET_RENDER_COMPLETE' - | 'WIDGET_UNMOUNTED' - | 'WIDGET_PROP_UPDATED'; + event: 'WIDGET_MOUNTED' | 'ERROR' | 'WIDGET_UNMOUNTED'; props?: Record; timestamp: number; additionalContext?: Record; }; export const logMetrics = (metric: WidgetMetrics) => { - console.log('[WIDGET_METRICS]', metric, null, 2); + store.logger.log(`CC-Widgets: UI Metrics: ${JSON.stringify(metric, null, 2)}`, { + module: 'metricsLogger.tsx', + method: 'logMetrics', + }); // Optional: send to Amplitude or a log endpoint }; @@ -50,23 +45,3 @@ export function havePropsChanged(prev: any, next: any): boolean { return true; // fallback to log if circular structure } } - -export function sanitizePropsForLogging(obj: Record, depth = 0): Record { - if (depth > 2) return {}; // prevent deep nesting issues - - const result: Record = {}; - - for (const key in obj) { - const value = obj[key]; - - if (typeof value === 'function') { - result[key] = '[Function]'; - } else if (value && typeof value === 'object') { - result[key] = sanitizePropsForLogging(value, depth + 1); - } else { - result[key] = value; - } - } - - return result; -} diff --git a/packages/contact-center/ui-metrics/src/withMetrics.tsx b/packages/contact-center/ui-metrics/src/withMetrics.tsx index 320c624d2..5bad8d91f 100644 --- a/packages/contact-center/ui-metrics/src/withMetrics.tsx +++ b/packages/contact-center/ui-metrics/src/withMetrics.tsx @@ -6,12 +6,10 @@ export default function withMetrics

(Component: any, widgetName (props: P) => { const previousProps = useRef

(); - // Handle mount and unmount events useEffect(() => { logMetrics({ widgetName, - event: 'WIDGET_INITIALIZED', - props, + event: 'WIDGET_MOUNTED', timestamp: Date.now(), }); @@ -24,24 +22,8 @@ export default function withMetrics

(Component: any, widgetName }; }, []); - // Handle prop updates - useEffect(() => { - if (previousProps.current && havePropsChanged(previousProps.current, props)) { - logMetrics({ - widgetName, - event: 'WIDGET_PROP_UPDATED', - props, - timestamp: Date.now(), - }); - } - previousProps.current = props; - }); - return ; }, - (prevProps, nextProps) => { - const hasChanged = havePropsChanged(prevProps, nextProps); - return !hasChanged; - } + (prevProps, nextProps) => !havePropsChanged(prevProps, nextProps) ); } diff --git a/packages/contact-center/user-state/package.json b/packages/contact-center/user-state/package.json index e6b05cef9..76050ed28 100644 --- a/packages/contact-center/user-state/package.json +++ b/packages/contact-center/user-state/package.json @@ -22,7 +22,6 @@ "dependencies": { "@webex/cc-components": "workspace:*", "@webex/cc-store": "workspace:*", - "@webex/ui-metrics": "workspace:*", "mobx-react-lite": "^4.1.0", "typescript": "5.6.3" }, 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 046597202..6e03ad675 100644 --- a/packages/contact-center/user-state/src/user-state/index.tsx +++ b/packages/contact-center/user-state/src/user-state/index.tsx @@ -5,7 +5,6 @@ import {observer} from 'mobx-react-lite'; import {useUserState} from '../helper'; import {UserStateComponent, IUserState} from '@webex/cc-components'; import {IUserStateProps} from '../user-state.types'; -import {withMetrics} from '@webex/ui-metrics'; const UserState: React.FunctionComponent = observer(({onStateChange}) => { const { @@ -36,8 +35,7 @@ const UserState: React.FunctionComponent = observer(({onStateCh logger, }; - const UserStateWithMetrics = withMetrics(UserStateComponent, 'UserState'); - return ; + return ; }); export {UserState}; diff --git a/webpack.config.js b/webpack.config.js index 3bbe67e5d..9d020e6b3 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -16,7 +16,7 @@ module.exports = { vm: require.resolve('vm-browserify'), util: require.resolve('util/'), url: require.resolve('url/'), - } + }, }, plugins: [ new webpack.ProvidePlugin({ @@ -38,12 +38,15 @@ module.exports = { { test: /\.scss$/, use: [ - "style-loader", // Injects styles into DOM - "css-loader", // Turns CSS into CommonJS - "sass-loader" // Compiles Sass to CSS + 'style-loader', // Injects styles into DOM + 'css-loader', // Turns CSS into CommonJS + 'sass-loader', // Compiles Sass to CSS ], - exclude: /node_modules/ - } + exclude: /node_modules/, + }, ], }, + stats: { + warningsFilter: [/sass-loader/], + }, }; diff --git a/yarn.lock b/yarn.lock index ba87ce679..660d723a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9409,6 +9409,7 @@ __metadata: "@types/jest": "npm:29.5.14" "@types/react-test-renderer": "npm:18" "@webex/cc-store": "workspace:*" + "@webex/ui-metrics": "workspace:*" babel-loader: "npm:9.2.1" eslint: "npm:^9.20.1" eslint-config-prettier: "npm:^10.0.1" @@ -9453,7 +9454,6 @@ __metadata: "@types/react-test-renderer": "npm:18" "@webex/cc-components": "workspace:*" "@webex/cc-store": "workspace:*" - "@webex/ui-metrics": "workspace:*" babel-jest: "npm:29.7.0" babel-loader: "npm:9.2.1" eslint: "npm:^9.20.1" @@ -9539,7 +9539,6 @@ __metadata: "@types/react-test-renderer": "npm:18" "@webex/cc-components": "workspace:*" "@webex/cc-store": "workspace:*" - "@webex/ui-metrics": "workspace:*" babel-jest: "npm:29.7.0" babel-loader: "npm:9.2.1" eslint: "npm:^9.20.1" @@ -9585,7 +9584,6 @@ __metadata: "@types/react-test-renderer": "npm:18" "@webex/cc-components": "workspace:*" "@webex/cc-store": "workspace:*" - "@webex/ui-metrics": "workspace:*" babel-jest: "npm:29.7.0" babel-loader: "npm:9.2.1" eslint: "npm:^9.20.1" @@ -12310,6 +12308,7 @@ __metadata: resolution: "@webex/ui-metrics@workspace:packages/contact-center/ui-metrics" dependencies: "@types/uuid": "npm:^9.0.0" + "@webex/cc-store": "workspace:*" typescript: "npm:^5.6.3" uuid: "npm:^9.0.0" webpack: "npm:^5.96.1" From e800f38e7a0fd1f7df462455f9169f5b74d539a1 Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Tue, 22 Jul 2025 14:07:45 +0530 Subject: [PATCH 03/12] feat(cc-ui-metrics): change name to cc-ui-metrics --- .../contact-center/cc-components/package.json | 2 +- .../components/StationLogin/station-login.tsx | 2 +- .../src/components/UserState/user-state.tsx | 2 +- .../task/CallControl/call-control.tsx | 2 +- .../task/CallControlCAD/call-control-cad.tsx | 2 +- .../task/IncomingTask/incoming-task.tsx | 2 +- .../task/OutdialCall/outdial-call.tsx | 2 +- .../components/task/TaskList/task-list.tsx | 2 +- .../cc/samples-cc-react-app/webpack.config.js | 2 +- yarn.lock | 47 ++++++++++--------- 10 files changed, 33 insertions(+), 32 deletions(-) diff --git a/packages/contact-center/cc-components/package.json b/packages/contact-center/cc-components/package.json index eea2fee23..23ad630d5 100644 --- a/packages/contact-center/cc-components/package.json +++ b/packages/contact-center/cc-components/package.json @@ -27,7 +27,7 @@ "@momentum-ui/illustrations": "^1.24.0", "@r2wc/react-to-web-component": "2.0.3", "@webex/cc-store": "workspace:*", - "@webex/ui-metrics": "workspace:*" + "@webex/cc-ui-metrics": "workspace:*" }, "devDependencies": { "@babel/core": "7.25.2", diff --git a/packages/contact-center/cc-components/src/components/StationLogin/station-login.tsx b/packages/contact-center/cc-components/src/components/StationLogin/station-login.tsx index adbc2b353..9d48901ef 100644 --- a/packages/contact-center/cc-components/src/components/StationLogin/station-login.tsx +++ b/packages/contact-center/cc-components/src/components/StationLogin/station-login.tsx @@ -16,7 +16,7 @@ import { updateDialNumberLabel, handleCCSignoutKeyDown, } from './station-login.utils'; -import {withMetrics} from '@webex/ui-metrics'; +import {withMetrics} from '@webex/cc-ui-metrics'; const StationLoginComponent: React.FunctionComponent = (props) => { const { diff --git a/packages/contact-center/cc-components/src/components/UserState/user-state.tsx b/packages/contact-center/cc-components/src/components/UserState/user-state.tsx index 74eb13a1d..38d00604e 100644 --- a/packages/contact-center/cc-components/src/components/UserState/user-state.tsx +++ b/packages/contact-center/cc-components/src/components/UserState/user-state.tsx @@ -17,7 +17,7 @@ import { getSelectedKey, buildDropdownItems, } from './user-state.utils'; -import {withMetrics} from '@webex/ui-metrics'; +import {withMetrics} from '@webex/cc-ui-metrics'; const UserStateComponent: React.FunctionComponent = (props) => { const { diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx index 22c202db5..fac96a8f2 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx @@ -22,7 +22,7 @@ import { SELECT, SUBMIT_WRAP_UP, } from '../constants'; -import {withMetrics} from '@webex/ui-metrics'; +import {withMetrics} from '@webex/cc-ui-metrics'; function CallControlComponent(props: CallControlComponentProps) { const [selectedWrapupReason, setSelectedWrapupReason] = useState(null); diff --git a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx index df9551578..784c77fd3 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx @@ -19,7 +19,7 @@ import { CUSTOMER_NAME, RONA, } from '../constants'; -import {withMetrics} from '@webex/ui-metrics'; +import {withMetrics} from '@webex/cc-ui-metrics'; const CallControlCADComponent: React.FC = (props) => { const { diff --git a/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.tsx b/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.tsx index 76053c044..a25aae918 100644 --- a/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.tsx +++ b/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {IncomingTaskComponentProps, MEDIA_CHANNEL} from '../task.types'; import Task from '../Task'; -import {withMetrics} from '@webex/ui-metrics'; +import {withMetrics} from '@webex/cc-ui-metrics'; const IncomingTaskComponent: React.FunctionComponent = (props) => { const {incomingTask, isBrowser, accept, reject, logger} = props; diff --git a/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.tsx b/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.tsx index c74486dcb..b40fcb979 100644 --- a/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.tsx +++ b/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.tsx @@ -1,7 +1,7 @@ import React, {useState} from 'react'; import {OutdialCallComponentProps} from '../task.types'; import './outdial-call.style.scss'; -import {withMetrics} from '@webex/ui-metrics'; +import {withMetrics} from '@webex/cc-ui-metrics'; const OutdialCallComponent: React.FunctionComponent = (props) => { const {startOutdial} = props; diff --git a/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx b/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx index 03f242e01..7a33e8084 100644 --- a/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx +++ b/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {TaskListComponentProps, MEDIA_CHANNEL} from '../task.types'; import Task from '../Task'; import './styles.scss'; -import {withMetrics} from '@webex/ui-metrics'; +import {withMetrics} from '@webex/cc-ui-metrics'; const TaskListComponent: React.FunctionComponent = (props) => { const {currentTask, taskList, acceptTask, declineTask, isBrowser, onTaskSelect, logger} = props; diff --git a/widgets-samples/cc/samples-cc-react-app/webpack.config.js b/widgets-samples/cc/samples-cc-react-app/webpack.config.js index ef43552bc..b0ac80296 100644 --- a/widgets-samples/cc/samples-cc-react-app/webpack.config.js +++ b/widgets-samples/cc/samples-cc-react-app/webpack.config.js @@ -17,7 +17,7 @@ module.exports = merge(baseConfig, { '@webex/cc-user-state': path.resolve(__dirname, '../../../packages/contact-center/user-state/src'), '@webex/cc-task': path.resolve(__dirname, '../../../packages/contact-center/task/src'), '@webex/cc-components': path.resolve(__dirname, '../../../packages/contact-center/cc-components/src'), - '@webex/ui-metrics': path.resolve(__dirname, '../../../packages/contact-center/ui-metrics/src'), + '@webex/cc-ui-metrics': path.resolve(__dirname, '../../../packages/contact-center/ui-metrics/src'), }, }, module: { diff --git a/yarn.lock b/yarn.lock index 660d723a0..60fdb3a39 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8516,13 +8516,6 @@ __metadata: languageName: node linkType: hard -"@types/uuid@npm:^9.0.0": - version: 9.0.8 - resolution: "@types/uuid@npm:9.0.8" - checksum: 10c0/b411b93054cb1d4361919579ef3508a1f12bf15b5fdd97337d3d351bece6c921b52b6daeef89b62340fd73fd60da407878432a1af777f40648cbe53a01723489 - languageName: node - linkType: hard - "@types/validator@npm:^13.1.3": version: 13.12.2 resolution: "@types/validator@npm:13.12.2" @@ -9409,7 +9402,7 @@ __metadata: "@types/jest": "npm:29.5.14" "@types/react-test-renderer": "npm:18" "@webex/cc-store": "workspace:*" - "@webex/ui-metrics": "workspace:*" + "@webex/cc-ui-metrics": "workspace:*" babel-loader: "npm:9.2.1" eslint: "npm:^9.20.1" eslint-config-prettier: "npm:^10.0.1" @@ -9568,6 +9561,29 @@ __metadata: languageName: unknown linkType: soft +"@webex/cc-ui-metrics@workspace:*, @webex/cc-ui-metrics@workspace:packages/contact-center/ui-metrics": + version: 0.0.0-use.local + resolution: "@webex/cc-ui-metrics@workspace:packages/contact-center/ui-metrics" + dependencies: + "@testing-library/dom": "npm:10.4.0" + "@testing-library/jest-dom": "npm:6.6.2" + "@testing-library/react": "npm:16.0.1" + "@types/jest": "npm:29.5.14" + "@types/react-test-renderer": "npm:18" + "@webex/cc-store": "workspace:*" + babel-jest: "npm:29.7.0" + jest: "npm:29.7.0" + jest-environment-jsdom: "npm:29.7.0" + typescript: "npm:^5.6.3" + uuid: "npm:^9.0.0" + webpack: "npm:^5.96.1" + webpack-cli: "npm:^5.1.4" + peerDependencies: + react: ">=18.3.1" + react-dom: ">=18.3.1" + languageName: unknown + linkType: soft + "@webex/cc-user-state@workspace:*, @webex/cc-user-state@workspace:packages/contact-center/user-state": version: 0.0.0-use.local resolution: "@webex/cc-user-state@workspace:packages/contact-center/user-state" @@ -12303,21 +12319,6 @@ __metadata: languageName: node linkType: hard -"@webex/ui-metrics@workspace:*, @webex/ui-metrics@workspace:packages/contact-center/ui-metrics": - version: 0.0.0-use.local - resolution: "@webex/ui-metrics@workspace:packages/contact-center/ui-metrics" - dependencies: - "@types/uuid": "npm:^9.0.0" - "@webex/cc-store": "workspace:*" - typescript: "npm:^5.6.3" - uuid: "npm:^9.0.0" - webpack: "npm:^5.96.1" - webpack-cli: "npm:^5.1.4" - peerDependencies: - react: ">=16.8.0" - languageName: unknown - linkType: soft - "@webex/web-capabilities@npm:^1.1.0, @webex/web-capabilities@npm:^1.4.0, @webex/web-capabilities@npm:^1.4.1": version: 1.4.1 resolution: "@webex/web-capabilities@npm:1.4.1" From c4982b2cc27a1668e23a4598abaa08681b191c08 Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Tue, 22 Jul 2025 14:08:14 +0530 Subject: [PATCH 04/12] fix(ui-metrics): add uts --- .../contact-center/ui-metrics/bable.config.js | 3 + .../contact-center/ui-metrics/jest.config.js | 6 + .../contact-center/ui-metrics/package.json | 31 ++-- .../ui-metrics/src/metricsLogger.ts | 5 +- .../ui-metrics/src/withMetrics.tsx | 2 - .../ui-metrics/tests/metricsLogger.test.ts | 103 +++++++++++++ .../ui-metrics/tests/withMetrics.test.tsx | 137 ++++++++++++++++++ .../ui-metrics/webpack.config.js | 5 + 8 files changed, 273 insertions(+), 19 deletions(-) create mode 100644 packages/contact-center/ui-metrics/bable.config.js create mode 100644 packages/contact-center/ui-metrics/jest.config.js create mode 100644 packages/contact-center/ui-metrics/tests/metricsLogger.test.ts create mode 100644 packages/contact-center/ui-metrics/tests/withMetrics.test.tsx diff --git a/packages/contact-center/ui-metrics/bable.config.js b/packages/contact-center/ui-metrics/bable.config.js new file mode 100644 index 000000000..0eaef236c --- /dev/null +++ b/packages/contact-center/ui-metrics/bable.config.js @@ -0,0 +1,3 @@ +const baseConfig = require('../../../babel.config.js'); + +module.exports = baseConfig; diff --git a/packages/contact-center/ui-metrics/jest.config.js b/packages/contact-center/ui-metrics/jest.config.js new file mode 100644 index 000000000..4f2ecff91 --- /dev/null +++ b/packages/contact-center/ui-metrics/jest.config.js @@ -0,0 +1,6 @@ +const jestConfig = require('../../../jest.config.js'); + +jestConfig.rootDir = '../../../'; +jestConfig.testMatch = ['**/ui-metrics/tests/**/*.ts', '**/ui-metrics/tests/**/*.tsx']; + +module.exports = jestConfig; diff --git a/packages/contact-center/ui-metrics/package.json b/packages/contact-center/ui-metrics/package.json index c172d51b7..883c411e7 100644 --- a/packages/contact-center/ui-metrics/package.json +++ b/packages/contact-center/ui-metrics/package.json @@ -1,5 +1,5 @@ { - "name": "@webex/ui-metrics", + "name": "@webex/cc-ui-metrics", "version": "1.0.0", "description": "UI metrics tracking for Webex widgets", "main": "dist/index.js", @@ -8,29 +8,28 @@ "build:src": "webpack --mode=development", "clean": "rm -rf dist", "clean:dist": "rm -rf dist", - "test:unit": "jest", + "test:unit": "jest --coverage", "test:styles": "echo 'No styles to test'" }, "dependencies": { - "@webex/cc-store": "workspace:*", - "uuid": "^9.0.0" + "@webex/cc-store": "workspace:*" }, "devDependencies": { - "@types/uuid": "^9.0.0", + "@testing-library/dom": "10.4.0", + "@testing-library/jest-dom": "6.6.2", + "@testing-library/react": "16.0.1", + "@types/jest": "29.5.14", + "@types/react-test-renderer": "18", + "babel-jest": "29.7.0", + "jest": "29.7.0", + "jest-environment-jsdom": "29.7.0", "typescript": "^5.6.3", + "uuid": "^9.0.0", "webpack": "^5.96.1", "webpack-cli": "^5.1.4" }, "peerDependencies": { - "react": ">=16.8.0" - }, - "keywords": [ - "webex", - "widgets", - "metrics", - "analytics", - "ui-tracking" - ], - "author": "devsupport@webex.com", - "license": "MIT" + "react": ">=18.3.1", + "react-dom": ">=18.3.1" + } } diff --git a/packages/contact-center/ui-metrics/src/metricsLogger.ts b/packages/contact-center/ui-metrics/src/metricsLogger.ts index 36a0ead8f..d59a25712 100644 --- a/packages/contact-center/ui-metrics/src/metricsLogger.ts +++ b/packages/contact-center/ui-metrics/src/metricsLogger.ts @@ -9,11 +9,14 @@ export type WidgetMetrics = { }; export const logMetrics = (metric: WidgetMetrics) => { + if (!store.logger) { + console.log('CC-Widgets: UI Metrics: No logger found'); + return; + } store.logger.log(`CC-Widgets: UI Metrics: ${JSON.stringify(metric, null, 2)}`, { module: 'metricsLogger.tsx', method: 'logMetrics', }); - // Optional: send to Amplitude or a log endpoint }; export function havePropsChanged(prev: any, next: any): boolean { diff --git a/packages/contact-center/ui-metrics/src/withMetrics.tsx b/packages/contact-center/ui-metrics/src/withMetrics.tsx index 5bad8d91f..12156e429 100644 --- a/packages/contact-center/ui-metrics/src/withMetrics.tsx +++ b/packages/contact-center/ui-metrics/src/withMetrics.tsx @@ -4,8 +4,6 @@ import {havePropsChanged, logMetrics} from './metricsLogger'; export default function withMetrics

(Component: any, widgetName: string) { return React.memo( (props: P) => { - const previousProps = useRef

(); - useEffect(() => { logMetrics({ widgetName, diff --git a/packages/contact-center/ui-metrics/tests/metricsLogger.test.ts b/packages/contact-center/ui-metrics/tests/metricsLogger.test.ts new file mode 100644 index 000000000..5bfcc28ab --- /dev/null +++ b/packages/contact-center/ui-metrics/tests/metricsLogger.test.ts @@ -0,0 +1,103 @@ +import store from '@webex/cc-store'; +import {logMetrics, havePropsChanged, WidgetMetrics} from '../src/metricsLogger'; + +// Mock the store +jest.mock('@webex/cc-store', () => ({ + logger: { + log: jest.fn(), + }, +})); + +describe('metricsLogger', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('logMetrics', () => { + it('should log metrics when logger is available', () => { + const metric: WidgetMetrics = { + widgetName: 'TestWidget', + event: 'WIDGET_MOUNTED', + timestamp: 1234567890, + props: {test: 'prop'}, + additionalContext: {context: 'test'}, + }; + + logMetrics(metric); + + expect(store.logger.log).toHaveBeenCalledWith(`CC-Widgets: UI Metrics: ${JSON.stringify(metric, null, 2)}`, { + module: 'metricsLogger.tsx', + method: 'logMetrics', + }); + }); + + it('should handle case when logger is not available', () => { + const consoleSpy = jest.spyOn(console, 'log'); + (store as any).logger = null; + + const metric: WidgetMetrics = { + widgetName: 'TestWidget', + event: 'WIDGET_MOUNTED', + timestamp: 1234567890, + }; + + logMetrics(metric); + + expect(consoleSpy).toHaveBeenCalledWith('CC-Widgets: UI Metrics: No logger found'); + consoleSpy.mockRestore(); + }); + }); + + describe('havePropsChanged', () => { + it('should return false for identical primitives', () => { + expect(havePropsChanged(1, 1)).toBe(false); + expect(havePropsChanged('test', 'test')).toBe(false); + expect(havePropsChanged(true, true)).toBe(false); + }); + + it('should return true for different primitives', () => { + expect(havePropsChanged(1, 2)).toBe(true); + expect(havePropsChanged('test', 'test2')).toBe(true); + expect(havePropsChanged(true, false)).toBe(true); + }); + + it('should return true for different types', () => { + expect(havePropsChanged(1, '1')).toBe(true); + expect(havePropsChanged({}, [])).toBe(true); + expect(havePropsChanged(null, undefined)).toBe(true); + }); + + it('should return true when object keys differ', () => { + const obj1 = {a: 1, b: 2}; + const obj2 = {a: 1}; + expect(havePropsChanged(obj1, obj2)).toBe(true); + }); + + it('should return true when nested values differ', () => { + const obj1 = {a: {b: 1}}; + const obj2 = {a: {b: 2}}; + expect(havePropsChanged(obj1, obj2)).toBe(true); + }); + + it('should return false for identical objects', () => { + const obj1 = {a: 1, b: {c: 2}}; + const obj2 = {a: 1, b: {c: 2}}; + expect(havePropsChanged(obj1, obj2)).toBe(false); + }); + + it('should handle null and undefined', () => { + expect(havePropsChanged(null, null)).toBe(false); + expect(havePropsChanged(undefined, undefined)).toBe(false); + expect(havePropsChanged(null, undefined)).toBe(true); + }); + + it('should handle circular references gracefully', () => { + const obj1: any = {a: 1}; + const obj2: any = {a: 1}; + obj1.self = obj1; + obj2.self = obj2; + + expect(havePropsChanged(obj1, obj2)).toBe(true); + }); + }); +}); diff --git a/packages/contact-center/ui-metrics/tests/withMetrics.test.tsx b/packages/contact-center/ui-metrics/tests/withMetrics.test.tsx new file mode 100644 index 000000000..11c5cdaf3 --- /dev/null +++ b/packages/contact-center/ui-metrics/tests/withMetrics.test.tsx @@ -0,0 +1,137 @@ +import React from 'react'; +import {render} from '@testing-library/react'; +import '@testing-library/jest-dom'; +import withMetrics from '../src/withMetrics'; +import * as metricsLogger from '../src/metricsLogger'; + +interface TestComponentProps { + name?: string; + [key: string]: any; +} + +describe('withMetrics HOC', () => { + const logMetricsSpy = jest.spyOn(metricsLogger, 'logMetrics'); + + const TestComponent: React.FC = (props) =>

Test Component {props.name}
; + const WrappedComponent = withMetrics(TestComponent, 'TestWidget'); + + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should log metrics on mount', () => { + const mockTime = 1234567890; + jest.setSystemTime(mockTime); + + render(); + + expect(logMetricsSpy).toHaveBeenCalledWith({ + widgetName: 'TestWidget', + event: 'WIDGET_MOUNTED', + timestamp: mockTime, + }); + }); + + it('should log metrics on unmount', () => { + const mockTime = 1234567890; + jest.setSystemTime(mockTime); + + const {unmount} = render(); + + // Clear the mount log + logMetricsSpy.mockClear(); + + // Unmount the component + unmount(); + + expect(logMetricsSpy).toHaveBeenCalledWith({ + widgetName: 'TestWidget', + event: 'WIDGET_UNMOUNTED', + timestamp: mockTime, + }); + }); + + it('should pass through props to wrapped component', () => { + const {getByText} = render(); + expect(getByText('Test Component test-name')).toBeInTheDocument(); + }); + + it('should not re-render when props have not changed', () => { + const renderSpy = jest.fn(); + const SpyComponent: React.FC = (props) => { + renderSpy(); + return
Test Component {props.name}
; + }; + + const WrappedSpy = withMetrics(SpyComponent, 'TestWidget'); + + const {rerender} = render(); + expect(renderSpy).toHaveBeenCalledTimes(1); + + // Re-render with same props + rerender(); + expect(renderSpy).toHaveBeenCalledTimes(1); + }); + + it('should re-render when props have changed', () => { + const renderSpy = jest.fn(); + const SpyComponent: React.FC = (props) => { + renderSpy(); + return
Test Component {props.name}
; + }; + + const WrappedSpy = withMetrics(SpyComponent, 'TestWidget'); + + const {rerender} = render(); + expect(renderSpy).toHaveBeenCalledTimes(1); + + // Re-render with different props + rerender(); + expect(renderSpy).toHaveBeenCalledTimes(2); + }); + + it('should handle complex prop changes', () => { + const renderSpy = jest.fn(); + const SpyComponent: React.FC = (props) => { + renderSpy(); + return
Test Component
; + }; + + const WrappedSpy = withMetrics(SpyComponent, 'TestWidget'); + + const initialProps = { + obj: {a: 1, b: 2}, + arr: [1, 2, 3], + }; + + const {rerender} = render(); + expect(renderSpy).toHaveBeenCalledTimes(1); + + // Re-render with same structure but different reference + rerender( + + ); + expect(renderSpy).toHaveBeenCalledTimes(1); + + // Re-render with different values + rerender( + + ); + expect(renderSpy).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/contact-center/ui-metrics/webpack.config.js b/packages/contact-center/ui-metrics/webpack.config.js index 247cf2783..fa737ac93 100644 --- a/packages/contact-center/ui-metrics/webpack.config.js +++ b/packages/contact-center/ui-metrics/webpack.config.js @@ -9,4 +9,9 @@ module.exports = merge(baseConfig, { filename: 'index.js', // Set the output filename to index.js libraryTarget: 'commonjs2', }, + externals: { + react: 'react', + 'react-dom': 'react-dom', + '@webex/cc-store': '@webex/cc-store', + }, }); From a5bfab93da590c576c6b76940078294a0373fa9d Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Tue, 22 Jul 2025 14:43:05 +0530 Subject: [PATCH 05/12] fix(ui-metrics): merged latest and typed properly --- packages/contact-center/ui-metrics/package.json | 1 + .../ui-metrics/tests/metricsLogger.test.ts | 16 ++++++++-------- .../ui-metrics/tests/withMetrics.test.tsx | 8 ++++++++ .../contact-center/ui-metrics/tsconfig.test.json | 9 +++++++++ yarn.lock | 1 + 5 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 packages/contact-center/ui-metrics/tsconfig.test.json diff --git a/packages/contact-center/ui-metrics/package.json b/packages/contact-center/ui-metrics/package.json index 883c411e7..b98dfb4bf 100644 --- a/packages/contact-center/ui-metrics/package.json +++ b/packages/contact-center/ui-metrics/package.json @@ -20,6 +20,7 @@ "@testing-library/react": "16.0.1", "@types/jest": "29.5.14", "@types/react-test-renderer": "18", + "@webex/test-fixtures": "workspace:*", "babel-jest": "29.7.0", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", diff --git a/packages/contact-center/ui-metrics/tests/metricsLogger.test.ts b/packages/contact-center/ui-metrics/tests/metricsLogger.test.ts index 5bfcc28ab..c986cfa7c 100644 --- a/packages/contact-center/ui-metrics/tests/metricsLogger.test.ts +++ b/packages/contact-center/ui-metrics/tests/metricsLogger.test.ts @@ -1,14 +1,14 @@ import store from '@webex/cc-store'; import {logMetrics, havePropsChanged, WidgetMetrics} from '../src/metricsLogger'; -// Mock the store -jest.mock('@webex/cc-store', () => ({ - logger: { - log: jest.fn(), - }, -})); - describe('metricsLogger', () => { + store.store.logger = { + log: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + trace: jest.fn(), + }; beforeEach(() => { jest.clearAllMocks(); }); @@ -33,7 +33,7 @@ describe('metricsLogger', () => { it('should handle case when logger is not available', () => { const consoleSpy = jest.spyOn(console, 'log'); - (store as any).logger = null; + store.store.logger = undefined; const metric: WidgetMetrics = { widgetName: 'TestWidget', diff --git a/packages/contact-center/ui-metrics/tests/withMetrics.test.tsx b/packages/contact-center/ui-metrics/tests/withMetrics.test.tsx index 11c5cdaf3..f01718d31 100644 --- a/packages/contact-center/ui-metrics/tests/withMetrics.test.tsx +++ b/packages/contact-center/ui-metrics/tests/withMetrics.test.tsx @@ -2,6 +2,7 @@ import React from 'react'; import {render} from '@testing-library/react'; import '@testing-library/jest-dom'; import withMetrics from '../src/withMetrics'; +import store from '@webex/cc-store'; import * as metricsLogger from '../src/metricsLogger'; interface TestComponentProps { @@ -10,6 +11,13 @@ interface TestComponentProps { } describe('withMetrics HOC', () => { + store.store.logger = { + log: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + trace: jest.fn(), + }; const logMetricsSpy = jest.spyOn(metricsLogger, 'logMetrics'); const TestComponent: React.FC = (props) =>
Test Component {props.name}
; diff --git a/packages/contact-center/ui-metrics/tsconfig.test.json b/packages/contact-center/ui-metrics/tsconfig.test.json new file mode 100644 index 000000000..40bd84f5a --- /dev/null +++ b/packages/contact-center/ui-metrics/tsconfig.test.json @@ -0,0 +1,9 @@ +// This config is to do type checking in our files while running tests. +{ + "extends": "./tsconfig.json", + "include": ["./tests"], + "exclude": ["node_modules"], + "compilerOptions": { + "noEmit": true // Don't output any files + } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 868292180..5f6a65502 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9575,6 +9575,7 @@ __metadata: "@types/jest": "npm:29.5.14" "@types/react-test-renderer": "npm:18" "@webex/cc-store": "workspace:*" + "@webex/test-fixtures": "workspace:*" babel-jest: "npm:29.7.0" jest: "npm:29.7.0" jest-environment-jsdom: "npm:29.7.0" From 979f3d0d50bd4cf3db42d6c863c06a2ada5e225a Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Mon, 4 Aug 2025 13:38:11 +0530 Subject: [PATCH 06/12] fix(cc-loggin): review-comments --- .../contact-center/cc-components/package.json | 2 +- .../components/StationLogin/station-login.tsx | 2 +- .../src/components/UserState/user-state.tsx | 2 +- .../task/CallControl/call-control.tsx | 2 +- .../task/CallControlCAD/call-control-cad.tsx | 2 +- .../task/IncomingTask/incoming-task.tsx | 2 +- .../task/OutdialCall/outdial-call.tsx | 2 +- .../components/task/TaskList/task-list.tsx | 2 +- .../contact-center/ui-metrics/bable.config.js | 3 - .../contact-center/ui-metrics/package.json | 2 +- .../ui-metrics/src/metricsLogger.ts | 70 ++++++++++++++++--- .../ui-metrics/src/withMetrics.tsx | 2 + .../ui-metrics/tests/metricsLogger.test.ts | 21 +----- .../ui-metrics/tests/withMetrics.test.tsx | 40 ----------- webpack.config.js | 2 + .../cc/samples-cc-react-app/webpack.config.js | 2 +- yarn.lock | 6 +- 17 files changed, 80 insertions(+), 84 deletions(-) delete mode 100644 packages/contact-center/ui-metrics/bable.config.js diff --git a/packages/contact-center/cc-components/package.json b/packages/contact-center/cc-components/package.json index 8df918b72..8adaef1b7 100644 --- a/packages/contact-center/cc-components/package.json +++ b/packages/contact-center/cc-components/package.json @@ -36,7 +36,7 @@ "@momentum-ui/illustrations": "^1.24.0", "@r2wc/react-to-web-component": "2.0.3", "@webex/cc-store": "workspace:*", - "@webex/cc-ui-metrics": "workspace:*" + "@webex/cc-ui-logging": "workspace:*" }, "devDependencies": { "@babel/core": "7.25.2", diff --git a/packages/contact-center/cc-components/src/components/StationLogin/station-login.tsx b/packages/contact-center/cc-components/src/components/StationLogin/station-login.tsx index 47c29ecf7..e75da3564 100644 --- a/packages/contact-center/cc-components/src/components/StationLogin/station-login.tsx +++ b/packages/contact-center/cc-components/src/components/StationLogin/station-login.tsx @@ -17,7 +17,7 @@ import { updateDialNumberLabel, handleCCSignoutKeyDown, } from './station-login.utils'; -import {withMetrics} from '@webex/cc-ui-metrics'; +import {withMetrics} from '@webex/cc-ui-logging'; const StationLoginComponent: React.FunctionComponent = (props) => { const { diff --git a/packages/contact-center/cc-components/src/components/UserState/user-state.tsx b/packages/contact-center/cc-components/src/components/UserState/user-state.tsx index 7f6e20160..84b557116 100644 --- a/packages/contact-center/cc-components/src/components/UserState/user-state.tsx +++ b/packages/contact-center/cc-components/src/components/UserState/user-state.tsx @@ -17,7 +17,7 @@ import { getSelectedKey, buildDropdownItems, } from './user-state.utils'; -import {withMetrics} from '@webex/cc-ui-metrics'; +import {withMetrics} from '@webex/cc-ui-logging'; const UserStateComponent: React.FunctionComponent = (props) => { const { diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx index c404a4660..290ddc49e 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx @@ -25,7 +25,7 @@ import { MUTE_CALL, UNMUTE_CALL, } from '../constants'; -import {withMetrics} from '@webex/cc-ui-metrics'; +import {withMetrics} from '@webex/cc-ui-logging'; function CallControlComponent(props: CallControlComponentProps) { const [selectedWrapupReason, setSelectedWrapupReason] = useState(null); diff --git a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx index 64d4d40fc..db2c6d423 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx @@ -19,7 +19,7 @@ import { CUSTOMER_NAME, RONA, } from '../constants'; -import {withMetrics} from '@webex/cc-ui-metrics'; +import {withMetrics} from '@webex/cc-ui-logging'; const CallControlCADComponent: React.FC = (props) => { const { diff --git a/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.tsx b/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.tsx index 53a11cd66..d893ede65 100644 --- a/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.tsx +++ b/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {IncomingTaskComponentProps, MEDIA_CHANNEL} from '../task.types'; import Task from '../Task'; -import {withMetrics} from '@webex/cc-ui-metrics'; +import {withMetrics} from '@webex/cc-ui-logging'; const IncomingTaskComponent: React.FunctionComponent = (props) => { const {incomingTask, isBrowser, accept, reject, logger} = props; diff --git a/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.tsx b/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.tsx index b40fcb979..284520907 100644 --- a/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.tsx +++ b/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.tsx @@ -1,7 +1,7 @@ import React, {useState} from 'react'; import {OutdialCallComponentProps} from '../task.types'; import './outdial-call.style.scss'; -import {withMetrics} from '@webex/cc-ui-metrics'; +import {withMetrics} from '@webex/cc-ui-logging'; const OutdialCallComponent: React.FunctionComponent = (props) => { const {startOutdial} = props; diff --git a/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx b/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx index 45ab31e1e..d7308bf08 100644 --- a/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx +++ b/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {TaskListComponentProps, MEDIA_CHANNEL} from '../task.types'; import Task from '../Task'; import './styles.scss'; -import {withMetrics} from '@webex/cc-ui-metrics'; +import {withMetrics} from '@webex/cc-ui-logging'; const TaskListComponent: React.FunctionComponent = (props) => { const {currentTask, taskList, acceptTask, declineTask, isBrowser, onTaskSelect, logger} = props; diff --git a/packages/contact-center/ui-metrics/bable.config.js b/packages/contact-center/ui-metrics/bable.config.js deleted file mode 100644 index 0eaef236c..000000000 --- a/packages/contact-center/ui-metrics/bable.config.js +++ /dev/null @@ -1,3 +0,0 @@ -const baseConfig = require('../../../babel.config.js'); - -module.exports = baseConfig; diff --git a/packages/contact-center/ui-metrics/package.json b/packages/contact-center/ui-metrics/package.json index b98dfb4bf..729eb6562 100644 --- a/packages/contact-center/ui-metrics/package.json +++ b/packages/contact-center/ui-metrics/package.json @@ -1,5 +1,5 @@ { - "name": "@webex/cc-ui-metrics", + "name": "@webex/cc-ui-logging", "version": "1.0.0", "description": "UI metrics tracking for Webex widgets", "main": "dist/index.js", diff --git a/packages/contact-center/ui-metrics/src/metricsLogger.ts b/packages/contact-center/ui-metrics/src/metricsLogger.ts index d59a25712..02f2b8d87 100644 --- a/packages/contact-center/ui-metrics/src/metricsLogger.ts +++ b/packages/contact-center/ui-metrics/src/metricsLogger.ts @@ -2,15 +2,40 @@ import store from '@webex/cc-store'; export type WidgetMetrics = { widgetName: string; - event: 'WIDGET_MOUNTED' | 'ERROR' | 'WIDGET_UNMOUNTED'; + event: 'WIDGET_MOUNTED' | 'ERROR' | 'WIDGET_UNMOUNTED' | 'PROPS_UPDATED'; props?: Record; timestamp: number; additionalContext?: Record; }; +/** + * Logs UI metrics for contact center widgets. + * + * This function logs widget lifecycle events and errors to help monitor + * widget performance and user interactions. If no logger is available, + * it will emit a warning and skip logging. + * + * @param metric - The metrics data to be logged + * @param metric.widgetName - Name of the widget generating the metric + * @param metric.event - Type of event being logged + * @param metric.props - Optional properties associated with the widget + * @param metric.timestamp - Unix timestamp when the event occurred + * @param metric.additionalContext - Optional additional context data + * + * @example + * ```typescript + * logMetrics({ + * widgetName: 'CallControl', + * event: 'WIDGET_MOUNTED', + * props: { callId: '123' }, + * timestamp: Date.now(), + * additionalContext: { userId: 'user123' } + * }); + * ``` + */ export const logMetrics = (metric: WidgetMetrics) => { if (!store.logger) { - console.log('CC-Widgets: UI Metrics: No logger found'); + console.warn('CC-Widgets: UI Metrics: No logger found'); return; } store.logger.log(`CC-Widgets: UI Metrics: ${JSON.stringify(metric, null, 2)}`, { @@ -19,10 +44,41 @@ export const logMetrics = (metric: WidgetMetrics) => { }); }; +/** + * Determines if props have changed between two objects using shallow comparison. + * + * This function performs a shallow comparison between two objects to detect changes. + * It compares object keys and primitive values, but does not recursively compare + * nested objects. This is useful for determining when to log metrics based on prop changes. + * + * @param prev - The previous props object + * @param next - The next props object to compare against + * @returns `true` if the props have changed, `false` otherwise + * + * @example + * ```typescript + * const oldProps = { name: 'John', age: 30 }; + * const newProps = { name: 'John', age: 31 }; + * + * if (havePropsChanged(oldProps, newProps)) { + * // Props have changed, log metrics + * logMetrics({ + * widgetName: 'UserProfile', + * event: 'WIDGET_MOUNTED', + * props: newProps, + * timestamp: Date.now() + * }); + * } * ``` + * + * @remarks + * The function is important as we dont sanitize our props right now. + * Once we start sanitizing we can do a deep comparison. This is used to only re-render + * the HOC if the props have changed. + */ export function havePropsChanged(prev: any, next: any): boolean { if (prev === next) return false; - // Do shallow comparison first + // Do shallow comparison if (typeof prev !== typeof next) return true; if (!prev || !next) return prev !== next; @@ -41,10 +97,6 @@ export function havePropsChanged(prev: any, next: any): boolean { if (typeof nextVal !== 'object' || nextVal === null) return true; } - // Fall back to deep comparison only for objects that passed shallow check - try { - return JSON.stringify(prev) !== JSON.stringify(next); - } catch { - return true; // fallback to log if circular structure - } + // All shallow comparisons passed, consider props unchanged + return false; } diff --git a/packages/contact-center/ui-metrics/src/withMetrics.tsx b/packages/contact-center/ui-metrics/src/withMetrics.tsx index 12156e429..77354d3dd 100644 --- a/packages/contact-center/ui-metrics/src/withMetrics.tsx +++ b/packages/contact-center/ui-metrics/src/withMetrics.tsx @@ -20,6 +20,8 @@ export default function withMetrics

(Component: any, widgetName }; }, []); + // TODO: https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6890 PROPS_UPDATED event + return ; }, (prevProps, nextProps) => !havePropsChanged(prevProps, nextProps) diff --git a/packages/contact-center/ui-metrics/tests/metricsLogger.test.ts b/packages/contact-center/ui-metrics/tests/metricsLogger.test.ts index c986cfa7c..f7ecf15e5 100644 --- a/packages/contact-center/ui-metrics/tests/metricsLogger.test.ts +++ b/packages/contact-center/ui-metrics/tests/metricsLogger.test.ts @@ -32,7 +32,7 @@ describe('metricsLogger', () => { }); it('should handle case when logger is not available', () => { - const consoleSpy = jest.spyOn(console, 'log'); + const consoleSpy = jest.spyOn(console, 'warn'); store.store.logger = undefined; const metric: WidgetMetrics = { @@ -56,14 +56,12 @@ describe('metricsLogger', () => { }); it('should return true for different primitives', () => { - expect(havePropsChanged(1, 2)).toBe(true); expect(havePropsChanged('test', 'test2')).toBe(true); expect(havePropsChanged(true, false)).toBe(true); }); it('should return true for different types', () => { expect(havePropsChanged(1, '1')).toBe(true); - expect(havePropsChanged({}, [])).toBe(true); expect(havePropsChanged(null, undefined)).toBe(true); }); @@ -73,15 +71,9 @@ describe('metricsLogger', () => { expect(havePropsChanged(obj1, obj2)).toBe(true); }); - it('should return true when nested values differ', () => { + it('should return false when nested values differ', () => { const obj1 = {a: {b: 1}}; const obj2 = {a: {b: 2}}; - expect(havePropsChanged(obj1, obj2)).toBe(true); - }); - - it('should return false for identical objects', () => { - const obj1 = {a: 1, b: {c: 2}}; - const obj2 = {a: 1, b: {c: 2}}; expect(havePropsChanged(obj1, obj2)).toBe(false); }); @@ -90,14 +82,5 @@ describe('metricsLogger', () => { expect(havePropsChanged(undefined, undefined)).toBe(false); expect(havePropsChanged(null, undefined)).toBe(true); }); - - it('should handle circular references gracefully', () => { - const obj1: any = {a: 1}; - const obj2: any = {a: 1}; - obj1.self = obj1; - obj2.self = obj2; - - expect(havePropsChanged(obj1, obj2)).toBe(true); - }); }); }); diff --git a/packages/contact-center/ui-metrics/tests/withMetrics.test.tsx b/packages/contact-center/ui-metrics/tests/withMetrics.test.tsx index f01718d31..23ec71691 100644 --- a/packages/contact-center/ui-metrics/tests/withMetrics.test.tsx +++ b/packages/contact-center/ui-metrics/tests/withMetrics.test.tsx @@ -102,44 +102,4 @@ describe('withMetrics HOC', () => { rerender(); expect(renderSpy).toHaveBeenCalledTimes(2); }); - - it('should handle complex prop changes', () => { - const renderSpy = jest.fn(); - const SpyComponent: React.FC = (props) => { - renderSpy(); - return

Test Component
; - }; - - const WrappedSpy = withMetrics(SpyComponent, 'TestWidget'); - - const initialProps = { - obj: {a: 1, b: 2}, - arr: [1, 2, 3], - }; - - const {rerender} = render(); - expect(renderSpy).toHaveBeenCalledTimes(1); - - // Re-render with same structure but different reference - rerender( - - ); - expect(renderSpy).toHaveBeenCalledTimes(1); - - // Re-render with different values - rerender( - - ); - expect(renderSpy).toHaveBeenCalledTimes(2); - }); }); diff --git a/webpack.config.js b/webpack.config.js index 9d020e6b3..922c4b2ba 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -47,6 +47,8 @@ module.exports = { ], }, stats: { + // While building and running the sample app when sass-loader is used we get a lot of deprecation warnings + // This is a workaround to suppress them untill we move away from sass-loader warningsFilter: [/sass-loader/], }, }; diff --git a/widgets-samples/cc/samples-cc-react-app/webpack.config.js b/widgets-samples/cc/samples-cc-react-app/webpack.config.js index b0ac80296..9b6a8f850 100644 --- a/widgets-samples/cc/samples-cc-react-app/webpack.config.js +++ b/widgets-samples/cc/samples-cc-react-app/webpack.config.js @@ -17,7 +17,7 @@ module.exports = merge(baseConfig, { '@webex/cc-user-state': path.resolve(__dirname, '../../../packages/contact-center/user-state/src'), '@webex/cc-task': path.resolve(__dirname, '../../../packages/contact-center/task/src'), '@webex/cc-components': path.resolve(__dirname, '../../../packages/contact-center/cc-components/src'), - '@webex/cc-ui-metrics': path.resolve(__dirname, '../../../packages/contact-center/ui-metrics/src'), + '@webex/cc-ui-logging': path.resolve(__dirname, '../../../packages/contact-center/ui-metrics/src'), }, }, module: { diff --git a/yarn.lock b/yarn.lock index 5f6a65502..9e45002a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9402,7 +9402,7 @@ __metadata: "@types/jest": "npm:29.5.14" "@types/react-test-renderer": "npm:18" "@webex/cc-store": "workspace:*" - "@webex/cc-ui-metrics": "workspace:*" + "@webex/cc-ui-logging": "workspace:*" "@webex/test-fixtures": "workspace:*" babel-loader: "npm:9.2.1" eslint: "npm:^9.20.1" @@ -9565,9 +9565,9 @@ __metadata: languageName: unknown linkType: soft -"@webex/cc-ui-metrics@workspace:*, @webex/cc-ui-metrics@workspace:packages/contact-center/ui-metrics": +"@webex/cc-ui-logging@workspace:*, @webex/cc-ui-logging@workspace:packages/contact-center/ui-metrics": version: 0.0.0-use.local - resolution: "@webex/cc-ui-metrics@workspace:packages/contact-center/ui-metrics" + resolution: "@webex/cc-ui-logging@workspace:packages/contact-center/ui-metrics" dependencies: "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:6.6.2" From cab57af9f8e3d9b8ad6599edf89a188dffe65792 Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Fri, 8 Aug 2025 10:33:29 +0530 Subject: [PATCH 07/12] fix(ui-loggin): change folder name to logging --- packages/contact-center/{ui-metrics => ui-logging}/jest.config.js | 0 packages/contact-center/{ui-metrics => ui-logging}/package.json | 0 packages/contact-center/{ui-metrics => ui-logging}/src/index.ts | 0 .../{ui-metrics => ui-logging}/src/metricsLogger.ts | 0 .../contact-center/{ui-metrics => ui-logging}/src/withMetrics.tsx | 0 .../{ui-metrics => ui-logging}/tests/metricsLogger.test.ts | 0 .../{ui-metrics => ui-logging}/tests/withMetrics.test.tsx | 0 packages/contact-center/{ui-metrics => ui-logging}/tsconfig.json | 0 .../contact-center/{ui-metrics => ui-logging}/tsconfig.test.json | 0 .../contact-center/{ui-metrics => ui-logging}/webpack.config.js | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename packages/contact-center/{ui-metrics => ui-logging}/jest.config.js (100%) rename packages/contact-center/{ui-metrics => ui-logging}/package.json (100%) rename packages/contact-center/{ui-metrics => ui-logging}/src/index.ts (100%) rename packages/contact-center/{ui-metrics => ui-logging}/src/metricsLogger.ts (100%) rename packages/contact-center/{ui-metrics => ui-logging}/src/withMetrics.tsx (100%) rename packages/contact-center/{ui-metrics => ui-logging}/tests/metricsLogger.test.ts (100%) rename packages/contact-center/{ui-metrics => ui-logging}/tests/withMetrics.test.tsx (100%) rename packages/contact-center/{ui-metrics => ui-logging}/tsconfig.json (100%) rename packages/contact-center/{ui-metrics => ui-logging}/tsconfig.test.json (100%) rename packages/contact-center/{ui-metrics => ui-logging}/webpack.config.js (100%) diff --git a/packages/contact-center/ui-metrics/jest.config.js b/packages/contact-center/ui-logging/jest.config.js similarity index 100% rename from packages/contact-center/ui-metrics/jest.config.js rename to packages/contact-center/ui-logging/jest.config.js diff --git a/packages/contact-center/ui-metrics/package.json b/packages/contact-center/ui-logging/package.json similarity index 100% rename from packages/contact-center/ui-metrics/package.json rename to packages/contact-center/ui-logging/package.json diff --git a/packages/contact-center/ui-metrics/src/index.ts b/packages/contact-center/ui-logging/src/index.ts similarity index 100% rename from packages/contact-center/ui-metrics/src/index.ts rename to packages/contact-center/ui-logging/src/index.ts diff --git a/packages/contact-center/ui-metrics/src/metricsLogger.ts b/packages/contact-center/ui-logging/src/metricsLogger.ts similarity index 100% rename from packages/contact-center/ui-metrics/src/metricsLogger.ts rename to packages/contact-center/ui-logging/src/metricsLogger.ts diff --git a/packages/contact-center/ui-metrics/src/withMetrics.tsx b/packages/contact-center/ui-logging/src/withMetrics.tsx similarity index 100% rename from packages/contact-center/ui-metrics/src/withMetrics.tsx rename to packages/contact-center/ui-logging/src/withMetrics.tsx diff --git a/packages/contact-center/ui-metrics/tests/metricsLogger.test.ts b/packages/contact-center/ui-logging/tests/metricsLogger.test.ts similarity index 100% rename from packages/contact-center/ui-metrics/tests/metricsLogger.test.ts rename to packages/contact-center/ui-logging/tests/metricsLogger.test.ts diff --git a/packages/contact-center/ui-metrics/tests/withMetrics.test.tsx b/packages/contact-center/ui-logging/tests/withMetrics.test.tsx similarity index 100% rename from packages/contact-center/ui-metrics/tests/withMetrics.test.tsx rename to packages/contact-center/ui-logging/tests/withMetrics.test.tsx diff --git a/packages/contact-center/ui-metrics/tsconfig.json b/packages/contact-center/ui-logging/tsconfig.json similarity index 100% rename from packages/contact-center/ui-metrics/tsconfig.json rename to packages/contact-center/ui-logging/tsconfig.json diff --git a/packages/contact-center/ui-metrics/tsconfig.test.json b/packages/contact-center/ui-logging/tsconfig.test.json similarity index 100% rename from packages/contact-center/ui-metrics/tsconfig.test.json rename to packages/contact-center/ui-logging/tsconfig.test.json diff --git a/packages/contact-center/ui-metrics/webpack.config.js b/packages/contact-center/ui-logging/webpack.config.js similarity index 100% rename from packages/contact-center/ui-metrics/webpack.config.js rename to packages/contact-center/ui-logging/webpack.config.js From 3c85248394f9c5ae726891670e59b903a527ff2a Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Mon, 11 Aug 2025 11:22:00 +0530 Subject: [PATCH 08/12] fix(ui-logging): build issue --- packages/contact-center/ui-logging/jest.config.js | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/contact-center/ui-logging/jest.config.js b/packages/contact-center/ui-logging/jest.config.js index 4f2ecff91..bef75ab06 100644 --- a/packages/contact-center/ui-logging/jest.config.js +++ b/packages/contact-center/ui-logging/jest.config.js @@ -1,6 +1,6 @@ const jestConfig = require('../../../jest.config.js'); jestConfig.rootDir = '../../../'; -jestConfig.testMatch = ['**/ui-metrics/tests/**/*.ts', '**/ui-metrics/tests/**/*.tsx']; +jestConfig.testMatch = ['**/ui-logging/tests/**/*.ts', '**/ui-logging/tests/**/*.tsx']; module.exports = jestConfig; diff --git a/yarn.lock b/yarn.lock index 9e45002a8..280e4455c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9565,9 +9565,9 @@ __metadata: languageName: unknown linkType: soft -"@webex/cc-ui-logging@workspace:*, @webex/cc-ui-logging@workspace:packages/contact-center/ui-metrics": +"@webex/cc-ui-logging@workspace:*, @webex/cc-ui-logging@workspace:packages/contact-center/ui-logging": version: 0.0.0-use.local - resolution: "@webex/cc-ui-logging@workspace:packages/contact-center/ui-metrics" + resolution: "@webex/cc-ui-logging@workspace:packages/contact-center/ui-logging" dependencies: "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:6.6.2" From 40ded9898b7842f92992b001ea305506c776c3c5 Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Mon, 11 Aug 2025 14:43:56 +0530 Subject: [PATCH 09/12] fix(samples): fix samples building issues --- widgets-samples/cc/samples-cc-react-app/webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widgets-samples/cc/samples-cc-react-app/webpack.config.js b/widgets-samples/cc/samples-cc-react-app/webpack.config.js index 9b6a8f850..86fb2c134 100644 --- a/widgets-samples/cc/samples-cc-react-app/webpack.config.js +++ b/widgets-samples/cc/samples-cc-react-app/webpack.config.js @@ -17,7 +17,7 @@ module.exports = merge(baseConfig, { '@webex/cc-user-state': path.resolve(__dirname, '../../../packages/contact-center/user-state/src'), '@webex/cc-task': path.resolve(__dirname, '../../../packages/contact-center/task/src'), '@webex/cc-components': path.resolve(__dirname, '../../../packages/contact-center/cc-components/src'), - '@webex/cc-ui-logging': path.resolve(__dirname, '../../../packages/contact-center/ui-metrics/src'), + '@webex/cc-ui-logging': path.resolve(__dirname, '../../../packages/contact-center/ui-logging/src'), }, }, module: { From 128ec21c51174bae57bec778b084d28dc8ddd7e2 Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Tue, 19 Aug 2025 16:40:46 +0530 Subject: [PATCH 10/12] feat(ccwidgets): add error boundary for all widgets --- jest.config.js | 2 +- .../contact-center/station-login/package.json | 3 +- .../station-login/src/station-login/index.tsx | 17 +++- .../tests/station-login/index.tsx | 16 ++-- .../contact-center/store/src/store.types.ts | 2 + .../store/src/storeEventsWrapper.ts | 19 +++++ packages/contact-center/task/package.json | 3 +- .../task/src/CallControl/index.tsx | 16 +++- .../task/src/CallControlCAD/index.tsx | 16 +++- .../task/src/IncomingTask/index.tsx | 26 +++++- .../task/src/TaskList/index.tsx | 16 +++- .../contact-center/user-state/package.json | 4 +- .../user-state/src/user-state/index.tsx | 16 +++- .../user-state/tsconfig.test.json | 2 +- .../cc/samples-cc-react-app/src/App.tsx | 6 ++ .../cc/samples-cc-react-app/webpack.config.js | 80 ++++++++++++++++--- yarn.lock | 14 ++++ 17 files changed, 228 insertions(+), 30 deletions(-) diff --git a/jest.config.js b/jest.config.js index 6b871d1d7..a7f9e419e 100644 --- a/jest.config.js +++ b/jest.config.js @@ -9,7 +9,7 @@ module.exports = { testEnvironment: 'jsdom', testMatch: ['**/tooling/tests/**/*.js'], transformIgnorePatterns: [ - '/node_modules/(?!(@momentum-design/components|@momentum-ui/react-collaboration|@lit|lit|cheerio))', + '/node_modules/(?!(@momentum-design/components|@momentum-ui/react-collaboration|@lit|lit|cheerio|react-error-boundary))', ], // Use babel-jest or ts-jest depending on your setup transform: { diff --git a/packages/contact-center/station-login/package.json b/packages/contact-center/station-login/package.json index afea8da60..e06ee5b0c 100644 --- a/packages/contact-center/station-login/package.json +++ b/packages/contact-center/station-login/package.json @@ -23,7 +23,8 @@ "dependencies": { "@webex/cc-components": "workspace:*", "@webex/cc-store": "workspace:*", - "mobx-react-lite": "^4.1.0" + "mobx-react-lite": "^4.1.0", + "react-error-boundary": "^6.0.0" }, "devDependencies": { "@babel/core": "7.25.2", 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 04cdbec20..ef4febfa0 100644 --- a/packages/contact-center/station-login/src/station-login/index.tsx +++ b/packages/contact-center/station-login/src/station-login/index.tsx @@ -1,12 +1,13 @@ import React from 'react'; import store from '@webex/cc-store'; import {observer} from 'mobx-react-lite'; +import {ErrorBoundary} from 'react-error-boundary'; import {StationLoginComponent, StationLoginComponentProps} from '@webex/cc-components'; import {useStationLogin} from '../helper'; import {StationLoginProps} from './station-login.types'; -const StationLogin: React.FunctionComponent = observer( +const StationLoginInternal: React.FunctionComponent = observer( ({onLogin, onLogout, onCCSignOut, profileMode, onSaveStart, onSaveEnd, doStationLogout}) => { const { cc, @@ -58,4 +59,18 @@ const StationLogin: React.FunctionComponent = observer( } ); +// Main component wrapped with ErrorBoundary +const StationLogin: React.FunctionComponent = (props) => { + return ( + <>} + onError={(error: Error) => { + if (store.onErrorCallback) store.onErrorCallback('StationLogin', error); + }} + > + + + ); +}; + export {StationLogin}; 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 7c49ba995..1321f803f 100644 --- a/packages/contact-center/station-login/tests/station-login/index.tsx +++ b/packages/contact-center/station-login/tests/station-login/index.tsx @@ -13,7 +13,6 @@ const loginOptionsMock = ['EXTENSION', 'AGENT_DN', 'BROWSER']; const deviceTypeMock = 'BROWSER'; const dialNumberMock = '12345'; const dialNumberRegexMock = '1[0-9]{3}[2-9][0-9]{6}([,]{1,10}[0-9]+){0,1}'; -const loggerMock = {}; const isAgentLoggedInMock = false; // Mock the store import @@ -28,7 +27,12 @@ jest.mock('@webex/cc-store', () => { deviceType: deviceTypeMock, dialNumber: dialNumberMock, dialNumberRegex: dialNumberRegexMock, - logger: loggerMock, + logger: { + log: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + }, isAgentLoggedIn: isAgentLoggedInMock, setCCCallback: jest.fn(), setLogoutCallback: jest.fn(), @@ -39,12 +43,6 @@ jest.mock('@webex/cc-store', () => { }; }); -jest.mock('@webex/cc-components', () => { - return { - StationLoginComponent: () =>
StationLoginComponent
, - }; -}); - const loginCb = jest.fn(); const logoutCb = jest.fn(); const ccLogoutCb = jest.fn(); @@ -71,7 +69,7 @@ describe('StationLogin Component', () => { cc: ccMock, onLogin: loginCb, onLogout: logoutCb, - logger: loggerMock, + logger: expect.any(Object), deviceType: deviceTypeMock, dialNumber: dialNumberMock, isAgentLoggedIn: false, diff --git a/packages/contact-center/store/src/store.types.ts b/packages/contact-center/store/src/store.types.ts index bca5792e3..d4a9ae834 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -116,6 +116,7 @@ interface IStore { interface IStoreWrapper extends IStore { store: IStore; + onErrorCallback?: (widgetName: string, error: Error) => void; setCurrentTask(task: ITask): void; refreshTaskList(): void; setDeviceType(option: string): void; @@ -135,6 +136,7 @@ interface IStoreWrapper extends IStore { setAgentProfile(profile: Profile): void; setTeamId(id: string): void; setIsMuted(value: boolean): void; + setOnError(callback: (widgetName: string, error: Error) => void): void; } interface IWrapupCode { diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index 4522c303f..19d3a03f8 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -28,6 +28,7 @@ class StoreWrapper implements IStoreWrapper { onTaskRejected?: (task: ITask, reason: string) => void; onTaskAssigned?: (task: ITask) => void; onTaskSelected?: (task: ITask, isClicked: boolean) => void; + onErrorCallback?: (widgetName: string, error: Error) => void; constructor() { this.store = Store.getInstance(); @@ -223,6 +224,24 @@ class StoreWrapper implements IStoreWrapper { }); }; + setOnError = (callback: (widgetName: string, error: Error) => void) => { + this.onErrorCallback = (widgetName: string, error: Error) => { + // @ts-expect-error - test error boundary + this.store.cc.webex.internal.newMetrics.submitBehavioralEvent({ + product: 'wxcc-widgets', + agent: 'browser', + target: 'browser', + verb: 'error', + payload: { + widgets: widgetName, + name: error.name, + message: error.message, + }, + }); + callback(widgetName, error); + }; + }; + refreshTaskList = (): void => { runInAction(() => { this.store.taskList = this.store.cc.taskManager.getAllTasks(); diff --git a/packages/contact-center/task/package.json b/packages/contact-center/task/package.json index bc02550bc..bcd08aaf8 100644 --- a/packages/contact-center/task/package.json +++ b/packages/contact-center/task/package.json @@ -23,7 +23,8 @@ "dependencies": { "@webex/cc-components": "workspace:*", "@webex/cc-store": "workspace:*", - "mobx-react-lite": "^4.1.0" + "mobx-react-lite": "^4.1.0", + "react-error-boundary": "^6.0.0" }, "devDependencies": { "@babel/core": "7.25.2", diff --git a/packages/contact-center/task/src/CallControl/index.tsx b/packages/contact-center/task/src/CallControl/index.tsx index 037dfdef0..03c8ddfa5 100644 --- a/packages/contact-center/task/src/CallControl/index.tsx +++ b/packages/contact-center/task/src/CallControl/index.tsx @@ -1,12 +1,13 @@ import React from 'react'; import {observer} from 'mobx-react-lite'; +import {ErrorBoundary} from 'react-error-boundary'; import store from '@webex/cc-store'; import {useCallControl} from '../helper'; import {CallControlProps} from '../task.types'; import {CallControlComponent} from '@webex/cc-components'; -const CallControl: React.FunctionComponent = observer( +const CallControlInternal: React.FunctionComponent = observer( ({onHoldResume, onEnd, onWrapUp, onRecordingToggle, onToggleMute}) => { const { logger, @@ -53,4 +54,17 @@ const CallControl: React.FunctionComponent = observer( } ); +const CallControl: React.FunctionComponent = (props) => { + return ( + <>} + onError={(error: Error) => { + if (store.onErrorCallback) store.onErrorCallback('CallControl', error); + }} + > + + + ); +}; + export {CallControl}; diff --git a/packages/contact-center/task/src/CallControlCAD/index.tsx b/packages/contact-center/task/src/CallControlCAD/index.tsx index 25fdf0137..d4ea6b8bb 100644 --- a/packages/contact-center/task/src/CallControlCAD/index.tsx +++ b/packages/contact-center/task/src/CallControlCAD/index.tsx @@ -1,12 +1,13 @@ import React from 'react'; import {observer} from 'mobx-react-lite'; +import {ErrorBoundary} from 'react-error-boundary'; import store from '@webex/cc-store'; import {useCallControl} from '../helper'; import {CallControlProps} from '../task.types'; import {CallControlCADComponent} from '@webex/cc-components'; -const CallControlCAD: React.FunctionComponent = observer( +const CallControlCADInternal: React.FunctionComponent = observer( ({ onHoldResume, onEnd, @@ -62,4 +63,17 @@ const CallControlCAD: React.FunctionComponent = observer( } ); +const CallControlCAD: React.FunctionComponent = (props) => { + return ( + <>} + onError={(error: Error) => { + if (store.onErrorCallback) store.onErrorCallback('CallControlCAD', error); + }} + > + + + ); +}; + export {CallControlCAD}; diff --git a/packages/contact-center/task/src/IncomingTask/index.tsx b/packages/contact-center/task/src/IncomingTask/index.tsx index 6dfe5d4db..de845834a 100644 --- a/packages/contact-center/task/src/IncomingTask/index.tsx +++ b/packages/contact-center/task/src/IncomingTask/index.tsx @@ -1,11 +1,26 @@ import React from 'react'; import {observer} from 'mobx-react-lite'; +import {ErrorBoundary} from 'react-error-boundary'; import store from '@webex/cc-store'; import {useIncomingTask} from '../helper'; import {IncomingTaskComponent} from '@webex/cc-components'; import {IncomingTaskProps} from '../task.types'; +const IncomingTaskInternal: React.FunctionComponent = observer( + ({incomingTask, onAccepted, onRejected}) => { + const {deviceType, logger} = store; + const result = useIncomingTask({incomingTask, onAccepted, onRejected, deviceType, logger}); + + const props = { + ...result, + logger, + }; + + return ; + } +); + const IncomingTask: React.FunctionComponent = observer(({incomingTask, onAccepted, onRejected}) => { const {deviceType, logger} = store; const result = useIncomingTask({incomingTask, onAccepted, onRejected, deviceType, logger}); @@ -15,7 +30,16 @@ const IncomingTask: React.FunctionComponent = observer(({inco logger, }; - return ; + return ( + <>} + onError={(error: Error) => { + if (store.onErrorCallback) store.onErrorCallback('IncomingTask', error); + }} + > + + + ); }); export {IncomingTask}; diff --git a/packages/contact-center/task/src/TaskList/index.tsx b/packages/contact-center/task/src/TaskList/index.tsx index 6d3ed878d..999c317ac 100644 --- a/packages/contact-center/task/src/TaskList/index.tsx +++ b/packages/contact-center/task/src/TaskList/index.tsx @@ -1,12 +1,13 @@ import React from 'react'; import store from '@webex/cc-store'; import {observer} from 'mobx-react-lite'; +import {ErrorBoundary} from 'react-error-boundary'; import {TaskListComponent} from '@webex/cc-components'; import {useTaskList} from '../helper'; import {TaskListProps} from '../task.types'; -const TaskList: React.FunctionComponent = observer( +const TaskListInternal: React.FunctionComponent = observer( ({onTaskAccepted, onTaskDeclined, onTaskSelected}) => { const {cc, taskList, currentTask, deviceType, logger} = store; @@ -21,4 +22,17 @@ const TaskList: React.FunctionComponent = observer( } ); +const TaskList: React.FunctionComponent = (props) => { + return ( + <>} + onError={(error: Error) => { + if (store.onErrorCallback) store.onErrorCallback('TaskList', error); + }} + > + + + ); +}; + export {TaskList}; diff --git a/packages/contact-center/user-state/package.json b/packages/contact-center/user-state/package.json index 3f1bc7a30..7712c45b0 100644 --- a/packages/contact-center/user-state/package.json +++ b/packages/contact-center/user-state/package.json @@ -24,7 +24,8 @@ "@webex/cc-components": "workspace:*", "@webex/cc-store": "workspace:*", "mobx-react-lite": "^4.1.0", - "typescript": "5.6.3" + "typescript": "5.6.3", + "react-error-boundary": "^6.0.0" }, "devDependencies": { "@babel/core": "7.25.2", @@ -37,6 +38,7 @@ "@testing-library/react": "16.0.1", "@types/jest": "29.5.14", "@types/react-test-renderer": "18", + "@webex/test-fixtures": "workspace:*", "babel-jest": "29.7.0", "babel-loader": "9.2.1", "eslint": "^9.20.1", 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 5348e89c9..aabdb26ef 100644 --- a/packages/contact-center/user-state/src/user-state/index.tsx +++ b/packages/contact-center/user-state/src/user-state/index.tsx @@ -1,12 +1,13 @@ import React from 'react'; import store from '@webex/cc-store'; import {observer} from 'mobx-react-lite'; +import {ErrorBoundary} from 'react-error-boundary'; import {useUserState} from '../helper'; import {UserStateComponent, UserStateComponentsProps} from '@webex/cc-components'; import {IUserStateProps} from '../user-state.types'; -const UserState: React.FunctionComponent = observer(({onStateChange}) => { +const UserStateInternal: React.FunctionComponent = observer(({onStateChange}) => { const { cc, idleCodes, @@ -36,4 +37,17 @@ const UserState: React.FunctionComponent = observer(({onStateCh return ; }); +const UserState: React.FunctionComponent = (props) => { + return ( + <>} + onError={(error: Error) => { + if (store.onErrorCallback) store.onErrorCallback('UserState', error); + }} + > + + + ); +}; + export {UserState}; diff --git a/packages/contact-center/user-state/tsconfig.test.json b/packages/contact-center/user-state/tsconfig.test.json index 40bd84f5a..1b8766912 100644 --- a/packages/contact-center/user-state/tsconfig.test.json +++ b/packages/contact-center/user-state/tsconfig.test.json @@ -2,7 +2,7 @@ { "extends": "./tsconfig.json", "include": ["./tests"], - "exclude": ["node_modules"], + "exclude": ["**/node_modules/**"], "compilerOptions": { "noEmit": true // Don't output any files } 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 245a23cf1..6185792ee 100644 --- a/widgets-samples/cc/samples-cc-react-app/src/App.tsx +++ b/widgets-samples/cc/samples-cc-react-app/src/App.tsx @@ -349,13 +349,19 @@ const onToggleMute = ({isMuted, task}) => { useEffect(() => { store.setIncomingTaskCb(onIncomingTaskCB); + store.setOnError(onError); return () => { + store.setOnError(undefined); store.setTaskRejected(undefined); store.setIncomingTaskCb(undefined); }; }, []); + const onError = (widgetName: string, error: Error) => { + console.log('Error in widgets:', widgetName, error); + }; + const onStateChange = (status) => { console.log('onStateChange invoked', status); //adding a log to be used for automation diff --git a/widgets-samples/cc/samples-cc-react-app/webpack.config.js b/widgets-samples/cc/samples-cc-react-app/webpack.config.js index 86fb2c134..768708b69 100644 --- a/widgets-samples/cc/samples-cc-react-app/webpack.config.js +++ b/widgets-samples/cc/samples-cc-react-app/webpack.config.js @@ -1,15 +1,44 @@ const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); -const {merge} = require('webpack-merge'); - -const baseConfig = require('../../../webpack.config'); - +const {HotModuleReplacementPlugin, ProvidePlugin} = require('webpack'); // Helper function to resolve paths relative to the monorepo root const resolveMonorepoRoot = (...segments) => path.resolve(__dirname, '../../../', ...segments); -module.exports = merge(baseConfig, { +// Workspaces we want to alias/include +const PKG_SRC = [ + 'packages/contact-center/store/src', + 'packages/contact-center/cc-widgets/src', + 'packages/contact-center/station-login/src', + 'packages/contact-center/user-state/src', + 'packages/contact-center/task/src', + 'packages/contact-center/cc-components/src', + 'packages/contact-center/ui-logging/src', +].map((p) => resolveMonorepoRoot(p)); + +module.exports = { + mode: process.env.NODE_ENV || 'development', + entry: './src/index.tsx', // Entry file for bundling + + output: { + path: path.resolve(__dirname, '../../../docs/samples-cc-react-app'), + filename: 'bundle.js', + clean: true, + }, + resolve: { + fallback: { + fs: false, + process: require.resolve('process/browser'), + crypto: require.resolve('crypto-browserify'), + querystring: require.resolve('querystring-es3'), + os: require.resolve('os-browserify/browser'), + stream: require.resolve('stream-browserify'), + vm: require.resolve('vm-browserify'), + util: require.resolve('util/'), + url: require.resolve('url/'), + }, + extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'], alias: { '@webex/cc-store': path.resolve(__dirname, '../../../packages/contact-center/store/src'), '@webex/cc-widgets': path.resolve(__dirname, '../../../packages/contact-center/cc-widgets/src'), @@ -19,9 +48,19 @@ module.exports = merge(baseConfig, { '@webex/cc-components': path.resolve(__dirname, '../../../packages/contact-center/cc-components/src'), '@webex/cc-ui-logging': path.resolve(__dirname, '../../../packages/contact-center/ui-logging/src'), }, + symlinks: true, }, module: { rules: [ + // TS/TSX → transpile only (skip type-checking) + { + test: /\.[jt]sx?$/, + include: [path.resolve(__dirname, 'src'), ...PKG_SRC], + loader: 'ts-loader', + options: { + transpileOnly: true, // ✅ disables type-checking + }, + }, { test: /\.css$/, use: ['style-loader', 'css-loader'], @@ -29,20 +68,23 @@ module.exports = merge(baseConfig, { resolveMonorepoRoot('node_modules/@momentum-ui'), // Include specific node module, resolveMonorepoRoot('node_modules/react-toastify'), // Include specific node module resolveMonorepoRoot('node_modules/@momentum-design'), - path.resolve(__dirname, 'widgets-samples/cc'), // Include all CSS from the local package + path.resolve(__dirname, 'src'), + ...PKG_SRC, ], }, { test: /\.scss$/, use: [ - 'style-loader', // Injects styles into DOM - 'css-loader', // Turns CSS into CommonJS - 'sass-loader', // Compiles Sass to CSS + 'style-loader', + {loader: 'css-loader', options: {importLoaders: 1}}, + {loader: 'sass-loader', options: {implementation: require('sass')}}, ], include: [ resolveMonorepoRoot('node_modules/@momentum-ui'), // Include specific node module path.resolve(__dirname, 'widgets-samples/cc'), // Include all CSS from the local package resolveMonorepoRoot('node_modules/@momentum-design'), + path.resolve(__dirname, 'src'), // your app + ...PKG_SRC, // all workspace packages ], }, { @@ -80,7 +122,12 @@ module.exports = merge(baseConfig, { template: './public/index.html', // Template HTML file filename: 'index.html', }), + new ProvidePlugin({ + process: 'process/browser', + }), + new HotModuleReplacementPlugin(), ], + devServer: { static: path.join(__dirname, 'public'), // Serve files from public folder compress: true, // Enable gzip compression @@ -88,5 +135,18 @@ module.exports = merge(baseConfig, { hot: true, // Enable hot module replacement open: false, // Open the app in browser on start liveReload: true, // Reload page on changes + watchFiles: { + paths: [path.resolve(__dirname, 'src'), ...PKG_SRC], + }, + }, + + watchOptions: { + followSymlinks: true, + ignored: /node_modules\/(?!@webex)/, + }, + stats: { + // While building and running the sample app when sass-loader is used we get a lot of deprecation warnings + // This is a workaround to suppress them untill we move away from sass-loader + warningsFilter: [/sass-loader/], }, -}); +}; diff --git a/yarn.lock b/yarn.lock index 059bf4302..c245584e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9472,6 +9472,7 @@ __metadata: jest-environment-jsdom: "npm:29.7.0" mobx-react-lite: "npm:^4.1.0" prettier: "npm:^3.5.1" + react-error-boundary: "npm:^6.0.0" ts-loader: "npm:9.5.1" typescript: "npm:5.6.3" typescript-eslint: "npm:^8.24.1" @@ -9559,6 +9560,7 @@ __metadata: jest-environment-jsdom: "npm:29.7.0" mobx-react-lite: "npm:^4.1.0" prettier: "npm:^3.5.1" + react-error-boundary: "npm:^6.0.0" ts-loader: "npm:9.5.1" typescript: "npm:5.6.3" typescript-eslint: "npm:^8.24.1" @@ -9628,6 +9630,7 @@ __metadata: jest-environment-jsdom: "npm:29.7.0" mobx-react-lite: "npm:^4.1.0" prettier: "npm:^3.5.1" + react-error-boundary: "npm:^6.0.0" ts-loader: "npm:9.5.1" typescript: "npm:5.6.3" typescript-eslint: "npm:^8.24.1" @@ -30010,6 +30013,17 @@ __metadata: languageName: node linkType: hard +"react-error-boundary@npm:^6.0.0": + version: 6.0.0 + resolution: "react-error-boundary@npm:6.0.0" + dependencies: + "@babel/runtime": "npm:^7.12.5" + peerDependencies: + react: ">=16.13.1" + checksum: 10c0/1914d600dee95a14f14af4afe9867b0d35c26c4f7826d23208800ba2a99728659029aad60a6ef95e13430b4d79c2c4c9b3585f50bf508450478760d2e4e732d8 + languageName: node + linkType: hard + "react-focus-lock@npm:2.11.3": version: 2.11.3 resolution: "react-focus-lock@npm:2.11.3" From b20e5a193b0fe230594435496601f81eb51cf5c0 Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Wed, 20 Aug 2025 11:50:26 +0530 Subject: [PATCH 11/12] fix(ccwidgets): wrap all widget logic in try catch --- .../components/StationLogin/station-login.tsx | 24 +- .../StationLogin/station-login.utils.tsx | 360 ++++++---- .../src/components/UserState/user-state.tsx | 24 +- .../components/UserState/user-state.utils.ts | 307 ++++++--- .../task/AutoWrapupTimer/AutoWrapupTimer.tsx | 3 +- .../AutoWrapupTimer/AutoWrapupTimer.utils.ts | 103 ++- .../call-control-custom.utils.ts | 518 +++++++++++---- .../task/CallControl/call-control.tsx | 28 +- .../task/CallControl/call-control.utils.ts | 295 ++++++--- .../task/IncomingTask/incoming-task.tsx | 4 +- .../task/IncomingTask/incoming-task.utils.tsx | 104 +-- .../src/components/task/Task/task.utils.ts | 269 +++++--- .../task/TaskList/task-list.utils.ts | 227 ++++--- .../src/components/task/task.types.ts | 3 +- .../station-login.snapshot.tsx.snap | 28 +- .../components/StationLogin/station-login.tsx | 118 +++- .../StationLogin/station-login.utils.tsx | 205 +++++- .../tests/components/UserState/user-state.tsx | 50 +- .../components/UserState/user-state.utils.tsx | 240 ++++++- .../task/AutoWrapupTimer/AutoWrapupTimer.tsx | 14 +- .../task/CallControl/call-control.utils.tsx | 4 +- .../IncomingTask/incoming-task.snapshot.tsx | 8 + .../task/IncomingTask/incoming-task.tsx | 16 +- .../station-login/src/helper.ts | 274 +++++--- .../station-login/tests/helper.ts | 192 ++++++ .../tests/station-login/index.tsx | 29 + .../task/src/IncomingTask/index.tsx | 12 +- .../task/src/OutdialCall/index.tsx | 16 +- .../task/src/Utils/task-util.ts | 116 ++-- packages/contact-center/task/src/helper.ts | 613 ++++++++++++------ .../task/tests/CallControl/index.tsx | 35 + .../task/tests/IncomingTask/index.tsx | 26 + .../task/tests/OutdialCall/index.tsx | 40 ++ .../task/tests/TaskList/index.tsx | 32 +- packages/contact-center/task/tests/helper.ts | 402 +++++++++++- .../task/tests/utils/task-util.ts | 76 +++ .../contact-center/user-state/package.json | 4 +- .../contact-center/user-state/src/helper.ts | 269 +++++--- .../contact-center/user-state/tests/helper.ts | 338 +++++++++- .../user-state/tests/user-state/index.tsx | 34 + yarn.lock | 1 + 41 files changed, 4181 insertions(+), 1280 deletions(-) diff --git a/packages/contact-center/cc-components/src/components/StationLogin/station-login.tsx b/packages/contact-center/cc-components/src/components/StationLogin/station-login.tsx index e75da3564..5d61f896f 100644 --- a/packages/contact-center/cc-components/src/components/StationLogin/station-login.tsx +++ b/packages/contact-center/cc-components/src/components/StationLogin/station-login.tsx @@ -57,12 +57,11 @@ const StationLoginComponent: React.FunctionComponent const [showCCSignOutModal, setShowCCSignOutModal] = useState(false); const [showDNError, setShowDNError] = useState(false); const [dnErrorText, setDNErrorText] = useState(''); - const {multiSignInModalRef, ccSignOutModalRef, saveConfirmDialogRef} = createStationLoginRefs(); + const {multiSignInModalRef, ccSignOutModalRef, saveConfirmDialogRef} = createStationLoginRefs(logger); useEffect(() => { if (deviceType !== DESKTOP && Object.keys(LoginOptions).includes(deviceType)) { - setDialNumberLabel(LoginOptions[deviceType]); - setDialNumberPlaceholder(LoginOptions[deviceType]); + updateDialNumberLabel(deviceType, setDialNumberLabel, setDialNumberPlaceholder, logger); } }, [selectedDeviceType, isAgentLoggedIn]); @@ -74,7 +73,8 @@ const StationLoginComponent: React.FunctionComponent saveConfirmDialogRef, showMultipleLoginAlert, showCCSignOutModal, - showSaveConfirmDialog + showSaveConfirmDialog, + logger ); }, [showMultipleLoginAlert, showCCSignOutModal, showSaveConfirmDialog]); @@ -92,7 +92,7 @@ const StationLoginComponent: React.FunctionComponent @@ -145,7 +145,7 @@ const StationLoginComponent: React.FunctionComponent type="button" role="button" aria-label="Close" - onClick={() => saveConfirmCancelClicked(saveConfirmDialogRef, setShowSaveConfirmDialog)} + onClick={() => saveConfirmCancelClicked(saveConfirmDialogRef, setShowSaveConfirmDialog, logger)} className="cancelSaveLoginOptions" > @@ -154,13 +154,15 @@ const StationLoginComponent: React.FunctionComponent
-
diff --git a/packages/contact-center/cc-components/src/components/StationLogin/station-login.utils.tsx b/packages/contact-center/cc-components/src/components/StationLogin/station-login.utils.tsx index 58d740f24..80b0ecd3c 100644 --- a/packages/contact-center/cc-components/src/components/StationLogin/station-login.utils.tsx +++ b/packages/contact-center/cc-components/src/components/StationLogin/station-login.utils.tsx @@ -8,23 +8,32 @@ const handleModals = ( saveConfirmDialogRef, showMultipleLoginAlert, showCCSignOutModal, - showSaveConfirmDialog + showSaveConfirmDialog, + logger ) => { - const modalStates = [ - {ref: modalRef, show: showMultipleLoginAlert}, - {ref: ccSignOutModalRef, show: showCCSignOutModal}, - {ref: saveConfirmDialogRef, show: showSaveConfirmDialog}, - ]; - - modalStates.forEach(({ref, show}) => { - if (ref?.current) { - if (show && !ref.current.open) { - ref.current.showModal(); - } else if (!show && ref.current.open) { - ref.current.close(); + try { + const modalStates = [ + {ref: modalRef, show: showMultipleLoginAlert}, + {ref: ccSignOutModalRef, show: showCCSignOutModal}, + {ref: saveConfirmDialogRef, show: showSaveConfirmDialog}, + ]; + + modalStates.forEach(({ref, show}) => { + if (ref?.current) { + if (show && !ref.current.open) { + ref.current.showModal(); + } else if (!show && ref.current.open) { + ref.current.close(); + } } - } - }); + }); + } catch (error) { + logger?.error('CC-Widgets: StationLogin: Error in handleModals', { + module: 'cc-components#station-login.utils.tsx', + method: 'handleModals', + error: error.message, + }); + } }; /** * Handler for the Contact Center modal continue button @@ -35,11 +44,19 @@ const handleModals = ( * Closes the dialog if it is currently open and calls the provided callback function */ -const continueClicked = (modalRef, callback, setShowCCSignOutModal) => { - setShowCCSignOutModal(false); - if (modalRef.current) { - modalRef.current.close(); - callback(); +const continueClicked = (modalRef, callback, setShowCCSignOutModal, logger) => { + try { + setShowCCSignOutModal(false); + if (modalRef.current) { + modalRef.current.close(); + callback(); + } + } catch (error) { + logger?.error('CC-Widgets: StationLogin: Error in continueClicked', { + module: 'cc-components#station-login.utils.tsx', + method: 'continueClicked', + error: error.message, + }); } }; @@ -50,22 +67,40 @@ const continueClicked = (modalRef, callback, setShowCCSignOutModal) => { */ const ccCancelButtonClicked = ( ccSignOutModalRef: React.RefObject, - setShowCCSignOutModal: (show: boolean) => void + setShowCCSignOutModal: (show: boolean) => void, + logger ) => { - if (ccSignOutModalRef?.current?.open) { - ccSignOutModalRef.current.close(); - setShowCCSignOutModal(false); + try { + if (ccSignOutModalRef?.current?.open) { + ccSignOutModalRef.current.close(); + setShowCCSignOutModal(false); + } + } catch (error) { + logger?.error('CC-Widgets: StationLogin: Error in ccCancelButtonClicked', { + module: 'cc-components#station-login.utils.tsx', + method: 'ccCancelButtonClicked', + error: error.message, + }); } }; const updateDialNumberLabel = ( selectedOption: string, setDialNumberLabel: (label: string) => void, - setDialNumberPlaceholder: (placeholder: string) => void + setDialNumberPlaceholder: (placeholder: string) => void, + logger ): void => { - if (selectedOption !== DESKTOP && Object.keys(LoginOptions).includes(selectedOption)) { - setDialNumberLabel(LoginOptions[selectedOption]); - setDialNumberPlaceholder(LoginOptions[selectedOption]); + try { + if (selectedOption !== DESKTOP && Object.keys(LoginOptions).includes(selectedOption)) { + setDialNumberLabel(LoginOptions[selectedOption]); + setDialNumberPlaceholder(LoginOptions[selectedOption]); + } + } catch (error) { + logger?.error('CC-Widgets: StationLogin: Error in updateDialNumberLabel', { + module: 'cc-components#station-login.utils.tsx', + method: 'updateDialNumberLabel', + error: error.message, + }); } }; @@ -77,34 +112,74 @@ const updateDialNumberLabel = ( const validateDialNumber = ( input: string, dialNumberRegex: null | string, - setDNErrorText: (error: string) => void + setDNErrorText: (error: string) => void, + logger ): boolean => { - const regexForDn = new RegExp(dialNumberRegex ?? '1[0-9]{3}[2-9][0-9]{6}([,]{1,10}[0-9]+){0,1}'); - if (regexForDn.test(input)) { - return false; + try { + const regexForDn = new RegExp(dialNumberRegex ?? '1[0-9]{3}[2-9][0-9]{6}([,]{1,10}[0-9]+){0,1}'); + if (regexForDn.test(input)) { + return false; + } + setDNErrorText(StationLoginLabels.DN_FORMAT_ERROR); + return true; + } catch (error) { + logger?.error('CC-Widgets: StationLogin: Error in validateDialNumber', { + module: 'cc-components#station-login.utils.tsx', + method: 'validateDialNumber', + error: error.message, + }); + setDNErrorText(StationLoginLabels.DN_FORMAT_ERROR); + return true; } - setDNErrorText(StationLoginLabels.DN_FORMAT_ERROR); - return true; }; -const createStationLoginRefs = () => { - return { - multiSignInModalRef: useRef(null), - ccSignOutModalRef: useRef(null), - saveConfirmDialogRef: useRef(null), - }; +const createStationLoginRefs = (logger) => { + try { + return { + multiSignInModalRef: useRef(null), + ccSignOutModalRef: useRef(null), + saveConfirmDialogRef: useRef(null), + }; + } catch (error) { + logger?.error('CC-Widgets: StationLogin: Error in createStationLoginRefs', { + module: 'cc-components#station-login.utils.tsx', + method: 'createStationLoginRefs', + error: error.message, + }); + return { + multiSignInModalRef: useRef(null), + ccSignOutModalRef: useRef(null), + saveConfirmDialogRef: useRef(null), + }; + } }; -const saveConfirmCancelClicked = (saveConfirmDialogRef, setShowSaveConfirmDialog) => { - if (saveConfirmDialogRef?.current?.open) { - saveConfirmDialogRef.current.close(); - setShowSaveConfirmDialog(false); +const saveConfirmCancelClicked = (saveConfirmDialogRef, setShowSaveConfirmDialog, logger) => { + try { + if (saveConfirmDialogRef?.current?.open) { + saveConfirmDialogRef.current.close(); + setShowSaveConfirmDialog(false); + } + } catch (error) { + logger?.error('CC-Widgets: StationLogin: Error in saveConfirmCancelClicked', { + module: 'cc-components#station-login.utils.tsx', + method: 'saveConfirmCancelClicked', + error: error.message, + }); } }; -const handleSaveConfirm = (saveConfirmDialogRef, setShowSaveConfirmDialog, saveLoginOptions) => { - saveConfirmCancelClicked(saveConfirmDialogRef, setShowSaveConfirmDialog); - saveLoginOptions(); +const handleSaveConfirm = (saveConfirmDialogRef, setShowSaveConfirmDialog, saveLoginOptions, logger) => { + try { + saveConfirmCancelClicked(saveConfirmDialogRef, setShowSaveConfirmDialog, logger); + saveLoginOptions(); + } catch (error) { + logger?.error('CC-Widgets: StationLogin: Error in handleSaveConfirm', { + module: 'cc-components#station-login.utils.tsx', + method: 'handleSaveConfirm', + error: error.message, + }); + } }; const handleLoginOptionChanged = ( @@ -124,41 +199,49 @@ const handleLoginOptionChanged = ( logger, selectedTeamId ) => { - const selectedOption = event.detail.value; - logger.info(`CC-Widgets: StationLogin: login option changed to: ${selectedOption}`, { - module: 'cc-components#station-login.tsx', - method: 'loginOptionChanged', - }); - // TODO: Select component is calling onChange with first label on load - // bug ticket: https://jira-eng-gpk2.cisco.com/jira/browse/MOMENTUM-668 - if (Object.keys(LoginOptions).includes(selectedOption)) { - setDeviceType(selectedOption); - setSelectedDeviceType(selectedOption); - updateDialNumberLabel(selectedOption, setDialNumberLabel, setDialNumberPlaceholder); - // clear dial number when switching between DN and Extension - setDialNumber(''); - setShowDNError(false); - // If switching to the device type the user logged in with, restore its value - if (selectedOption === originalLoginOptions.deviceType) { - setCurrentLoginOptions({ - deviceType: selectedOption, - dialNumber: originalLoginOptions.dialNumber || '', - teamId: originalLoginOptions.teamId || '', - }); - setDialNumberValue(originalLoginOptions.dialNumber || ''); - setDialNumber(originalLoginOptions.dialNumber || ''); - setSelectedTeamId(originalLoginOptions.teamId || ''); - setTeamId(originalLoginOptions.teamId || ''); - } else { - // If switching to a different device type, clear the input - setCurrentLoginOptions({ - deviceType: selectedOption, - dialNumber: '', - teamId: selectedTeamId || '', - }); - setDialNumberValue(''); + try { + const selectedOption = event.detail.value; + logger.info(`CC-Widgets: StationLogin: login option changed to: ${selectedOption}`, { + module: 'cc-components#station-login.tsx', + method: 'loginOptionChanged', + }); + // TODO: Select component is calling onChange with first label on load + // bug ticket: https://jira-eng-gpk2.cisco.com/jira/browse/MOMENTUM-668 + if (Object.keys(LoginOptions).includes(selectedOption)) { + setDeviceType(selectedOption); + setSelectedDeviceType(selectedOption); + updateDialNumberLabel(selectedOption, setDialNumberLabel, setDialNumberPlaceholder, logger); + // clear dial number when switching between DN and Extension setDialNumber(''); + setShowDNError(false); + // If switching to the device type the user logged in with, restore its value + if (selectedOption === originalLoginOptions.deviceType) { + setCurrentLoginOptions({ + deviceType: selectedOption, + dialNumber: originalLoginOptions.dialNumber || '', + teamId: originalLoginOptions.teamId || '', + }); + setDialNumberValue(originalLoginOptions.dialNumber || ''); + setDialNumber(originalLoginOptions.dialNumber || ''); + setSelectedTeamId(originalLoginOptions.teamId || ''); + setTeamId(originalLoginOptions.teamId || ''); + } else { + // If switching to a different device type, clear the input + setCurrentLoginOptions({ + deviceType: selectedOption, + dialNumber: '', + teamId: selectedTeamId || '', + }); + setDialNumberValue(''); + setDialNumber(''); + } } + } catch (error) { + logger?.error('CC-Widgets: StationLogin: Error in handleLoginOptionChanged', { + module: 'cc-components#station-login.utils.tsx', + method: 'handleLoginOptionChanged', + error: error.message, + }); } }; @@ -173,60 +256,93 @@ const handleDNInputChanged = ( selectedDeviceType, logger ) => { - const input = (event.target as HTMLInputElement).value.trim(); - logger.info(`CC-Widgets: StationLogin: dialNumber input changed: ${input}`, { - module: 'cc-components#station-login.tsx', - method: 'dialNumberInputChanged', - }); - setDialNumberValue(input); - setDialNumber(input); - - // validation - if (input.length === 0) { - // show error for empty string - setDNErrorText(`${LoginOptions[selectedDeviceType]} ${StationLoginLabels.IS_REQUIRED}`); - setShowDNError(true); - } else if (selectedDeviceType === DIALNUMBER) { - setShowDNError(validateDialNumber(input, dialNumberRegex, setDNErrorText)); - } else { - setShowDNError(false); + try { + const input = (event.target as HTMLInputElement).value.trim(); + logger.info(`CC-Widgets: StationLogin: dialNumber input changed: ${input}`, { + module: 'cc-components#station-login.tsx', + method: 'dialNumberInputChanged', + }); + setDialNumberValue(input); + setDialNumber(input); + + // validation + if (input.length === 0) { + // show error for empty string + setDNErrorText(`${LoginOptions[selectedDeviceType]} ${StationLoginLabels.IS_REQUIRED}`); + setShowDNError(true); + } else if (selectedDeviceType === DIALNUMBER) { + setShowDNError(validateDialNumber(input, dialNumberRegex, setDNErrorText, logger)); + } else { + setShowDNError(false); + } + setCurrentLoginOptions((prev) => { + return { + ...prev, + dialNumber: input, + }; + }); + } catch (error) { + logger?.error('CC-Widgets: StationLogin: Error in handleDNInputChanged', { + module: 'cc-components#station-login.utils.tsx', + method: 'handleDNInputChanged', + error: error.message, + }); } - setCurrentLoginOptions((prev) => { - return { - ...prev, - dialNumber: input, - }; - }); }; const handleTeamSelectChanged = (event, setSelectedTeamId, setTeamId, setCurrentLoginOptions, setTeam, logger) => { - const value = event.detail.value; - logger.info(`CC-Widgets: StationLogin: team selected: ${value}`, { - module: 'cc-components#station-login.tsx', - method: 'teamSelected', - }); - setTeam(value); - setSelectedTeamId(event.detail.value); - setTeamId(event.detail.value); - setCurrentLoginOptions((prev) => ({ - ...prev, - teamId: value, - })); + try { + const value = event.detail.value; + logger.info(`CC-Widgets: StationLogin: team selected: ${value}`, { + module: 'cc-components#station-login.tsx', + method: 'teamSelected', + }); + setTeam(value); + setSelectedTeamId(event.detail.value); + setTeamId(event.detail.value); + setCurrentLoginOptions((prev) => ({ + ...prev, + teamId: value, + })); + } catch (error) { + logger?.error('CC-Widgets: StationLogin: Error in handleTeamSelectChanged', { + module: 'cc-components#station-login.utils.tsx', + method: 'handleTeamSelectChanged', + error: error.message, + }); + } }; -const handleOnCCSignOut = (ccSignOutModalRef: React.RefObject, onCCSignOut) => { - if (ccSignOutModalRef?.current?.open) { - ccSignOutModalRef.current.close(); +const handleOnCCSignOut = (ccSignOutModalRef: React.RefObject, onCCSignOut, logger) => { + try { + if (ccSignOutModalRef?.current?.open) { + ccSignOutModalRef.current.close(); + } + onCCSignOut(); + } catch (error) { + logger?.error('CC-Widgets: StationLogin: Error in handleOnCCSignOut', { + module: 'cc-components#station-login.utils.tsx', + method: 'handleOnCCSignOut', + error: error.message, + }); } - onCCSignOut(); }; const handleCCSignoutKeyDown = ( event: React.KeyboardEvent, - setShowCCSignOutModal: (show: boolean) => void + setShowCCSignOutModal: (show: boolean) => void, + logger ) => { - if (event.key === 'Escape') { - setShowCCSignOutModal(false); + try { + if (event.key === 'Escape') { + setShowCCSignOutModal(false); + } + } catch (error) { + logger?.error('CC-Widgets: StationLogin: Error in handleCCSignoutKeyDown', { + module: 'cc-components#station-login.utils.tsx', + method: 'handleCCSignoutKeyDown', + error: error.message, + }); } }; diff --git a/packages/contact-center/cc-components/src/components/UserState/user-state.tsx b/packages/contact-center/cc-components/src/components/UserState/user-state.tsx index 0a43f21e7..0c34adf23 100644 --- a/packages/contact-center/cc-components/src/components/UserState/user-state.tsx +++ b/packages/contact-center/cc-components/src/components/UserState/user-state.tsx @@ -31,10 +31,10 @@ const UserStateComponent: React.FunctionComponent = (p logger, } = props; - const previousSelectableState = useMemo(() => getPreviousSelectableState(idleCodes), [idleCodes]); - const selectedKey = getSelectedKey(customState, currentState, idleCodes); - const items = buildDropdownItems(customState, idleCodes, currentState); - const sortedItems = sortDropdownItems(items); + const previousSelectableState = useMemo(() => getPreviousSelectableState(idleCodes, logger), [idleCodes, logger]); + const selectedKey = getSelectedKey(customState, currentState, idleCodes, logger); + const items = buildDropdownItems(customState, idleCodes, currentState, logger); + const sortedItems = sortDropdownItems(items, logger); return (
@@ -47,7 +47,7 @@ const UserStateComponent: React.FunctionComponent = (p showBorder selectedKey={selectedKey} items={sortedItems} - className={`state-select ${getDropdownClass(customState, currentState, idleCodes)}`} + className={`state-select ${getDropdownClass(customState, currentState, idleCodes, logger)}`} data-testid="state-select" > {(item) => { @@ -60,15 +60,19 @@ const UserStateComponent: React.FunctionComponent = (p
- + {item.name}
@@ -86,7 +90,7 @@ const UserStateComponent: React.FunctionComponent = (p triggerID="user-state-tooltip" > - {getTooltipText(customState, currentState, idleCodes)} + {getTooltipText(customState, currentState, idleCodes, logger)} diff --git a/packages/contact-center/cc-components/src/components/UserState/user-state.utils.ts b/packages/contact-center/cc-components/src/components/UserState/user-state.utils.ts index c0dfcf1bc..05622b29f 100644 --- a/packages/contact-center/cc-components/src/components/UserState/user-state.utils.ts +++ b/packages/contact-center/cc-components/src/components/UserState/user-state.utils.ts @@ -5,61 +5,101 @@ import {userStateLabels} from './constant'; /** * Gets the CSS class for the dropdown based on current state */ -export const getDropdownClass = (customState: ICustomState, currentState: string, idleCodes: IdleCode[]): string => { - if (customState) { - return 'custom'; // Custom state class - } - if (currentState === '0') { - return ''; - } - for (const item of idleCodes) { - if (item.id === currentState && item.name === ERROR_TRIGGERING_IDLE_CODES.RONA) { - return 'rona'; +export const getDropdownClass = ( + customState: ICustomState, + currentState: string, + idleCodes: IdleCode[], + logger? +): string => { + try { + if (customState) { + return 'custom'; // Custom state class + } + if (currentState === '0') { + return ''; } + for (const item of idleCodes) { + if (item.id === currentState && item.name === ERROR_TRIGGERING_IDLE_CODES.RONA) { + return 'rona'; + } + } + return 'idle'; + } catch (error) { + logger?.error('CC-Widgets: UserState: Error in getDropdownClass', { + module: 'cc-components#user-state.utils.ts', + method: 'getDropdownClass', + error: error.message, + }); + return 'idle'; // Default fallback } - return 'idle'; }; /** * Gets the icon style configuration for a given item */ -export const getIconStyle = (item: { - id: string; - name: string; - developerName?: string; -}): {class: string; iconName: string} => { - if (item.developerName) { - return {class: 'custom', iconName: 'busy-presence-light'}; - } +export const getIconStyle = ( + item: { + id: string; + name: string; + developerName?: string; + }, + logger? +): {class: string; iconName: string} => { + try { + if (item.developerName) { + return {class: 'custom', iconName: 'busy-presence-light'}; + } - switch (item.name) { - case AgentUserState.Available: - return {class: 'available', iconName: 'active-presence-small-filled'}; - case ERROR_TRIGGERING_IDLE_CODES.RONA: - return {class: 'rona', iconName: 'dnd-presence-filled'}; - case ERROR_TRIGGERING_IDLE_CODES.INVALID_NUMBER: - case ERROR_TRIGGERING_IDLE_CODES.UNAVAILABLE: - case ERROR_TRIGGERING_IDLE_CODES.DECLINED: - case ERROR_TRIGGERING_IDLE_CODES.BUSY: - case ERROR_TRIGGERING_IDLE_CODES.CHANNEL_FAILURE: - return {class: 'idle', iconName: 'dnd-presence-filled'}; - default: - return {class: 'idle', iconName: 'recents-presence-filled'}; + switch (item.name) { + case AgentUserState.Available: + return {class: 'available', iconName: 'active-presence-small-filled'}; + case ERROR_TRIGGERING_IDLE_CODES.RONA: + return {class: 'rona', iconName: 'dnd-presence-filled'}; + case ERROR_TRIGGERING_IDLE_CODES.INVALID_NUMBER: + case ERROR_TRIGGERING_IDLE_CODES.UNAVAILABLE: + case ERROR_TRIGGERING_IDLE_CODES.DECLINED: + case ERROR_TRIGGERING_IDLE_CODES.BUSY: + case ERROR_TRIGGERING_IDLE_CODES.CHANNEL_FAILURE: + return {class: 'idle', iconName: 'dnd-presence-filled'}; + default: + return {class: 'idle', iconName: 'recents-presence-filled'}; + } + } catch (error) { + logger?.error('CC-Widgets: UserState: Error in getIconStyle', { + module: 'cc-components#user-state.utils.ts', + method: 'getIconStyle', + error: error.message, + }); + return {class: 'idle', iconName: 'recents-presence-filled'}; // Default fallback } }; /** * Gets the tooltip text based on current state */ -export const getTooltipText = (customState: ICustomState, currentState: string, idleCodes: IdleCode[]): string => { - if (customState) { - const currentIdleCode = idleCodes.find((code) => code.id === currentState); - if (currentIdleCode?.name === AgentUserState.Available) { - return userStateLabels.customWithAvailableTooltip; +export const getTooltipText = ( + customState: ICustomState, + currentState: string, + idleCodes: IdleCode[], + logger? +): string => { + try { + if (customState) { + const currentIdleCode = idleCodes.find((code) => code.id === currentState); + if (currentIdleCode?.name === AgentUserState.Available) { + return userStateLabels.customWithAvailableTooltip; + } + return userStateLabels.customWithIdleStateTooltip.replace('{{currentState}}', currentIdleCode?.name || ''); } - return userStateLabels.customWithIdleStateTooltip.replace('{{currentState}}', currentIdleCode?.name || ''); + return userStateLabels.availableTooltip; + } catch (error) { + logger?.error('CC-Widgets: UserState: Error in getTooltipText', { + module: 'cc-components#user-state.utils.ts', + method: 'getTooltipText', + error: error.message, + }); + return userStateLabels.availableTooltip; // Default fallback } - return userStateLabels.availableTooltip; }; /** @@ -71,68 +111,111 @@ export const handleSelectionChange = ( setAgentStatus: (auxCodeId: string) => void, logger ): void => { - // Remove 'hide-' prefix if present (for RONA states) - const cleanKey = key.startsWith('hide-') ? key.substring(5) : key; + try { + // Remove 'hide-' prefix if present (for RONA states) + const cleanKey = key.startsWith('hide-') ? key.substring(5) : key; - if (cleanKey !== currentState) { - logger?.info(`CC-Widgets: UserState: state changed to: ${cleanKey}`, { - module: 'cc-components#user-state.tsx', + if (cleanKey !== currentState) { + logger?.info(`CC-Widgets: UserState: state changed to: ${cleanKey}`, { + module: 'cc-components#user-state.tsx', + method: 'handleSelectionChange', + }); + setAgentStatus(cleanKey); + } + } catch (error) { + logger?.error('CC-Widgets: UserState: Error in handleSelectionChange', { + module: 'cc-components#user-state.utils.ts', method: 'handleSelectionChange', + error: error.message, }); - setAgentStatus(cleanKey); } }; /** * Sorts dropdown items with Available first, then others */ -export const sortDropdownItems = (items: Array<{id: string; name: string}>): Array<{id: string; name: string}> => { - // Keep Available at the top, sort the rest alphabetically - return [ - ...items.filter((item) => item.name === AgentUserState.Available), - ...items.filter((item) => item.name !== AgentUserState.Available).sort((a, b) => a.name.localeCompare(b.name)), - ]; +export const sortDropdownItems = ( + items: Array<{id: string; name: string}>, + logger? +): Array<{id: string; name: string}> => { + try { + // Keep Available at the top, sort the rest alphabetically + return [ + ...items.filter((item) => item.name === AgentUserState.Available), + ...items.filter((item) => item.name !== AgentUserState.Available).sort((a, b) => a.name.localeCompare(b.name)), + ]; + } catch (error) { + logger?.error('CC-Widgets: UserState: Error in sortDropdownItems', { + module: 'cc-components#user-state.utils.ts', + method: 'sortDropdownItems', + error: error.message, + }); + return items || []; // Return original array or empty array as fallback + } }; /** * Gets the previous selectable state (first non-RONA/Engaged state) */ -export const getPreviousSelectableState = (idleCodes: IdleCode[]): string => { - const selectableState = idleCodes.find( - (code) => - ![...Object.values(ERROR_TRIGGERING_IDLE_CODES), AgentUserState.Engaged].includes(code.name as AgentUserState) - ); - return selectableState?.id || '0'; +export const getPreviousSelectableState = (idleCodes: IdleCode[], logger?): string => { + try { + const selectableState = idleCodes.find( + (code) => + ![...Object.values(ERROR_TRIGGERING_IDLE_CODES), AgentUserState.Engaged].includes(code.name as AgentUserState) + ); + return selectableState?.id || '0'; + } catch (error) { + logger?.error('CC-Widgets: UserState: Error in getPreviousSelectableState', { + module: 'cc-components#user-state.utils.ts', + method: 'getPreviousSelectableState', + error: error.message, + }); + return '0'; // Default fallback + } }; /** * Gets the selected key for the dropdown */ -export const getSelectedKey = (customState: ICustomState, currentState: string, idleCodes: IdleCode[]): string => { - if (customState && 'developerName' in customState) { - return `hide-${customState.developerName}`; - } +export const getSelectedKey = ( + customState: ICustomState, + currentState: string, + idleCodes: IdleCode[], + logger? +): string => { + try { + if (customState && 'developerName' in customState) { + return `hide-${customState.developerName}`; + } - // Check if current state exists in idleCodes first - const currentIdleCode = idleCodes.find((code) => code.id === currentState); + // Check if current state exists in idleCodes first + const currentIdleCode = idleCodes.find((code) => code.id === currentState); - // If currentIdleCode is not found, return currentState as-is - if (!currentIdleCode) { - return currentState; - } + // If currentIdleCode is not found, return currentState as-is + if (!currentIdleCode) { + return currentState; + } - // Check if current state is an error-triggering idle code (like RONA) - if (Object.values(ERROR_TRIGGERING_IDLE_CODES).includes(currentIdleCode.name)) { - return `hide-${currentState}`; - } + // Check if current state is an error-triggering idle code (like RONA) + if (Object.values(ERROR_TRIGGERING_IDLE_CODES).includes(currentIdleCode.name)) { + return `hide-${currentState}`; + } - // Check if current state is Engaged - const isEngaged = currentIdleCode.name === AgentUserState.Engaged; - if (isEngaged) { - return getPreviousSelectableState(idleCodes); - } + // Check if current state is Engaged + const isEngaged = currentIdleCode.name === AgentUserState.Engaged; + if (isEngaged) { + return getPreviousSelectableState(idleCodes, logger); + } - return currentState; + return currentState; + } catch (error) { + logger?.error('CC-Widgets: UserState: Error in getSelectedKey', { + module: 'cc-components#user-state.utils.ts', + method: 'getSelectedKey', + error: error.message, + }); + return currentState; // Return original state as fallback + } }; /** @@ -141,43 +224,53 @@ export const getSelectedKey = (customState: ICustomState, currentState: string, export const buildDropdownItems = ( customState: ICustomState, idleCodes: IdleCode[], - currentState: string + currentState: string, + logger? ): Array<{id: string; name: string; developerName?: string}> => { - const items: Array<{id: string; name: string; developerName?: string}> = []; - - // Add custom state if present - if (customState && 'developerName' in customState) { - items.push({ - name: customState.name, - id: `hide-${customState.developerName}`, - developerName: customState.developerName, - }); - } + try { + const items: Array<{id: string; name: string; developerName?: string}> = []; - // Add regular idle codes, but handle RONA specially - for (const code of idleCodes) { - // Skip Engaged states entirely - if (code.name === AgentUserState.Engaged) { - continue; + // Add custom state if present + if (customState && 'developerName' in customState) { + items.push({ + name: customState.name, + id: `hide-${customState.developerName}`, + developerName: customState.developerName, + }); } - // For RONA: only include if it's the current state - if (Object.values(ERROR_TRIGGERING_IDLE_CODES).includes(code.name)) { - if (code.id === currentState) { - items.push({ - ...code, - id: `hide-${code.id}`, // Use hide- prefix for RONA - }); + // Add regular idle codes, but handle RONA specially + for (const code of idleCodes) { + // Skip Engaged states entirely + if (code.name === AgentUserState.Engaged) { + continue; + } + + // For RONA: only include if it's the current state + if (Object.values(ERROR_TRIGGERING_IDLE_CODES).includes(code.name)) { + if (code.id === currentState) { + items.push({ + ...code, + id: `hide-${code.id}`, // Use hide- prefix for RONA + }); + } + continue; } - continue; + + // Add all other states normally + items.push({ + id: code.id, + name: code.name, + }); } - // Add all other states normally - items.push({ - id: code.id, - name: code.name, + return items; + } catch (error) { + logger?.error('CC-Widgets: UserState: Error in buildDropdownItems', { + module: 'cc-components#user-state.utils.ts', + method: 'buildDropdownItems', + error: error.message, }); + return []; // Return empty array as fallback } - - return items; }; diff --git a/packages/contact-center/cc-components/src/components/task/AutoWrapupTimer/AutoWrapupTimer.tsx b/packages/contact-center/cc-components/src/components/task/AutoWrapupTimer/AutoWrapupTimer.tsx index dc37e5405..8268714d2 100644 --- a/packages/contact-center/cc-components/src/components/task/AutoWrapupTimer/AutoWrapupTimer.tsx +++ b/packages/contact-center/cc-components/src/components/task/AutoWrapupTimer/AutoWrapupTimer.tsx @@ -9,8 +9,9 @@ const AutoWrapupTimer: React.FC = ({ secondsUntilAutoWrapup, allowCancelAutoWrapup, handleCancelWrapup, + logger, }) => { - const {containerClassName, iconClassName, iconName, formattedTime} = getTimerUIState(secondsUntilAutoWrapup); + const {containerClassName, iconClassName, iconName, formattedTime} = getTimerUIState(secondsUntilAutoWrapup, logger); return ( <> diff --git a/packages/contact-center/cc-components/src/components/task/AutoWrapupTimer/AutoWrapupTimer.utils.ts b/packages/contact-center/cc-components/src/components/task/AutoWrapupTimer/AutoWrapupTimer.utils.ts index dbef2f4a2..8e7a45ed2 100644 --- a/packages/contact-center/cc-components/src/components/task/AutoWrapupTimer/AutoWrapupTimer.utils.ts +++ b/packages/contact-center/cc-components/src/components/task/AutoWrapupTimer/AutoWrapupTimer.utils.ts @@ -3,53 +3,114 @@ import {TimerUIState} from '../task.types'; /** * Determines if the timer is in urgent state (10 seconds or less) */ -export const isTimerUrgent = (secondsUntilAutoWrapup: number): boolean => { - return secondsUntilAutoWrapup <= 10; +export const isTimerUrgent = (secondsUntilAutoWrapup: number, logger?): boolean => { + try { + return secondsUntilAutoWrapup <= 10; + } catch (error) { + logger?.error('CC-Widgets: AutoWrapupTimer: Error in isTimerUrgent', { + module: 'cc-components#AutoWrapupTimer.utils.ts', + method: 'isTimerUrgent', + error: error.message, + }); + return false; // Default safe fallback + } }; /** * Gets the appropriate container CSS class based on urgency */ -export const getContainerClassName = (isUrgent: boolean): string => { - return isUrgent ? 'wrapup-timer-container urgent' : 'wrapup-timer-container'; +export const getContainerClassName = (isUrgent: boolean, logger?): string => { + try { + return isUrgent ? 'wrapup-timer-container urgent' : 'wrapup-timer-container'; + } catch (error) { + logger?.error('CC-Widgets: AutoWrapupTimer: Error in getContainerClassName', { + module: 'cc-components#AutoWrapupTimer.utils.ts', + method: 'getContainerClassName', + error: error.message, + }); + return 'wrapup-timer-container'; // Default safe fallback + } }; /** * Gets the appropriate icon CSS class based on urgency */ -export const getIconClassName = (isUrgent: boolean): string => { - return isUrgent ? 'wrapup-timer-icon urgent' : 'wrapup-timer-icon'; +export const getIconClassName = (isUrgent: boolean, logger?): string => { + try { + return isUrgent ? 'wrapup-timer-icon urgent' : 'wrapup-timer-icon'; + } catch (error) { + logger?.error('CC-Widgets: AutoWrapupTimer: Error in getIconClassName', { + module: 'cc-components#AutoWrapupTimer.utils.ts', + method: 'getIconClassName', + error: error.message, + }); + return 'wrapup-timer-icon'; // Default safe fallback + } }; /** * Gets the appropriate icon name based on urgency */ -export const getIconName = (isUrgent: boolean): string => { - return isUrgent ? 'alert-active-bold' : 'recents-bold'; +export const getIconName = (isUrgent: boolean, logger?): string => { + try { + return isUrgent ? 'alert-active-bold' : 'recents-bold'; + } catch (error) { + logger?.error('CC-Widgets: AutoWrapupTimer: Error in getIconName', { + module: 'cc-components#AutoWrapupTimer.utils.ts', + method: 'getIconName', + error: error.message, + }); + return 'recents-bold'; // Default safe fallback + } }; /** * Formats seconds into MM:SS format */ -export const formatTimerDisplay = (secondsUntilAutoWrapup: number): string => { - const minutes = Math.floor(secondsUntilAutoWrapup / 60); - const seconds = secondsUntilAutoWrapup % 60; +export const formatTimerDisplay = (secondsUntilAutoWrapup: number, logger?): string => { + try { + const minutes = Math.floor(secondsUntilAutoWrapup / 60); + const seconds = secondsUntilAutoWrapup % 60; - return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + } catch (error) { + logger?.error('CC-Widgets: AutoWrapupTimer: Error in formatTimerDisplay', { + module: 'cc-components#AutoWrapupTimer.utils.ts', + method: 'formatTimerDisplay', + error: error.message, + }); + return '00:00'; // Default safe fallback + } }; /** * Gets all timer-related UI state in one object */ -export const getTimerUIState = (secondsUntilAutoWrapup: number): TimerUIState => { - const isUrgent = isTimerUrgent(secondsUntilAutoWrapup); +export const getTimerUIState = (secondsUntilAutoWrapup: number, logger?): TimerUIState => { + try { + const isUrgent = isTimerUrgent(secondsUntilAutoWrapup, logger); - return { - isUrgent, - containerClassName: getContainerClassName(isUrgent), - iconClassName: getIconClassName(isUrgent), - iconName: getIconName(isUrgent), - formattedTime: formatTimerDisplay(secondsUntilAutoWrapup), - }; + return { + isUrgent, + containerClassName: getContainerClassName(isUrgent, logger), + iconClassName: getIconClassName(isUrgent, logger), + iconName: getIconName(isUrgent, logger), + formattedTime: formatTimerDisplay(secondsUntilAutoWrapup, logger), + }; + } catch (error) { + logger?.error('CC-Widgets: AutoWrapupTimer: Error in getTimerUIState', { + module: 'cc-components#AutoWrapupTimer.utils.ts', + method: 'getTimerUIState', + error: error.message, + }); + // Return safe default state + return { + isUrgent: false, + containerClassName: 'wrapup-timer-container', + iconClassName: 'wrapup-timer-icon', + iconName: 'recents-bold', + formattedTime: '00:00', + }; + } }; diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-custom.utils.ts b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-custom.utils.ts index 5486b2215..06d48003d 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-custom.utils.ts +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-custom.utils.ts @@ -34,55 +34,86 @@ export const createConsultButtons = ( muteUnmute: boolean, onTransfer?: () => void, handleConsultMuteToggle?: () => void, - handleEndConsult?: () => void + handleEndConsult?: () => void, + logger? ): ButtonConfig[] => { - return [ - { - key: 'mute', - icon: isMuted ? 'microphone-muted-bold' : 'microphone-bold', - onClick: handleConsultMuteToggle || (() => {}), - tooltip: isMuted ? UNMUTE_CALL : MUTE_CALL, - className: `${isMuted ? 'call-control-button-muted' : 'call-control-button'}`, - disabled: isMuteDisabled, - shouldShow: muteUnmute, - }, - { - key: 'transfer', - icon: 'next-bold', - tooltip: 'Transfer Consult', - onClick: onTransfer || (() => {}), - className: 'call-control-button', - disabled: !consultCompleted, - shouldShow: isAgentBeingConsulted && !!onTransfer, - }, - { - key: 'cancel', - icon: 'headset-muted-bold', - tooltip: 'End Consult', - onClick: handleEndConsult || (() => {}), - className: 'call-control-consult-button-cancel', - shouldShow: isEndConsultEnabled || isAgentBeingConsulted, - }, - ]; + try { + return [ + { + key: 'mute', + icon: isMuted ? 'microphone-muted-bold' : 'microphone-bold', + onClick: handleConsultMuteToggle || (() => {}), + tooltip: isMuted ? UNMUTE_CALL : MUTE_CALL, + className: `${isMuted ? 'call-control-button-muted' : 'call-control-button'}`, + disabled: isMuteDisabled, + shouldShow: muteUnmute, + }, + { + key: 'transfer', + icon: 'next-bold', + tooltip: 'Transfer Consult', + onClick: onTransfer || (() => {}), + className: 'call-control-button', + disabled: !consultCompleted, + shouldShow: isAgentBeingConsulted && !!onTransfer, + }, + { + key: 'cancel', + icon: 'headset-muted-bold', + tooltip: 'End Consult', + onClick: handleEndConsult || (() => {}), + className: 'call-control-consult-button-cancel', + shouldShow: isEndConsultEnabled || isAgentBeingConsulted, + }, + ]; + } catch (error) { + logger?.error('CC-Widgets: CallControlCustom: Error in createConsultButtons', { + module: 'cc-components#call-control-custom.utils.ts', + method: 'createConsultButtons', + error: error.message, + }); + // Return empty safe fallback + return []; + } }; /** * Filters buttons that should be visible */ -export const getVisibleButtons = (buttons: ButtonConfig[]): ButtonConfig[] => { - return buttons.filter((button) => button.shouldShow); +export const getVisibleButtons = (buttons: ButtonConfig[], logger?): ButtonConfig[] => { + try { + return buttons.filter((button) => button.shouldShow); + } catch (error) { + logger?.error('CC-Widgets: CallControlCustom: Error in getVisibleButtons', { + module: 'cc-components#call-control-custom.utils.ts', + method: 'getVisibleButtons', + error: error.message, + }); + // Return empty safe fallback + return []; + } }; /** * Creates initials from a name string */ -export const createInitials = (name: string): string => { - return name - .split(' ') - .map((word) => word[0]) - .join('') - .slice(0, 2) - .toUpperCase(); +export const createInitials = (name: string, logger?): string => { + try { + return name + .split(' ') + .map((word) => word[0]) + .join('') + .slice(0, 2) + .toUpperCase(); + } catch (error) { + logger?.error('CC-Widgets: CallControlCustom: Error in createInitials', { + module: 'cc-components#call-control-custom.utils.ts', + method: 'createInitials', + error: error.message, + }); + // Return safe default + return '??'; + } }; /** @@ -159,8 +190,18 @@ export const handleMuteToggle = ( /** * Gets the consult status text based on completion state */ -export const getConsultStatusText = (consultCompleted: boolean): string => { - return consultCompleted ? 'Consulting' : 'Consult requested'; +export const getConsultStatusText = (consultCompleted: boolean, logger?): string => { + try { + return consultCompleted ? 'Consulting' : 'Consult requested'; + } catch (error) { + logger?.error('CC-Widgets: CallControlCustom: Error in getConsultStatusText', { + module: 'cc-components#call-control-custom.utils.ts', + method: 'getConsultStatusText', + error: error.message, + }); + // Return safe default + return 'Consulting'; + } }; /** @@ -177,35 +218,72 @@ export const handleListItemPress = (title: string, onButtonPress: () => void, lo /** * Determines if tabs should be shown based on available data */ -export const shouldShowTabs = (buddyAgents: BuddyDetails[], queues: ContactServiceQueue[]): boolean => { - const noAgents = !buddyAgents || buddyAgents.length === 0; - const noQueues = !queues || queues.length === 0; - return !(noAgents && noQueues); +export const shouldShowTabs = (buddyAgents: BuddyDetails[], queues: ContactServiceQueue[], logger?): boolean => { + try { + const noAgents = !buddyAgents || buddyAgents.length === 0; + const noQueues = !queues || queues.length === 0; + return !(noAgents && noQueues); + } catch (error) { + logger?.error('CC-Widgets: CallControlCustom: Error in shouldShowTabs', { + module: 'cc-components#call-control-custom.utils.ts', + method: 'shouldShowTabs', + error: error.message, + }); + // Return safe default + return false; + } }; /** * Checks if agents list is empty */ -export const isAgentsEmpty = (buddyAgents: BuddyDetails[]): boolean => { - return !buddyAgents || buddyAgents.length === 0; +export const isAgentsEmpty = (buddyAgents: BuddyDetails[], logger?): boolean => { + try { + return !buddyAgents || buddyAgents.length === 0; + } catch (error) { + logger?.error('CC-Widgets: CallControlCustom: Error in isAgentsEmpty', { + module: 'cc-components#call-control-custom.utils.ts', + method: 'isAgentsEmpty', + error: error.message, + }); + // Return safe default + return true; + } }; /** * Checks if queues list is empty */ -export const isQueuesEmpty = (queues: ContactServiceQueue[]): boolean => { - return !queues || queues.length === 0; +export const isQueuesEmpty = (queues: ContactServiceQueue[], logger?): boolean => { + try { + return !queues || queues.length === 0; + } catch (error) { + logger?.error('CC-Widgets: CallControlCustom: Error in isQueuesEmpty', { + module: 'cc-components#call-control-custom.utils.ts', + method: 'isQueuesEmpty', + error: error.message, + }); + // Return safe default + return true; + } }; /** * Handles tab selection with logging */ export const handleTabSelection = (key: string, setSelectedTab: (tab: string) => void, logger: ILogger): void => { - setSelectedTab(key); - logger.log(`CC-Widgets: ConsultTransferPopover: tab selected: ${key}`, { - module: 'consult-transfer-popover.tsx', - method: 'onTabSelection', - }); + try { + setSelectedTab(key); + logger.log(`CC-Widgets: ConsultTransferPopover: tab selected: ${key}`, { + module: 'consult-transfer-popover.tsx', + method: 'onTabSelection', + }); + } catch (error) { + logger.error(`CC-Widgets: CallControlCustom: Error in handleTabSelection: ${error.message}`, { + module: 'cc-components#call-control-custom.utils.ts', + method: 'handleTabSelection', + }); + } }; /** @@ -217,12 +295,19 @@ export const handleAgentSelection = ( onAgentSelect: ((agentId: string, agentName: string) => void) | undefined, logger: ILogger ): void => { - logger.info(`CC-Widgets: ConsultTransferPopover: agent selected: ${agentId}`, { - module: 'consult-transfer-popover.tsx', - method: 'onAgentSelect', - }); - if (onAgentSelect) { - onAgentSelect(agentId, agentName); + try { + logger.info(`CC-Widgets: ConsultTransferPopover: agent selected: ${agentId}`, { + module: 'consult-transfer-popover.tsx', + method: 'onAgentSelect', + }); + if (onAgentSelect) { + onAgentSelect(agentId, agentName); + } + } catch (error) { + logger.error(`CC-Widgets: CallControlCustom: Error in handleAgentSelection: ${error.message}`, { + module: 'cc-components#call-control-custom.utils.ts', + method: 'handleAgentSelection', + }); } }; @@ -235,89 +320,153 @@ export const handleQueueSelection = ( onQueueSelect: ((queueId: string, queueName: string) => void) | undefined, logger: ILogger ): void => { - logger.log(`CC-Widgets: ConsultTransferPopover: queue selected: ${queueId}`, { - module: 'consult-transfer-popover.tsx', - method: 'onQueueSelect', - }); - if (onQueueSelect) { - onQueueSelect(queueId, queueName); + try { + logger.log(`CC-Widgets: ConsultTransferPopover: queue selected: ${queueId}`, { + module: 'consult-transfer-popover.tsx', + method: 'onQueueSelect', + }); + if (onQueueSelect) { + onQueueSelect(queueId, queueName); + } + } catch (error) { + logger.error(`CC-Widgets: CallControlCustom: Error in handleQueueSelection: ${error.message}`, { + module: 'cc-components#call-control-custom.utils.ts', + method: 'handleQueueSelection', + }); } }; /** * Gets the appropriate empty state message based on context */ -export const getEmptyStateMessage = (selectedTab: string, showTabs: boolean): string => { - if (!showTabs) { - return "We can't find any queue or agent available for now."; - } +export const getEmptyStateMessage = (selectedTab: string, showTabs: boolean, logger?): string => { + try { + if (!showTabs) { + return "We can't find any queue or agent available for now."; + } - if (selectedTab === 'Agents') { - return "We can't find any agent available for now."; - } + if (selectedTab === 'Agents') { + return "We can't find any agent available for now."; + } - return "We can't find any queue available for now."; + return "We can't find any queue available for now."; + } catch (error) { + logger.error(`CC-Widgets: CallControlCustom: Error in getEmptyStateMessage: ${error.message}`, { + module: 'cc-components#call-control-custom.utils.ts', + method: 'getEmptyStateMessage', + }); + // Return safe default + return "We can't find any queue or agent available for now."; + } }; /** * Creates list item data from buddy agents */ -export const createAgentListData = (buddyAgents: BuddyDetails[]): ListItemData[] => { - return buddyAgents.map((agent) => ({ - id: agent.agentId, - name: agent.agentName, - })); +export const createAgentListData = (buddyAgents: BuddyDetails[], logger?): ListItemData[] => { + try { + return buddyAgents.map((agent) => ({ + id: agent.agentId, + name: agent.agentName, + })); + } catch (error) { + logger.error(`CC-Widgets: CallControlCustom: Error in createAgentListData: ${error.message}`, { + module: 'cc-components#call-control-custom.utils.ts', + method: 'createAgentListData', + }); + // Return empty safe fallback + return []; + } }; /** * Creates list item data from queues */ -export const createQueueListData = (queues: ContactServiceQueue[]): ListItemData[] => { - return queues.map((queue) => ({ - id: queue.id, - name: queue.name, - })); +export const createQueueListData = (queues: ContactServiceQueue[], logger?): ListItemData[] => { + try { + return queues.map((queue) => ({ + id: queue.id, + name: queue.name, + })); + } catch (error) { + logger.error(`CC-Widgets: CallControlCustom: Error in createQueueListData: ${error.message}`, { + module: 'cc-components#call-control-custom.utils.ts', + method: 'createQueueListData', + }); + // Return empty safe fallback + return []; + } }; /** * Creates a timer key based on timestamp */ -export const createTimerKey = (startTimeStamp: number): string => { - return `timer-${startTimeStamp}`; +export const createTimerKey = (startTimeStamp: number, logger?): string => { + try { + return `timer-${startTimeStamp}`; + } catch (error) { + logger.error(`CC-Widgets: CallControlCustom: Error in createTimerKey: ${error.message}`, { + module: 'cc-components#call-control-custom.utils.ts', + method: 'createTimerKey', + }); + // Return safe default + return 'timer-0'; + } }; /** * Handles popover open with logging */ export const handlePopoverOpen = (menuType: string, setActiveMenu: (menu: string) => void, logger: ILogger): void => { - logger.info(`CC-Widgets: CallControl: opening ${menuType} popover`, { - module: 'call-control.tsx', - method: 'handlePopoverOpen', - }); - setActiveMenu(menuType); + try { + logger.info(`CC-Widgets: CallControl: opening ${menuType} popover`, { + module: 'call-control.tsx', + method: 'handlePopoverOpen', + }); + setActiveMenu(menuType); + } catch (error) { + logger.error(`CC-Widgets: CallControlCustom: Error in handlePopoverOpen: ${error.message}`, { + module: 'cc-components#call-control-custom.utils.ts', + method: 'handlePopoverOpen', + }); + } }; /** * Handles popover close with logging */ export const handlePopoverClose = (setActiveMenu: (menu: string | null) => void, logger: ILogger): void => { - logger.info('CC-Widgets: CallControl: closing popover', { - module: 'call-control.tsx', - method: 'handlePopoverClose', - }); - setActiveMenu(null); + try { + logger.info('CC-Widgets: CallControl: closing popover', { + module: 'call-control.tsx', + method: 'handlePopoverClose', + }); + setActiveMenu(null); + } catch (error) { + logger.error(`CC-Widgets: CallControlCustom: Error in handlePopoverClose: ${error.message}`, { + module: 'cc-components#call-control-custom.utils.ts', + method: 'handlePopoverClose', + }); + } }; /** * Handles hold toggle with logging */ export const handleHoldToggle = (toggleHold: (() => void) | undefined, logger: ILogger): void => { - logger.info('CC-Widgets: CallControl: hold toggle clicked', { - module: 'call-control.tsx', - method: 'handleHoldToggle', - }); - if (toggleHold) { - toggleHold(); + try { + logger.info('CC-Widgets: CallControl: hold toggle clicked', { + module: 'call-control.tsx', + method: 'handleHoldToggle', + }); + if (toggleHold) { + toggleHold(); + } + } catch (error) { + logger.error(`CC-Widgets: CallControlCustom: Error in handleHoldToggle: ${error.message}`, { + module: 'cc-components#call-control-custom.utils.ts', + method: 'handleHoldToggle', + }); } }; @@ -325,21 +474,38 @@ export const handleHoldToggle = (toggleHold: (() => void) | undefined, logger: I * Handles wrapup call with logging */ export const handleWrapupCall = (onWrapupCall: (() => void) | undefined, logger: ILogger): void => { - logger.info('CC-Widgets: CallControl: wrapup call clicked', { - module: 'call-control.tsx', - method: 'handleWrapupCall', - }); - if (onWrapupCall) { - onWrapupCall(); + try { + logger.info('CC-Widgets: CallControl: wrapup call clicked', { + module: 'call-control.tsx', + method: 'handleWrapupCall', + }); + if (onWrapupCall) { + onWrapupCall(); + } + } catch (error) { + logger.error(`CC-Widgets: CallControlCustom: Error in handleWrapupCall: ${error.message}`, { + module: 'cc-components#call-control-custom.utils.ts', + method: 'handleWrapupCall', + }); } }; /** * Validates if a menu type is supported */ -export const isValidMenuType = (menuType: string): boolean => { - const validMenuTypes = ['Consult', 'Transfer']; - return validMenuTypes.includes(menuType); +export const isValidMenuType = (menuType: string, logger?): boolean => { + try { + const validMenuTypes = ['Consult', 'Transfer']; + return validMenuTypes.includes(menuType); + } catch (error) { + logger?.error('CC-Widgets: CallControlCustom: Error in isValidMenuType', { + module: 'cc-components#call-control-custom.utils.ts', + method: 'isValidMenuType', + error: error.message, + }); + // Return safe default + return false; + } }; /** @@ -348,58 +514,119 @@ export const isValidMenuType = (menuType: string): boolean => { export const getButtonStyleClass = ( isActive: boolean, isDisabled: boolean, - baseClass = 'call-control-button' + baseClass = 'call-control-button', + logger? ): string => { - if (isDisabled) { - return `${baseClass}-disabled`; - } - if (isActive) { - return `${baseClass}-active`; + try { + if (isDisabled) { + return `${baseClass}-disabled`; + } + if (isActive) { + return `${baseClass}-active`; + } + return baseClass; + } catch (error) { + logger?.error('CC-Widgets: CallControlCustom: Error in getButtonStyleClass', { + module: 'cc-components#call-control-custom.utils.ts', + method: 'getButtonStyleClass', + error: error.message, + }); + // Return safe default + return 'call-control-button'; } - return baseClass; }; /** * Formats elapsed time for display */ -export const formatElapsedTime = (startTime: number): string => { - const elapsed = Date.now() - startTime; - const seconds = Math.floor(elapsed / 1000); - const minutes = Math.floor(seconds / 60); - const hours = Math.floor(minutes / 60); +export const formatElapsedTime = (startTime: number, logger?): string => { + try { + const elapsed = Date.now() - startTime; + const seconds = Math.floor(elapsed / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); - if (hours > 0) { - return `${hours}:${(minutes % 60).toString().padStart(2, '0')}:${(seconds % 60).toString().padStart(2, '0')}`; + if (hours > 0) { + return `${hours}:${(minutes % 60).toString().padStart(2, '0')}:${(seconds % 60).toString().padStart(2, '0')}`; + } + return `${minutes}:${(seconds % 60).toString().padStart(2, '0')}`; + } catch (error) { + logger?.error('CC-Widgets: CallControlCustom: Error in formatElapsedTime', { + module: 'cc-components#call-control-custom.utils.ts', + method: 'formatElapsedTime', + error: error.message, + }); + // Return safe default + return '0:00'; } - return `${minutes}:${(seconds % 60).toString().padStart(2, '0')}`; }; /** * Checks if an agent is available for selection */ -export const isAgentAvailable = (agent: BuddyDetails): boolean => { - return agent && agent.agentId && agent.agentName && agent.agentName.trim().length > 0; +export const isAgentAvailable = (agent: BuddyDetails, logger?): boolean => { + try { + return agent && agent.agentId && agent.agentName && agent.agentName.trim().length > 0; + } catch (error) { + logger?.error('CC-Widgets: CallControlCustom: Error in isAgentAvailable', { + module: 'cc-components#call-control-custom.utils.ts', + method: 'isAgentAvailable', + error: error.message, + }); + // Return safe default + return false; + } }; /** * Checks if a queue is available for selection */ -export const isQueueAvailable = (queue: ContactServiceQueue): boolean => { - return queue && queue.id && queue.name && queue.name.trim().length > 0; +export const isQueueAvailable = (queue: ContactServiceQueue, logger?): boolean => { + try { + return queue && queue.id && queue.name && queue.name.trim().length > 0; + } catch (error) { + logger?.error('CC-Widgets: CallControlCustom: Error in isQueueAvailable', { + module: 'cc-components#call-control-custom.utils.ts', + method: 'isQueueAvailable', + error: error.message, + }); + // Return safe default + return false; + } }; /** * Filters available agents */ -export const filterAvailableAgents = (agents: BuddyDetails[]): BuddyDetails[] => { - return agents ? agents.filter(isAgentAvailable) : []; +export const filterAvailableAgents = (agents: BuddyDetails[], logger?): BuddyDetails[] => { + try { + return agents ? agents.filter((agent) => isAgentAvailable(agent, logger)) : []; + } catch (error) { + logger?.error('CC-Widgets: CallControlCustom: Error in filterAvailableAgents', { + module: 'cc-components#call-control-custom.utils.ts', + method: 'filterAvailableAgents', + error: error.message, + }); + // Return empty safe fallback + return []; + } }; /** * Filters available queues */ -export const filterAvailableQueues = (queues: ContactServiceQueue[]): ContactServiceQueue[] => { - return queues ? queues.filter(isQueueAvailable) : []; +export const filterAvailableQueues = (queues: ContactServiceQueue[], logger?): ContactServiceQueue[] => { + try { + return queues ? queues.filter((queue) => isQueueAvailable(queue, logger)) : []; + } catch (error) { + logger?.error('CC-Widgets: CallControlCustom: Error in filterAvailableQueues', { + module: 'cc-components#call-control-custom.utils.ts', + method: 'filterAvailableQueues', + error: error.message, + }); + // Return empty safe fallback + return []; + } }; /** @@ -407,11 +634,24 @@ export const filterAvailableQueues = (queues: ContactServiceQueue[]): ContactSer */ export const debounce = unknown>( func: T, - wait: number + wait: number, + logger? ): ((...args: Parameters) => void) => { - let timeout: NodeJS.Timeout; - return (...args: Parameters) => { - clearTimeout(timeout); - timeout = setTimeout(() => func(...args), wait); - }; + try { + let timeout: NodeJS.Timeout; + return (...args: Parameters) => { + clearTimeout(timeout); + timeout = setTimeout(() => func(...args), wait); + }; + } catch (error) { + logger?.error('CC-Widgets: CallControlCustom: Error in debounce', { + module: 'cc-components#call-control-custom.utils.ts', + method: 'debounce', + error: error.message, + }); + // Return safe fallback function + return (...args: Parameters) => { + func(...args); + }; + } }; diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx index ac09c248a..3967d610d 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx @@ -66,8 +66,8 @@ function CallControlComponent(props: CallControlComponentProps) { } = props; useEffect(() => { - updateCallStateFromTask(currentTask, setIsHeld, setIsRecording); - }, [currentTask]); + updateCallStateFromTask(currentTask, setIsHeld, setIsRecording, logger); + }, [currentTask, logger]); const handletoggleHold = () => { handleToggleHoldUtil(isHeld, toggleHold, setIsHeld, logger); @@ -89,7 +89,7 @@ function CallControlComponent(props: CallControlComponentProps) { }; const handleWrapupChange = (text, value) => { - handleWrapupChangeUtil(text, value, setSelectedWrapupReason, setSelectedWrapupId); + handleWrapupChangeUtil(text, value, setSelectedWrapupReason, setSelectedWrapupId, logger); }; const handleTargetSelect = (id: string, name: string, type: DestinationType) => { @@ -109,11 +109,12 @@ function CallControlComponent(props: CallControlComponentProps) { const currentMediaType = getMediaType( currentTask.data.interaction.mediaType as MediaChannelType, - currentTask.data.interaction.mediaChannel as MediaChannelType + currentTask.data.interaction.mediaChannel as MediaChannelType, + logger ); const mediaType = currentTask.data.interaction.mediaType as MediaChannelType; - const isTelephony = isTelephonyMediaType(mediaType); + const isTelephony = isTelephonyMediaType(mediaType, logger); const buttons = buildCallControlButtons( isMuted, @@ -125,16 +126,21 @@ function CallControlComponent(props: CallControlComponentProps) { handleMuteToggle, handletoggleHold, toggleRecording, - endCall + endCall, + logger ); - const filteredButtons = filterButtonsForConsultation(buttons, consultInitiated, isTelephony); + const filteredButtons = filterButtonsForConsultation(buttons, consultInitiated, isTelephony, logger); if (!currentTask) return null; return ( <> - +
{!(consultAccepted && isTelephony) && !controlVisibility.wrapup && (
@@ -171,7 +177,7 @@ function CallControlComponent(props: CallControlComponentProps) { closeButtonPlacement="top-right" closeButtonProps={{ 'aria-label': 'Close popover', - onPress: () => handleCloseButtonPress(setShowAgentMenu, setAgentMenuType), + onPress: () => handleCloseButtonPress(setShowAgentMenu, setAgentMenuType, logger), outline: true, }} triggerComponent={ @@ -293,7 +299,9 @@ function CallControlComponent(props: CallControlComponentProps) { className="wrapup-select" data-testid="call-control:wrapup-select" placeholder={SELECT} - onChange={(event: CustomEvent) => handleWrapupReasonChange(event, wrapupCodes, handleWrapupChange)} + onChange={(event: CustomEvent) => + handleWrapupReasonChange(event, wrapupCodes, handleWrapupChange, logger) + } > {wrapupCodes?.map((code) => (
@@ -547,7 +547,7 @@ exports[`Station Login Component Actions ProfileMode interaction confirmation Po
@@ -857,7 +857,7 @@ exports[`Station Login Component Actions SignOut Modal Popup should close modal
@@ -1177,7 +1177,7 @@ exports[`Station Login Component Actions SignOut Modal Popup show signOut modal @@ -1486,7 +1486,7 @@ exports[`Station Login Component Actions SignOut Modal Popup show signOut modal @@ -1795,7 +1795,7 @@ exports[`Station Login Component Actions SignOut Modal Popup show signOut modal @@ -2104,7 +2104,7 @@ exports[`Station Login Component Actions SignOut Modal Popup show signOut modal @@ -2413,7 +2413,7 @@ exports[`Station Login Component Actions calls handleDNInputChanged with correct @@ -2733,7 +2733,7 @@ exports[`Station Login Component Actions calls handleTeamSelectChanged utility f @@ -3032,7 +3032,7 @@ exports[`Station Login Component Rendering does not render loginOptions if not i @@ -3352,7 +3352,7 @@ exports[`Station Login Component Rendering renders login failure when passed 1`] @@ -3680,7 +3680,7 @@ exports[`Station Login Component Rendering renders save error when passed 1`] = @@ -4008,7 +4008,7 @@ exports[`Station Login Component Rendering renders the component correctly 1`] = @@ -4321,7 +4321,7 @@ exports[`Station Login Component Rendering renders the component correctly in pr diff --git a/packages/contact-center/cc-components/tests/components/StationLogin/station-login.tsx b/packages/contact-center/cc-components/tests/components/StationLogin/station-login.tsx index e99137e75..d4061e2fe 100644 --- a/packages/contact-center/cc-components/tests/components/StationLogin/station-login.tsx +++ b/packages/contact-center/cc-components/tests/components/StationLogin/station-login.tsx @@ -106,6 +106,13 @@ describe('Station Login Component', () => { saveConfirmDialogRef: {current: null}, })); const mockUpdateDialNumberLabel = jest.fn(); + const mockHandleCCSignoutKeyDown = jest.fn((event, setShowCCSignOutModal) => { + if (event.key === 'Escape') { + setShowCCSignOutModal(false); + } + }); + const mockValidateDialNumber = jest.fn(); + const mockHandleOnCCSignOut = jest.fn(); beforeEach(() => { // Mock all utility functions @@ -119,6 +126,9 @@ describe('Station Login Component', () => { jest.spyOn(stationLoginUtils, 'saveConfirmCancelClicked').mockImplementation(mockSaveConfirmCancelClicked); jest.spyOn(stationLoginUtils, 'createStationLoginRefs').mockImplementation(mockCreateStationLoginRefs); jest.spyOn(stationLoginUtils, 'updateDialNumberLabel').mockImplementation(mockUpdateDialNumberLabel); + jest.spyOn(stationLoginUtils, 'handleCCSignoutKeyDown').mockImplementation(mockHandleCCSignoutKeyDown); + jest.spyOn(stationLoginUtils, 'validateDialNumber').mockImplementation(mockValidateDialNumber); + jest.spyOn(stationLoginUtils, 'handleOnCCSignOut').mockImplementation(mockHandleOnCCSignOut); }); afterEach(() => { @@ -562,7 +572,12 @@ describe('Station Login Component', () => { expect(confirmSignOutButton).toHaveTextContent(StationLoginLabels.SIGN_OUT); fireEvent.click(confirmSignOutButton); - expect(mockContinueClicked).toHaveBeenCalledWith(mockSignOutModalRef, mockSignOut, expect.any(Function)); + expect(mockContinueClicked).toHaveBeenCalledWith( + mockSignOutModalRef, + mockSignOut, + expect.any(Function), + loggerMock + ); }); it('show signOut modal when onCCSignOut is present and sign out is clicked', async () => { @@ -617,13 +632,18 @@ describe('Station Login Component', () => { const continueBtn = screen.getByTestId('ContinueButton'); fireEvent.click(continueBtn); - expect(stationLoginUtils.continueClicked).toHaveBeenCalledWith(mockModal, handleContinue, expect.any(Function)); + expect(stationLoginUtils.continueClicked).toHaveBeenCalledWith( + mockModal, + handleContinue, + expect.any(Function), + loggerMock + ); }); it('should close sign-out modal and update state on cancel button click', async () => { const mockSetShowCCSignOutModal = jest.fn(); const mockEvent = {key: 'Escape'} as React.KeyboardEvent; - stationLoginUtils.handleCCSignoutKeyDown(mockEvent, mockSetShowCCSignOutModal); + stationLoginUtils.handleCCSignoutKeyDown(mockEvent, mockSetShowCCSignOutModal, loggerMock); expect(mockSetShowCCSignOutModal).toHaveBeenCalledWith(false); const screen = await render(); const cancelButton = screen.getByTestId('cc-cancel-button'); @@ -652,7 +672,7 @@ describe('Station Login Component', () => { const cancelButton = confirmationPopup.querySelectorAll('mdc-button')[0]; fireEvent.click(cancelButton); - expect(mockSaveConfirmCancelClicked).toHaveBeenCalledWith(mockRef, expect.any(Function)); + expect(mockSaveConfirmCancelClicked).toHaveBeenCalledWith(mockRef, expect.any(Function), loggerMock); }); it('it should call handleConfirmCancelClicked when clicked on cancel in popup', async () => { @@ -673,7 +693,7 @@ describe('Station Login Component', () => { const cancelButton = confirmationPopup.querySelectorAll('mdc-button')[1]; fireEvent.click(cancelButton); - expect(mockSaveConfirmCancelClicked).toHaveBeenCalledWith(mockRef, expect.any(Function)); + expect(mockSaveConfirmCancelClicked).toHaveBeenCalledWith(mockRef, expect.any(Function), loggerMock); }); it('it should call handleSaveConfirm when clicked on confirm in popup', async () => { @@ -694,7 +714,12 @@ describe('Station Login Component', () => { const confirmButton = confirmationPopup.querySelectorAll('mdc-button')[2]; fireEvent.click(confirmButton); - expect(mockHandleSaveConfirm).toHaveBeenCalledWith(mockRef, expect.any(Function), props.saveLoginOptions); + expect(mockHandleSaveConfirm).toHaveBeenCalledWith( + mockRef, + expect.any(Function), + props.saveLoginOptions, + loggerMock + ); }); }); describe('Save Login Options', () => { @@ -722,4 +747,85 @@ describe('Station Login Component', () => { }); }); }); + + describe('Error Handling ', () => { + // Clear all mocks before these tests to call actual implementations + beforeEach(() => { + jest.restoreAllMocks(); + }); + + // Restore mocks after these tests + afterEach(() => { + jest.spyOn(stationLoginUtils, 'handleLoginOptionChanged').mockImplementation(mockHandleLoginOptionChanged); + jest.spyOn(stationLoginUtils, 'handleDNInputChanged').mockImplementation(mockHandleDNInputChanged); + jest.spyOn(stationLoginUtils, 'handleTeamSelectChanged').mockImplementation(mockHandleTeamSelectChanged); + jest.spyOn(stationLoginUtils, 'handleModals').mockImplementation(mockHandleModals); + jest.spyOn(stationLoginUtils, 'continueClicked').mockImplementation(mockContinueClicked); + jest.spyOn(stationLoginUtils, 'ccCancelButtonClicked').mockImplementation(mockCcCancelButtonClicked); + jest.spyOn(stationLoginUtils, 'handleSaveConfirm').mockImplementation(mockHandleSaveConfirm); + jest.spyOn(stationLoginUtils, 'saveConfirmCancelClicked').mockImplementation(mockSaveConfirmCancelClicked); + jest.spyOn(stationLoginUtils, 'createStationLoginRefs').mockImplementation(mockCreateStationLoginRefs); + jest.spyOn(stationLoginUtils, 'updateDialNumberLabel').mockImplementation(mockUpdateDialNumberLabel); + jest.spyOn(stationLoginUtils, 'handleCCSignoutKeyDown').mockImplementation(mockHandleCCSignoutKeyDown); + jest.spyOn(stationLoginUtils, 'validateDialNumber').mockImplementation(mockValidateDialNumber); + jest.spyOn(stationLoginUtils, 'handleOnCCSignOut').mockImplementation(mockHandleOnCCSignOut); + }); + + it('should log errors when handleModals throws an exception', () => { + const mockError = new Error('Test error in handleModals'); + const mockRef = { + current: { + showModal: jest.fn(() => { + throw mockError; + }), + }, + }; + + stationLoginUtils.handleModals(mockRef, null, null, true, false, false, loggerMock); + + expect(loggerMock.error).toHaveBeenCalledWith('CC-Widgets: StationLogin: Error in handleModals', { + module: 'cc-components#station-login.utils.tsx', + method: 'handleModals', + error: 'Test error in handleModals', + }); + }); + + it('should log errors when continueClicked throws an exception', () => { + const mockError = new Error('Test error in continueClicked'); + const mockCallback = jest.fn(() => { + throw mockError; + }); + const mockRef = {current: {close: jest.fn()}}; + const mockSetShowModal = jest.fn(); + + stationLoginUtils.continueClicked(mockRef, mockCallback, mockSetShowModal, loggerMock); + + expect(loggerMock.error).toHaveBeenCalledWith('CC-Widgets: StationLogin: Error in continueClicked', { + module: 'cc-components#station-login.utils.tsx', + method: 'continueClicked', + error: 'Test error in continueClicked', + }); + }); + + it('should log errors when validateDialNumber throws an exception', () => { + // Mock RegExp constructor to throw an error + const originalRegExp = global.RegExp; + global.RegExp = jest.fn(() => { + throw new Error('Test error in validateDialNumber'); + }) as unknown as typeof RegExp; + + const mockSetError = jest.fn(); + const result = stationLoginUtils.validateDialNumber('invalid', null, mockSetError, loggerMock); + + expect(loggerMock.error).toHaveBeenCalledWith('CC-Widgets: StationLogin: Error in validateDialNumber', { + module: 'cc-components#station-login.utils.tsx', + method: 'validateDialNumber', + error: 'Test error in validateDialNumber', + }); + expect(result).toBe(true); // Should return true (error state) when exception occurs + + // Restore original RegExp + global.RegExp = originalRegExp; + }); + }); }); diff --git a/packages/contact-center/cc-components/tests/components/StationLogin/station-login.utils.tsx b/packages/contact-center/cc-components/tests/components/StationLogin/station-login.utils.tsx index 53c546a1a..c61bd416a 100644 --- a/packages/contact-center/cc-components/tests/components/StationLogin/station-login.utils.tsx +++ b/packages/contact-center/cc-components/tests/components/StationLogin/station-login.utils.tsx @@ -34,14 +34,14 @@ describe('Station Login Utils', () => { it('should set showCCSignOutModal to false when Escape key is pressed', () => { const mockSetShowCCSignOutModal = jest.fn(); const mockEvent = {key: 'Escape'} as React.KeyboardEvent; - handleCCSignoutKeyDown(mockEvent, mockSetShowCCSignOutModal); + handleCCSignoutKeyDown(mockEvent, mockSetShowCCSignOutModal, loggerMock); expect(mockSetShowCCSignOutModal).toHaveBeenCalledWith(false); }); it('should not set showCCSignOutModal for other keys', () => { const mockSetShowCCSignOutModal = jest.fn(); const mockEvent = {key: 'Enter'} as React.KeyboardEvent; - handleCCSignoutKeyDown(mockEvent, mockSetShowCCSignOutModal); + handleCCSignoutKeyDown(mockEvent, mockSetShowCCSignOutModal, loggerMock); expect(mockSetShowCCSignOutModal).not.toHaveBeenCalled(); }); }); @@ -76,7 +76,7 @@ describe('Station Login Utils', () => { }); it('should show modal when showMultipleLoginAlert is true and modal is not open', () => { - handleModals(mockModalRef, mockCCSignOutModalRef, mockSaveConfirmDialogRef, true, false, false); + handleModals(mockModalRef, mockCCSignOutModalRef, mockSaveConfirmDialogRef, true, false, false, loggerMock); expect(mockModalRef.current?.showModal).toHaveBeenCalled(); }); @@ -84,24 +84,24 @@ describe('Station Login Utils', () => { if (mockModalRef.current) { mockModalRef.current.open = true; } - handleModals(mockModalRef, mockCCSignOutModalRef, mockSaveConfirmDialogRef, false, false, false); + handleModals(mockModalRef, mockCCSignOutModalRef, mockSaveConfirmDialogRef, false, false, false, loggerMock); expect(mockModalRef.current?.close).toHaveBeenCalled(); }); it('should show CC sign out modal when showCCSignOutModal is true', () => { - handleModals(mockModalRef, mockCCSignOutModalRef, mockSaveConfirmDialogRef, false, true, false); + handleModals(mockModalRef, mockCCSignOutModalRef, mockSaveConfirmDialogRef, false, true, false, loggerMock); expect(mockCCSignOutModalRef.current?.showModal).toHaveBeenCalled(); }); it('should show save confirm dialog when showSaveConfirmDialog is true', () => { - handleModals(mockModalRef, mockCCSignOutModalRef, mockSaveConfirmDialogRef, false, false, true); + handleModals(mockModalRef, mockCCSignOutModalRef, mockSaveConfirmDialogRef, false, false, true, loggerMock); expect(mockSaveConfirmDialogRef.current?.showModal).toHaveBeenCalled(); }); it('should handle null refs gracefully', () => { const nullRef = {current: null}; expect(() => { - handleModals(nullRef, nullRef, nullRef, true, true, true); + handleModals(nullRef, nullRef, nullRef, true, true, true, loggerMock); }).not.toThrow(); }); }); @@ -116,7 +116,7 @@ describe('Station Login Utils', () => { const mockHandleContinue = jest.fn(); const mockSetShowCCSignOutModal = jest.fn(); - continueClicked(mockModalRef, mockHandleContinue, mockSetShowCCSignOutModal); + continueClicked(mockModalRef, mockHandleContinue, mockSetShowCCSignOutModal, loggerMock); expect(mockModalRef.current?.close).toHaveBeenCalled(); expect(mockHandleContinue).toHaveBeenCalled(); @@ -129,7 +129,7 @@ describe('Station Login Utils', () => { const mockSetShowCCSignOutModal = jest.fn(); expect(() => { - continueClicked(nullRef, mockHandleContinue, mockSetShowCCSignOutModal); + continueClicked(nullRef, mockHandleContinue, mockSetShowCCSignOutModal, loggerMock); }).not.toThrow(); }); }); @@ -144,7 +144,7 @@ describe('Station Login Utils', () => { }; const mockSetShowCCSignOutModal = jest.fn(); - ccCancelButtonClicked(mockRef, mockSetShowCCSignOutModal); + ccCancelButtonClicked(mockRef, mockSetShowCCSignOutModal, loggerMock); expect(mockRef.current?.close).toHaveBeenCalled(); expect(mockSetShowCCSignOutModal).toHaveBeenCalledWith(false); @@ -159,7 +159,7 @@ describe('Station Login Utils', () => { }; const mockSetShowCCSignOutModal = jest.fn(); - ccCancelButtonClicked(mockRef, mockSetShowCCSignOutModal); + ccCancelButtonClicked(mockRef, mockSetShowCCSignOutModal, loggerMock); expect(mockRef.current?.close).not.toHaveBeenCalled(); expect(mockSetShowCCSignOutModal).not.toHaveBeenCalled(); @@ -170,7 +170,7 @@ describe('Station Login Utils', () => { const mockSetShowCCSignOutModal = jest.fn(); expect(() => { - ccCancelButtonClicked(nullRef, mockSetShowCCSignOutModal); + ccCancelButtonClicked(nullRef, mockSetShowCCSignOutModal, loggerMock); }).not.toThrow(); }); }); @@ -180,7 +180,7 @@ describe('Station Login Utils', () => { const mockSetDialNumberLabel = jest.fn(); const mockSetDialNumberPlaceholder = jest.fn(); - updateDialNumberLabel(DIALNUMBER, mockSetDialNumberLabel, mockSetDialNumberPlaceholder); + updateDialNumberLabel(DIALNUMBER, mockSetDialNumberLabel, mockSetDialNumberPlaceholder, loggerMock); expect(mockSetDialNumberLabel).toHaveBeenCalledWith(LoginOptions[DIALNUMBER]); expect(mockSetDialNumberPlaceholder).toHaveBeenCalledWith(LoginOptions[DIALNUMBER]); @@ -190,7 +190,7 @@ describe('Station Login Utils', () => { const mockSetDialNumberLabel = jest.fn(); const mockSetDialNumberPlaceholder = jest.fn(); - updateDialNumberLabel(EXTENSION, mockSetDialNumberLabel, mockSetDialNumberPlaceholder); + updateDialNumberLabel(EXTENSION, mockSetDialNumberLabel, mockSetDialNumberPlaceholder, loggerMock); expect(mockSetDialNumberLabel).toHaveBeenCalledWith(LoginOptions[EXTENSION]); expect(mockSetDialNumberPlaceholder).toHaveBeenCalledWith(LoginOptions[EXTENSION]); @@ -200,7 +200,7 @@ describe('Station Login Utils', () => { const mockSetDialNumberLabel = jest.fn(); const mockSetDialNumberPlaceholder = jest.fn(); - updateDialNumberLabel(DESKTOP, mockSetDialNumberLabel, mockSetDialNumberPlaceholder); + updateDialNumberLabel(DESKTOP, mockSetDialNumberLabel, mockSetDialNumberPlaceholder, loggerMock); expect(mockSetDialNumberLabel).not.toHaveBeenCalled(); expect(mockSetDialNumberPlaceholder).not.toHaveBeenCalled(); @@ -210,7 +210,7 @@ describe('Station Login Utils', () => { const mockSetDialNumberLabel = jest.fn(); const mockSetDialNumberPlaceholder = jest.fn(); - updateDialNumberLabel('INVALID_OPTION', mockSetDialNumberLabel, mockSetDialNumberPlaceholder); + updateDialNumberLabel('INVALID_OPTION', mockSetDialNumberLabel, mockSetDialNumberPlaceholder, loggerMock); expect(mockSetDialNumberLabel).not.toHaveBeenCalled(); expect(mockSetDialNumberPlaceholder).not.toHaveBeenCalled(); @@ -226,21 +226,21 @@ describe('Station Login Utils', () => { it('should return false for valid dial number with default regex', () => { const validNumber = '15552234567'; // Changed 4th digit from 1 to 2 to match [2-9] pattern - const result = validateDialNumber(validNumber, null, mockSetDNErrorText); + const result = validateDialNumber(validNumber, null, mockSetDNErrorText, loggerMock); expect(result).toBe(false); expect(mockSetDNErrorText).not.toHaveBeenCalled(); }); it('should return true for invalid dial number and set error text', () => { const invalidNumber = '911'; // This should be invalid for the default regex (too short and doesn't match pattern) - const result = validateDialNumber(invalidNumber, null, mockSetDNErrorText); + const result = validateDialNumber(invalidNumber, null, mockSetDNErrorText, loggerMock); expect(result).toBe(true); expect(mockSetDNErrorText).toHaveBeenCalledWith(StationLoginLabels.DN_FORMAT_ERROR); }); it('should accept any input when empty string regex is provided', () => { const anyNumber = '911'; // With empty string regex, this becomes /(?:)/ which matches everything - const result = validateDialNumber(anyNumber, '', mockSetDNErrorText); + const result = validateDialNumber(anyNumber, '', mockSetDNErrorText, loggerMock); expect(result).toBe(false); // Should return false (no error) because empty regex matches everything expect(mockSetDNErrorText).not.toHaveBeenCalled(); }); @@ -248,7 +248,7 @@ describe('Station Login Utils', () => { it('should use custom regex when provided', () => { const customRegex = '^\\d{10}$'; const validNumber = '1234567890'; - const result = validateDialNumber(validNumber, customRegex, mockSetDNErrorText); + const result = validateDialNumber(validNumber, customRegex, mockSetDNErrorText, loggerMock); expect(result).toBe(false); expect(mockSetDNErrorText).not.toHaveBeenCalled(); }); @@ -256,7 +256,7 @@ describe('Station Login Utils', () => { it('should return true for invalid number with custom regex', () => { const customRegex = '^\\d{10}$'; const invalidNumber = '123'; - const result = validateDialNumber(invalidNumber, customRegex, mockSetDNErrorText); + const result = validateDialNumber(invalidNumber, customRegex, mockSetDNErrorText, loggerMock); expect(result).toBe(true); expect(mockSetDNErrorText).toHaveBeenCalledWith(StationLoginLabels.DN_FORMAT_ERROR); }); @@ -279,7 +279,7 @@ describe('Station Login Utils', () => { }; const mockSetShowSaveConfirmDialog = jest.fn(); - saveConfirmCancelClicked(mockRef, mockSetShowSaveConfirmDialog); + saveConfirmCancelClicked(mockRef, mockSetShowSaveConfirmDialog, loggerMock); expect(mockRef.current?.close).toHaveBeenCalled(); expect(mockSetShowSaveConfirmDialog).toHaveBeenCalledWith(false); @@ -290,7 +290,7 @@ describe('Station Login Utils', () => { const mockSetShowSaveConfirmDialog = jest.fn(); expect(() => { - saveConfirmCancelClicked(nullRef, mockSetShowSaveConfirmDialog); + saveConfirmCancelClicked(nullRef, mockSetShowSaveConfirmDialog, loggerMock); }).not.toThrow(); }); }); @@ -306,7 +306,7 @@ describe('Station Login Utils', () => { const mockSetShowSaveConfirmDialog = jest.fn(); const mockSaveLoginOptions = jest.fn(); - handleSaveConfirm(mockRef, mockSetShowSaveConfirmDialog, mockSaveLoginOptions); + handleSaveConfirm(mockRef, mockSetShowSaveConfirmDialog, mockSaveLoginOptions, loggerMock); expect(mockRef.current?.close).toHaveBeenCalled(); expect(mockSetShowSaveConfirmDialog).toHaveBeenCalledWith(false); @@ -726,7 +726,7 @@ describe('Station Login Utils', () => { }; const mockOnCCSignOut = jest.fn(); - handleOnCCSignOut(mockRef, mockOnCCSignOut); + handleOnCCSignOut(mockRef, mockOnCCSignOut, loggerMock); expect(mockRef.current?.close).toHaveBeenCalled(); expect(mockOnCCSignOut).toHaveBeenCalled(); @@ -741,7 +741,7 @@ describe('Station Login Utils', () => { }; const mockOnCCSignOut = jest.fn(); - handleOnCCSignOut(mockRef, mockOnCCSignOut); + handleOnCCSignOut(mockRef, mockOnCCSignOut, loggerMock); expect(mockOnCCSignOut).toHaveBeenCalled(); }); @@ -751,7 +751,7 @@ describe('Station Login Utils', () => { const mockOnCCSignOut = jest.fn(); expect(() => { - handleOnCCSignOut(nullRef, mockOnCCSignOut); + handleOnCCSignOut(nullRef, mockOnCCSignOut, loggerMock); }).not.toThrow(); expect(mockOnCCSignOut).toHaveBeenCalled(); @@ -764,7 +764,7 @@ describe('Station Login Utils', () => { current: document.createElement('dialog'), } as React.RefObject); - const refs = createStationLoginRefs(); + const refs = createStationLoginRefs(loggerMock); expect(useRefMock).toHaveBeenCalledTimes(3); expect(refs).toHaveProperty('multiSignInModalRef'); @@ -775,4 +775,153 @@ describe('Station Login Utils', () => { expect(refs.saveConfirmDialogRef.current).toBeInstanceOf(HTMLDialogElement); }); }); + + describe('Error Handling', () => { + beforeEach(() => { + loggerMock.error.mockClear(); + }); + + it('should log error when handleModals throws exception', () => { + const mockRef = { + current: { + open: false, + showModal: jest.fn(() => { + throw new Error('Test showModal error'); + }), + }, + } as unknown as HTMLDialogElement; + + handleModals(mockRef, null, null, true, false, false, loggerMock); + + expect(loggerMock.error).toHaveBeenCalledWith('CC-Widgets: StationLogin: Error in handleModals', { + module: 'cc-components#station-login.utils.tsx', + method: 'handleModals', + error: 'Test showModal error', + }); + }); + + it('should log error when continueClicked throws exception', () => { + const mockCallback = jest.fn(() => { + throw new Error('Test callback error'); + }); + const mockRef = {current: {close: jest.fn()}}; + const mockSetShowModal = jest.fn(); + + continueClicked(mockRef, mockCallback, mockSetShowModal, loggerMock); + + expect(loggerMock.error).toHaveBeenCalledWith('CC-Widgets: StationLogin: Error in continueClicked', { + module: 'cc-components#station-login.utils.tsx', + method: 'continueClicked', + error: 'Test callback error', + }); + }); + + it('should log error when validateDialNumber throws exception and return true', () => { + // Mock RegExp constructor to throw an error + const originalRegExp = global.RegExp; + global.RegExp = jest.fn(() => { + throw new Error('Test validation error'); + }) as unknown as typeof RegExp; + + const mockSetError = jest.fn(); + const result = validateDialNumber('15551234567', null, mockSetError, loggerMock); + + expect(loggerMock.error).toHaveBeenCalledWith('CC-Widgets: StationLogin: Error in validateDialNumber', { + module: 'cc-components#station-login.utils.tsx', + method: 'validateDialNumber', + error: 'Test validation error', + }); + expect(result).toBe(true); // Should return true to indicate error state + + // Restore original RegExp + global.RegExp = originalRegExp; + }); + + it('should log error when updateDialNumberLabel throws exception', () => { + const mockSetLabel = jest.fn(() => { + throw new Error('Test label error'); + }); + + updateDialNumberLabel(EXTENSION, mockSetLabel, jest.fn(), loggerMock); + + expect(loggerMock.error).toHaveBeenCalledWith('CC-Widgets: StationLogin: Error in updateDialNumberLabel', { + module: 'cc-components#station-login.utils.tsx', + method: 'updateDialNumberLabel', + error: 'Test label error', + }); + }); + + it('should log error when handleLoginOptionChanged throws exception', () => { + const event = {detail: {value: EXTENSION}}; + const mockSetters = { + setDeviceType: jest.fn(() => { + throw new Error('Test setter error'); + }), + setSelectedDeviceType: jest.fn(), + setDialNumber: jest.fn(), + setDialNumberValue: jest.fn(), + setCurrentLoginOptions: jest.fn(), + setDialNumberLabel: jest.fn(), + setDialNumberPlaceholder: jest.fn(), + setShowDNError: jest.fn(), + setSelectedTeamId: jest.fn(), + setTeamId: jest.fn(), + }; + + handleLoginOptionChanged( + event, + mockSetters.setDeviceType, + mockSetters.setSelectedDeviceType, + jest.fn(), + mockSetters.setDialNumber, + mockSetters.setDialNumberValue, + mockSetters.setCurrentLoginOptions, + {}, + mockSetters.setDialNumberLabel, + mockSetters.setDialNumberPlaceholder, + mockSetters.setShowDNError, + mockSetters.setSelectedTeamId, + mockSetters.setTeamId, + loggerMock, + 'team123' + ); + + expect(loggerMock.error).toHaveBeenCalledWith('CC-Widgets: StationLogin: Error in handleLoginOptionChanged', { + module: 'cc-components#station-login.utils.tsx', + method: 'handleLoginOptionChanged', + error: 'Test setter error', + }); + }); + + it('should log error when handleDNInputChanged throws exception', () => { + const event = {target: {value: 'test'}}; + const mockSetters = { + setDialNumberValue: jest.fn(() => { + throw new Error('Test input error'); + }), + setDialNumber: jest.fn(), + setShowDNError: jest.fn(), + setDNErrorText: jest.fn(), + setCurrentLoginOptions: jest.fn(), + }; + + handleDNInputChanged( + event, + mockSetters.setDialNumberValue, + mockSetters.setDialNumber, + mockSetters.setShowDNError, + mockSetters.setDNErrorText, + null, + mockSetters.setCurrentLoginOptions, + EXTENSION, + loggerMock + ); + + expect(loggerMock.error).toHaveBeenCalledWith('CC-Widgets: StationLogin: Error in handleDNInputChanged', { + module: 'cc-components#station-login.utils.tsx', + method: 'handleDNInputChanged', + error: 'Test input error', + }); + }); + }); }); diff --git a/packages/contact-center/cc-components/tests/components/UserState/user-state.tsx b/packages/contact-center/cc-components/tests/components/UserState/user-state.tsx index a046816da..c91712b58 100644 --- a/packages/contact-center/cc-components/tests/components/UserState/user-state.tsx +++ b/packages/contact-center/cc-components/tests/components/UserState/user-state.tsx @@ -152,7 +152,7 @@ describe('UserStateComponent', () => { render(); }); - expect(getPreviousSelectableStateSpy).toHaveBeenCalledWith(defaultProps.idleCodes); + expect(getPreviousSelectableStateSpy).toHaveBeenCalledWith(defaultProps.idleCodes, defaultProps.logger); }); it('should call getSelectedKey with correct parameters', async () => { @@ -163,7 +163,8 @@ describe('UserStateComponent', () => { expect(getSelectedKeySpy).toHaveBeenCalledWith( defaultProps.customState, defaultProps.currentState, - defaultProps.idleCodes + defaultProps.idleCodes, + defaultProps.logger ); }); @@ -175,7 +176,8 @@ describe('UserStateComponent', () => { expect(buildDropdownItemsSpy).toHaveBeenCalledWith( defaultProps.customState, defaultProps.idleCodes, - defaultProps.currentState + defaultProps.currentState, + defaultProps.logger ); }); @@ -184,7 +186,10 @@ describe('UserStateComponent', () => { render(); }); - expect(sortDropdownItemsSpy).toHaveBeenCalledWith(buildDropdownItemsSpy.mock.results[0].value); + expect(sortDropdownItemsSpy).toHaveBeenCalledWith( + buildDropdownItemsSpy.mock.results[0].value, + defaultProps.logger + ); }); it('should call getDropdownClass with correct parameters', async () => { @@ -195,7 +200,8 @@ describe('UserStateComponent', () => { expect(getDropdownClassSpy).toHaveBeenCalledWith( defaultProps.customState, defaultProps.currentState, - defaultProps.idleCodes + defaultProps.idleCodes, + defaultProps.logger ); }); @@ -207,7 +213,8 @@ describe('UserStateComponent', () => { expect(getTooltipTextSpy).toHaveBeenCalledWith( defaultProps.customState, defaultProps.currentState, - defaultProps.idleCodes + defaultProps.idleCodes, + defaultProps.logger ); }); @@ -217,7 +224,7 @@ describe('UserStateComponent', () => { }); // getIconStyle should be called for the items being rendered - expect(getIconStyleSpy).toHaveBeenCalledWith({id: '0', name: 'Available'}); + expect(getIconStyleSpy).toHaveBeenCalledWith({id: '0', name: 'Available'}, defaultProps.logger); }); }); @@ -245,11 +252,17 @@ describe('UserStateComponent', () => { render(); }); - expect(getSelectedKeySpy).toHaveBeenCalledWith(customState, defaultProps.currentState, defaultProps.idleCodes); + expect(getSelectedKeySpy).toHaveBeenCalledWith( + customState, + defaultProps.currentState, + defaultProps.idleCodes, + defaultProps.logger + ); expect(buildDropdownItemsSpy).toHaveBeenCalledWith( customState, defaultProps.idleCodes, - defaultProps.currentState + defaultProps.currentState, + defaultProps.logger ); }); }); @@ -268,7 +281,8 @@ describe('UserStateComponent', () => { expect(getSelectedKeySpy).toHaveBeenLastCalledWith( customState, defaultProps.currentState, - defaultProps.idleCodes + defaultProps.idleCodes, + defaultProps.logger ); }); @@ -280,11 +294,21 @@ describe('UserStateComponent', () => { screen.rerender(); - expect(getSelectedKeySpy).toHaveBeenCalledWith(defaultProps.customState, '3', defaultProps.idleCodes); + expect(getSelectedKeySpy).toHaveBeenCalledWith( + defaultProps.customState, + '3', + defaultProps.idleCodes, + defaultProps.logger + ); screen.rerender(); - expect(getSelectedKeySpy).toHaveBeenCalledWith(defaultProps.customState, '4', defaultProps.idleCodes); + expect(getSelectedKeySpy).toHaveBeenCalledWith( + defaultProps.customState, + '4', + defaultProps.idleCodes, + defaultProps.logger + ); }); it('should handle elapsed time formatting correctly', async () => { @@ -321,7 +345,7 @@ describe('UserStateComponent', () => { // The getIconStyle should have been called with the item that has an empty name // due to the || '' fallback when currentState is not found in idleCodes - expect(getIconStyleSpy).toHaveBeenCalledWith({id: '0', name: 'Available'}); + expect(getIconStyleSpy).toHaveBeenCalledWith({id: '0', name: 'Available'}, defaultProps.logger); }); }); }); diff --git a/packages/contact-center/cc-components/tests/components/UserState/user-state.utils.tsx b/packages/contact-center/cc-components/tests/components/UserState/user-state.utils.tsx index 4a866056e..4b6be2d03 100644 --- a/packages/contact-center/cc-components/tests/components/UserState/user-state.utils.tsx +++ b/packages/contact-center/cc-components/tests/components/UserState/user-state.utils.tsx @@ -34,27 +34,27 @@ describe('UserState Utils', () => { describe('getDropdownClass', () => { it('should return "custom" when customState is present', () => { const customState = {name: 'Custom State', developerName: 'CUSTOM'}; - const result = getDropdownClass(customState, '1', mockIdleCodes); + const result = getDropdownClass(customState, '1', mockIdleCodes, loggerMock); expect(result).toBe('custom'); }); it('should return "" for "0"', () => { - const result = getDropdownClass(null, '0', mockIdleCodes); + const result = getDropdownClass(null, '0', mockIdleCodes, loggerMock); expect(result).toBe(''); }); it('should return "rona" for RONA state', () => { - const result = getDropdownClass(null, '3', mockIdleCodes); + const result = getDropdownClass(null, '3', mockIdleCodes, loggerMock); expect(result).toBe('rona'); }); it('should return "idle" for other states', () => { - const result = getDropdownClass(null, '1', mockIdleCodes); + const result = getDropdownClass(null, '1', mockIdleCodes, loggerMock); expect(result).toBe('idle'); }); it('should return "idle" when currentState is not found in idleCodes', () => { - const result = getDropdownClass(null, '999', mockIdleCodes); + const result = getDropdownClass(null, '999', mockIdleCodes, loggerMock); expect(result).toBe('idle'); }); }); @@ -62,7 +62,7 @@ describe('UserState Utils', () => { describe('getIconStyle', () => { it('should return correct style for Available state', () => { const item = {id: '0', name: 'Available'}; - const result = getIconStyle(item); + const result = getIconStyle(item, loggerMock); expect(result).toEqual({ class: 'available', iconName: 'active-presence-small-filled', @@ -71,7 +71,7 @@ describe('UserState Utils', () => { it('should return correct style for RONA state', () => { const item = {id: '3', name: 'RONA'}; - const result = getIconStyle(item); + const result = getIconStyle(item, loggerMock); expect(result).toEqual({ class: 'rona', iconName: 'dnd-presence-filled', @@ -80,7 +80,7 @@ describe('UserState Utils', () => { it('should return default style for other states', () => { const item = {id: '1', name: 'Break'}; - const result = getIconStyle(item); + const result = getIconStyle(item, loggerMock); expect(result).toEqual({ class: 'idle', iconName: 'recents-presence-filled', @@ -89,7 +89,7 @@ describe('UserState Utils', () => { it('should return custom style for items with developerName', () => { const item = {id: '1', name: 'Custom State', developerName: 'CUSTOM'}; - const result = getIconStyle(item); + const result = getIconStyle(item, loggerMock); expect(result).toEqual({ class: 'custom', iconName: 'busy-presence-light', @@ -100,24 +100,24 @@ describe('UserState Utils', () => { describe('getTooltipText', () => { it('should return custom available tooltip when customState is present and current state is Available', () => { const customState = {name: 'Custom State', developerName: 'CUSTOM'}; - const result = getTooltipText(customState, '0', mockIdleCodes); + const result = getTooltipText(customState, '0', mockIdleCodes, loggerMock); expect(result).toBe(userStateLabels.customWithAvailableTooltip); }); it('should return custom idle tooltip when customState is present and current state is not Available', () => { const customState = {name: 'Custom State', developerName: 'CUSTOM'}; - const result = getTooltipText(customState, '1', mockIdleCodes); + const result = getTooltipText(customState, '1', mockIdleCodes, loggerMock); expect(result).toBe(userStateLabels.customWithIdleStateTooltip.replace('{{currentState}}', 'Break')); }); it('should return default tooltip when no customState', () => { - const result = getTooltipText(null, '1', mockIdleCodes); + const result = getTooltipText(null, '1', mockIdleCodes, loggerMock); expect(result).toBe(userStateLabels.availableTooltip); }); it('should handle missing currentState in idleCodes', () => { const customState = {name: 'Custom State', developerName: 'CUSTOM'}; - const result = getTooltipText(customState, '999', mockIdleCodes); + const result = getTooltipText(customState, '999', mockIdleCodes, loggerMock); expect(result).toBe(userStateLabels.customWithIdleStateTooltip.replace('{{currentState}}', '')); }); }); @@ -167,7 +167,7 @@ describe('UserState Utils', () => { {id: '3', name: 'Lunch'}, ]; - const result = sortDropdownItems(items); + const result = sortDropdownItems(items, loggerMock); expect(result).toEqual([ {id: '0', name: 'Available'}, @@ -178,7 +178,7 @@ describe('UserState Utils', () => { }); it('should handle empty array', () => { - const result = sortDropdownItems([]); + const result = sortDropdownItems([], loggerMock); expect(result).toEqual([]); }); @@ -189,7 +189,7 @@ describe('UserState Utils', () => { ]; const originalItems = [...items]; - sortDropdownItems(items); + sortDropdownItems(items, loggerMock); expect(items).toEqual(originalItems); }); @@ -201,7 +201,7 @@ describe('UserState Utils', () => { ]; const originalItems = [...items]; - sortDropdownItems(items); + sortDropdownItems(items, loggerMock); expect(items).toEqual(originalItems); }); @@ -209,7 +209,7 @@ describe('UserState Utils', () => { describe('getPreviousSelectableState', () => { it('should return first non-RONA/Engaged state', () => { - const result = getPreviousSelectableState(mockIdleCodes); + const result = getPreviousSelectableState(mockIdleCodes, loggerMock); expect(result).toBe('0'); // Available }); @@ -219,12 +219,12 @@ describe('UserState Utils', () => { {id: '4', name: 'ENGAGED', isSystem: true, isDefault: false}, ]; - const result = getPreviousSelectableState(nonSelectableStates); + const result = getPreviousSelectableState(nonSelectableStates, loggerMock); expect(result).toBe('0'); }); it('should handle empty idleCodes array', () => { - const result = getPreviousSelectableState([]); + const result = getPreviousSelectableState([], loggerMock); expect(result).toBe('0'); }); }); @@ -232,34 +232,34 @@ describe('UserState Utils', () => { describe('getSelectedKey', () => { it('should return custom key when customState is present', () => { const customState = {name: 'Custom State', developerName: 'CUSTOM'}; - const result = getSelectedKey(customState, '1', mockIdleCodes); + const result = getSelectedKey(customState, '1', mockIdleCodes, loggerMock); expect(result).toBe('hide-CUSTOM'); }); it('should return currentState for normal states', () => { - const result = getSelectedKey(null, '1', mockIdleCodes); + const result = getSelectedKey(null, '1', mockIdleCodes, loggerMock); expect(result).toBe('1'); }); it('should return hide-prefixed key for RONA when it is current state', () => { - const result = getSelectedKey(null, '3', mockIdleCodes); + const result = getSelectedKey(null, '3', mockIdleCodes, loggerMock); expect(result).toBe('hide-3'); }); it('should return previous selectable state for ENGAGED', () => { - const result = getSelectedKey(null, '4', mockIdleCodes); + const result = getSelectedKey(null, '4', mockIdleCodes, loggerMock); expect(result).toBe('0'); }); it('should handle missing currentState in idleCodes', () => { - const result = getSelectedKey(null, '999', mockIdleCodes); + const result = getSelectedKey(null, '999', mockIdleCodes, loggerMock); expect(result).toBe('999'); }); }); describe('buildDropdownItems', () => { it('should filter out RONA and ENGAGED states', () => { - const result = buildDropdownItems(null, mockIdleCodes, '1'); + const result = buildDropdownItems(null, mockIdleCodes, '1', loggerMock); expect(result).toEqual([ {id: '0', name: 'Available'}, @@ -270,7 +270,7 @@ describe('UserState Utils', () => { it('should include custom state when present', () => { const customState = {name: 'Custom State', developerName: 'CUSTOM'}; - const result = buildDropdownItems(customState, mockIdleCodes, '1'); + const result = buildDropdownItems(customState, mockIdleCodes, '1', loggerMock); expect(result).toEqual([ {id: 'hide-CUSTOM', name: 'Custom State', developerName: 'CUSTOM'}, @@ -281,13 +281,13 @@ describe('UserState Utils', () => { }); it('should handle empty idleCodes array', () => { - const result = buildDropdownItems(null, [], '1'); + const result = buildDropdownItems(null, [], '1', loggerMock); expect(result).toEqual([]); }); it('should handle empty idleCodes array with custom state', () => { const customState = {name: 'Custom State', developerName: 'CUSTOM'}; - const result = buildDropdownItems(customState, [], '1'); + const result = buildDropdownItems(customState, [], '1', loggerMock); expect(result).toEqual([{id: 'hide-CUSTOM', name: 'Custom State', developerName: 'CUSTOM'}]); }); @@ -298,12 +298,12 @@ describe('UserState Utils', () => { {id: '4', name: 'ENGAGED', isSystem: true, isDefault: false}, ]; - const result = buildDropdownItems(null, ronaEngagedOnly, '1'); + const result = buildDropdownItems(null, ronaEngagedOnly, '1', loggerMock); expect(result).toEqual([]); }); it('should include RONA when it is the current state', () => { - const result = buildDropdownItems(null, mockIdleCodes, '3'); + const result = buildDropdownItems(null, mockIdleCodes, '3', loggerMock); expect(result).toEqual([ {id: '0', name: 'Available'}, @@ -315,7 +315,7 @@ describe('UserState Utils', () => { it('should use correct prefix for custom state', () => { const customState = {name: 'Custom State', developerName: 'CUSTOM'}; - const result = buildDropdownItems(customState, mockIdleCodes, '1'); + const result = buildDropdownItems(customState, mockIdleCodes, '1', loggerMock); expect(result[0]).toEqual({ id: 'hide-CUSTOM', @@ -324,4 +324,180 @@ describe('UserState Utils', () => { }); }); }); + + describe('Error Handling', () => { + beforeEach(() => { + loggerMock.error.mockClear(); + }); + + it('should log error when getDropdownClass throws exception', () => { + // Create corrupted idleCodes that will throw when iterating in the for loop + const corruptedIdleCodes = new Proxy(mockIdleCodes, { + get(target, prop) { + if (prop === Symbol.iterator) { + return function () { + throw new Error('Test getDropdownClass error'); + }; + } + return target[prop]; + }, + }); + + const result = getDropdownClass(null, '3', corruptedIdleCodes, loggerMock); + + expect(loggerMock.error).toHaveBeenCalledWith('CC-Widgets: UserState: Error in getDropdownClass', { + module: 'cc-components#user-state.utils.ts', + method: 'getDropdownClass', + error: 'Test getDropdownClass error', + }); + expect(result).toBe('idle'); // Should return default fallback + }); + + it('should log error when getIconStyle throws exception', () => { + const item = {id: '0', name: 'Available'}; + // Mock the switch statement to throw an error by corrupting the item + const corruptedItem = new Proxy(item, { + get() { + throw new Error('Test getIconStyle error'); + }, + }); + + const result = getIconStyle(corruptedItem, loggerMock); + + expect(loggerMock.error).toHaveBeenCalledWith('CC-Widgets: UserState: Error in getIconStyle', { + module: 'cc-components#user-state.utils.ts', + method: 'getIconStyle', + error: 'Test getIconStyle error', + }); + expect(result).toEqual({class: 'idle', iconName: 'recents-presence-filled'}); + }); + + it('should log error when getTooltipText throws exception', () => { + const customState = {name: 'Custom State', developerName: 'CUSTOM'}; + // Mock array.find to throw an error + const corruptedIdleCodes = new Proxy(mockIdleCodes, { + get(target, prop) { + if (prop === 'find') { + return () => { + throw new Error('Test getTooltipText error'); + }; + } + return target[prop]; + }, + }); + + const result = getTooltipText(customState, '1', corruptedIdleCodes, loggerMock); + + expect(loggerMock.error).toHaveBeenCalledWith('CC-Widgets: UserState: Error in getTooltipText', { + module: 'cc-components#user-state.utils.ts', + method: 'getTooltipText', + error: 'Test getTooltipText error', + }); + expect(result).toBe(userStateLabels.availableTooltip); + }); + + it('should log error when handleSelectionChange throws exception', () => { + const mockSetAgentStatus = jest.fn(() => { + throw new Error('Test handleSelectionChange error'); + }); + + handleSelectionChange('2', '1', mockSetAgentStatus, loggerMock); + + expect(loggerMock.error).toHaveBeenCalledWith('CC-Widgets: UserState: Error in handleSelectionChange', { + module: 'cc-components#user-state.utils.ts', + method: 'handleSelectionChange', + error: 'Test handleSelectionChange error', + }); + }); + + it('should log error when sortDropdownItems throws exception', () => { + // Create corrupted items that will throw on filter operations + const items = [ + {id: '0', name: 'Available'}, + {id: '1', name: 'Break'}, + ]; + const corruptedItems = new Proxy(items, { + get(target, prop) { + if (prop === 'filter') { + return () => { + throw new Error('Test sortDropdownItems error'); + }; + } + return target[prop]; + }, + }); + + const result = sortDropdownItems(corruptedItems, loggerMock); + + expect(loggerMock.error).toHaveBeenCalledWith('CC-Widgets: UserState: Error in sortDropdownItems', { + module: 'cc-components#user-state.utils.ts', + method: 'sortDropdownItems', + error: 'Test sortDropdownItems error', + }); + expect(result).toEqual(items); // Should return original array as fallback + }); + + it('should log error when getPreviousSelectableState throws exception', () => { + // Mock array.find to throw an error + const corruptedIdleCodes = new Proxy(mockIdleCodes, { + get(target, prop) { + if (prop === 'find') { + return () => { + throw new Error('Test getPreviousSelectableState error'); + }; + } + return target[prop]; + }, + }); + + const result = getPreviousSelectableState(corruptedIdleCodes, loggerMock); + + expect(loggerMock.error).toHaveBeenCalledWith('CC-Widgets: UserState: Error in getPreviousSelectableState', { + module: 'cc-components#user-state.utils.ts', + method: 'getPreviousSelectableState', + error: 'Test getPreviousSelectableState error', + }); + expect(result).toBe('0'); // Should return default fallback + }); + + it('should log error when getSelectedKey throws exception', () => { + // Mock Object.values to throw an error + const originalValues = Object.values; + Object.values = jest.fn(() => { + throw new Error('Test getSelectedKey error'); + }); + + const result = getSelectedKey(null, '1', mockIdleCodes, loggerMock); + + expect(loggerMock.error).toHaveBeenCalledWith('CC-Widgets: UserState: Error in getSelectedKey', { + module: 'cc-components#user-state.utils.ts', + method: 'getSelectedKey', + error: 'Test getSelectedKey error', + }); + expect(result).toBe('1'); // Should return original state as fallback + + // Restore original function + Object.values = originalValues; + }); + + it('should log error when buildDropdownItems throws exception', () => { + // Mock Object.values to throw an error + const originalValues = Object.values; + Object.values = jest.fn(() => { + throw new Error('Test buildDropdownItems error'); + }); + + const result = buildDropdownItems(null, mockIdleCodes, '1', loggerMock); + + expect(loggerMock.error).toHaveBeenCalledWith('CC-Widgets: UserState: Error in buildDropdownItems', { + module: 'cc-components#user-state.utils.ts', + method: 'buildDropdownItems', + error: 'Test buildDropdownItems error', + }); + expect(result).toEqual([]); // Should return empty array as fallback + + // Restore original function + Object.values = originalValues; + }); + }); }); diff --git a/packages/contact-center/cc-components/tests/components/task/AutoWrapupTimer/AutoWrapupTimer.tsx b/packages/contact-center/cc-components/tests/components/task/AutoWrapupTimer/AutoWrapupTimer.tsx index 4f9afb52b..2f070e76f 100644 --- a/packages/contact-center/cc-components/tests/components/task/AutoWrapupTimer/AutoWrapupTimer.tsx +++ b/packages/contact-center/cc-components/tests/components/task/AutoWrapupTimer/AutoWrapupTimer.tsx @@ -8,11 +8,19 @@ import {UNTIL_AUTO_WRAPUP, CANCEL} from '../../../../src/components/task/constan describe('AutoWrapupTimer', () => { const mockHandleCancelWrapup = jest.fn(); + const loggerMock = { + error: jest.fn(), + info: jest.fn(), + log: jest.fn(), + warn: jest.fn(), + trace: jest.fn(), + }; const defaultProps: AutoWrapupTimerProps = { secondsUntilAutoWrapup: 30, allowCancelAutoWrapup: false, handleCancelWrapup: mockHandleCancelWrapup, + logger: loggerMock, }; const getTimerUIStateSpy = jest.spyOn(autoWrapupUtils, 'getTimerUIState'); @@ -32,7 +40,7 @@ describe('AutoWrapupTimer', () => { expect(screen.getByText(UNTIL_AUTO_WRAPUP)).toBeInTheDocument(); expect(screen.getByText('00:30')).toBeInTheDocument(); - expect(getTimerUIStateSpy).toHaveBeenCalledWith(30); + expect(getTimerUIStateSpy).toHaveBeenCalledWith(30, loggerMock); const normalListItem = normalContainer.querySelector('mdc-listitem'); const normalIcon = normalContainer.querySelector('mdc-icon'); @@ -64,7 +72,7 @@ describe('AutoWrapupTimer', () => { expect(screen.getByText('00:05')).toBeInTheDocument(); expect(screen.getByText(CANCEL)).toBeInTheDocument(); - expect(getTimerUIStateSpy).toHaveBeenCalledWith(5); + expect(getTimerUIStateSpy).toHaveBeenCalledWith(5, loggerMock); const urgentListItem = urgentContainer.querySelector('mdc-listitem'); const urgentIcon = urgentContainer.querySelector('mdc-icon'); @@ -94,7 +102,7 @@ describe('AutoWrapupTimer', () => { const {container: zeroContainer} = await render(); expect(screen.getByText('00:00')).toBeInTheDocument(); - expect(getTimerUIStateSpy).toHaveBeenCalledWith(0); + expect(getTimerUIStateSpy).toHaveBeenCalledWith(0, loggerMock); const zeroListItem = zeroContainer.querySelector('mdc-listitem'); const zeroIcon = zeroContainer.querySelector('mdc-icon'); diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.utils.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.utils.tsx index bbc77b0a0..93e707a43 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.utils.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.utils.tsx @@ -776,9 +776,9 @@ describe('CallControl Utils', () => { {id: 'code-2', name: 'Customer Inquiry'}, ]; - handleWrapupReasonChange(mockEvent, mockWrapupCodes, mockHandleWrapupChange); + handleWrapupReasonChange(mockEvent, mockWrapupCodes, mockHandleWrapupChange, loggerMock); - expect(mockHandleWrapupChange).toHaveBeenCalledWith('Technical Issue', 'code-1'); + expect(mockHandleWrapupChange).toHaveBeenCalledWith('Technical Issue', 'code-1', loggerMock); }); it('should handle wrapup reason change with unknown selection', () => { diff --git a/packages/contact-center/cc-components/tests/components/task/IncomingTask/incoming-task.snapshot.tsx b/packages/contact-center/cc-components/tests/components/task/IncomingTask/incoming-task.snapshot.tsx index 90d0aaf25..7e35cf0d5 100644 --- a/packages/contact-center/cc-components/tests/components/task/IncomingTask/incoming-task.snapshot.tsx +++ b/packages/contact-center/cc-components/tests/components/task/IncomingTask/incoming-task.snapshot.tsx @@ -30,6 +30,13 @@ describe('IncomingTaskComponent', () => { // Mock functions const mockAccept = jest.fn(); const mockReject = jest.fn(); + const loggerMock = { + error: jest.fn(), + info: jest.fn(), + log: jest.fn(), + warn: jest.fn(), + trace: jest.fn(), + }; // Default props using IncomingTaskComponentProps interface const defaultProps: IncomingTaskComponentProps = { @@ -37,6 +44,7 @@ describe('IncomingTaskComponent', () => { isBrowser: true, accept: mockAccept, reject: mockReject, + logger: loggerMock, }; // Utility function spies diff --git a/packages/contact-center/cc-components/tests/components/task/IncomingTask/incoming-task.tsx b/packages/contact-center/cc-components/tests/components/task/IncomingTask/incoming-task.tsx index 87dafb3a5..1be995f06 100644 --- a/packages/contact-center/cc-components/tests/components/task/IncomingTask/incoming-task.tsx +++ b/packages/contact-center/cc-components/tests/components/task/IncomingTask/incoming-task.tsx @@ -30,6 +30,13 @@ describe('IncomingTaskComponent', () => { // Mock functions const mockAccept = jest.fn(); const mockReject = jest.fn(); + const loggerMock = { + error: jest.fn(), + info: jest.fn(), + log: jest.fn(), + warn: jest.fn(), + trace: jest.fn(), + }; // Default props using IncomingTaskComponentProps interface const defaultProps: IncomingTaskComponentProps = { @@ -37,6 +44,7 @@ describe('IncomingTaskComponent', () => { isBrowser: true, accept: mockAccept, reject: mockReject, + logger: loggerMock, }; // Utility function spies @@ -208,10 +216,10 @@ describe('IncomingTaskComponent', () => { // Verify utility function was called correctly for each task expect(extractIncomingTaskDataSpy).toHaveBeenCalledTimes(4); - expect(extractIncomingTaskDataSpy).toHaveBeenNthCalledWith(1, telephonyWebRTCTask, true); - expect(extractIncomingTaskDataSpy).toHaveBeenNthCalledWith(2, telephonyExtensionTask, false); - expect(extractIncomingTaskDataSpy).toHaveBeenNthCalledWith(3, chatTask, true); - expect(extractIncomingTaskDataSpy).toHaveBeenNthCalledWith(4, socialTask, true); + expect(extractIncomingTaskDataSpy).toHaveBeenNthCalledWith(1, telephonyWebRTCTask, true, loggerMock); + expect(extractIncomingTaskDataSpy).toHaveBeenNthCalledWith(2, telephonyExtensionTask, false, loggerMock); + expect(extractIncomingTaskDataSpy).toHaveBeenNthCalledWith(3, chatTask, true, loggerMock); + expect(extractIncomingTaskDataSpy).toHaveBeenNthCalledWith(4, socialTask, true, loggerMock); // === WebRTC Telephony Task Assertions === const webRTCListItem = webRTCContainer.querySelector('li.task-list-item'); diff --git a/packages/contact-center/station-login/src/helper.ts b/packages/contact-center/station-login/src/helper.ts index 806fe8fcc..e66285be6 100644 --- a/packages/contact-center/station-login/src/helper.ts +++ b/packages/contact-center/station-login/src/helper.ts @@ -27,9 +27,16 @@ export const useStationLogin = (props: UseStationLoginProps) => { // useEffect to be called on mount useEffect(() => { - setSelectedDeviceType(deviceType || ''); - setDialNumberValue(dialNumber || ''); - setSelectedTeamId(teamId || ''); + try { + setSelectedDeviceType(deviceType || ''); + setDialNumberValue(dialNumber || ''); + setSelectedTeamId(teamId || ''); + } catch (error) { + logger.error(`CC-Widgets: Error in useEffect (mount) - ${error.message}`, { + module: 'widget-station-login#helper.ts', + method: 'useEffect', + }); + } }, [isAgentLoggedIn]); // Track original and current login options as a single object @@ -49,16 +56,23 @@ export const useStationLogin = (props: UseStationLoginProps) => { // Set original login options after successful login useEffect(() => { - if (store.isAgentLoggedIn) { - setOriginalLoginOptions({ - deviceType: store.deviceType, - dialNumber: store.dialNumber, - teamId: store.teamId || '', - }); - setCurrentLoginOptions({ - deviceType: store.deviceType, - dialNumber: store.dialNumber, - teamId: store.teamId || '', + try { + if (store.isAgentLoggedIn) { + setOriginalLoginOptions({ + deviceType: store.deviceType, + dialNumber: store.dialNumber, + teamId: store.teamId || '', + }); + setCurrentLoginOptions({ + deviceType: store.deviceType, + dialNumber: store.dialNumber, + teamId: store.teamId || '', + }); + } + } catch (error) { + logger.error(`CC-Widgets: Error in useEffect (setOriginalLoginOptions) - ${error.message}`, { + module: 'widget-station-login#helper.ts', + method: 'useEffect', }); } }, [store.isAgentLoggedIn]); @@ -71,68 +85,98 @@ export const useStationLogin = (props: UseStationLoginProps) => { originalLoginOptions.teamId !== currentLoginOptions.teamId; const saveLoginOptions = () => { - setSaveError(''); - if (!isLoginOptionsChanged) { - setSaveError('No changes detected in login options.'); - logger.log('No changes detected in login options.', { - module: 'widget-station-login#helper.ts', - method: 'saveLoginOptions', - }); - if (props.onSaveEnd) props.onSaveEnd(false); - return; - } + try { + setSaveError(''); + if (!isLoginOptionsChanged) { + setSaveError('No changes detected in login options.'); + logger.log('No changes detected in login options.', { + module: 'widget-station-login#helper.ts', + method: 'saveLoginOptions', + }); + if (props.onSaveEnd) props.onSaveEnd(false); + return; + } - if (props.onSaveStart) props.onSaveStart(); + if (props.onSaveStart) props.onSaveStart(); - // Prepare payload for updateAgentProfile - const payload: AgentProfileUpdate = { - loginOption: currentLoginOptions.deviceType as LoginOption, - teamId: currentLoginOptions.teamId || undefined, - }; - if (currentLoginOptions.deviceType !== 'BROWSER') { - payload.dialNumber = currentLoginOptions.dialNumber; - } + // Prepare payload for updateAgentProfile + const payload: AgentProfileUpdate = { + loginOption: currentLoginOptions.deviceType as LoginOption, + teamId: currentLoginOptions.teamId || undefined, + }; + if (currentLoginOptions.deviceType !== 'BROWSER') { + payload.dialNumber = currentLoginOptions.dialNumber; + } - logger.log('Saving login options:', { - module: 'widget-station-login#helper.ts', - method: 'saveLoginOptions', - }); + logger.log('Saving login options:', { + module: 'widget-station-login#helper.ts', + method: 'saveLoginOptions', + }); - cc.updateAgentProfile(payload) - .then(() => { - setOriginalLoginOptions({...currentLoginOptions}); - setSaveError(''); - logger.log('Agent profile updated successfully.', { - module: 'widget-station-login#helper.ts', - method: 'saveLoginOptions', + cc.updateAgentProfile(payload) + .then(() => { + setOriginalLoginOptions({...currentLoginOptions}); + setSaveError(''); + logger.log('Agent profile updated successfully.', { + module: 'widget-station-login#helper.ts', + method: 'saveLoginOptions', + }); + if (props.onSaveEnd) props.onSaveEnd(true); + }) + .catch((error: Error) => { + logger.error('Failed to update agent device type', { + module: 'widget-station-login#helper.ts', + method: 'saveLoginOptions', + }); + setSaveError(error.message || 'Failed to update device type'); + if (props.onSaveEnd) props.onSaveEnd(false); }); - if (props.onSaveEnd) props.onSaveEnd(true); - }) - .catch((error: Error) => { - logger.error('Failed to update agent device type', { - module: 'widget-station-login#helper.ts', - method: 'saveLoginOptions', - }); - setSaveError(error.message || 'Failed to update device type'); - if (props.onSaveEnd) props.onSaveEnd(false); + } catch (error) { + logger.error(`CC-Widgets: Error in saveLoginOptions - ${error.message}`, { + module: 'widget-station-login#helper.ts', + method: 'saveLoginOptions', }); + setSaveError('Failed to save login options'); + if (props.onSaveEnd) props.onSaveEnd(false); + } }; useEffect(() => { - if (loginCb && store.isAgentLoggedIn) { - loginCb(); + try { + if (loginCb && store.isAgentLoggedIn) { + loginCb(); + } + } catch (error) { + logger.error(`CC-Widgets: Error in useEffect (loginCb) - ${error.message}`, { + module: 'widget-station-login#helper.ts', + method: 'useEffect', + }); } }, []); const handleLogout = () => { - if (logoutCb) { - logoutCb(); + try { + if (logoutCb) { + logoutCb(); + } + } catch (error) { + logger.error(`CC-Widgets: Error in handleLogout - ${error.message}`, { + module: 'widget-station-login#helper.ts', + method: 'handleLogout', + }); } }; const handleLogin = () => { - if (loginCb) { - loginCb(); + try { + if (loginCb) { + loginCb(); + } + } catch (error) { + logger.error(`CC-Widgets: Error in handleLogin - ${error.message}`, { + module: 'widget-station-login#helper.ts', + method: 'handleLogin', + }); } }; @@ -153,14 +197,21 @@ export const useStationLogin = (props: UseStationLoginProps) => { // Make sure to set the callback are same and change the logout logic useEffect(() => { - store.setCCCallback(CC_EVENTS.AGENT_STATION_LOGIN_SUCCESS, handleLogin); - store.setCCCallback(CC_EVENTS.AGENT_LOGOUT_SUCCESS, handleLogout); - - // TODO: WHen we close this event listener it closes the event listener from storeEventWrapper - // return () => { - // store.removeCCCallback(CC_EVENTS.AGENT_STATION_LOGIN_SUCCESS, handleLogin); - // store.removeCCCallback(CC_EVENTS.AGENT_LOGOUT_SUCCESS, handleLogout); - // }; + try { + store.setCCCallback(CC_EVENTS.AGENT_STATION_LOGIN_SUCCESS, handleLogin); + store.setCCCallback(CC_EVENTS.AGENT_LOGOUT_SUCCESS, handleLogout); + + // TODO: WHen we close this event listener it closes the event listener from storeEventWrapper + // return () => { + // store.removeCCCallback(CC_EVENTS.AGENT_STATION_LOGIN_SUCCESS, handleLogin); + // store.removeCCCallback(CC_EVENTS.AGENT_LOGOUT_SUCCESS, handleLogout); + // }; + } catch (error) { + logger.error(`CC-Widgets: Error in useEffect (setCCCallback) - ${error.message}`, { + module: 'widget-station-login#helper.ts', + method: 'useEffect', + }); + } }, [store.isAgentLoggedIn]); const handleContinue = async () => { @@ -187,49 +238,76 @@ export const useStationLogin = (props: UseStationLoginProps) => { }; const login = () => { - cc.stationLogin({teamId: team, loginOption: deviceType, dialNumber}) - .then((res: StationLoginSuccessResponse) => { - logger.log('CC-Widgets: useStationLogin login(): stationLogin success', { - module: 'widget-station-login#helper.ts', - method: 'login', - }); - setLoginSuccess(res); - setLoginFailure(undefined); - }) - .catch((error: Error) => { - logger.error(`Error logging in: ${error}`, { - module: 'widget-station-login#helper.ts', - method: 'login', + try { + cc.stationLogin({teamId: team, loginOption: deviceType, dialNumber}) + .then((res: StationLoginSuccessResponse) => { + logger.log('CC-Widgets: useStationLogin login(): stationLogin success', { + module: 'widget-station-login#helper.ts', + method: 'login', + }); + setLoginSuccess(res); + setLoginFailure(undefined); + }) + .catch((error: Error) => { + logger.error(`Error logging in: ${error}`, { + module: 'widget-station-login#helper.ts', + method: 'login', + }); + setLoginSuccess(undefined); + setLoginFailure(error); }); - setLoginSuccess(undefined); - setLoginFailure(error); + } catch (error) { + logger.error(`CC-Widgets: Error in login - ${error.message}`, { + module: 'widget-station-login#helper.ts', + method: 'login', }); + setLoginSuccess(undefined); + setLoginFailure(error); + } }; const logout = () => { - logger.info('CC-Widgets: useStationLogin logout(): invoking stationLogout', { - module: 'widget-station-login#helper.ts', - method: 'logout', - }); - cc.stationLogout({logoutReason: 'User requested logout'}) - .then((res: LogoutSuccess) => { - logger.log('CC-Widgets: useStationLogin logout(): stationLogout success', { - module: 'widget-station-login#helper.ts', - method: 'logout', - }); - setLogoutSuccess(res); - }) - .catch((error: Error) => { - logger.error(`CC-Widgets: Error logging out: ${error}`, { - module: 'widget-station-login#helper.ts', - method: 'logout', + try { + logger.info('CC-Widgets: useStationLogin logout(): invoking stationLogout', { + module: 'widget-station-login#helper.ts', + method: 'logout', + }); + cc.stationLogout({logoutReason: 'User requested logout'}) + .then((res: LogoutSuccess) => { + logger.log('CC-Widgets: useStationLogin logout(): stationLogout success', { + module: 'widget-station-login#helper.ts', + method: 'logout', + }); + setLogoutSuccess(res); + }) + .catch((error: Error) => { + logger.error(`CC-Widgets: Error logging out: ${error}`, { + module: 'widget-station-login#helper.ts', + method: 'logout', + }); }); + } catch (error) { + logger.error(`CC-Widgets: Error in logout - ${error.message}`, { + module: 'widget-station-login#helper.ts', + method: 'logout', + }); + } + }; + + const handleSetTeam = (teamValue: string) => { + try { + setTeam(teamValue); + } catch (error) { + logger.error(`CC-Widgets: Error in setTeam - ${error.message}`, { + module: 'widget-station-login#helper.ts', + method: 'setTeam', }); + } }; return { name: 'StationLogin', - setTeam, + setTeam: handleSetTeam, login, logout, loginSuccess, diff --git a/packages/contact-center/station-login/tests/helper.ts b/packages/contact-center/station-login/tests/helper.ts index f0f30c491..ad9ccb852 100644 --- a/packages/contact-center/station-login/tests/helper.ts +++ b/packages/contact-center/station-login/tests/helper.ts @@ -708,6 +708,198 @@ describe('useStationLogin Hook', () => { ); }); + describe('Error Handling', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should handle errors in saveLoginOptions main logic', () => { + const mockOnSaveEnd = jest.fn(); + const mockCC = { + ...ccMock, + updateAgentProfile: jest.fn().mockImplementation(() => { + throw new Error('Test error in saveLoginOptions'); + }), + }; + + const {result} = renderHook(() => + useStationLogin({ + ...baseStationLoginProps, + cc: mockCC, + onSaveEnd: mockOnSaveEnd, + }) + ); + + act(() => { + result.current.setCurrentLoginOptions({ + deviceType: 'EXTENSION', + dialNumber: '999999999', + teamId: 'team123', + }); + }); + + act(() => { + result.current.saveLoginOptions(); + }); + + expect(logger.error).toHaveBeenCalledWith( + 'CC-Widgets: Error in saveLoginOptions - Test error in saveLoginOptions', + { + module: 'widget-station-login#helper.ts', + method: 'saveLoginOptions', + } + ); + expect(mockOnSaveEnd).toHaveBeenCalledWith(false); + }); + + it('should handle errors in main logout method', () => { + const mockCC = { + ...ccMock, + stationLogout: jest.fn().mockImplementation(() => { + throw new Error('Test error in logout method'); + }), + }; + + const {result} = renderHook(() => + useStationLogin({ + ...baseStationLoginProps, + cc: mockCC, + }) + ); + + act(() => { + result.current.logout(); + }); + + expect(logger.error).toHaveBeenCalledWith('CC-Widgets: Error in logout - Test error in logout method', { + module: 'widget-station-login#helper.ts', + method: 'logout', + }); + }); + + it('should handle errors in main login method', () => { + const mockCC = { + ...ccMock, + stationLogin: jest.fn().mockImplementation(() => { + throw new Error('Test error in login method'); + }), + }; + + const {result} = renderHook(() => + useStationLogin({ + ...baseStationLoginProps, + cc: mockCC, + }) + ); + + act(() => { + result.current.setTeam('team123'); + }); + + act(() => { + result.current.login(); + }); + + expect(logger.error).toHaveBeenCalledWith('CC-Widgets: Error in login - Test error in login method', { + module: 'widget-station-login#helper.ts', + method: 'login', + }); + }); + + it('should handle errors when stationLogout is unavailable', () => { + const mockCC = { + ...ccMock, + stationLogout: undefined, // This will cause an error when trying to call it + }; + + const {result} = renderHook(() => + useStationLogin({ + ...baseStationLoginProps, + cc: mockCC, + }) + ); + + act(() => { + result.current.logout(); + }); + + expect(logger.error).toHaveBeenCalledWith(expect.stringMatching(/^CC-Widgets: Error in logout/), { + module: 'widget-station-login#helper.ts', + method: 'logout', + }); + }); + + it('should handle errors in setTeam gracefully', () => { + const {result} = renderHook(() => useStationLogin(baseStationLoginProps)); + + // Test that setTeam works without errors for normal case + act(() => { + result.current.setTeam('test-team'); + }); + + // The actual error handling in setTeam is internal to React state management + // So we verify it doesn't throw and handles gracefully + expect(() => { + act(() => { + result.current.setTeam('another-team'); + }); + }).not.toThrow(); + + // We can't easily test internal try-catch of useState, but we ensure + // the wrapper function works correctly + expect(result.current.setTeam).toBeDefined(); + expect(typeof result.current.setTeam).toBe('function'); + }); + + it('should handle errors in useEffect initialization gracefully', () => { + // Test that useEffect error handling doesn't break the hook + const {result} = renderHook(() => + useStationLogin({ + ...baseStationLoginProps, + deviceType: 'BROWSER', + dialNumber: '12345', + teamId: 'testTeam', + }) + ); + + // Verify the hook still works correctly even with potential internal errors + expect(result.current.selectedDeviceType).toBe('BROWSER'); + expect(result.current.dialNumberValue).toBe('12345'); + expect(result.current.selectedTeamId).toBe('testTeam'); + }); + + it('should handle callback errors in login/logout handlers', () => { + const mockLoginCallback = jest.fn(() => { + throw new Error('Login callback error'); + }); + + const mockLogoutCallback = jest.fn(() => { + throw new Error('Logout callback error'); + }); + + const {result} = renderHook(() => + useStationLogin({ + ...baseStationLoginProps, + onLogin: mockLoginCallback, + onLogout: mockLogoutCallback, + }) + ); + + // These should not throw errors due to our try-catch wrappers + expect(() => { + act(() => { + result.current.login(); + }); + }).not.toThrow(); + + expect(() => { + act(() => { + result.current.logout(); + }); + }).not.toThrow(); + }); + }); + describe('#onCCSignOut', () => { beforeEach(() => { jest.clearAllMocks(); 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 1321f803f..c3cf16bd6 100644 --- a/packages/contact-center/station-login/tests/station-login/index.tsx +++ b/packages/contact-center/station-login/tests/station-login/index.tsx @@ -3,6 +3,7 @@ import {render} from '@testing-library/react'; import {StationLogin} from '../../src'; import * as helper from '../../src/helper'; import '@testing-library/jest-dom'; +import store from '@webex/cc-store'; const teamsMock = ['team123', 'team456']; const ccMock = { @@ -40,6 +41,7 @@ jest.mock('@webex/cc-store', () => { CC_EVENTS: { AGENT_STATION_LOGIN_SUCCESS: 'AgentStationLoginSuccess', }, + onErrorCallback: jest.fn(), }; }); @@ -50,6 +52,16 @@ const onSaveStart = jest.fn(); const onSaveEnd = jest.fn(); describe('StationLogin Component', () => { + beforeEach(() => { + jest.clearAllMocks(); + // Suppress console.error for error boundary tests + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + it('renders StationLoginPresentational with correct props', () => { const useStationLoginSpy = jest.spyOn(helper, 'useStationLogin'); @@ -80,4 +92,21 @@ describe('StationLogin Component', () => { doStationLogout: undefined, }); }); + + describe('ErrorBoundary Tests', () => { + const mockOnErrorCallback = jest.fn(); + store.onErrorCallback = mockOnErrorCallback; + it('should render empty fragment when ErrorBoundary catches an error', () => { + // Mock the StationLoginInternal to throw an error by overriding the helper + jest.spyOn(helper, 'useStationLogin').mockImplementation(() => { + throw new Error('Test error in useStationLogin'); + }); + + const {container} = render(); + + // The fallback should render an empty fragment (no content) + expect(container.firstChild).toBeNull(); + expect(store.onErrorCallback).toHaveBeenCalledWith('StationLogin', Error('Test error in useStationLogin')); + }); + }); }); diff --git a/packages/contact-center/task/src/IncomingTask/index.tsx b/packages/contact-center/task/src/IncomingTask/index.tsx index de845834a..146686e71 100644 --- a/packages/contact-center/task/src/IncomingTask/index.tsx +++ b/packages/contact-center/task/src/IncomingTask/index.tsx @@ -21,15 +21,7 @@ const IncomingTaskInternal: React.FunctionComponent = observe } ); -const IncomingTask: React.FunctionComponent = observer(({incomingTask, onAccepted, onRejected}) => { - const {deviceType, logger} = store; - const result = useIncomingTask({incomingTask, onAccepted, onRejected, deviceType, logger}); - - const props = { - ...result, - logger, - }; - +const IncomingTask: React.FunctionComponent = (props) => { return ( <>} @@ -40,6 +32,6 @@ const IncomingTask: React.FunctionComponent = observer(({inco ); -}); +}; export {IncomingTask}; diff --git a/packages/contact-center/task/src/OutdialCall/index.tsx b/packages/contact-center/task/src/OutdialCall/index.tsx index aa6eff781..9f554c293 100644 --- a/packages/contact-center/task/src/OutdialCall/index.tsx +++ b/packages/contact-center/task/src/OutdialCall/index.tsx @@ -1,10 +1,11 @@ import React from 'react'; import store from '@webex/cc-store'; import {observer} from 'mobx-react-lite'; +import {ErrorBoundary} from 'react-error-boundary'; import {OutdialCallComponent} from '@webex/cc-components'; import {useOutdialCall} from '../helper'; -const OutdialCall: React.FunctionComponent = observer(() => { +const OutdialCallInternal: React.FunctionComponent = observer(() => { const {cc, logger} = store; const result = useOutdialCall({cc, logger}); @@ -15,4 +16,17 @@ const OutdialCall: React.FunctionComponent = observer(() => { return ; }); +const OutdialCall: React.FunctionComponent = (props) => { + return ( + <>} + onError={(error: Error) => { + if (store.onErrorCallback) store.onErrorCallback('OutdialCall', error); + }} + > + + + ); +}; + export {OutdialCall}; diff --git a/packages/contact-center/task/src/Utils/task-util.ts b/packages/contact-center/task/src/Utils/task-util.ts index 568dd354c..5248334db 100644 --- a/packages/contact-center/task/src/Utils/task-util.ts +++ b/packages/contact-center/task/src/Utils/task-util.ts @@ -1,3 +1,4 @@ +import {ILogger} from '@webex/cc-store'; import {ITask} from '@webex/plugin-cc'; /** @@ -5,49 +6,84 @@ import {ITask} from '@webex/plugin-cc'; * @param task The task object * @returns An object containing the visibility of various controls based on the task's data */ -export function getControlsVisibility(deviceType: string, featureFlags: {[key: string]: boolean}, task: ITask) { - const {mediaType} = task?.data?.interaction || {}; - - const isCall = mediaType === 'telephony'; - const isChat = mediaType === 'chat'; - const isEmail = mediaType === 'email'; - - const isBrowser = deviceType === 'BROWSER'; - const isAgentDN = deviceType === 'AGENT_DN'; - const isExtension = deviceType === 'EXTENSION'; - - const {isEndCallEnabled, isEndConsultEnabled, webRtcEnabled} = featureFlags; - - const controls = { - accept: - (isBrowser && ((webRtcEnabled && isCall) || isChat || isEmail)) || - (isAgentDN && (isChat || isEmail)) || - (isExtension && (isChat || isEmail)), - decline: isBrowser && webRtcEnabled && isCall, - end: isBrowser || (isEndCallEnabled && isCall) || !isCall, - muteUnmute: isBrowser && webRtcEnabled && isCall, - - holdResume: isCall && ((isBrowser && webRtcEnabled) || isAgentDN || isExtension), // Applicable for all type of station login - consult: isCall && ((isBrowser && webRtcEnabled) || isAgentDN || isExtension), // Applicable for all type of station login - transfer: isBrowser ? webRtcEnabled : true, // Applicable for all type of station login and media type - - conference: (isBrowser && isCall && webRtcEnabled) || isChat, // This needs further testing after we add support - wrapup: task?.data?.wrapUpRequired ?? false, // Applicable for all type of station login and media type and getting actual value from task data - pauseResumeRecording: isCall && ((isBrowser && webRtcEnabled) || isAgentDN || isExtension), // Getting feature flag (isRecordingManagementEnabled) value as undefined, need further testing - endConsult: isEndConsultEnabled && isCall && ((isBrowser && webRtcEnabled) || isAgentDN || isExtension), - recordingIndicator: isCall, - }; - - return controls; +export function getControlsVisibility( + deviceType: string, + featureFlags: {[key: string]: boolean}, + task: ITask, + logger?: ILogger +) { + try { + const {mediaType} = task?.data?.interaction || {}; + + const isCall = mediaType === 'telephony'; + const isChat = mediaType === 'chat'; + const isEmail = mediaType === 'email'; + + const isBrowser = deviceType === 'BROWSER'; + const isAgentDN = deviceType === 'AGENT_DN'; + const isExtension = deviceType === 'EXTENSION'; + + const {isEndCallEnabled, isEndConsultEnabled, webRtcEnabled} = featureFlags; + + const controls = { + accept: + (isBrowser && ((webRtcEnabled && isCall) || isChat || isEmail)) || + (isAgentDN && (isChat || isEmail)) || + (isExtension && (isChat || isEmail)), + decline: isBrowser && webRtcEnabled && isCall, + end: isBrowser || (isEndCallEnabled && isCall) || !isCall, + muteUnmute: isBrowser && webRtcEnabled && isCall, + + holdResume: isCall && ((isBrowser && webRtcEnabled) || isAgentDN || isExtension), // Applicable for all type of station login + consult: isCall && ((isBrowser && webRtcEnabled) || isAgentDN || isExtension), // Applicable for all type of station login + transfer: isBrowser ? webRtcEnabled : true, // Applicable for all type of station login and media type + + conference: (isBrowser && isCall && webRtcEnabled) || isChat, // This needs further testing after we add support + wrapup: task?.data?.wrapUpRequired ?? false, // Applicable for all type of station login and media type and getting actual value from task data + pauseResumeRecording: isCall && ((isBrowser && webRtcEnabled) || isAgentDN || isExtension), // Getting feature flag (isRecordingManagementEnabled) value as undefined, need further testing + endConsult: isEndConsultEnabled && isCall && ((isBrowser && webRtcEnabled) || isAgentDN || isExtension), + recordingIndicator: isCall, + }; + + return controls; + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in getControlsVisibility - ${error.message}`, { + module: 'task-util', + method: 'getControlsVisibility', + }); + // Return safe default controls + return { + accept: false, + decline: false, + end: false, + muteUnmute: false, + holdResume: false, + consult: false, + transfer: false, + conference: false, + wrapup: false, + pauseResumeRecording: false, + endConsult: false, + recordingIndicator: false, + }; + } } //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 -export function findHoldTimestamp(interaction: Interaction, mType = 'mainCall'): number | null { - if (!interaction?.media) return null; - for (const key in interaction.media) { - if (interaction.media[key].mType === mType) { - return interaction.media[key].holdTimestamp ?? null; +export function findHoldTimestamp(interaction: Interaction, mType = 'mainCall', logger?: ILogger): number | null { + try { + if (!interaction?.media) return null; + for (const key in interaction.media) { + if (interaction.media[key].mType === mType) { + return interaction.media[key].holdTimestamp ?? null; + } } + return null; + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in findHoldTimestamp - ${error.message}`, { + module: 'task-util', + method: 'findHoldTimestamp', + }); + return null; } - return null; } diff --git a/packages/contact-center/task/src/helper.ts b/packages/contact-center/task/src/helper.ts index 73e8ed83a..bd57ac74a 100644 --- a/packages/contact-center/task/src/helper.ts +++ b/packages/contact-center/task/src/helper.ts @@ -20,58 +20,107 @@ export const useTaskList = (props: UseTaskListProps) => { }; useEffect(() => { - if (onTaskAccepted) { - store.setTaskAssigned(function (task) { - logger.log(`CC-Widgets: taskAssigned event for ${task.data.interactionId}`, { - module: 'useTaskList', - method: 'setTaskAssigned', + try { + if (onTaskAccepted) { + store.setTaskAssigned(function (task) { + try { + logger.log(`CC-Widgets: taskAssigned event for ${task.data.interactionId}`, { + module: 'useTaskList', + method: 'setTaskAssigned', + }); + onTaskAccepted(task); + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in taskAssigned callback - ${error.message}`, { + module: 'useTaskList', + method: 'setTaskAssigned', + }); + } }); - onTaskAccepted(task); - }); - } + } - if (onTaskDeclined) { - store.setTaskRejected(function (task, reason) { - logger.log(`CC-Widgets: taskRejected event for ${task.data.interactionId}`, { - module: 'useTaskList', - method: 'setTaskRejected', + if (onTaskDeclined) { + store.setTaskRejected(function (task, reason) { + try { + logger.log(`CC-Widgets: taskRejected event for ${task.data.interactionId}`, { + module: 'useTaskList', + method: 'setTaskRejected', + }); + onTaskDeclined(task, reason); + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in taskRejected callback - ${error.message}`, { + module: 'useTaskList', + method: 'setTaskRejected', + }); + } }); - onTaskDeclined(task, reason); - }); - } + } - if (onTaskSelected) { - store.setTaskSelected(function (task: ITask, isClicked: boolean) { - onTaskSelected({task, isClicked}); + if (onTaskSelected) { + store.setTaskSelected(function (task: ITask, isClicked: boolean) { + try { + onTaskSelected({task, isClicked}); + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in taskSelected callback - ${error.message}`, { + module: 'useTaskList', + method: 'setTaskSelected', + }); + } + }); + } + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in useTaskList useEffect - ${error.message}`, { + module: 'useTaskList', + method: 'useEffect', }); } }, []); const acceptTask = (task: ITask) => { - logger.info(`CC-Widgets: acceptTask called for ${task.data.interactionId}`, { - module: 'useTaskList', - method: 'acceptTask', - }); - task.accept().catch((error) => { - logError(`CC-Widgets: Error accepting task: ${error}`, 'acceptTask'); - }); + try { + logger.info(`CC-Widgets: acceptTask called for ${task.data.interactionId}`, { + module: 'useTaskList', + method: 'acceptTask', + }); + task.accept().catch((error) => { + logError(`CC-Widgets: Error accepting task: ${error}`, 'acceptTask'); + }); + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in acceptTask - ${error.message}`, { + module: 'useTaskList', + method: 'acceptTask', + }); + } }; const declineTask = (task: ITask) => { - logger.info(`CC-Widgets: declineTask called for ${task.data.interactionId}`, { - module: 'useTaskList', - method: 'declineTask', - }); - task.decline().catch((error) => { - logError(`CC-Widgets: Error declining task: ${error}`, 'declineTask'); - }); - logger.log(`CC-Widgets: incoming task declined for ${task.data.interactionId}`, { - module: 'useTaskList', - method: 'declineTask', - }); + try { + logger.info(`CC-Widgets: declineTask called for ${task.data.interactionId}`, { + module: 'useTaskList', + method: 'declineTask', + }); + task.decline().catch((error) => { + logError(`CC-Widgets: Error declining task: ${error}`, 'declineTask'); + }); + logger.log(`CC-Widgets: incoming task declined for ${task.data.interactionId}`, { + module: 'useTaskList', + method: 'declineTask', + }); + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in declineTask - ${error.message}`, { + module: 'useTaskList', + method: 'declineTask', + }); + } }; const onTaskSelect = (task: ITask) => { - store.setCurrentTask(task, true); + try { + store.setCurrentTask(task, true); + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in onTaskSelect - ${error.message}`, { + module: 'useTaskList', + method: 'onTaskSelect', + }); + } }; return {taskList, acceptTask, declineTask, onTaskSelect, isBrowser}; @@ -82,33 +131,73 @@ export const useIncomingTask = (props: UseTaskProps) => { const isBrowser = deviceType === 'BROWSER'; const taskAssignCallback = () => { - if (onAccepted) onAccepted({task: incomingTask}); + try { + if (onAccepted) onAccepted({task: incomingTask}); + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in taskAssignCallback - ${error.message}`, { + module: 'useIncomingTask', + method: 'taskAssignCallback', + }); + } }; const taskRejectCallback = () => { - if (onRejected) onRejected({task: incomingTask}); + try { + if (onRejected) onRejected({task: incomingTask}); + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in taskRejectCallback - ${error.message}`, { + module: 'useIncomingTask', + method: 'taskRejectCallback', + }); + } }; useEffect(() => { - if (!incomingTask) return; - store.setTaskCallback( - TASK_EVENTS.TASK_ASSIGNED, - () => { - if (onAccepted) onAccepted({task: incomingTask}); - }, - incomingTask.data.interactionId - ); - store.setTaskCallback(TASK_EVENTS.TASK_CONSULT_ACCEPTED, taskAssignCallback, incomingTask?.data.interactionId); - store.setTaskCallback(TASK_EVENTS.TASK_END, taskRejectCallback, incomingTask?.data.interactionId); - store.setTaskCallback(TASK_EVENTS.TASK_REJECT, taskRejectCallback, incomingTask?.data.interactionId); - store.setTaskCallback(TASK_EVENTS.TASK_CONSULT_END, taskRejectCallback, incomingTask?.data.interactionId); - return () => { - store.removeTaskCallback(TASK_EVENTS.TASK_ASSIGNED, taskAssignCallback, incomingTask?.data.interactionId); - store.removeTaskCallback(TASK_EVENTS.TASK_CONSULT_ACCEPTED, taskAssignCallback, incomingTask?.data.interactionId); - store.removeTaskCallback(TASK_EVENTS.TASK_END, taskRejectCallback, incomingTask?.data.interactionId); - store.removeTaskCallback(TASK_EVENTS.TASK_REJECT, taskRejectCallback, incomingTask?.data.interactionId); - store.removeTaskCallback(TASK_EVENTS.TASK_CONSULT_END, taskRejectCallback, incomingTask?.data.interactionId); - }; + try { + if (!incomingTask) return; + store.setTaskCallback( + TASK_EVENTS.TASK_ASSIGNED, + () => { + try { + if (onAccepted) onAccepted({task: incomingTask}); + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in TASK_ASSIGNED callback - ${error.message}`, { + module: 'useIncomingTask', + method: 'TASK_ASSIGNED_callback', + }); + } + }, + incomingTask.data.interactionId + ); + store.setTaskCallback(TASK_EVENTS.TASK_CONSULT_ACCEPTED, taskAssignCallback, incomingTask?.data.interactionId); + store.setTaskCallback(TASK_EVENTS.TASK_END, taskRejectCallback, incomingTask?.data.interactionId); + store.setTaskCallback(TASK_EVENTS.TASK_REJECT, taskRejectCallback, incomingTask?.data.interactionId); + store.setTaskCallback(TASK_EVENTS.TASK_CONSULT_END, taskRejectCallback, incomingTask?.data.interactionId); + + return () => { + try { + store.removeTaskCallback(TASK_EVENTS.TASK_ASSIGNED, taskAssignCallback, incomingTask?.data.interactionId); + store.removeTaskCallback( + TASK_EVENTS.TASK_CONSULT_ACCEPTED, + taskAssignCallback, + incomingTask?.data.interactionId + ); + store.removeTaskCallback(TASK_EVENTS.TASK_END, taskRejectCallback, incomingTask?.data.interactionId); + store.removeTaskCallback(TASK_EVENTS.TASK_REJECT, taskRejectCallback, incomingTask?.data.interactionId); + store.removeTaskCallback(TASK_EVENTS.TASK_CONSULT_END, taskRejectCallback, incomingTask?.data.interactionId); + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in useIncomingTask cleanup - ${error.message}`, { + module: 'useIncomingTask', + method: 'useEffect_cleanup', + }); + } + }; + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in useIncomingTask useEffect - ${error.message}`, { + module: 'useIncomingTask', + method: 'useEffect', + }); + } }, [incomingTask]); const logError = (message: string, method: string) => { @@ -119,33 +208,47 @@ export const useIncomingTask = (props: UseTaskProps) => { }; const accept = () => { - logger.info(`CC-Widgets: incomingTask.accept() called`, { - module: 'useIncomingTask', - method: 'accept', - }); - if (!incomingTask?.data.interactionId) return; - incomingTask.accept().catch((error) => { - logError(`CC-Widgets: Error accepting incoming task: ${error}`, 'accept'); - }); - logger.log(`CC-Widgets: incomingTask accepted`, { - module: 'useIncomingTask', - method: 'accept', - }); + try { + logger.info(`CC-Widgets: incomingTask.accept() called`, { + module: 'useIncomingTask', + method: 'accept', + }); + if (!incomingTask?.data.interactionId) return; + incomingTask.accept().catch((error) => { + logError(`CC-Widgets: Error accepting incoming task: ${error}`, 'accept'); + }); + logger.log(`CC-Widgets: incomingTask accepted`, { + module: 'useIncomingTask', + method: 'accept', + }); + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in accept - ${error.message}`, { + module: 'useIncomingTask', + method: 'accept', + }); + } }; const reject = () => { - logger.info(`CC-Widgets: incomingTask.reject() called`, { - module: 'useIncomingTask', - method: 'reject', - }); - if (!incomingTask?.data.interactionId) return; - incomingTask.decline().catch((error) => { - logError(`CC-Widgets: Error rejecting incoming task: ${error}`, 'reject'); - }); - logger.log(`CC-Widgets: incomingTask rejected`, { - module: 'useIncomingTask', - method: 'reject', - }); + try { + logger.info(`CC-Widgets: incomingTask.reject() called`, { + module: 'useIncomingTask', + method: 'reject', + }); + if (!incomingTask?.data.interactionId) return; + incomingTask.decline().catch((error) => { + logError(`CC-Widgets: Error rejecting incoming task: ${error}`, 'reject'); + }); + logger.log(`CC-Widgets: incomingTask rejected`, { + module: 'useIncomingTask', + method: 'reject', + }); + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in reject - ${error.message}`, { + module: 'useIncomingTask', + method: 'reject', + }); + } }; return { @@ -214,7 +317,7 @@ export const useCallControl = (props: useCallControlProps) => { // Get holdTimestamp from the interaction object const holdTimestamp = currentTask?.data?.interaction - ? findHoldTimestamp(currentTask.data.interaction, 'mainCall') + ? findHoldTimestamp(currentTask.data.interaction, 'mainCall', logger) : null; if (holdTimestamp) { @@ -247,26 +350,34 @@ export const useCallControl = (props: useCallControlProps) => { }, [currentTask?.data?.interaction]); // Function to extract consulting agent information const extractConsultingAgent = useCallback(() => { - if (!currentTask?.data?.interaction?.participants) return; + try { + if (!currentTask?.data?.interaction?.participants) return; - const {interaction} = currentTask.data; - const myAgentId = store.cc.agentConfig?.agentId; + const {interaction} = currentTask.data; + const myAgentId = store.cc.agentConfig?.agentId; - // Find all agent participants except the current agent - const otherAgents = Object.values(interaction.participants || {}).filter( - (participant): participant is Participant => - (participant as Participant).pType === 'Agent' && (participant as Participant).id !== myAgentId - ); + // Find all agent participants except the current agent + const otherAgents = Object.values(interaction.participants || {}).filter( + (participant): participant is Participant => + (participant as Participant).pType === 'Agent' && (participant as Participant).id !== myAgentId + ); - // Pick the first other agent (should only be one in a consult) - const foundAgent = otherAgents.length > 0 ? {id: otherAgents[0].id, name: otherAgents[0].name} : null; + // Pick the first other agent (should only be one in a consult) + const foundAgent = otherAgents.length > 0 ? {id: otherAgents[0].id, name: otherAgents[0].name} : null; - if (foundAgent) { - setConsultAgentName(foundAgent.name); - setConsultAgentId(foundAgent.id); - logger.info(`Consulting agent detected: ${foundAgent.name} ${foundAgent.id}`, { - module: 'widget-cc-task#helper.ts', - method: 'useCallControl#extractConsultingAgent', + if (foundAgent) { + setConsultAgentName(foundAgent.name); + setConsultAgentId(foundAgent.id); + logger.info(`Consulting agent detected: ${foundAgent.name} ${foundAgent.id}`, { + module: 'widget-cc-task#helper.ts', + method: 'useCallControl#extractConsultingAgent', + }); + } + } catch (error) { + console.log('error', error); + logger.error(`CC-Widgets: Task: Error in extractConsultingAgent - ${error.message}`, { + module: 'useCallControl', + method: 'extractConsultingAgent', }); } }, [currentTask, logger, consultInitiated]); @@ -289,7 +400,10 @@ export const useCallControl = (props: useCallControlProps) => { logger.info(`Loaded ${agents.length} buddy agents`, {module: 'helper.ts', method: 'loadBuddyAgents'}); setBuddyAgents(agents); } catch (error) { - logger.error(`Error loading buddy agents: ${error}`, {module: 'helper.ts', method: 'loadBuddyAgents'}); + logger?.error(`CC-Widgets: Task: Error loading buddy agents - ${error.message || error}`, { + module: 'useCallControl', + method: 'loadBuddyAgents', + }); setBuddyAgents([]); } }, [logger]); @@ -299,63 +413,108 @@ export const useCallControl = (props: useCallControlProps) => { const queues = await store.getQueues(); setQueues(queues); } catch (error) { - logError(`Error loading queues: ${error}`, 'loadQueues'); + logger?.error(`CC-Widgets: Task: Error loading queues - ${error.message || error}`, { + module: 'useCallControl', + method: 'loadQueues', + }); setQueues([]); } }, [logger]); const holdCallback = () => { - setIsHeld(true); - if (onHoldResume) { - onHoldResume({ - isHeld: true, - task: currentTask, + try { + setIsHeld(true); + if (onHoldResume) { + onHoldResume({ + isHeld: true, + task: currentTask, + }); + } + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in holdCallback - ${error.message}`, { + module: 'useCallControl', + method: 'holdCallback', }); } }; const resumeCallback = () => { - setIsHeld(false); - if (onHoldResume) { - onHoldResume({ - isHeld: false, - task: currentTask, + try { + setIsHeld(false); + if (onHoldResume) { + onHoldResume({ + isHeld: false, + task: currentTask, + }); + } + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in resumeCallback - ${error.message}`, { + module: 'useCallControl', + method: 'resumeCallback', }); } }; const endCallCallback = () => { - if (onEnd) { - onEnd({ - task: currentTask, + try { + if (onEnd) { + onEnd({ + task: currentTask, + }); + } + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in endCallCallback - ${error.message}`, { + module: 'useCallControl', + method: 'endCallCallback', }); } }; const wrapupCallCallback = ({wrapUpAuxCodeId}) => { - const wrapUpReason = store.wrapupCodes.find((code) => code.id === wrapUpAuxCodeId)?.name; - if (onWrapUp) { - onWrapUp({ - task: currentTask, - wrapUpReason: wrapUpReason, + try { + const wrapUpReason = store.wrapupCodes.find((code) => code.id === wrapUpAuxCodeId)?.name; + if (onWrapUp) { + onWrapUp({ + task: currentTask, + wrapUpReason: wrapUpReason, + }); + } + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in wrapupCallCallback - ${error.message}`, { + module: 'useCallControl', + method: 'wrapupCallCallback', }); } }; const pauseRecordingCallback = () => { - setIsRecording(false); - onRecordingToggle({ - isRecording: false, - task: currentTask, - }); + try { + setIsRecording(false); + onRecordingToggle({ + isRecording: false, + task: currentTask, + }); + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in pauseRecordingCallback - ${error.message}`, { + module: 'useCallControl', + method: 'pauseRecordingCallback', + }); + } }; const resumeRecordingCallback = () => { - setIsRecording(true); - onRecordingToggle({ - isRecording: true, - task: currentTask, - }); + try { + setIsRecording(true); + onRecordingToggle({ + isRecording: true, + task: currentTask, + }); + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in resumeRecordingCallback - ${error.message}`, { + module: 'useCallControl', + method: 'resumeRecordingCallback', + }); + } }; useEffect(() => { @@ -403,90 +562,127 @@ export const useCallControl = (props: useCallControlProps) => { }; const toggleHold = (hold: boolean) => { - logger.info(`toggleHold(${hold}) called`, {module: 'useCallControl', method: 'toggleHold'}); - if (hold) { - currentTask - .hold() - .catch((e) => logger.error(`Hold failed: ${e}`, {module: 'useCallControl', method: 'toggleHold'})); - } else { - currentTask - .resume() - .catch((e) => logger.error(`Resume failed: ${e}`, {module: 'useCallControl', method: 'toggleHold'})); + try { + logger.info(`toggleHold(${hold}) called`, {module: 'useCallControl', method: 'toggleHold'}); + if (hold) { + currentTask + .hold() + .catch((e) => logger.error(`Hold failed: ${e}`, {module: 'useCallControl', method: 'toggleHold'})); + } else { + currentTask + .resume() + .catch((e) => logger.error(`Resume failed: ${e}`, {module: 'useCallControl', method: 'toggleHold'})); + } + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in toggleHold - ${error.message}`, { + module: 'useCallControl', + method: 'toggleHold', + }); } }; const toggleRecording = () => { - if (isRecording) { - currentTask.pauseRecording().catch((error: Error) => { - logError(`Error pausing recording: ${error}`, 'toggleRecording'); - }); - } else { - currentTask.resumeRecording({autoResumed: false}).catch((error: Error) => { - logError(`Error resuming recording: ${error}`, 'toggleRecording'); + try { + if (isRecording) { + currentTask.pauseRecording().catch((error: Error) => { + logError(`Error pausing recording: ${error}`, 'toggleRecording'); + }); + } else { + currentTask.resumeRecording({autoResumed: false}).catch((error: Error) => { + logError(`Error resuming recording: ${error}`, 'toggleRecording'); + }); + } + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in toggleRecording - ${error.message}`, { + module: 'useCallControl', + method: 'toggleRecording', }); } }; const toggleMute = async () => { - console.log('Mute control not available', controlVisibility); - if (!controlVisibility?.muteUnmute) { - logger.warn('Mute control not available', {module: 'useCallControl', method: 'toggleMute'}); - return; - } + try { + console.log('Mute control not available', controlVisibility); + if (!controlVisibility?.muteUnmute) { + logger.warn('Mute control not available', {module: 'useCallControl', method: 'toggleMute'}); + return; + } - logger.info('toggleMute() called', {module: 'useCallControl', method: 'toggleMute'}); + logger.info('toggleMute() called', {module: 'useCallControl', method: 'toggleMute'}); - // Store the intended new state - const intendedMuteState = !isMuted; + // Store the intended new state + const intendedMuteState = !isMuted; - try { - //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 - await currentTask.toggleMute(); + try { + //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 + await currentTask.toggleMute(); - // Only update state after successful SDK call - store.setIsMuted(intendedMuteState); + // Only update state after successful SDK call + store.setIsMuted(intendedMuteState); - if (onToggleMute) { - onToggleMute({ - isMuted: intendedMuteState, - task: currentTask, - }); - } + if (onToggleMute) { + onToggleMute({ + isMuted: intendedMuteState, + task: currentTask, + }); + } - logger.info(`Mute state toggled to: ${intendedMuteState}`, {module: 'useCallControl', method: 'toggleMute'}); - } catch (error) { - logger.error(`toggleMute failed: ${error}`, {module: 'useCallControl', method: 'toggleMute'}); + logger.info(`Mute state toggled to: ${intendedMuteState}`, {module: 'useCallControl', method: 'toggleMute'}); + } catch (error) { + logger.error(`toggleMute failed: ${error}`, {module: 'useCallControl', method: 'toggleMute'}); - if (onToggleMute) { - onToggleMute({ - isMuted: isMuted, - task: currentTask, - }); + if (onToggleMute) { + onToggleMute({ + isMuted: isMuted, + task: currentTask, + }); + } } + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in toggleMute - ${error.message}`, { + module: 'useCallControl', + method: 'toggleMute', + }); } }; const endCall = () => { - logger.info('endCall() called', {module: 'useCallControl', method: 'endCall'}); - currentTask.end().catch((e) => logger.error(`endCall failed: ${e}`, {module: 'useCallControl', method: 'endCall'})); + try { + logger.info('endCall() called', {module: 'useCallControl', method: 'endCall'}); + currentTask + .end() + .catch((e) => logger.error(`endCall failed: ${e}`, {module: 'useCallControl', method: 'endCall'})); + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in endCall - ${error.message}`, { + module: 'useCallControl', + method: 'endCall', + }); + } }; const wrapupCall = (wrapUpReason: string, auxCodeId: string) => { - currentTask - .wrapup({wrapUpReason: wrapUpReason, auxCodeId: auxCodeId}) - .then(() => { - const taskKeys = Object.keys(store.taskList); - if (taskKeys.length > 0) { - store.setCurrentTask(store.taskList[taskKeys[0]]); - store.setState({ - developerName: ENGAGED_LABEL, - name: ENGAGED_USERNAME, - }); - } - }) - .catch((error: Error) => { - logError(`Error wrapping up call: ${error}`, 'wrapupCall'); + try { + currentTask + .wrapup({wrapUpReason: wrapUpReason, auxCodeId: auxCodeId}) + .then(() => { + const taskKeys = Object.keys(store.taskList); + if (taskKeys.length > 0) { + store.setCurrentTask(store.taskList[taskKeys[0]]); + store.setState({ + developerName: ENGAGED_LABEL, + name: ENGAGED_USERNAME, + }); + } + }) + .catch((error: Error) => { + logError(`Error wrapping up call: ${error}`, 'wrapupCall'); + }); + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in wrapupCall - ${error.message}`, { + module: 'useCallControl', + method: 'wrapupCall', }); + } }; const transferCall = async (to: string, type: DestinationType) => { @@ -572,8 +768,8 @@ export const useCallControl = (props: useCallControlProps) => { }; const controlVisibility = useMemo( - () => getControlsVisibility(deviceType, featureFlags, currentTask), - [deviceType, featureFlags, currentTask] + () => getControlsVisibility(deviceType, featureFlags, currentTask, logger), + [deviceType, featureFlags, currentTask, logger] ); // Add useEffect for auto wrap-up timer @@ -651,22 +847,29 @@ export const useOutdialCall = (props: useOutdialCallProps) => { const {cc, logger} = props; const startOutdial = (destination: string) => { - // Perform validation on destination number. - if (!destination || !destination.trim()) { - alert('Destination number is required, it cannot be empty'); - return; - } - //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 - cc.startOutdial(destination) - .then((response) => { - logger.info('Outdial call started', response); - }) - .catch((error: Error) => { - logger.error(`${error}`, { - module: 'widget-OutdialCall#helper.ts', - method: 'startOutdial', + try { + // Perform validation on destination number. + if (!destination || !destination.trim()) { + alert('Destination number is required, it cannot be empty'); + return; + } + //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 + cc.startOutdial(destination) + .then((response) => { + logger.info('Outdial call started', response); + }) + .catch((error: Error) => { + logger.error(`${error}`, { + module: 'widget-OutdialCall#helper.ts', + method: 'startOutdial', + }); }); + } catch (error) { + logger?.error(`CC-Widgets: Task: Error in startOutdial - ${error.message}`, { + module: 'useOutdialCall', + method: 'startOutdial', }); + } }; return { diff --git a/packages/contact-center/task/tests/CallControl/index.tsx b/packages/contact-center/task/tests/CallControl/index.tsx index c89af5998..afd21a7b1 100644 --- a/packages/contact-center/task/tests/CallControl/index.tsx +++ b/packages/contact-center/task/tests/CallControl/index.tsx @@ -12,6 +12,16 @@ const onWrapUpCb = jest.fn(); const onRecordingToggleCb = jest.fn(); describe('CallControl Component', () => { + beforeEach(() => { + jest.clearAllMocks(); + // Suppress console.error for error boundary tests + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + it('renders CallControlPresentational with correct props', () => { const useCallControlSpy = jest.spyOn(helper, 'useCallControl').mockReturnValue({ currentTask: mockTask, @@ -83,4 +93,29 @@ describe('CallControl Component', () => { onToggleMute: undefined, }); }); + + describe('ErrorBoundary Tests', () => { + it('should render empty fragment when ErrorBoundary catches an error', () => { + const mockOnErrorCallback = jest.fn(); + store.onErrorCallback = mockOnErrorCallback; + + // Mock the useCallControl to throw an error + jest.spyOn(helper, 'useCallControl').mockImplementation(() => { + throw new Error('Test error in useCallControl'); + }); + + const {container} = render( + + ); + + // The fallback should render an empty fragment (no content) + expect(container.firstChild).toBeNull(); + expect(mockOnErrorCallback).toHaveBeenCalledWith('CallControl', Error('Test error in useCallControl')); + }); + }); }); diff --git a/packages/contact-center/task/tests/IncomingTask/index.tsx b/packages/contact-center/task/tests/IncomingTask/index.tsx index 7b0fdc862..4658367e7 100644 --- a/packages/contact-center/task/tests/IncomingTask/index.tsx +++ b/packages/contact-center/task/tests/IncomingTask/index.tsx @@ -17,6 +17,15 @@ const onAcceptedCb = jest.fn(); const onRejectedCb = jest.fn(); describe('IncomingTask Component', () => { + beforeEach(() => { + jest.clearAllMocks(); + // Suppress console.error for error boundary tests + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + it('renders IncomingTaskPresentational with correct props', () => { const useIncomingTaskSpy = jest.spyOn(helper, 'useIncomingTask'); @@ -38,4 +47,21 @@ describe('IncomingTask Component', () => { onRejected: onRejectedCb, }); }); + + describe('ErrorBoundary Tests', () => { + it('should render empty fragment when ErrorBoundary catches an error', () => { + jest.spyOn(helper, 'useIncomingTask').mockImplementation(() => { + throw new Error('Test error in useIncomingTask'); + }); + const mockOnErrorCallback = jest.fn(); + store.onErrorCallback = mockOnErrorCallback; + const {container} = render( + + ); + + expect(container.firstChild).toBeNull(); + expect(mockOnErrorCallback).toHaveBeenCalledWith('IncomingTask', Error('Test error in useIncomingTask')); + expect(mockOnErrorCallback).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/packages/contact-center/task/tests/OutdialCall/index.tsx b/packages/contact-center/task/tests/OutdialCall/index.tsx index ab415bcbf..4db942fbe 100644 --- a/packages/contact-center/task/tests/OutdialCall/index.tsx +++ b/packages/contact-center/task/tests/OutdialCall/index.tsx @@ -2,6 +2,7 @@ import React from 'react'; import {render} from '@testing-library/react'; import * as helper from '../../src/helper'; import {OutdialCall} from '../../src/OutdialCall'; +import store from '@webex/cc-store'; // Mock dependencies jest.mock('@webex/cc-store', () => ({ @@ -12,9 +13,20 @@ jest.mock('@webex/cc-store', () => ({ error: jest.fn(), warn: jest.fn(), }, + onErrorCallback: jest.fn(), })); describe('OutdialCall Component', () => { + beforeEach(() => { + jest.clearAllMocks(); + // Suppress console.error for error boundary tests + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + it('render OutdialCallComponent with correct props', () => { const useOutdialCallSpy = jest.spyOn(helper, 'useOutdialCall'); render(); @@ -29,4 +41,32 @@ describe('OutdialCall Component', () => { }, }); }); + + describe('ErrorBoundary Tests', () => { + it('should render empty fragment when ErrorBoundary catches an error and call the callback', () => { + // Mock the useOutdialCall to throw an error + jest.spyOn(helper, 'useOutdialCall').mockImplementation(() => { + throw new Error('Test error in useOutdialCall'); + }); + + const {container} = render(); + + // The fallback should render an empty fragment (no content) + expect(container.firstChild).toBeNull(); + expect(store.onErrorCallback).toHaveBeenCalledWith('OutdialCall', Error('Test error in useOutdialCall')); + }); + it('should render empty fragment when ErrorBoundary catches an error and call the callback', () => { + // Mock the useOutdialCall to throw an error + jest.spyOn(helper, 'useOutdialCall').mockImplementation(() => { + throw new Error('Test error in useOutdialCall'); + }); + store.onErrorCallback = undefined; + + const {container} = render(); + + // The fallback should render an empty fragment (no content) + expect(container.firstChild).toBeNull(); + expect(store.onErrorCallback).toBeUndefined(); + }); + }); }); diff --git a/packages/contact-center/task/tests/TaskList/index.tsx b/packages/contact-center/task/tests/TaskList/index.tsx index 18326e80b..78d7fd71b 100644 --- a/packages/contact-center/task/tests/TaskList/index.tsx +++ b/packages/contact-center/task/tests/TaskList/index.tsx @@ -31,7 +31,17 @@ jest.mock('@webex/cc-store', () => ({ describe('TaskList Component', () => { const helperSpy = jest.spyOn(helper, 'useTaskList'); - afterEach(cleanup); + + beforeEach(() => { + jest.clearAllMocks(); + // Suppress console.error for error boundary tests + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + cleanup(); + jest.restoreAllMocks(); + }); it('renders TaskListPresentational with the correct props', () => { render(); @@ -51,4 +61,24 @@ describe('TaskList Component', () => { taskList: taskListMock, }); }); + + describe('ErrorBoundary Tests', () => { + it('should render empty fragment when ErrorBoundary catches an error', () => { + const mockOnErrorCallback = jest.fn(); + store.onErrorCallback = mockOnErrorCallback; + // Mock the useTaskList to throw an error + jest.spyOn(helper, 'useTaskList').mockImplementation(() => { + throw new Error('Test error in useTaskList'); + }); + + const {container} = render( + + ); + + // The fallback should render an empty fragment (no content) + expect(container.firstChild).toBeNull(); + expect(mockOnErrorCallback).toHaveBeenCalledWith('TaskList', expect.any(Error)); + expect(mockOnErrorCallback).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/packages/contact-center/task/tests/helper.ts b/packages/contact-center/task/tests/helper.ts index 5749c4c1b..14ec2f7c7 100644 --- a/packages/contact-center/task/tests/helper.ts +++ b/packages/contact-center/task/tests/helper.ts @@ -323,6 +323,71 @@ describe('useIncomingTask Hook', () => { method: 'useIncomingTask#reject', }); }); + describe('useIncomingTask Error Handling', () => { + const onRejected = jest.fn(); + it('should handle errors in taskAssignCallback', () => { + const errorOnAccepted = jest.fn().mockImplementation(() => { + throw new Error('Test error in onAccepted'); + }); + + const setTaskCallbackSpy = jest.spyOn(store, 'setTaskCallback'); + + renderHook(() => + useIncomingTask({ + onAccepted: errorOnAccepted, + onRejected, + deviceType: 'BROWSER', + incomingTask: taskMock, + logger, + }) + ); + + // Find the taskAssignCallback + const taskAssignCallback = setTaskCallbackSpy.mock.calls.find( + (call) => call[0] === TASK_EVENTS.TASK_ASSIGNED + )?.[1]; + + act(() => { + taskAssignCallback(); + }); + + expect(logger.error).toHaveBeenCalledWith( + 'CC-Widgets: Task: Error in TASK_ASSIGNED callback - Test error in onAccepted', + { + module: 'useIncomingTask', + method: 'TASK_ASSIGNED_callback', + } + ); + }); + + it('should handle errors in accept method', () => { + const mockErrorTask = { + ...taskMock, + accept: jest.fn().mockImplementation(() => { + throw new Error('Accept synchronous error'); + }), + }; + + const {result} = renderHook(() => + useIncomingTask({ + onAccepted, + onRejected, + deviceType: 'BROWSER', + incomingTask: mockErrorTask, + logger, + }) + ); + + act(() => { + result.current.accept(); + }); + + expect(logger.error).toHaveBeenCalledWith('CC-Widgets: Task: Error in accept - Accept synchronous error', { + module: 'useIncomingTask', + method: 'accept', + }); + }); + }); }); describe('useTaskList Hook', () => { @@ -531,6 +596,135 @@ describe('useTaskList Hook', () => { // Ensure no errors are logged expect(logger.error).not.toHaveBeenCalled(); }); + describe('useTaskList Error Handling', () => { + it('should handle errors in taskAssigned callback', () => { + const errorOnTaskAccepted = jest.fn().mockImplementation(() => { + throw new Error('Test error in onTaskAccepted'); + }); + + const setTaskAssignedSpy = jest.spyOn(store, 'setTaskAssigned'); + + renderHook(() => + useTaskList({ + onTaskAccepted: errorOnTaskAccepted, + onTaskDeclined, + onTaskSelected, + logger, + taskList: {}, + deviceType: 'BROWSER', + cc: mockCC, + }) + ); + + // Trigger the callback + const taskAssignedCallback = setTaskAssignedSpy.mock.calls[0][0]; + act(() => { + taskAssignedCallback(taskMock); + }); + + expect(logger.error).toHaveBeenCalledWith( + 'CC-Widgets: Task: Error in taskAssigned callback - Test error in onTaskAccepted', + { + module: 'useTaskList', + method: 'setTaskAssigned', + } + ); + }); + + it('should handle errors in taskSelected callback', () => { + const errorOnTaskSelected = jest.fn().mockImplementation(() => { + throw new Error('Test error in onTaskSelected'); + }); + + const setTaskSelectedSpy = jest.spyOn(store, 'setTaskSelected'); + + renderHook(() => + useTaskList({ + onTaskAccepted: onTaskAccepted, + onTaskDeclined, + onTaskSelected: errorOnTaskSelected, + logger, + taskList: {}, + deviceType: 'BROWSER', + cc: mockCC, + }) + ); + + // Trigger the callback + const taskSelectedCallback = setTaskSelectedSpy.mock.calls[0][0]; + act(() => { + taskSelectedCallback(taskMock, true); + }); + + expect(logger.error).toHaveBeenCalledWith( + 'CC-Widgets: Task: Error in taskSelected callback - Test error in onTaskSelected', + { + module: 'useTaskList', + method: 'setTaskSelected', + } + ); + }); + + it('should handle errors in acceptTask', () => { + const mockErrorTask = { + ...taskMock, + accept: jest.fn().mockImplementation(() => { + throw new Error('Task accept error'); + }), + }; + + const {result} = renderHook(() => + useTaskList({ + onTaskAccepted, + onTaskDeclined, + onTaskSelected, + logger, + taskList: {}, + deviceType: 'BROWSER', + cc: mockCC, + }) + ); + + act(() => { + result.current.acceptTask(mockErrorTask); + }); + + expect(logger.error).toHaveBeenCalledWith('CC-Widgets: Task: Error in acceptTask - Task accept error', { + module: 'useTaskList', + method: 'acceptTask', + }); + }); + + it('should handle errors in onTaskSelect', () => { + const originalSetCurrentTask = store.setCurrentTask; + store.setCurrentTask = jest.fn().mockImplementation(() => { + throw new Error('setCurrentTask error'); + }); + + const {result} = renderHook(() => + useTaskList({ + onTaskAccepted, + onTaskDeclined, + onTaskSelected, + logger, + taskList: {}, + deviceType: 'BROWSER', + cc: mockCC, + }) + ); + + act(() => { + result.current.onTaskSelect(taskMock); + }); + + expect(logger.error).toHaveBeenCalledWith('CC-Widgets: Task: Error in onTaskSelect - setCurrentTask error', { + module: 'useTaskList', + method: 'onTaskSelect', + }); + + store.setCurrentTask = originalSetCurrentTask; + }); + }); }); describe('useCallControl', () => { @@ -1120,10 +1314,13 @@ describe('useCallControl', () => { await result.current.loadBuddyAgents(); }); expect(result.current.buddyAgents).toEqual([]); - expect(mockLogger.error).toHaveBeenCalledWith('Error loading buddy agents: Error: Buddy agents loading failed', { - module: 'helper.ts', - method: 'loadBuddyAgents', - }); + expect(mockLogger.error).toHaveBeenCalledWith( + 'CC-Widgets: Task: Error loading buddy agents - Buddy agents loading failed', + { + module: 'useCallControl', + method: 'loadBuddyAgents', + } + ); getBuddyAgentsSpy.mockRestore(); }); @@ -2261,6 +2458,174 @@ describe('useCallControl', () => { expect(mockCurrentTask.toggleMute).not.toHaveBeenCalled(); }); }); + + describe('useCallControl Error Handling', () => { + const onHoldResume = jest.fn(); + const onEnd = jest.fn(); + const onWrapUp = jest.fn(); + const onRecordingToggle = jest.fn(); + const onToggleMute = jest.fn(); + const logger = { + error: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + log: jest.fn(), + trace: jest.fn(), + }; + + it('should handle errors in extractConsultingAgent', () => { + // Mock currentTask with problematic participants structure + jest.spyOn(logger, 'info').mockImplementation(() => { + throw new Error('Participants access error'); + }); + const problematicTask = { + ...taskMock, + data: { + ...taskMock.data, + interaction: { + participants: { + '123': { + pType: 'Agent', + id: '123', + name: 'Agent 1', + }, + }, + }, + }, + }; + + renderHook(() => + useCallControl({ + currentTask: problematicTask, + onHoldResume, + onEnd, + onWrapUp, + onRecordingToggle, + onToggleMute, + logger, + consultInitiated: false, + deviceType: 'BROWSER', + featureFlags: {webRtcEnabled: true}, + isMuted: false, + }) + ); + + expect(logger.error).toHaveBeenCalledWith( + 'CC-Widgets: Task: Error in extractConsultingAgent - Participants access error', + { + module: 'useCallControl', + method: 'extractConsultingAgent', + } + ); + }); + + it('should handle errors in holdCallback', () => { + const errorOnHoldResume = jest.fn().mockImplementation(() => { + throw new Error('Hold resume callback error'); + }); + + const setTaskCallbackSpy = jest.spyOn(store, 'setTaskCallback'); + + renderHook(() => + useCallControl({ + currentTask: taskMock, + onHoldResume: errorOnHoldResume, + onEnd, + onWrapUp, + onRecordingToggle, + onToggleMute, + logger, + consultInitiated: false, + deviceType: 'BROWSER', + featureFlags: {webRtcEnabled: true}, + isMuted: false, + }) + ); + + // Find the hold callback + const holdCallback = setTaskCallbackSpy.mock.calls.find((call) => call[0] === TASK_EVENTS.TASK_HOLD)?.[1]; + + act(() => { + holdCallback(); + }); + + expect(logger.error).toHaveBeenCalledWith( + 'CC-Widgets: Task: Error in holdCallback - Hold resume callback error', + { + module: 'useCallControl', + method: 'holdCallback', + } + ); + }); + + it('should handle errors in toggleHold', () => { + const mockErrorTask = { + ...taskMock, + hold: jest.fn().mockImplementation(() => { + throw new Error('Hold method error'); + }), + }; + + const {result} = renderHook(() => + useCallControl({ + currentTask: mockErrorTask, + onHoldResume, + onEnd, + onWrapUp, + onRecordingToggle, + onToggleMute, + logger, + consultInitiated: false, + deviceType: 'BROWSER', + featureFlags: {webRtcEnabled: true}, + isMuted: false, + }) + ); + + act(() => { + result.current.toggleHold(true); + }); + + expect(logger.error).toHaveBeenCalledWith('CC-Widgets: Task: Error in toggleHold - Participants access error', { + module: 'useCallControl', + method: 'toggleHold', + }); + }); + + it('should handle errors in loadBuddyAgents', async () => { + const originalGetBuddyAgents = store.getBuddyAgents; + store.getBuddyAgents = jest.fn().mockImplementation(() => { + throw new Error('getBuddyAgents error'); + }); + + const {result} = renderHook(() => + useCallControl({ + currentTask: taskMock, + onHoldResume, + onEnd, + onWrapUp, + onRecordingToggle, + onToggleMute, + logger, + consultInitiated: false, + deviceType: 'BROWSER', + featureFlags: {webRtcEnabled: true}, + isMuted: false, + }) + ); + + await act(async () => { + await result.current.loadBuddyAgents(); + }); + + expect(logger.error).toHaveBeenCalledWith('CC-Widgets: Task: Error loading buddy agents - getBuddyAgents error', { + module: 'useCallControl', + method: 'loadBuddyAgents', + }); + + store.getBuddyAgents = originalGetBuddyAgents; + }); + }); }); describe('useOutdialCall', () => { @@ -2363,4 +2728,33 @@ describe('useOutdialCall', () => { expect(ccMock.startOutdial).not.toHaveBeenCalled(); expect(logger.info).not.toHaveBeenCalled(); }); + describe('useOutdialCall Error Handling', () => { + it('should handle errors in startOutdial', () => { + const mockErrorCC = { + ...mockCC, + startOutdial: jest.fn().mockImplementation(() => { + throw new Error('startOutdial synchronous error'); + }), + }; + + const {result} = renderHook(() => + useOutdialCall({ + cc: mockErrorCC, + logger, + }) + ); + + act(() => { + result.current.startOutdial('1234567890'); + }); + + expect(logger.error).toHaveBeenCalledWith( + 'CC-Widgets: Task: Error in startOutdial - startOutdial synchronous error', + { + module: 'useOutdialCall', + method: 'startOutdial', + } + ); + }); + }); }); diff --git a/packages/contact-center/task/tests/utils/task-util.ts b/packages/contact-center/task/tests/utils/task-util.ts index ac4b9375c..3fc45299b 100644 --- a/packages/contact-center/task/tests/utils/task-util.ts +++ b/packages/contact-center/task/tests/utils/task-util.ts @@ -223,6 +223,51 @@ describe('getControlsVisibility', () => { expect(getControlsVisibility(deviceType, featureFlags, task)).toEqual(expectedControls); }); + + it('should handle errors when accessing featureFlags and return safe defaults', () => { + const logger = { + error: jest.fn(), + log: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + trace: jest.fn(), + }; + const deviceType = 'BROWSER'; + // Create problematic featureFlags that throw when accessing properties + const problematicFeatureFlags = new Proxy( + {}, + { + get: () => { + throw new Error('FeatureFlags access error'); + }, + } + ); + + const result = getControlsVisibility(deviceType, problematicFeatureFlags, mockTask, logger); + + expect(logger.error).toHaveBeenCalledWith( + 'CC-Widgets: Task: Error in getControlsVisibility - FeatureFlags access error', + { + module: 'task-util', + method: 'getControlsVisibility', + } + ); + + expect(result).toEqual({ + accept: false, + decline: false, + end: false, + muteUnmute: false, + holdResume: false, + consult: false, + transfer: false, + conference: false, + wrapup: false, + pauseResumeRecording: false, + endConsult: false, + recordingIndicator: false, + }); + }); }); describe('findHoldTimestamp', () => { @@ -278,4 +323,35 @@ describe('findHoldTimestamp', () => { }; expect(findHoldTimestamp(interaction, 'mainCall')).toBe(42); }); + + it('should handle errors when accessing interaction media and return null', () => { + const logger = { + error: jest.fn(), + log: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + trace: jest.fn(), + }; + // Create a problematic interaction that throws when accessing media + const problematicInteraction = new Proxy( + {}, + { + get: (target, prop) => { + if (prop === 'media') { + throw new Error('Media access error'); + } + return target[prop]; + }, + } + ); + + const result = findHoldTimestamp(problematicInteraction, 'mainCall', logger); + + expect(logger.error).toHaveBeenCalledWith('CC-Widgets: Task: Error in findHoldTimestamp - Media access error', { + module: 'task-util', + method: 'findHoldTimestamp', + }); + + expect(result).toBeNull(); + }); }); diff --git a/packages/contact-center/user-state/package.json b/packages/contact-center/user-state/package.json index 7712c45b0..96f680f9e 100644 --- a/packages/contact-center/user-state/package.json +++ b/packages/contact-center/user-state/package.json @@ -24,8 +24,8 @@ "@webex/cc-components": "workspace:*", "@webex/cc-store": "workspace:*", "mobx-react-lite": "^4.1.0", - "typescript": "5.6.3", - "react-error-boundary": "^6.0.0" + "react-error-boundary": "^6.0.0", + "typescript": "5.6.3" }, "devDependencies": { "@babel/core": "7.25.2", diff --git a/packages/contact-center/user-state/src/helper.ts b/packages/contact-center/user-state/src/helper.ts index 13041579a..63a06e946 100644 --- a/packages/contact-center/user-state/src/helper.ts +++ b/packages/contact-center/user-state/src/helper.ts @@ -21,21 +21,28 @@ export const useUserState = ({ const prevStateRef = useRef(currentState); const callOnStateChange = () => { - logger.info('CC-Widgets: useUserState callOnStateChange(): invoking onStateChange', { - module: 'useUserState', - method: 'callOnStateChange', - }); - if (onStateChange) { - if (customState && 'developerName' in customState && customState?.developerName) { - onStateChange(customState); - return; - } - for (const code of idleCodes) { - if (code.id === currentState) { - onStateChange(code); - break; + try { + logger.info('CC-Widgets: useUserState callOnStateChange(): invoking onStateChange', { + module: 'useUserState', + method: 'callOnStateChange', + }); + if (onStateChange) { + if (customState && 'developerName' in customState && customState?.developerName) { + onStateChange(customState); + return; + } + for (const code of idleCodes) { + if (code.id === currentState) { + onStateChange(code); + break; + } } } + } catch (error) { + logger?.error(`CC-Widgets: UserState: Error in callOnStateChange - ${error.message}`, { + module: 'useUserState', + method: 'callOnStateChange', + }); } }; @@ -92,130 +99,188 @@ export const useUserState = ({ `; useEffect(() => { - logger.info(`CC-Widgets: Initializing worker`, { - module: 'useUserState', - method: 'useEffect - initial', - }); - - 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.postMessage({type: 'startIdleCode', startTime: Date.now()}); - workerRef.current.onmessage = (event) => { - if (event.data.type === 'elapsedTime') { - setElapsedTime(event.data.elapsedTime > 0 ? event.data.elapsedTime : 0); - } else if (event.data.type === 'lastIdleStateChangeElapsedTime') { - setLastIdleStateChangeElapsedTime(event.data.elapsedTime > 0 ? event.data.elapsedTime : 0); - } else if (event.data.type === 'stopIdleCodeTimer') { - setLastIdleStateChangeElapsedTime(-1); - } - }; + try { + logger.info(`CC-Widgets: Initializing worker`, { + module: 'useUserState', + method: 'useEffect - initial', + }); - return () => { - logger.log('CC-Widgets: useUserState cleanup: terminating worker', { + 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.postMessage({type: 'startIdleCode', startTime: Date.now()}); + workerRef.current.onmessage = (event) => { + try { + if (event.data.type === 'elapsedTime') { + setElapsedTime(event.data.elapsedTime > 0 ? event.data.elapsedTime : 0); + } else if (event.data.type === 'lastIdleStateChangeElapsedTime') { + setLastIdleStateChangeElapsedTime(event.data.elapsedTime > 0 ? event.data.elapsedTime : 0); + } else if (event.data.type === 'stopIdleCodeTimer') { + setLastIdleStateChangeElapsedTime(-1); + } + } catch (error) { + logger?.error(`CC-Widgets: UserState: Error in worker onmessage - ${error.message}`, { + module: 'useUserState', + method: 'useEffect - initial - onmessage', + }); + } + }; + } catch (error) { + logger?.error(`CC-Widgets: UserState: Error initializing worker - ${error.message}`, { module: 'useUserState', - method: 'useEffect - initial cleanup', + method: 'useEffect - initial', }); - if (workerRef.current) { - workerRef.current.postMessage({type: 'stop'}); - workerRef.current.postMessage({type: 'stopIdleCode'}); - workerRef.current.terminate(); - workerRef.current = null; + } + + return () => { + try { + logger.log('CC-Widgets: useUserState cleanup: terminating worker', { + module: 'useUserState', + method: 'useEffect - initial cleanup', + }); + if (workerRef.current) { + workerRef.current.postMessage({type: 'stop'}); + workerRef.current.postMessage({type: 'stopIdleCode'}); + workerRef.current.terminate(); + workerRef.current = null; + } + } catch (error) { + logger?.error(`CC-Widgets: UserState: Error in cleanup - ${error.message}`, { + module: 'useUserState', + method: 'useEffect - initial cleanup', + }); } }; }, []); useEffect(() => { - if (prevStateRef.current !== currentState) { - logger.info(`CC-Widgets: State change action started: ${prevStateRef.current} -> ${currentState}`, { + try { + if (prevStateRef.current !== currentState) { + logger.info(`CC-Widgets: State change action started: ${prevStateRef.current} -> ${currentState}`, { + module: 'useUserState', + method: 'useEffect - currentState', + }); + + // Call setAgentStatus and update prevStateRef after promise resolves + updateAgentState(currentState) + .then(() => { + logger.log(`CC-Widgets: State change updated: ${prevStateRef.current} -> ${currentState}`, { + module: 'useUserState', + method: 'useEffect - currentState', + }); + prevStateRef.current = currentState; + callOnStateChange(); + }) + .catch((error) => { + logger.error(`Failed to update state: ${error.toString()}`, { + module: 'useUserState', + method: 'useEffect - currentState', + }); + }); + } + } catch (error) { + logger?.error(`CC-Widgets: UserState: Error in currentState useEffect - ${error.message}`, { module: 'useUserState', method: 'useEffect - currentState', }); - - // Call setAgentStatus and update prevStateRef after promise resolves - updateAgentState(currentState) - .then(() => { - logger.log(`CC-Widgets: State change updated: ${prevStateRef.current} -> ${currentState}`, { - module: 'useUserState', - method: 'useEffect - currentState', - }); - prevStateRef.current = currentState; - callOnStateChange(); - }) - .catch((error) => { - logger.error(`Failed to update state: ${error.toString()}`, { - module: 'useUserState', - method: 'useEffect - currentState', - }); - }); } }, [currentState]); useEffect(() => { - callOnStateChange(); + try { + callOnStateChange(); + } catch (error) { + logger?.error(`CC-Widgets: UserState: Error in customState useEffect - ${error.message}`, { + module: 'useUserState', + method: 'useEffect - customState', + }); + } }, [customState]); useEffect(() => { - if (workerRef.current && lastStateChangeTimestamp) { - logger.info('CC-Widgets: useUserState timers reset', { + try { + if (workerRef.current && lastStateChangeTimestamp) { + logger.info('CC-Widgets: useUserState timers reset', { + module: 'useUserState', + method: 'useEffect - reset timers', + }); + workerRef.current.postMessage({type: 'reset', startTime: lastStateChangeTimestamp}); + + if (lastIdleCodeChangeTimestamp && lastIdleCodeChangeTimestamp !== lastStateChangeTimestamp) { + workerRef.current.postMessage({type: 'resetIdleCode', startTime: lastIdleCodeChangeTimestamp}); + } else { + workerRef.current.postMessage({type: 'stopIdleCode', startTime: lastIdleCodeChangeTimestamp}); + } + } + } catch (error) { + logger?.error(`CC-Widgets: UserState: Error in timestamp useEffect - ${error.message}`, { module: 'useUserState', method: 'useEffect - reset timers', }); - workerRef.current.postMessage({type: 'reset', startTime: lastStateChangeTimestamp}); - - if (lastIdleCodeChangeTimestamp && lastIdleCodeChangeTimestamp !== lastStateChangeTimestamp) { - workerRef.current.postMessage({type: 'resetIdleCode', startTime: lastIdleCodeChangeTimestamp}); - } else { - workerRef.current.postMessage({type: 'stopIdleCode', startTime: lastIdleCodeChangeTimestamp}); - } } }, [lastStateChangeTimestamp, lastIdleCodeChangeTimestamp]); // UI change calls this method and gets the store updated const setAgentStatus = (selectedCode) => { - logger.info('CC-Widgets: useUserState setAgentStatus(): updating currentState', { - module: 'useUserState', - method: 'setAgentStatus', - }); - store.setCurrentState(selectedCode); + try { + logger.info('CC-Widgets: useUserState setAgentStatus(): updating currentState', { + module: 'useUserState', + method: 'setAgentStatus', + }); + store.setCurrentState(selectedCode); + } catch (error) { + logger?.error(`CC-Widgets: UserState: Error in setAgentStatus - ${error.message}`, { + module: 'useUserState', + method: 'setAgentStatus', + }); + } }; // Store change calls the useEffect above which calls this method // This method updates the agent state in the backend const updateAgentState = (selectedCode) => { - selectedCode = idleCodes?.filter((code) => code.id === selectedCode)[0]; + try { + selectedCode = idleCodes?.filter((code) => code.id === selectedCode)[0]; - const {auxCodeId, state} = { - auxCodeId: selectedCode.id, - state: selectedCode.name, - }; - setIsSettingAgentStatus(true); - const chosenState = state === 'Available' ? 'Available' : 'Idle'; + const {auxCodeId, state} = { + auxCodeId: selectedCode.id, + state: selectedCode.name, + }; + setIsSettingAgentStatus(true); + const chosenState = state === 'Available' ? 'Available' : 'Idle'; - return cc - .setAgentState({state: chosenState, auxCodeId, agentId, lastStateChangeReason: state}) - .then((response) => { - logger.log(`CC-Widgets: Agent state set successfully to ${chosenState}`, { - module: 'useUserState', - method: 'updateAgentState', - }); - if ('data' in response) { - store.setLastStateChangeTimestamp(response.data.lastStateChangeTimestamp); - store.setLastIdleCodeChangeTimestamp(response.data.lastIdleCodeChangeTimestamp); - } - }) - .catch((error) => { - logger.error(`Error setting agent state: ${error.toString()}`, { - module: 'useUserState', - method: 'updateAgentState', + return cc + .setAgentState({state: chosenState, auxCodeId, agentId, lastStateChangeReason: state}) + .then((response) => { + logger.log(`CC-Widgets: Agent state set successfully to ${chosenState}`, { + module: 'useUserState', + method: 'updateAgentState', + }); + if ('data' in response) { + store.setLastStateChangeTimestamp(response.data.lastStateChangeTimestamp); + store.setLastIdleCodeChangeTimestamp(response.data.lastIdleCodeChangeTimestamp); + } + }) + .catch((error) => { + logger.error(`Error setting agent state: ${error.toString()}`, { + module: 'useUserState', + method: 'updateAgentState', + }); + store.setCurrentState(prevStateRef.current); + throw error; + }) + .finally(() => { + setIsSettingAgentStatus(false); }); - store.setCurrentState(prevStateRef.current); - throw error; - }) - .finally(() => { - setIsSettingAgentStatus(false); + } catch (error) { + logger?.error(`CC-Widgets: UserState: Error in updateAgentState - ${error.message}`, { + module: 'useUserState', + method: 'updateAgentState', }); + setIsSettingAgentStatus(false); + throw error; + } }; return { diff --git a/packages/contact-center/user-state/tests/helper.ts b/packages/contact-center/user-state/tests/helper.ts index 2f67cab75..15f090f81 100644 --- a/packages/contact-center/user-state/tests/helper.ts +++ b/packages/contact-center/user-state/tests/helper.ts @@ -1,3 +1,4 @@ +import React from 'react'; import {renderHook, act, waitFor} from '@testing-library/react'; import {useUserState} from '../src/helper'; import store from '@webex/cc-store'; @@ -106,7 +107,8 @@ describe('useUserState Hook', () => { onmessage: null, }; - global.Worker = jest.fn(() => workerMock); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (global as any).Worker = jest.fn(() => workerMock); blobMock = jest.fn(() => 'blob:http://localhost:3000/12345'); global.URL.createObjectURL = blobMock; jest.spyOn(store, 'setCurrentState'); @@ -446,4 +448,338 @@ describe('useUserState Hook', () => { expect(onStateChange).toHaveBeenCalledWith(idleCodes[0]); }); + + describe('Error Handling', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should handle errors in callOnStateChange', () => { + const errorOnStateChange = jest.fn().mockImplementation(() => { + throw new Error('Test error in onStateChange callback'); + }); + + renderHook(() => + useUserState({ + idleCodes, + agentId, + cc: mockCC, + currentState: '1', + customState: null, + lastStateChangeTimestamp: new Date().getTime(), + lastIdleCodeChangeTimestamp: undefined, + logger, + onStateChange: errorOnStateChange, + }) + ); + + expect(logger.error).toHaveBeenCalledWith( + 'CC-Widgets: UserState: Error in callOnStateChange - Test error in onStateChange callback', + { + module: 'useUserState', + method: 'callOnStateChange', + } + ); + }); + + it('should handle errors in worker initialization', () => { + const originalWorker = global.Worker; + + global.Worker = jest.fn().mockImplementation(() => { + throw new Error('Worker initialization failed'); + }); + + renderHook(() => + useUserState({ + idleCodes, + agentId, + cc: mockCC, + currentState: '0', + customState: null, + lastStateChangeTimestamp: new Date().getTime(), + lastIdleCodeChangeTimestamp: undefined, + logger, + onStateChange, + }) + ); + + expect(logger.error).toHaveBeenCalledWith( + 'CC-Widgets: UserState: Error initializing worker - Worker initialization failed', + { + module: 'useUserState', + method: 'useEffect - initial', + } + ); + + global.Worker = originalWorker; + }); + + it('should handle errors in worker onmessage handler', () => { + const mockSetElapsedTime = jest.fn().mockImplementation(() => { + throw new Error('Error in setElapsedTime'); + }); + + const originalUseState = React.useState; + // @ts-expect-error: only for testing + jest.spyOn(React, 'useState').mockImplementation((initial: unknown) => { + if (initial === 0) { + // Mock setElapsedTime to throw error + return [0, mockSetElapsedTime]; + } + // Return normal mock for other useState calls + return originalUseState(initial); + }); + + renderHook(() => + useUserState({ + idleCodes, + agentId, + cc: mockCC, + currentState: '0', + customState: null, + lastStateChangeTimestamp: new Date().getTime(), + lastIdleCodeChangeTimestamp: undefined, + logger, + onStateChange, + }) + ); + + act(() => { + workerMock.onmessage({data: {type: 'elapsedTime', elapsedTime: 1}}); + }); + + expect(logger.error).toHaveBeenCalledWith( + 'CC-Widgets: UserState: Error in worker onmessage - Error in setElapsedTime', + { + module: 'useUserState', + method: 'useEffect - initial - onmessage', + } + ); + }); + + it('should handle errors in currentState useEffect', () => { + // Mock updateAgentState to be called directly, which might throw + const errorCC = { + ...mockCC, + setAgentState: jest.fn().mockImplementation(() => { + throw new Error('setAgentState synchronous error'); + }), + }; + + const {rerender} = renderHook( + ({currentState}) => + useUserState({ + idleCodes, + agentId, + cc: errorCC, + currentState, + customState: null, + lastStateChangeTimestamp: new Date().getTime(), + lastIdleCodeChangeTimestamp: undefined, + logger, + onStateChange, + }), + {initialProps: {currentState: '0'}} + ); + + act(() => { + rerender({currentState: '1'}); + }); + + expect(logger.error).toHaveBeenCalledWith( + 'CC-Widgets: UserState: Error in currentState useEffect - setAgentState synchronous error', + { + module: 'useUserState', + method: 'useEffect - currentState', + } + ); + }); + + it('should handle errors in customState useEffect', () => { + // Mock onStateChange to throw error + const errorOnStateChange = jest.fn().mockImplementation(() => { + throw new Error('customState callback error'); + }); + + const {rerender} = renderHook( + ({customState}) => + useUserState({ + idleCodes, + agentId, + cc: mockCC, + currentState: '0', + customState, + lastStateChangeTimestamp: new Date().getTime(), + lastIdleCodeChangeTimestamp: undefined, + logger, + onStateChange: errorOnStateChange, + }), + {initialProps: {customState: null}} + ); + + act(() => { + rerender({customState: {developerName: 'Custom State', name: 'Custom State'}}); + }); + + expect(logger.error).toHaveBeenCalledWith( + 'CC-Widgets: UserState: Error in callOnStateChange - customState callback error', + { + module: 'useUserState', + method: 'callOnStateChange', + } + ); + }); + + it('should handle errors in timestamp useEffect', () => { + // Mock the worker to throw error when postMessage is called + const mockWorkerWithError = { + postMessage: jest.fn().mockImplementation(() => { + throw new Error('Worker postMessage error'); + }), + terminate: jest.fn(), + onmessage: null, + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (global as any).Worker = jest.fn(() => mockWorkerWithError); + + const {rerender} = renderHook( + ({lastStateChangeTimestamp}) => + useUserState({ + idleCodes, + agentId, + cc: mockCC, + currentState: '0', + customState: null, + lastStateChangeTimestamp, + lastIdleCodeChangeTimestamp: undefined, + logger, + onStateChange, + }), + {initialProps: {lastStateChangeTimestamp: new Date().getTime()}} + ); + + act(() => { + rerender({lastStateChangeTimestamp: new Date().getTime() + 1000}); + }); + + expect(logger.error).toHaveBeenCalledWith( + 'CC-Widgets: UserState: Error in timestamp useEffect - Worker postMessage error', + { + module: 'useUserState', + method: 'useEffect - reset timers', + } + ); + }); + + it('should handle errors in setAgentStatus', () => { + // Mock store.setCurrentState to throw error + const originalSetCurrentState = store.setCurrentState; + store.setCurrentState = jest.fn().mockImplementation(() => { + throw new Error('setCurrentState error'); + }); + + const {result} = renderHook(() => + useUserState({ + idleCodes, + agentId, + cc: mockCC, + currentState: '0', + customState: null, + lastStateChangeTimestamp: new Date().getTime(), + lastIdleCodeChangeTimestamp: undefined, + logger, + onStateChange, + }) + ); + + act(() => { + result.current.setAgentStatus('1'); + }); + + expect(logger.error).toHaveBeenCalledWith( + 'CC-Widgets: UserState: Error in setAgentStatus - setCurrentState error', + { + module: 'useUserState', + method: 'setAgentStatus', + } + ); + + store.setCurrentState = originalSetCurrentState; + }); + + it('should handle errors in updateAgentState', () => { + // Mock idleCodes.filter to throw error + const errorIdleCodes = new Proxy([], { + get: (target, prop) => { + if (prop === 'filter') { + throw new Error('idleCodes filter error'); + } + return target[prop]; + }, + }); + + const {rerender} = renderHook( + ({currentState}) => + useUserState({ + idleCodes: errorIdleCodes, + agentId, + cc: mockCC, + currentState, + customState: null, + lastStateChangeTimestamp: new Date().getTime(), + lastIdleCodeChangeTimestamp: undefined, + logger, + onStateChange, + }), + {initialProps: {currentState: '0'}} + ); + + act(() => { + rerender({currentState: '1'}); + }); + + expect(logger.error).toHaveBeenCalledWith( + 'CC-Widgets: UserState: Error in updateAgentState - idleCodes filter error', + { + module: 'useUserState', + method: 'updateAgentState', + } + ); + }); + + it('should handle errors in cleanup function', () => { + const mockWorkerWithCleanupError = { + postMessage: jest.fn(), + terminate: jest.fn().mockImplementation(() => { + throw new Error('Worker terminate error'); + }), + onmessage: null, + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (global as any).Worker = jest.fn(() => mockWorkerWithCleanupError); + + const {unmount} = renderHook(() => + useUserState({ + idleCodes, + agentId, + cc: mockCC, + currentState: '0', + customState: null, + lastStateChangeTimestamp: new Date().getTime(), + lastIdleCodeChangeTimestamp: undefined, + logger, + onStateChange, + }) + ); + + unmount(); + + expect(logger.error).toHaveBeenCalledWith('CC-Widgets: UserState: Error in cleanup - Worker terminate error', { + module: 'useUserState', + method: 'useEffect - initial cleanup', + }); + }); + }); }); diff --git a/packages/contact-center/user-state/tests/user-state/index.tsx b/packages/contact-center/user-state/tests/user-state/index.tsx index aad1270c3..9fde381e8 100644 --- a/packages/contact-center/user-state/tests/user-state/index.tsx +++ b/packages/contact-center/user-state/tests/user-state/index.tsx @@ -2,6 +2,7 @@ import React from 'react'; import {render} from '@testing-library/react'; import {UserState} from '../../src'; import * as helper from '../../src/helper'; +import store from '@webex/cc-store'; import '@testing-library/jest-dom'; // Mock the store import @@ -16,10 +17,14 @@ jest.mock('@webex/cc-store', () => { logger: { log: jest.fn(), info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), }, lastStateChangeTimestamp: new Date().getTime(), + lastIdleCodeChangeTimestamp: undefined, customState: null, currentState: '0', + onErrorCallback: jest.fn(), }; }); @@ -28,6 +33,10 @@ describe('UserState Component', () => { const onStateChange = jest.fn(); beforeEach(() => { + jest.clearAllMocks(); + // Suppress console.error for error boundary tests + jest.spyOn(console, 'error').mockImplementation(() => {}); + workerMock = { postMessage: jest.fn(), terminate: jest.fn(), @@ -42,6 +51,10 @@ describe('UserState Component', () => { } }); + afterEach(() => { + jest.restoreAllMocks(); + }); + it('renders UserStateComponent with correct props', () => { const useUserStateSpy = jest.spyOn(helper, 'useUserState'); @@ -57,11 +70,32 @@ describe('UserState Component', () => { currentState: '0', customState: null, lastStateChangeTimestamp: expect.any(Number), + lastIdleCodeChangeTimestamp: undefined, logger: { log: expect.any(Function), info: expect.any(Function), + error: expect.any(Function), + warn: expect.any(Function), }, onStateChange: expect.any(Function), }); }); + + describe('ErrorBoundary Tests', () => { + it('should render empty fragment when ErrorBoundary catches an error', () => { + const mockOnErrorCallback = jest.fn(); + store.onErrorCallback = mockOnErrorCallback; + // Mock the useUserState to throw an error + jest.spyOn(helper, 'useUserState').mockImplementation(() => { + throw new Error('Test error in useUserState'); + }); + + const {container} = render(); + + // The fallback should render an empty fragment (no content) + expect(container.firstChild).toBeNull(); + expect(mockOnErrorCallback).toHaveBeenCalledWith('UserState', Error('Test error in useUserState')); + expect(mockOnErrorCallback).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/yarn.lock b/yarn.lock index c245584e9..42801983d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9614,6 +9614,7 @@ __metadata: "@types/react-test-renderer": "npm:18" "@webex/cc-components": "workspace:*" "@webex/cc-store": "workspace:*" + "@webex/test-fixtures": "workspace:*" babel-jest: "npm:29.7.0" babel-loader: "npm:9.2.1" eslint: "npm:^9.20.1" From d56292fb341998deed63974b0d334f395d534c77 Mon Sep 17 00:00:00 2001 From: Shreyas Sharma Date: Mon, 15 Sep 2025 11:31:49 +0530 Subject: [PATCH 12/12] fix(widgets): add the ts ignores back --- .../contact-center/cc-components/package.json | 2 +- .../task/CallControlCAD/call-control-cad.tsx | 19 +++++++++++++++++-- .../task/IncomingTask/incoming-task.utils.tsx | 1 + .../task/TaskList/task-list.utils.ts | 1 + .../contact-center/cc-widgets/package.json | 2 +- .../contact-center/station-login/package.json | 2 +- packages/contact-center/store/package.json | 2 +- packages/contact-center/task/package.json | 2 +- packages/contact-center/task/src/helper.ts | 5 +++++ .../contact-center/ui-logging/package.json | 2 +- .../contact-center/user-state/package.json | 2 +- 11 files changed, 31 insertions(+), 9 deletions(-) diff --git a/packages/contact-center/cc-components/package.json b/packages/contact-center/cc-components/package.json index 1086e4a4d..6291f9d5d 100644 --- a/packages/contact-center/cc-components/package.json +++ b/packages/contact-center/cc-components/package.json @@ -76,4 +76,4 @@ "react": ">=18.3.1", "react-dom": ">=18.3.1" } -} \ No newline at end of file +} diff --git a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx index f5a15dd01..5816b137c 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx @@ -61,7 +61,10 @@ const CallControlCADComponent: React.FC = (props) => const isSocial = mediaChannel === MediaChannelType.SOCIAL; const isTelephony = mediaChannel === MediaChannelType.TELEPHONY; + //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 const customerName = currentTask?.data?.interaction?.callAssociatedDetails?.customerName; + + //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 const ani = currentTask?.data?.interaction?.callAssociatedDetails?.ani; // Create unique IDs for tooltips @@ -191,12 +194,24 @@ const CallControlCADComponent: React.FC = (props) =>
{QUEUE}{' '} - {currentTask?.data?.interaction?.callAssociatedDetails?.virtualTeamName || NO_TEAM_NAME} + + { + //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 + + currentTask?.data?.interaction?.callAssociatedDetails?.virtualTeamName || NO_TEAM_NAME + } + {renderPhoneNumber()} {RONA}{' '} - {currentTask?.data?.interaction?.callAssociatedDetails?.ronaTimeout || NO_RONA} + + { + //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 + + currentTask?.data?.interaction?.callAssociatedDetails?.ronaTimeout || NO_RONA + } +
diff --git a/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.utils.tsx b/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.utils.tsx index 4a5c64cc7..391719f17 100644 --- a/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.utils.tsx +++ b/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.utils.tsx @@ -26,6 +26,7 @@ export interface IncomingTaskData { export const extractIncomingTaskData = (incomingTask: ITask, isBrowser: boolean, logger?): IncomingTaskData => { try { // Extract basic data from task + //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 const callAssociationDetails = incomingTask?.data?.interaction?.callAssociatedDetails; const ani = callAssociationDetails?.ani; const customerName = callAssociationDetails?.customerName; diff --git a/packages/contact-center/cc-components/src/components/task/TaskList/task-list.utils.ts b/packages/contact-center/cc-components/src/components/task/TaskList/task-list.utils.ts index 58dc1828c..d745b8b48 100644 --- a/packages/contact-center/cc-components/src/components/task/TaskList/task-list.utils.ts +++ b/packages/contact-center/cc-components/src/components/task/TaskList/task-list.utils.ts @@ -10,6 +10,7 @@ import {isIncomingTask} from '@webex/cc-store'; export const extractTaskListItemData = (task: ITask, isBrowser: boolean, logger?): TaskListItemData => { try { // Extract basic data from task + //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 const callAssociationDetails = task?.data?.interaction?.callAssociatedDetails; const ani = callAssociationDetails?.ani; const customerName = callAssociationDetails?.customerName; diff --git a/packages/contact-center/cc-widgets/package.json b/packages/contact-center/cc-widgets/package.json index 7fbc358e9..4fd3714e1 100644 --- a/packages/contact-center/cc-widgets/package.json +++ b/packages/contact-center/cc-widgets/package.json @@ -95,4 +95,4 @@ "^.+\\.(css|less|scss)$": "babel-jest" } } -} \ No newline at end of file +} diff --git a/packages/contact-center/station-login/package.json b/packages/contact-center/station-login/package.json index e637f45cb..1a459b51e 100644 --- a/packages/contact-center/station-login/package.json +++ b/packages/contact-center/station-login/package.json @@ -66,4 +66,4 @@ "react": ">=18.3.1", "react-dom": ">=18.3.1" } -} \ No newline at end of file +} diff --git a/packages/contact-center/store/package.json b/packages/contact-center/store/package.json index f0c9f5690..1d6b28ebc 100644 --- a/packages/contact-center/store/package.json +++ b/packages/contact-center/store/package.json @@ -59,4 +59,4 @@ "webpack-cli": "5.1.4", "webpack-merge": "6.0.1" } -} \ No newline at end of file +} diff --git a/packages/contact-center/task/package.json b/packages/contact-center/task/package.json index 3c5c14f81..f8bfd322d 100644 --- a/packages/contact-center/task/package.json +++ b/packages/contact-center/task/package.json @@ -65,4 +65,4 @@ "react": ">=18.3.1", "react-dom": ">=18.3.1" } -} \ No newline at end of file +} diff --git a/packages/contact-center/task/src/helper.ts b/packages/contact-center/task/src/helper.ts index a1c465ddc..5b0743640 100644 --- a/packages/contact-center/task/src/helper.ts +++ b/packages/contact-center/task/src/helper.ts @@ -614,6 +614,7 @@ export const useCallControl = (props: useCallControlProps) => { const intendedMuteState = !isMuted; try { + //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 await currentTask.toggleMute(); // Only update state after successful SDK call @@ -686,6 +687,7 @@ export const useCallControl = (props: useCallControlProps) => { const transferCall = async (to: string, type: DestinationType) => { try { + //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 await currentTask.transfer({to, destinationType: type}); logger.info('transferCall success', {module: 'useCallControl', method: 'transferCall'}); } catch (error) { @@ -707,6 +709,7 @@ export const useCallControl = (props: useCallControlProps) => { } try { + //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 await currentTask.consult(consultPayload); store.setIsQueueConsultInProgress(false); if (destinationType === 'queue') { @@ -733,6 +736,7 @@ export const useCallControl = (props: useCallControlProps) => { }; try { + //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 await currentTask.endConsult(consultEndPayload); } catch (error) { logError(`Error ending consult call: ${error}`, 'endConsultCall'); @@ -746,6 +750,7 @@ export const useCallControl = (props: useCallControlProps) => { destinationType: destinationType, }; try { + //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 await currentTask.consultTransfer(consultTransferPayload); store.setConsultInitiated(true); } catch (error) { diff --git a/packages/contact-center/ui-logging/package.json b/packages/contact-center/ui-logging/package.json index 1709ac389..489310787 100644 --- a/packages/contact-center/ui-logging/package.json +++ b/packages/contact-center/ui-logging/package.json @@ -36,4 +36,4 @@ "react": ">=18.3.1", "react-dom": ">=18.3.1" } -} \ No newline at end of file +} diff --git a/packages/contact-center/user-state/package.json b/packages/contact-center/user-state/package.json index 02528b527..0e4bb37ee 100644 --- a/packages/contact-center/user-state/package.json +++ b/packages/contact-center/user-state/package.json @@ -64,4 +64,4 @@ "react": ">=18.3.1", "react-dom": ">=18.3.1" } -} \ No newline at end of file +}