diff --git a/packages/contact-center/cc-components/package.json b/packages/contact-center/cc-components/package.json index fd50e2802..6ba1fef8f 100644 --- a/packages/contact-center/cc-components/package.json +++ b/packages/contact-center/cc-components/package.json @@ -35,7 +35,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/cc-ui-logging": "workspace:*" }, "devDependencies": { "@babel/core": "7.25.2", @@ -75,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/StationLogin/station-login.tsx b/packages/contact-center/cc-components/src/components/StationLogin/station-login.tsx index 37633544c..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,6 +17,7 @@ import { updateDialNumberLabel, handleCCSignoutKeyDown, } from './station-login.utils'; +import {withMetrics} from '@webex/cc-ui-logging'; const StationLoginComponent: React.FunctionComponent = (props) => { const { @@ -344,4 +345,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 704127aa6..0a43f21e7 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/cc-ui-logging'; const UserStateComponent: React.FunctionComponent = (props) => { const { @@ -103,4 +104,5 @@ const UserStateComponent: React.FunctionComponent = (p ); }; -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 e03564c48..ac09c248a 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 @@ -24,6 +24,7 @@ import { filterButtonsForConsultation, updateCallStateFromTask, } from './call-control.utils'; +import {withMetrics} from '@webex/cc-ui-logging'; function CallControlComponent(props: CallControlComponentProps) { const [selectedWrapupReason, setSelectedWrapupReason] = useState(null); @@ -322,4 +323,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 23a86b8d8..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,6 +19,7 @@ import { CUSTOMER_NAME, RONA, } from '../constants'; +import {withMetrics} from '@webex/cc-ui-logging'; const CallControlCADComponent: React.FC = (props) => { const { @@ -235,4 +236,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 5766b9140..f2f3d1ab5 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/cc-ui-logging'; import {extractIncomingTaskData} from './incoming-task.utils'; const IncomingTaskComponent: React.FunctionComponent = (props) => { @@ -37,4 +38,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..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,6 +1,7 @@ import React, {useState} from 'react'; import {OutdialCallComponentProps} from '../task.types'; import './outdial-call.style.scss'; +import {withMetrics} from '@webex/cc-ui-logging'; 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 ca0e0f504..76a3fae83 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 @@ -9,6 +9,7 @@ import { isCurrentTaskSelected, } from './task-list.utils'; import './styles.scss'; +import {withMetrics} from '@webex/cc-ui-logging'; const TaskListComponent: React.FunctionComponent = (props) => { const {currentTask, taskList, acceptTask, declineTask, isBrowser, onTaskSelect, logger} = props; @@ -57,4 +58,5 @@ const TaskListComponent: React.FunctionComponent = (prop ); }; -export default TaskListComponent; +const TaskListComponentWithMetrics = withMetrics(TaskListComponent, 'TaskList'); +export default TaskListComponentWithMetrics; diff --git a/packages/contact-center/cc-widgets/package.json b/packages/contact-center/cc-widgets/package.json index 631cbf84b..33d869ea8 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 c4d9973c1..f46fce359 100644 --- a/packages/contact-center/station-login/package.json +++ b/packages/contact-center/station-login/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/station-login/src/station-login/index.tsx b/packages/contact-center/station-login/src/station-login/index.tsx index 7ad23401e..04cdbec20 100644 --- a/packages/contact-center/station-login/src/station-login/index.tsx +++ b/packages/contact-center/station-login/src/station-login/index.tsx @@ -22,6 +22,7 @@ const StationLogin: React.FunctionComponent = observer( teamId, setTeamId, } = store; + const result = useStationLogin({ cc, onLogin, @@ -52,6 +53,7 @@ const StationLogin: React.FunctionComponent = observer( logger, profileMode, }; + return ; } ); diff --git a/packages/contact-center/store/package.json b/packages/contact-center/store/package.json index fb049d2cd..882af9414 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 a0f70c05a..f02d0fae8 100644 --- a/packages/contact-center/task/package.json +++ b/packages/contact-center/task/package.json @@ -64,4 +64,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 d4db7ad42..037dfdef0 100644 --- a/packages/contact-center/task/src/CallControl/index.tsx +++ b/packages/contact-center/task/src/CallControl/index.tsx @@ -48,6 +48,7 @@ const CallControl: React.FunctionComponent = observer( allowConsultToQueue, logger, }; + return ; } ); diff --git a/packages/contact-center/task/tests/OutdialCall/index.tsx b/packages/contact-center/task/tests/OutdialCall/index.tsx index 24befeb8f..ab415bcbf 100644 --- a/packages/contact-center/task/tests/OutdialCall/index.tsx +++ b/packages/contact-center/task/tests/OutdialCall/index.tsx @@ -7,9 +7,10 @@ import {OutdialCall} from '../../src/OutdialCall'; jest.mock('@webex/cc-store', () => ({ cc: {}, logger: { - // Adding mock logger methods info: jest.fn(), + log: jest.fn(), error: jest.fn(), + warn: jest.fn(), }, })); @@ -23,6 +24,8 @@ describe('OutdialCall Component', () => { logger: { info: expect.any(Function), error: expect.any(Function), + log: expect.any(Function), + warn: expect.any(Function), }, }); }); diff --git a/packages/contact-center/task/tests/TaskList/index.tsx b/packages/contact-center/task/tests/TaskList/index.tsx index 66d61b8b4..18326e80b 100644 --- a/packages/contact-center/task/tests/TaskList/index.tsx +++ b/packages/contact-center/task/tests/TaskList/index.tsx @@ -3,7 +3,6 @@ import {render, screen, cleanup} from '@testing-library/react'; import '@testing-library/jest-dom'; import {TaskList} from '../../src/TaskList'; import * as helper from '../../src/helper'; -import * as components from '@webex/cc-components'; import store from '@webex/cc-store'; // Mock `@webex/cc-store`. @@ -31,7 +30,6 @@ jest.mock('@webex/cc-store', () => ({ })); describe('TaskList Component', () => { - const taskListComponentSpy = jest.spyOn(components, 'TaskListComponent'); const helperSpy = jest.spyOn(helper, 'useTaskList'); afterEach(cleanup); @@ -42,20 +40,6 @@ describe('TaskList Component', () => { const taskListPresentational = screen.getByTestId('task-list'); expect(taskListPresentational).toBeInTheDocument(); - // Verify that `TaskListPresentational` is called with the correct props. - expect(taskListComponentSpy).toHaveBeenCalledWith( - { - currentTask: undefined, - isBrowser: true, - logger: store.logger, - taskList: taskListMock, - acceptTask: expect.any(Function), - declineTask: expect.any(Function), - onTaskSelect: expect.any(Function), - }, - {} - ); - // Verify that `useTaskList` is called with the correct arguments. expect(helperSpy).toHaveBeenCalledWith({ cc: store.cc, diff --git a/packages/contact-center/ui-logging/jest.config.js b/packages/contact-center/ui-logging/jest.config.js new file mode 100644 index 000000000..bef75ab06 --- /dev/null +++ b/packages/contact-center/ui-logging/jest.config.js @@ -0,0 +1,6 @@ +const jestConfig = require('../../../jest.config.js'); + +jestConfig.rootDir = '../../../'; +jestConfig.testMatch = ['**/ui-logging/tests/**/*.ts', '**/ui-logging/tests/**/*.tsx']; + +module.exports = jestConfig; diff --git a/packages/contact-center/ui-logging/package.json b/packages/contact-center/ui-logging/package.json new file mode 100644 index 000000000..729eb6562 --- /dev/null +++ b/packages/contact-center/ui-logging/package.json @@ -0,0 +1,36 @@ +{ + "name": "@webex/cc-ui-logging", + "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 --coverage", + "test:styles": "echo 'No styles to test'" + }, + "dependencies": { + "@webex/cc-store": "workspace:*" + }, + "devDependencies": { + "@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", + "@webex/test-fixtures": "workspace:*", + "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": ">=18.3.1", + "react-dom": ">=18.3.1" + } +} diff --git a/packages/contact-center/ui-logging/src/index.ts b/packages/contact-center/ui-logging/src/index.ts new file mode 100644 index 000000000..522b06d0b --- /dev/null +++ b/packages/contact-center/ui-logging/src/index.ts @@ -0,0 +1,5 @@ +import withMetrics from './withMetrics'; +import {WidgetMetrics} from './metricsLogger'; + +export {withMetrics}; +export type {WidgetMetrics}; diff --git a/packages/contact-center/ui-logging/src/metricsLogger.ts b/packages/contact-center/ui-logging/src/metricsLogger.ts new file mode 100644 index 000000000..02f2b8d87 --- /dev/null +++ b/packages/contact-center/ui-logging/src/metricsLogger.ts @@ -0,0 +1,102 @@ +import store from '@webex/cc-store'; + +export type WidgetMetrics = { + widgetName: string; + 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.warn('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', + }); +}; + +/** + * 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 + 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; + } + + // All shallow comparisons passed, consider props unchanged + return false; +} diff --git a/packages/contact-center/ui-logging/src/withMetrics.tsx b/packages/contact-center/ui-logging/src/withMetrics.tsx new file mode 100644 index 000000000..77354d3dd --- /dev/null +++ b/packages/contact-center/ui-logging/src/withMetrics.tsx @@ -0,0 +1,29 @@ +import React, {useEffect, useRef} from 'react'; +import {havePropsChanged, logMetrics} from './metricsLogger'; + +export default function withMetrics

