diff --git a/packages/contact-center/cc-components/src/components/task/CampaignErrorDialog/campaign-error-dialog.style.scss b/packages/contact-center/cc-components/src/components/task/CampaignErrorDialog/campaign-error-dialog.style.scss new file mode 100644 index 000000000..c0ce78d71 --- /dev/null +++ b/packages/contact-center/cc-components/src/components/task/CampaignErrorDialog/campaign-error-dialog.style.scss @@ -0,0 +1,26 @@ +.campaign-error-dialog { + width: 25rem; + border-radius: 0.5rem; + border-color: transparent; + padding: 1rem; + box-shadow: 0rem 0.25rem 0.5rem 0rem rgba(0, 0, 0, 0.16), 0rem 0rem 0.0625rem 0rem rgba(0, 0, 0, 0.16); + + &::backdrop { + background-color: rgba(0, 0, 0, 0.5); + } + + .campaign-error-dialog-title { + margin: 0; + flex: 1; + } + + .campaign-error-dialog-message { + margin-bottom: 1.5rem; + } + + .campaign-error-dialog-actions { + display: flex; + justify-content: flex-end; + gap: 0.5rem; + } +} diff --git a/packages/contact-center/cc-components/src/components/task/CampaignErrorDialog/campaign-error-dialog.tsx b/packages/contact-center/cc-components/src/components/task/CampaignErrorDialog/campaign-error-dialog.tsx new file mode 100644 index 000000000..26a62badf --- /dev/null +++ b/packages/contact-center/cc-components/src/components/task/CampaignErrorDialog/campaign-error-dialog.tsx @@ -0,0 +1,70 @@ +import React, {useEffect, useRef} from 'react'; +import {Button, Text} from '@momentum-design/components/dist/react'; +import {CampaignErrorDialogProps, ERROR_TITLES, ERROR_MESSAGE} from './campaign-error-dialog.types'; +import {withMetrics} from '@webex/cc-ui-logging'; +import './campaign-error-dialog.style.scss'; + +const CampaignErrorDialog: React.FunctionComponent = ({errorType, isOpen, onClose}) => { + const dialogRef = useRef(null); + + useEffect(() => { + const dialog = dialogRef.current; + if (!dialog) return; + + if (isOpen && !dialog.open) { + dialog.showModal(); + } else if (!isOpen && dialog.open) { + dialog.close(); + } + }, [isOpen]); + + useEffect(() => { + const dialog = dialogRef.current; + if (!dialog) return; + + const handleClose = () => { + if (isOpen) { + onClose(); + } + }; + + dialog.addEventListener('close', handleClose); + return () => dialog.removeEventListener('close', handleClose); + }, [isOpen, onClose]); + + return ( + + + {ERROR_TITLES[errorType]} + + + {ERROR_MESSAGE} + +
+ +
+
+ ); +}; + +const CampaignErrorDialogWithMetrics = withMetrics(CampaignErrorDialog, 'CampaignErrorDialog'); +export default CampaignErrorDialogWithMetrics; diff --git a/packages/contact-center/cc-components/src/components/task/CampaignErrorDialog/campaign-error-dialog.types.ts b/packages/contact-center/cc-components/src/components/task/CampaignErrorDialog/campaign-error-dialog.types.ts new file mode 100644 index 000000000..78f35bbbd --- /dev/null +++ b/packages/contact-center/cc-components/src/components/task/CampaignErrorDialog/campaign-error-dialog.types.ts @@ -0,0 +1,16 @@ +export type CampaignErrorType = 'ACCEPT_FAILED' | 'SKIP_FAILED' | 'REMOVE_FAILED'; + +export interface CampaignErrorDialogProps { + errorType: CampaignErrorType; + isOpen: boolean; + onClose: () => void; +} + +export const ERROR_TITLES: Record = { + ACCEPT_FAILED: "Can't accept contact", + SKIP_FAILED: "Can't skip contact", + REMOVE_FAILED: "Can't remove contact", +}; + +export const ERROR_MESSAGE = + 'We ran into an issue connecting you with this contact. Check your network connection and try again.'; diff --git a/packages/contact-center/cc-components/src/index.ts b/packages/contact-center/cc-components/src/index.ts index a7df5d896..97a152d57 100644 --- a/packages/contact-center/cc-components/src/index.ts +++ b/packages/contact-center/cc-components/src/index.ts @@ -5,6 +5,7 @@ import CallControlCADComponent from './components/task/CallControlCAD/call-contr import IncomingTaskComponent from './components/task/IncomingTask/incoming-task'; import TaskListComponent from './components/task/TaskList/task-list'; import OutdialCallComponent from './components/task/OutdialCall/outdial-call'; +import CampaignErrorDialogComponent from './components/task/CampaignErrorDialog/campaign-error-dialog'; import RealTimeTranscriptComponent from './components/task/RealTimeTranscript/real-time-transcript'; export { @@ -15,9 +16,11 @@ export { IncomingTaskComponent, TaskListComponent, OutdialCallComponent, + CampaignErrorDialogComponent, RealTimeTranscriptComponent, }; export * from './components/StationLogin/constants'; export * from './components/StationLogin/station-login.types'; export * from './components/UserState/user-state.types'; export * from './components/task/task.types'; +export * from './components/task/CampaignErrorDialog/campaign-error-dialog.types'; diff --git a/packages/contact-center/cc-components/tests/components/task/CampaignErrorDialog/__snapshots__/campaign-error-dialog.snapshot.tsx.snap b/packages/contact-center/cc-components/tests/components/task/CampaignErrorDialog/__snapshots__/campaign-error-dialog.snapshot.tsx.snap new file mode 100644 index 000000000..48a414979 --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/CampaignErrorDialog/__snapshots__/campaign-error-dialog.snapshot.tsx.snap @@ -0,0 +1,137 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CampaignErrorDialog Snapshots Rendering should match snapshot for ACCEPT_FAILED error type 1`] = ` +
+ + + Can't accept contact + + + We ran into an issue connecting you with this contact. Check your network connection and try again. + +
+ + OK + +
+
+
+`; + +exports[`CampaignErrorDialog Snapshots Rendering should match snapshot for REMOVE_FAILED error type 1`] = ` +
+ + + Can't remove contact + + + We ran into an issue connecting you with this contact. Check your network connection and try again. + +
+ + OK + +
+
+
+`; + +exports[`CampaignErrorDialog Snapshots Rendering should match snapshot for SKIP_FAILED error type 1`] = ` +
+ + + Can't skip contact + + + We ran into an issue connecting you with this contact. Check your network connection and try again. + +
+ + OK + +
+
+
+`; + +exports[`CampaignErrorDialog Snapshots Rendering should match snapshot when dialog is closed 1`] = ` +
+ + + Can't accept contact + + + We ran into an issue connecting you with this contact. Check your network connection and try again. + +
+ + OK + +
+
+
+`; diff --git a/packages/contact-center/cc-components/tests/components/task/CampaignErrorDialog/campaign-error-dialog.snapshot.tsx b/packages/contact-center/cc-components/tests/components/task/CampaignErrorDialog/campaign-error-dialog.snapshot.tsx new file mode 100644 index 000000000..7d553622f --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/CampaignErrorDialog/campaign-error-dialog.snapshot.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import {render} from '@testing-library/react'; +import '@testing-library/jest-dom'; +import CampaignErrorDialogComponent from '../../../../src/components/task/CampaignErrorDialog/campaign-error-dialog'; +import {CampaignErrorDialogProps} from '../../../../src/components/task/CampaignErrorDialog/campaign-error-dialog.types'; + +// Mock HTMLDialogElement methods +HTMLDialogElement.prototype.showModal = jest.fn(); +HTMLDialogElement.prototype.close = jest.fn(); + +describe('CampaignErrorDialog Snapshots', () => { + const mockOnClose = jest.fn(); + + const defaultProps: CampaignErrorDialogProps = { + errorType: 'ACCEPT_FAILED', + isOpen: false, + onClose: mockOnClose, + }; + + beforeEach(() => { + jest.clearAllMocks(); + (HTMLDialogElement.prototype.showModal as jest.Mock).mockClear(); + (HTMLDialogElement.prototype.close as jest.Mock).mockClear(); + }); + + describe('Rendering', () => { + it('should match snapshot for ACCEPT_FAILED error type', () => { + const {container} = render( + + ); + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot for SKIP_FAILED error type', () => { + const {container} = render( + + ); + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot for REMOVE_FAILED error type', () => { + const {container} = render( + + ); + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot when dialog is closed', () => { + const {container} = render(); + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/contact-center/cc-components/tests/components/task/CampaignErrorDialog/campaign-error-dialog.tsx b/packages/contact-center/cc-components/tests/components/task/CampaignErrorDialog/campaign-error-dialog.tsx new file mode 100644 index 000000000..08629ac4a --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/CampaignErrorDialog/campaign-error-dialog.tsx @@ -0,0 +1,164 @@ +import React from 'react'; +import {render, fireEvent, screen} from '@testing-library/react'; +import '@testing-library/jest-dom'; +import CampaignErrorDialogComponent from '../../../../src/components/task/CampaignErrorDialog/campaign-error-dialog'; +import { + CampaignErrorDialogProps, + CampaignErrorType, + ERROR_TITLES, + ERROR_MESSAGE, +} from '../../../../src/components/task/CampaignErrorDialog/campaign-error-dialog.types'; + +// Mock HTMLDialogElement methods +HTMLDialogElement.prototype.showModal = jest.fn(); +HTMLDialogElement.prototype.close = jest.fn(); + +describe('CampaignErrorDialogComponent', () => { + const mockOnClose = jest.fn(); + + const defaultProps: CampaignErrorDialogProps = { + errorType: 'ACCEPT_FAILED', + isOpen: false, + onClose: mockOnClose, + }; + + beforeEach(() => { + jest.clearAllMocks(); + (HTMLDialogElement.prototype.showModal as jest.Mock).mockClear(); + (HTMLDialogElement.prototype.close as jest.Mock).mockClear(); + }); + + describe('Rendering', () => { + it('should render the dialog element', () => { + render(); + + const dialog = screen.getByTestId('campaign-error-dialog'); + expect(dialog).toBeInTheDocument(); + expect(dialog).toHaveClass('campaign-error-dialog'); + }); + + it('should render the correct title for ACCEPT_FAILED error type', () => { + render(); + + const title = screen.getByTestId('campaign-error-dialog-title'); + expect(title).toHaveTextContent(ERROR_TITLES.ACCEPT_FAILED); + expect(title).toHaveTextContent("Can't accept contact"); + }); + + it('should render the correct title for SKIP_FAILED error type', () => { + render(); + + const title = screen.getByTestId('campaign-error-dialog-title'); + expect(title).toHaveTextContent(ERROR_TITLES.SKIP_FAILED); + expect(title).toHaveTextContent("Can't skip contact"); + }); + + it('should render the correct title for REMOVE_FAILED error type', () => { + render(); + + const title = screen.getByTestId('campaign-error-dialog-title'); + expect(title).toHaveTextContent(ERROR_TITLES.REMOVE_FAILED); + expect(title).toHaveTextContent("Can't remove contact"); + }); + + it('should render the error message', () => { + render(); + + const message = screen.getByTestId('campaign-error-dialog-message'); + expect(message).toHaveTextContent(ERROR_MESSAGE); + expect(message).toHaveTextContent( + 'We ran into an issue connecting you with this contact. Check your network connection and try again.' + ); + }); + + it('should render the OK button', () => { + render(); + + const okButton = screen.getByTestId('campaign-error-dialog-ok-button'); + expect(okButton).toBeInTheDocument(); + expect(okButton).toHaveTextContent('OK'); + }); + }); + + describe('Dialog Open/Close Behavior', () => { + it('should call showModal when isOpen changes to true', () => { + const {rerender} = render(); + + expect(HTMLDialogElement.prototype.showModal).not.toHaveBeenCalled(); + + rerender(); + + expect(HTMLDialogElement.prototype.showModal).toHaveBeenCalledTimes(1); + }); + + it('should call close when isOpen changes to false', () => { + const {rerender} = render(); + + // Simulate dialog being open + const dialog = screen.getByTestId('campaign-error-dialog') as HTMLDialogElement; + Object.defineProperty(dialog, 'open', {value: true, writable: true}); + + rerender(); + + expect(HTMLDialogElement.prototype.close).toHaveBeenCalledTimes(1); + }); + }); + + describe('User Interactions', () => { + it('should call onClose when OK button is clicked', () => { + render(); + + const okButton = screen.getByTestId('campaign-error-dialog-ok-button'); + fireEvent.click(okButton); + + expect(mockOnClose).toHaveBeenCalledTimes(1); + }); + + it('should call onClose when native close event fires while isOpen is true', () => { + render(); + + const dialog = screen.getByTestId('campaign-error-dialog'); + fireEvent(dialog, new Event('close')); + + expect(mockOnClose).toHaveBeenCalledTimes(1); + }); + + it('should not call onClose when native close event fires while isOpen is false', () => { + render(); + + const dialog = screen.getByTestId('campaign-error-dialog'); + fireEvent(dialog, new Event('close')); + + expect(mockOnClose).not.toHaveBeenCalled(); + }); + }); + + describe('Error Type Mapping', () => { + const errorTypes: CampaignErrorType[] = ['ACCEPT_FAILED', 'SKIP_FAILED', 'REMOVE_FAILED']; + + errorTypes.forEach((errorType) => { + it(`should display correct title for ${errorType}`, () => { + render(); + + const title = screen.getByTestId('campaign-error-dialog-title'); + expect(title).toHaveTextContent(ERROR_TITLES[errorType]); + }); + }); + }); + + describe('Accessibility', () => { + it('should have proper dialog structure', () => { + render(); + + const dialog = screen.getByTestId('campaign-error-dialog'); + expect(dialog.tagName).toBe('DIALOG'); + }); + + it('should have heading element for title', () => { + render(); + + const title = screen.getByTestId('campaign-error-dialog-title'); + expect(title).toBeInTheDocument(); + }); + }); +});