(Component: any, widgetName: string) { + return React.memo( + (props: P) => { + useEffect(() => { + logMetrics({ + widgetName, + event: 'WIDGET_MOUNTED', + timestamp: Date.now(), + }); + + return () => { + logMetrics({ + widgetName, + event: 'WIDGET_UNMOUNTED', + timestamp: Date.now(), + }); + }; + }, []); + + // 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-logging/tests/metricsLogger.test.ts b/packages/contact-center/ui-logging/tests/metricsLogger.test.ts new file mode 100644 index 000000000..f7ecf15e5 --- /dev/null +++ b/packages/contact-center/ui-logging/tests/metricsLogger.test.ts @@ -0,0 +1,86 @@ +import store from '@webex/cc-store'; +import {logMetrics, havePropsChanged, WidgetMetrics} from '../src/metricsLogger'; + +describe('metricsLogger', () => { + store.store.logger = { + log: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + trace: jest.fn(), + }; + 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, 'warn'); + store.store.logger = undefined; + + 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('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(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 false when nested values differ', () => { + const obj1 = {a: {b: 1}}; + const obj2 = {a: {b: 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); + }); + }); +}); diff --git a/packages/contact-center/ui-logging/tests/withMetrics.test.tsx b/packages/contact-center/ui-logging/tests/withMetrics.test.tsx new file mode 100644 index 000000000..23ec71691 --- /dev/null +++ b/packages/contact-center/ui-logging/tests/withMetrics.test.tsx @@ -0,0 +1,105 @@ +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 { + name?: string; + [key: string]: any; +} + +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}
; + 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); + }); +}); diff --git a/packages/contact-center/ui-logging/tsconfig.json b/packages/contact-center/ui-logging/tsconfig.json new file mode 100644 index 000000000..50b68ce6a --- /dev/null +++ b/packages/contact-center/ui-logging/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-logging/tsconfig.test.json b/packages/contact-center/ui-logging/tsconfig.test.json new file mode 100644 index 000000000..40bd84f5a --- /dev/null +++ b/packages/contact-center/ui-logging/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/packages/contact-center/ui-logging/webpack.config.js b/packages/contact-center/ui-logging/webpack.config.js new file mode 100644 index 000000000..fa737ac93 --- /dev/null +++ b/packages/contact-center/ui-logging/webpack.config.js @@ -0,0 +1,17 @@ +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', + }, + externals: { + react: 'react', + 'react-dom': 'react-dom', + '@webex/cc-store': '@webex/cc-store', + }, +}); diff --git a/packages/contact-center/user-state/package.json b/packages/contact-center/user-state/package.json index 9c7231ad9..ca31f7d18 100644 --- a/packages/contact-center/user-state/package.json +++ b/packages/contact-center/user-state/package.json @@ -62,4 +62,4 @@ "react": ">=18.3.1", "react-dom": ">=18.3.1" } -} \ No newline at end of file +} diff --git a/webpack.config.js b/webpack.config.js index 3bbe67e5d..922c4b2ba 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,17 @@ 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: { + // 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 cfd17aaf2..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,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/cc-ui-logging': path.resolve(__dirname, '../../../packages/contact-center/ui-logging/src'), }, }, module: { diff --git a/yarn.lock b/yarn.lock index 1d56eb61c..ea4331817 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/cc-ui-logging": "workspace:*" "@webex/test-fixtures": "workspace:*" babel-loader: "npm:9.2.1" eslint: "npm:^9.20.1" @@ -9571,6 +9572,30 @@ __metadata: languageName: unknown linkType: soft +"@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-logging" + 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:*" + "@webex/test-fixtures": "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" @@ -9811,15 +9836,15 @@ __metadata: linkType: hard "@webex/event-dictionary-ts@npm:^1.0.1753": - version: 1.0.1848 - resolution: "@webex/event-dictionary-ts@npm:1.0.1848" + version: 1.0.1757 + resolution: "@webex/event-dictionary-ts@npm:1.0.1757" dependencies: amf-client-js: "npm:^5.2.6" json-schema-to-typescript: "npm:^12.0.0" minimist: "npm:^1.2.8" shelljs: "npm:^0.8.5" webapi-parser: "npm:^0.5.0" - checksum: 10c0/2bcbdabe6d2bff9459422161be51462e718d663412437b16a86577a9e39cfd97d1118c465a89fb3c47c13706ba09329c413c5fa43a5c9f03ef1cca984a39cc65 + checksum: 10c0/e941c694a6389a6d91dc6018b3d84fc759dce96dce5d7cf8aa10a065b2f2575358c81bfec5869e9dd34ebeaba6ab185282ce76a5dc75495afdc1c907c8403662 languageName: node linkType: hard