From ed4d2df5974dcdb9f72b775834b16e365f2798e4 Mon Sep 17 00:00:00 2001 From: adhmenon Date: Fri, 3 Oct 2025 18:58:33 +0530 Subject: [PATCH 01/20] feat(cc-wudgets): added-address-book --- .../call-control-custom.utils.ts | 85 + .../consult-transfer-popover.tsx | 422 ++- .../task/CallControl/call-control.styles.scss | 17 +- .../task/CallControl/call-control.tsx | 13 +- .../src/components/task/task.types.ts | 31 +- ...ransfer-dial-number.snapshot.test.tsx.snap | 29 - ...consult-transfer-popover.snapshot.tsx.snap | 3090 +---------------- .../call-control-custom.util.tsx | 107 +- ...ult-transfer-dial-number.snapshot.test.tsx | 26 - .../consult-transfer-popover.snapshot.tsx | 223 +- .../consult-transfer-popover.tsx | 78 +- packages/contact-center/store/package.json | 4 +- .../contact-center/store/src/store.types.ts | 29 +- .../store/src/storeEventsWrapper.ts | 57 +- .../store/tests/storeEventsWrapper.ts | 50 +- packages/contact-center/task/src/helper.ts | 49 +- .../task/tests/CallControl/index.tsx | 3 + packages/contact-center/task/tests/helper.ts | 67 +- .../test-fixtures/src/fixtures.ts | 8 +- yarn.lock | 232 +- 20 files changed, 1125 insertions(+), 3495 deletions(-) delete mode 100644 packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-dial-number.snapshot.test.tsx.snap delete mode 100644 packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-dial-number.snapshot.test.tsx 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 06d48003d..f497a38b0 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 @@ -1,5 +1,7 @@ import {BuddyDetails, ContactServiceQueue, ILogger} from '@webex/cc-store'; +import {useState, useCallback} from 'react'; import {MUTE_CALL, UNMUTE_CALL} from '../../constants'; +import {FetchFunction, TransformFunction} from '../../task.types'; /** * Interface for button configuration @@ -655,3 +657,86 @@ export const debounce = unknown>( }; } }; + +export const usePaginatedData = ( + fetchFunction: FetchFunction | undefined, + transformFunction: TransformFunction, + categoryName: string, + logger? +) => { + const [data, setData] = useState([]); + const [page, setPage] = useState(0); + const [hasMore, setHasMore] = useState(true); + const [loading, setLoading] = useState(false); + + const loadData = useCallback( + async (currentPage = 0, search = '', reset = false) => { + if (!fetchFunction) { + setData([]); + setHasMore(false); + return; + } + + setLoading(true); + try { + const apiParams: {page: number; pageSize: number; search?: string} = { + page: currentPage, + pageSize: 25, + }; + + if (search && search.trim()) { + apiParams.search = search; + } + + logger?.info(`CC-Components: Loading ${categoryName} - page: ${currentPage}, search: "${search}"`); + const response = await fetchFunction(apiParams); + + if (!response || !response.data) { + logger?.error(`CC-Components: Invalid response from fetch function for ${categoryName}`); + if (reset || currentPage === 0) { + setData([]); + } + setHasMore(false); + return; + } + + logger?.info(`CC-Components: Loaded ${response.data.length} ${categoryName} for page ${currentPage}`); + + const transformedEntries = response.data.map((entry, index) => transformFunction(entry, currentPage, index)); + + if (reset || currentPage === 0) { + setData(transformedEntries); + } else { + setData((prev) => [...prev, ...transformedEntries]); + } + + const newPage = response.meta?.page ?? currentPage; + const totalPages = response.meta?.totalPages ?? 1; + + setPage(newPage); + setHasMore(newPage < totalPages - 1); + + logger?.info( + `CC-Components: ${categoryName} pagination state - current: ${newPage}, total: ${totalPages}, hasMore: ${newPage < totalPages - 1}` + ); + } catch (error) { + logger?.error(`CC-Components: Error loading ${categoryName}:`, error); + if (reset || currentPage === 0) { + setData([]); + } + setHasMore(false); + } finally { + setLoading(false); + } + }, + [fetchFunction, transformFunction, logger, categoryName] + ); + + const reset = useCallback(() => { + setData([]); + setPage(0); + setHasMore(true); + }, []); + + return {data, page, hasMore, loading, loadData, reset}; +}; diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx index 593456e1a..9a2dd6c13 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx @@ -1,143 +1,383 @@ -import React, {useState} from 'react'; -import {Text, TabListNext, TabNext, ListNext} from '@momentum-ui/react-collaboration'; +import React, {useState, useRef, useEffect, useCallback} from 'react'; +import {Text, ListNext, TextInput, Button} from '@momentum-ui/react-collaboration'; import ConsultTransferListComponent from './consult-transfer-list-item'; import {ConsultTransferPopoverComponentProps} from '../../task.types'; +import {AddressBookEntry, EntryPointRecord, ContactServiceQueue} from '@webex/cc-store'; import ConsultTransferEmptyState from './consult-transfer-empty-state'; import { isAgentsEmpty, - isQueuesEmpty, - handleTabSelection, handleAgentSelection, handleQueueSelection, - getEmptyStateMessage, + debounce, + usePaginatedData, } from './call-control-custom.utils'; -import DialNumberUI from './consult-transfer-dial-number'; -import {AGENTS, DIAL_NUMBER, QUEUES} from '../../constants'; +type CategoryType = 'Agents' | 'Queues' | 'Dial Number' | 'Entry Point'; const ConsultTransferPopoverComponent: React.FC = ({ heading, buttonIcon, buddyAgents, - queues, + getAddressBookEntries, + getEntryPoints, + getQueues, onAgentSelect, onQueueSelect, onDialNumberSelect, + onEntryPointSelect, allowConsultToQueue, logger, }) => { - const [selectedTab, setSelectedTab] = useState(AGENTS); - const filteredAgents = buddyAgents; - const filteredQueues = queues; + const [selectedCategory, setSelectedCategory] = useState('Agents'); + const [searchQuery, setSearchQuery] = useState(''); + const loadMoreRef = useRef(null); - const noAgents = isAgentsEmpty(filteredAgents); - const noQueues = isQueuesEmpty(filteredQueues); - const showTabs = true; // Always show tabs to allow dial number option + const noAgents = isAgentsEmpty(buddyAgents, logger); - const renderList = (items, getKey, getTitle, handleSelect) => ( + const renderList = ( + items: T[], + onButtonPress: (item: T) => void + ) => ( {items.map((item) => ( -
e.stopPropagation()} - style={{cursor: 'pointer', pointerEvents: 'auto'}} - > +
e.stopPropagation()} style={{cursor: 'pointer', pointerEvents: 'auto'}}> handleSelect(getKey(item), getTitle(item))} + onButtonPress={() => onButtonPress(item)} logger={logger} />
))} {items.length === 0 && ( - No {selectedTab.toLowerCase()} found + No {selectedCategory.toLowerCase()} found )} ); + const { + data: dialNumbers, + page: dialNumbersPage, + hasMore: hasMoreDialNumbers, + loading: loadingDialNumbers, + loadData: loadDialNumbers, + reset: resetDialNumbers, + } = usePaginatedData( + getAddressBookEntries, + (entry) => ({ + id: entry.id, + name: entry.name, + number: entry.number, + organizationId: entry.organizationId, + version: entry.version, + createdTime: entry.createdTime, + lastUpdatedTime: entry.lastUpdatedTime, + }), + 'Dial Numbers', + logger + ); + + const { + data: entryPoints, + page: entryPointsPage, + hasMore: hasMoreEntryPoints, + loading: loadingEntryPoints, + loadData: loadEntryPoints, + reset: resetEntryPoints, + } = usePaginatedData( + getEntryPoints, + (entry) => ({id: entry.id, name: entry.name}), + 'Entry Points', + logger + ); + + const { + data: queuesData, + page: queuesPage, + hasMore: hasMoreQueues, + loading: loadingQueues, + loadData: loadQueues, + reset: resetQueues, + } = usePaginatedData( + getQueues, + (entry) => ({id: entry.id, name: entry.name, description: entry.description}), + 'Queues', + logger + ); + + const loadNextPage = useCallback(() => { + if (selectedCategory === 'Dial Number' && hasMoreDialNumbers && !loadingDialNumbers) { + loadDialNumbers(dialNumbersPage + 1, searchQuery); + } else if (selectedCategory === 'Entry Point' && hasMoreEntryPoints && !loadingEntryPoints) { + loadEntryPoints(entryPointsPage + 1, searchQuery); + } else if (selectedCategory === 'Queues' && hasMoreQueues && !loadingQueues) { + loadQueues(queuesPage + 1, searchQuery); + } + }, [ + selectedCategory, + hasMoreDialNumbers, + hasMoreEntryPoints, + hasMoreQueues, + loadingDialNumbers, + loadingEntryPoints, + loadingQueues, + dialNumbersPage, + entryPointsPage, + queuesPage, + searchQuery, + loadDialNumbers, + loadEntryPoints, + loadQueues, + ]); + + const debouncedSearchRef = useRef>(); + if (!debouncedSearchRef.current) { + debouncedSearchRef.current = debounce((query: string, category: CategoryType) => { + if (query.length === 0 || query.length >= 2) { + if (category === 'Dial Number') { + loadDialNumbers(0, query, true); + } else if (category === 'Entry Point') { + loadEntryPoints(0, query, true); + } else if (category === 'Queues') { + loadQueues(0, query, true); + } + } + }, 500); + } + + useEffect(() => { + return () => { + debouncedSearchRef.current = undefined; + }; + }, []); + + const handleSearchChange = useCallback( + (value: string) => { + setSearchQuery(value); + if (selectedCategory !== 'Agents') { + debouncedSearchRef.current?.(value, selectedCategory); + } + }, + [selectedCategory] + ); + + const handleCategoryChange = useCallback( + (category: CategoryType) => { + setSelectedCategory(category); + setSearchQuery(''); + resetDialNumbers(); + resetEntryPoints(); + resetQueues(); + }, + [resetDialNumbers, resetEntryPoints, resetQueues] + ); + + const createCategoryClickHandler = (category: CategoryType) => () => handleCategoryChange(category); + const handleAgentsClick = createCategoryClickHandler('Agents'); + const handleQueuesClick = createCategoryClickHandler('Queues'); + const handleDialNumberClick = createCategoryClickHandler('Dial Number'); + const handleEntryPointClick = createCategoryClickHandler('Entry Point'); + + useEffect(() => { + const loadMoreElement = loadMoreRef.current; + if (!loadMoreElement) return; + const observer = new IntersectionObserver( + (entries) => { + const [entry] = entries; + if (entry.isIntersecting) { + loadNextPage(); + } + }, + {threshold: 1.0} + ); + observer.observe(loadMoreElement); + return () => { + observer.unobserve(loadMoreElement); + }; + }, [loadNextPage]); + + useEffect(() => { + if (selectedCategory === 'Dial Number' && dialNumbers.length === 0) { + loadDialNumbers(0, '', true); + } else if (selectedCategory === 'Entry Point' && entryPoints.length === 0) { + loadEntryPoints(0, '', true); + } else if (selectedCategory === 'Queues' && queuesData.length === 0) { + loadQueues(0, '', true); + } + }, [selectedCategory]); + + const noQueues = queuesData.length === 0; + const noDialNumbers = dialNumbers.length === 0; + const noEntryPoints = entryPoints.length === 0; + + const hasAnyData = !noAgents || !noQueues || !noDialNumbers || !noEntryPoints; + return (
{heading} - {/* Only show tabs if at least one list is available */} - {showTabs && ( - { - handleTabSelection(key as string, setSelectedTab, logger); - }} - > - - Agents - - - Queues - - - Dial Number - - - )} - - {/* If both are empty, show the big empty state */} - {!showTabs && } +
+ handleSearchChange(value)} + clearAriaLabel="Clear search" + aria-labelledby="consult-search-label" + style={{width: '100%'}} + /> +
- {/* If agents tab is selected and empty */} - {showTabs && selectedTab === AGENTS && noAgents && ( - - )} +
+ + + + +
- {/* If queues tab is selected and empty */} - {showTabs && selectedTab === QUEUES && noQueues && ( - - )} + {!hasAnyData && } - {/* Render lists if not empty */} - {showTabs && - selectedTab === AGENTS && + {selectedCategory === 'Agents' && !noAgents && renderList( - filteredAgents, - (agent) => agent.agentId, - (agent) => agent.agentName, - (id, name) => { - handleAgentSelection(id, name, onAgentSelect, logger); - } + buddyAgents.map((agent) => ({id: agent.agentId, name: agent.agentName})), + (item) => handleAgentSelection(item.id, item.name, onAgentSelect, logger) )} - {showTabs && - selectedTab === QUEUES && - !noQueues && - renderList( - filteredQueues, - (queue) => queue.id, - (queue) => queue.name, - (id, name) => { - handleQueueSelection(id, name, onQueueSelect, logger); - } - )} - {showTabs && selectedTab === DIAL_NUMBER && ( - { - onDialNumberSelect(dialNumber); - }} - logger={logger} - /> + {selectedCategory === 'Queues' && !noQueues && ( +
+ {renderList( + queuesData.map((q) => ({id: q.id, name: q.name})), + (item) => handleQueueSelection(item.id, item.name, onQueueSelect, logger) + )} + {hasMoreQueues && ( +
+ {loadingQueues ? ( + + Loading more queues... + + ) : ( + + Scroll to load more + + )} +
+ )} +
+ )} + + {selectedCategory === 'Dial Number' && !noDialNumbers && ( +
+ {renderList( + dialNumbers.map((d) => ({id: d.id, name: d.name, number: d.number})), + (item) => { + if (item.number) { + if (onDialNumberSelect) { + onDialNumberSelect(item.number); + } + } + } + )} + {hasMoreDialNumbers && ( +
+ {loadingDialNumbers ? ( + + Loading more dial numbers... + + ) : ( + + Scroll to load more + + )} +
+ )} +
+ )} + + {selectedCategory === 'Entry Point' && !noEntryPoints && ( +
+ {renderList( + entryPoints.map((e) => ({id: e.id, name: e.name})), + (item) => { + if (onEntryPointSelect) { + onEntryPointSelect(item.id, item.name); + } + } + )} + {hasMoreEntryPoints && ( +
+ {loadingEntryPoints ? ( + + Loading more entry points... + + ) : ( + + Scroll to load more + + )} +
+ )} +
)}
); diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss index c27d7c03b..2ea24b6d6 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss +++ b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss @@ -71,7 +71,7 @@ .md-button-circle-wrapper.call-control-button { width: 1rem; - height: 1rem; + height: 1rem; min-width: 2rem; min-height: 2rem; padding: 0.5rem; @@ -115,8 +115,8 @@ --mdc-icon-fill-color: var(--mds-color-theme-button-primary-disabled); } .call-control-consult-button-cancel-icon { - --mdc-icon-fill-color: var(--mds-color-theme-button-primary-disabled); -} + --mdc-icon-fill-color: var(--mds-color-theme-button-primary-disabled); + } } .call-control-button-icon { @@ -139,7 +139,7 @@ .md-button-circle-wrapper.call-control-button-muted { width: 1rem; - height: 1rem; + height: 1rem; min-width: 2rem; min-height: 2rem; padding: 0.5rem; @@ -167,7 +167,6 @@ } } - .wrapup-button { display: inline-flex; justify-content: center; @@ -305,14 +304,14 @@ } .consult-empty-message { - color: var(--mds-color-theme-text-secondary-normal, #000000B2); + color: var(--mds-color-theme-text-secondary-normal, #000000b2); margin-top: 1.5rem; } @media only screen and (max-width: 600px) { .agent-popover { - .agent-popover-content { - min-width: 20rem; + .agent-popover-content { + min-width: 20rem; + } } } -} \ No newline at end of file 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 aea258963..09bf0ef44 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 @@ -48,8 +48,6 @@ function CallControlComponent(props: CallControlComponentProps) { setIsRecording, buddyAgents, loadBuddyAgents, - queues, - loadQueues, transferCall, consultCall, consultInitiated, @@ -63,6 +61,9 @@ function CallControlComponent(props: CallControlComponentProps) { logger, secondsUntilAutoWrapup, cancelAutoWrapup, + getAddressBookEntries, + getEntryPoints, + getQueuesFetcher, } = props; useEffect(() => { @@ -159,7 +160,6 @@ function CallControlComponent(props: CallControlComponentProps) { setShowAgentMenu(true); setAgentMenuType(button.menuType as CallControlMenuType); loadBuddyAgents(); - loadQueues(); }} onHide={() => { setShowAgentMenu(false); @@ -209,9 +209,14 @@ function CallControlComponent(props: CallControlComponentProps) { heading={button.menuType} buttonIcon={button.icon} buddyAgents={buddyAgents} - queues={queues} + getAddressBookEntries={getAddressBookEntries} + getEntryPoints={getEntryPoints} + getQueues={getQueuesFetcher} onAgentSelect={(agentId, agentName) => handleTargetSelect(agentId, agentName, 'agent')} onQueueSelect={(queueId, queueName) => handleTargetSelect(queueId, queueName, 'queue')} + onEntryPointSelect={(entryPointId, entryPointName) => + handleTargetSelect(entryPointId, entryPointName, 'entryPoint') + } onDialNumberSelect={(dialNumber) => handleTargetSelect(dialNumber, dialNumber, 'dialNumber')} allowConsultToQueue={allowConsultToQueue} logger={logger} diff --git a/packages/contact-center/cc-components/src/components/task/task.types.ts b/packages/contact-center/cc-components/src/components/task/task.types.ts index a96c41b06..cd2d280c5 100644 --- a/packages/contact-center/cc-components/src/components/task/task.types.ts +++ b/packages/contact-center/cc-components/src/components/task/task.types.ts @@ -6,6 +6,8 @@ import { BuddyDetails, DestinationType, ContactServiceQueue, + AddressBookEntry, + EntryPointRecord, } from '@webex/cc-store'; type Enum> = T[keyof T]; @@ -409,6 +411,13 @@ export interface ControlProps { * Function to cancel the auto wrap-up timer. */ cancelAutoWrapup: () => void; + + /** Fetch paginated address book entries for dial numbers */ + getAddressBookEntries?: FetchFunction; + /** Fetch paginated entry points */ + getEntryPoints?: FetchFunction; + /** Fetch paginated queues (filtered by media type in store) */ + getQueuesFetcher?: FetchFunction; } export type CallControlComponentProps = Pick< @@ -454,6 +463,9 @@ export type CallControlComponentProps = Pick< | 'logger' | 'secondsUntilAutoWrapup' | 'cancelAutoWrapup' + | 'getAddressBookEntries' + | 'getEntryPoints' + | 'getQueuesFetcher' >; /** @@ -509,9 +521,12 @@ export interface ConsultTransferPopoverComponentProps { heading: string; buttonIcon: string; buddyAgents: BuddyDetails[]; - queues?: ContactServiceQueue[]; + getAddressBookEntries?: FetchFunction; + getEntryPoints?: FetchFunction; + getQueues?: FetchFunction; onAgentSelect?: (agentId: string, agentName: string) => void; onQueueSelect?: (queueId: string, queueName: string) => void; + onEntryPointSelect?: (entryPointId: string, entryPointName: string) => void; onDialNumberSelect?: (dialNumber: string) => void; allowConsultToQueue: boolean; logger: ILogger; @@ -664,3 +679,17 @@ export interface TimerUIState { iconName: string; formattedTime: string; } + +/** + * Type for fetch function + */ +export type FetchFunction = (params: { + page: number; + pageSize: number; + search?: string; +}) => Promise<{data: T[]; meta?: {page?: number; totalPages?: number}}>; + +/** + * Type for transform function + */ +export type TransformFunction = (item: T, page: number, index: number) => U; diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-dial-number.snapshot.test.tsx.snap b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-dial-number.snapshot.test.tsx.snap deleted file mode 100644 index 4953147a4..000000000 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-dial-number.snapshot.test.tsx.snap +++ /dev/null @@ -1,29 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DialNumberUI snapshot matches snapshot 1`] = ` -
-
- - -
-
-`; diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap index 9ae44f4a0..a1df70cd8 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap @@ -1,2663 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ConsultTransferPopoverComponent Snapshots Interactions should render component after agent selection 1`] = ` -
- - Select an Agent - -
- - - -
-
    -
    -
  • -
    - -
    -
    - - Agent One - -
    -
    -
    - -
    -
    -
  • -
    -
    -
  • -
    - -
    -
    - - Agent Two - -
    -
    -
    - -
    -
    -
  • -
    -
-
-`; - -exports[`ConsultTransferPopoverComponent Snapshots Interactions should render component after queue selection 1`] = ` -
- - Select an Agent - -
- - - -
-
    -
    -
  • -
    - -
    -
    - - Queue One - -
    -
    -
    - -
    -
    -
  • -
    -
    -
  • -
    - -
    -
    - - Queue Two - -
    -
    -
    - -
    -
    -
  • -
    -
-
-`; - -exports[`ConsultTransferPopoverComponent Snapshots Interactions should render component after switching to queues tab 1`] = ` -
- - Select an Agent - -
- - - -
-
    -
    -
  • -
    - -
    -
    - - Queue One - -
    -
    -
    - -
    -
    -
  • -
    -
    -
  • -
    - -
    -
    - - Queue Two - -
    -
    -
    - -
    -
    -
  • -
    -
-
-`; - -exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render the component with tabs and agents 1`] = ` -
- - Select an Agent - -
- - - -
-
    -
    -
  • -
    - -
    -
    - - Agent One - -
    -
    -
    - -
    -
    -
  • -
    -
    -
  • -
    - -
    -
    - - Agent Two - -
    -
    -
    - -
    -
    -
  • -
    -
-
-`; - -exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with agents having different states 1`] = ` -
- - Select an Agent - -
- - - -
-
    -
    -
  • -
    - -
    -
    - - Available Agent - -
    -
    -
    - -
    -
    -
  • -
    -
    -
  • -
    - -
    -
    - - Busy Agent - -
    -
    -
    - -
    -
    -
  • -
    -
    -
  • -
    - -
    -
    - - Idle Agent - -
    -
    -
    - -
    -
    -
  • -
    -
-
-`; - -exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with allowConsultToQueue false 1`] = ` -
- - Select an Agent - -
- - - -
-
    -
    -
  • -
    - -
    -
    - - Agent One - -
    -
    -
    - -
    -
    -
  • -
    -
    -
  • -
    - -
    -
    - - Agent Two - -
    -
    -
    - -
    -
    -
  • -
    -
-
-`; - -exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with custom empty message 1`] = ` -
- - Select an Agent - -
- - - -
-
-
-
- We can't find any agent available for now. -
-
-
-`; - -exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with different heading 1`] = ` -
- - Choose Transfer Target - -
- - - -
-
    -
    -
  • -
    - -
    -
    - - Agent One - -
    -
    -
    - -
    -
    -
  • -
    -
    -
  • -
    - -
    -
    - - Agent Two - -
    -
    -
    - -
    -
    -
  • -
    -
-
-`; - -exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with empty agents list 1`] = ` -
- - Select an Agent - -
- - - -
-
-
-
- We can't find any agent available for now. -
-
-
-`; - -exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with empty queues list 1`] = ` -
- - Select an Agent - -
- - - -
-
    -
    -
  • -
    - -
    -
    - - Agent One - -
    -
    -
    - -
    -
    -
  • -
    -
    -
  • -
    - -
    -
    - - Agent Two - -
    -
    -
    - -
    -
    -
  • -
    -
-
-`; - -exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with single agent 1`] = ` -
- - Select an Agent - -
- - - -
-
    -
    -
  • -
    - -
    -
    - - Single Agent - -
    -
    -
    - -
    -
    -
  • -
    -
-
-`; - -exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with single queue 1`] = ` -
- - Select an Agent - -
- - - -
-
    -
    -
  • -
    - -
    -
    - - Agent One - -
    -
    -
    - -
    -
    -
  • -
    -
    -
  • -
    - -
    -
    - - Agent Two - -
    -
    -
    - -
    -
    -
  • -
    -
-
-`; - -exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render without tabs when showTabs is false 1`] = ` -
- - Select an Agent - -
- - - -
-
    -
    -
  • -
    - -
    -
    - - Agent One - -
    -
    -
    - -
    -
    -
  • -
    -
    -
  • -
    - -
    -
    - - Agent Two - -
    -
    -
    - -
    -
    -
  • -
    -
-
-`; - -exports[`ConsultTransferPopoverComponent Snapshots State Management should update when agents list changes 1`] = ` +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with agents having different states 1`] = `
@@ -2670,212 +13,106 @@ exports[`ConsultTransferPopoverComponent Snapshots State Management should updat Select an Agent
+
+
+ +
+
+
+
+
-
-
    -
    -
  • -
    - -
    -
    - - New Agent One - -
    -
    -
    - -
    -
    -
  • -
    -
-
-`; - -exports[`ConsultTransferPopoverComponent Snapshots State Management should update when heading changes 1`] = ` -
- - New Heading - -
- -
  • -
    - -
    -
    - - Agent One - -
    -
    -
    - -
    -
    -
  • -
-
-
@@ -2999,7 +162,7 @@ exports[`ConsultTransferPopoverComponent Snapshots State Management should updat tagname="p" type="body-large-regular" > - Agent Two + Available Agent
- -
-`; - -exports[`ConsultTransferPopoverComponent Snapshots State Management should update when queues list changes 1`] = ` -
- - Select an Agent - -
- - - -
-
@@ -3152,7 +236,7 @@ exports[`ConsultTransferPopoverComponent Snapshots State Management should updat tagname="p" type="body-large-regular" > - Agent One + Busy Agent
@@ -3226,7 +310,7 @@ exports[`ConsultTransferPopoverComponent Snapshots State Management should updat tagname="p" type="body-large-regular" > - Agent Two + Idle Agent
({ + organizationId: 'org-1', + id, + version: 1, + name, + description, + queueType: 'INBOUND', + checkAgentAvailability: true, + channelType: 'TELEPHONY', + serviceLevelThreshold: 20, + maxActiveContacts: 25, + maxTimeInQueue: 600, + defaultMusicInQueueMediaFileId: 'media-1', + active: true, + monitoringPermitted: true, + parkingPermitted: true, + recordingPermitted: true, + recordingAllCallsPermitted: true, + pauseRecordingPermitted: true, + controlFlowScriptUrl: 'https://example.com/control-flow', + ivrRequeueUrl: 'https://example.com/requeue', + routingType: 'LONGEST_AVAILABLE_AGENT', + queueRoutingType: 'TEAM_BASED', + callDistributionGroups: [], +}); + const mockBuddyAgents: BuddyDetails[] = [ { agentId: 'agent1', @@ -77,38 +104,10 @@ const mockBuddyAgents: BuddyDetails[] = [ ]; const mockQueues: ContactServiceQueue[] = [ - { - id: 'queue1', - name: 'Support Queue', - description: 'Support Queue Description', - queueType: 'inbound', - checkAgentAvailability: true, - channelType: 'telephony', - } as ContactServiceQueue, - { - id: 'queue2', - name: 'Sales Queue', - description: 'Sales Queue Description', - queueType: 'inbound', - checkAgentAvailability: true, - channelType: 'telephony', - } as ContactServiceQueue, - { - id: 'queue3', - name: '', - description: 'Empty Name Queue', - queueType: 'inbound', - checkAgentAvailability: true, - channelType: 'telephony', - } as ContactServiceQueue, - { - id: '', - name: 'Invalid Queue', - description: 'Invalid Queue Description', - queueType: 'inbound', - checkAgentAvailability: true, - channelType: 'telephony', - } as ContactServiceQueue, + buildQueue('queue1', 'Support Queue', 'Support Queue Description'), + buildQueue('queue2', 'Sales Queue', 'Sales Queue Description'), + buildQueue('queue3', '', 'Empty Name Queue'), + buildQueue('', 'Invalid Queue', 'Invalid Queue Description'), ]; describe('Call Control Custom Utils', () => { @@ -800,55 +799,19 @@ describe('Call Control Custom Utils', () => { describe('isQueueAvailable', () => { it('should return true for valid queue', () => { - expect( - isQueueAvailable({ - id: 'queue1', - name: 'Support Queue', - description: 'Support Queue Description', - queueType: 'inbound', - checkAgentAvailability: true, - channelType: 'telephony', - } as ContactServiceQueue) - ).toBe(true); + expect(isQueueAvailable(buildQueue('queue1', 'Support Queue'))).toBe(true); }); it('should return false for missing id', () => { - expect( - isQueueAvailable({ - id: '', - name: 'Support Queue', - description: 'Support Queue Description', - queueType: 'inbound', - checkAgentAvailability: true, - channelType: 'telephony', - } as ContactServiceQueue) - ).toBeFalsy(); + expect(isQueueAvailable(buildQueue('', 'Support Queue'))).toBeFalsy(); }); it('should return false for missing name', () => { - expect( - isQueueAvailable({ - id: 'queue1', - name: '', - description: 'Support Queue Description', - queueType: 'inbound', - checkAgentAvailability: true, - channelType: 'telephony', - } as ContactServiceQueue) - ).toBeFalsy(); + expect(isQueueAvailable(buildQueue('queue1', ''))).toBeFalsy(); }); it('should return false for whitespace-only name', () => { - expect( - isQueueAvailable({ - id: 'queue1', - name: ' ', - description: 'Support Queue Description', - queueType: 'inbound', - checkAgentAvailability: true, - channelType: 'telephony', - } as ContactServiceQueue) - ).toBeFalsy(); + expect(isQueueAvailable(buildQueue('queue1', ' '))).toBeFalsy(); }); it('should return false for null queue', () => { diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-dial-number.snapshot.test.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-dial-number.snapshot.test.tsx deleted file mode 100644 index 64a7f6461..000000000 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-dial-number.snapshot.test.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import {render} from '@testing-library/react'; -import DialNumberUI from '../../../../../src/components/task/CallControl/CallControlCustom/consult-transfer-dial-number'; - -describe('DialNumberUI snapshot', () => { - const loggerMock = { - log: jest.fn(), - info: jest.fn(), - warn: jest.fn(), - trace: jest.fn(), - error: jest.fn(), - debug: jest.fn(), - }; - const onButtonPressMock = jest.fn(); - const defaultProps = { - title: 'Dial Number', - buttonIcon: 'call', - onButtonPress: onButtonPressMock, - logger: loggerMock, - }; - - it('matches snapshot', () => { - const {container} = render(); - expect(container).toMatchSnapshot(); - }); -}); diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.snapshot.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.snapshot.tsx index baf2dc497..0d4e1b054 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.snapshot.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.snapshot.tsx @@ -1,6 +1,6 @@ import React from 'react'; import '@testing-library/jest-dom'; -import {render, fireEvent, act} from '@testing-library/react'; +import {render, fireEvent, act, waitFor} from '@testing-library/react'; import ConsultTransferPopoverComponent from '../../../../../src/components/task/CallControl/CallControlCustom/consult-transfer-popover'; import {ContactServiceQueue} from '@webex/cc-store'; @@ -29,6 +29,32 @@ describe('ConsultTransferPopoverComponent Snapshots', () => { const mockOnAgentSelect = jest.fn(); const mockOnQueueSelect = jest.fn(); + const buildQueue = (id: string, name: string, description: string = 'Queue'): ContactServiceQueue => ({ + organizationId: 'org-test', + id, + version: 1, + name, + description, + queueType: 'INBOUND', + checkAgentAvailability: true, + channelType: 'TELEPHONY', + serviceLevelThreshold: 20, + maxActiveContacts: 25, + maxTimeInQueue: 600, + defaultMusicInQueueMediaFileId: 'media-1', + active: true, + monitoringPermitted: true, + parkingPermitted: true, + recordingPermitted: true, + recordingAllCallsPermitted: true, + pauseRecordingPermitted: true, + controlFlowScriptUrl: 'https://example.com/flow', + ivrRequeueUrl: 'https://example.com/requeue', + routingType: 'LONGEST_AVAILABLE_AGENT', + queueRoutingType: 'TEAM_BASED', + callDistributionGroups: [], + }); + const defaultProps = { heading: 'Select an Agent', buttonIcon: 'agent-icon', @@ -50,16 +76,11 @@ describe('ConsultTransferPopoverComponent Snapshots', () => { siteId: 'site1', }, ], - queues: [ - {id: 'queue1', name: 'Queue One'} as ContactServiceQueue, - {id: 'queue2', name: 'Queue Two'} as ContactServiceQueue, - ], + getQueues: async () => ({data: [buildQueue('queue1', 'Queue One')], meta: {page: 0, totalPages: 1}}), onAgentSelect: mockOnAgentSelect, onQueueSelect: mockOnQueueSelect, allowConsultToQueue: true, logger: mockLogger, - showTabs: true, - emptyMessage: 'No agents or queues available', }; beforeEach(() => { @@ -67,27 +88,26 @@ describe('ConsultTransferPopoverComponent Snapshots', () => { }); describe('Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component', () => { - it('should render the component with tabs and agents', async () => { + it('should render the component with heading and category buttons', async () => { let screen; await act(async () => { screen = render(); }); - - const container = screen.container.querySelector('.agent-popover-content'); - mockUIDProps(container); - expect(container).toMatchSnapshot(); + expect(screen.getByText('Select an Agent')).toBeInTheDocument(); + const btns = Array.from(screen.container.querySelectorAll('button')).map( + (b) => (b as HTMLButtonElement).textContent + ); + expect(btns).toEqual(expect.arrayContaining(['Agents', 'Queues', 'Dial Number', 'Entry Point'])); }); - it('should render without tabs when showTabs is false', async () => { - const noTabsProps = {...defaultProps, showTabs: false}; + it('should render without errors when rendered with minimal props', async () => { let screen; await act(async () => { - screen = render(); + screen = render(); }); - const container = screen.container.querySelector('.agent-popover-content'); mockUIDProps(container); - expect(container).toMatchSnapshot(); + expect(container).toBeInTheDocument(); }); it('should render with different heading', async () => { @@ -96,10 +116,7 @@ describe('ConsultTransferPopoverComponent Snapshots', () => { await act(async () => { screen = render(); }); - - const container = screen.container.querySelector('.agent-popover-content'); - mockUIDProps(container); - expect(container).toMatchSnapshot(); + expect(screen.getByText('Choose Transfer Target')).toBeInTheDocument(); }); it('should render with empty agents list', async () => { @@ -108,22 +125,27 @@ describe('ConsultTransferPopoverComponent Snapshots', () => { await act(async () => { screen = render(); }); - - const container = screen.container.querySelector('.agent-popover-content'); - mockUIDProps(container); - expect(container).toMatchSnapshot(); + const emptyState = screen.container.querySelector('.consult-empty-message'); + expect(emptyState?.textContent).toBe('No data available for consult transfer.'); }); it('should render with empty queues list', async () => { - const emptyQueuesProps = {...defaultProps, queues: []}; + const emptyQueuesProps = { + ...defaultProps, + getQueues: async () => ({data: [], meta: {page: 0, totalPages: 0}}), + }; let screen; await act(async () => { screen = render(); }); - const container = screen.container.querySelector('.agent-popover-content'); - mockUIDProps(container); - expect(container).toMatchSnapshot(); + const queuesButton = Array.from(screen.container.querySelectorAll('button')).find( + (btn) => (btn as HTMLButtonElement).textContent === 'Queues' + ) as HTMLButtonElement | undefined; + if (queuesButton) fireEvent.click(queuesButton); + const list = screen.container.querySelector('.agent-list'); + expect(list).toBeNull(); + expect(screen.container.querySelectorAll('.call-control-list-item').length).toBe(0); }); it('should render with allowConsultToQueue false', async () => { @@ -133,26 +155,28 @@ describe('ConsultTransferPopoverComponent Snapshots', () => { screen = render(); }); - const container = screen.container.querySelector('.agent-popover-content'); - mockUIDProps(container); - expect(container).toMatchSnapshot(); + const queuesButton = Array.from(screen.container.querySelectorAll('button')).find((btn) => { + const el = btn as HTMLButtonElement; + return el && el.textContent === 'Queues'; + }) as HTMLButtonElement | undefined; + expect(queuesButton).toBeDefined(); + expect(queuesButton?.disabled).toBe(true); }); it('should render with custom empty message', async () => { const customEmptyProps = { ...defaultProps, buddyAgents: [], - queues: [], - emptyMessage: 'Custom empty state message', + getQueues: async () => ({data: [], meta: {page: 0, totalPages: 0}}), }; let screen; await act(async () => { screen = render(); }); - const container = screen.container.querySelector('.agent-popover-content'); - mockUIDProps(container); - expect(container).toMatchSnapshot(); + const emptyState = screen.container.querySelector('.consult-empty-state'); + expect(emptyState).toBeInTheDocument(); + expect(screen.getByText('No data available for consult transfer.')).toBeInTheDocument(); }); it('should render with single agent', async () => { @@ -174,24 +198,29 @@ describe('ConsultTransferPopoverComponent Snapshots', () => { screen = render(); }); - const container = screen.container.querySelector('.agent-popover-content'); - mockUIDProps(container); - expect(container).toMatchSnapshot(); + const singleAgentItem = screen.container.querySelector('[aria-label="Single Agent"]'); + expect(singleAgentItem).toBeInTheDocument(); }); it('should render with single queue', async () => { const singleQueueProps = { ...defaultProps, - queues: [{id: 'queue1', name: 'Single Queue'} as ContactServiceQueue], + getQueues: async () => ({ + data: [buildQueue('queue1', 'Single Queue')], + meta: {page: 0, totalPages: 1}, + }), }; let screen; await act(async () => { screen = render(); }); - const container = screen.container.querySelector('.agent-popover-content'); - mockUIDProps(container); - expect(container).toMatchSnapshot(); + const queuesButton = Array.from(screen.container.querySelectorAll('button')).find((btn) => { + const el = btn as HTMLButtonElement; + return el && el.textContent === 'Queues'; + }) as HTMLButtonElement | undefined; + if (queuesButton) fireEvent.click(queuesButton); + await waitFor(() => expect(screen.container.querySelector('[aria-label="Single Queue"]')).toBeInTheDocument()); }); it('should render with agents having different states', async () => { @@ -237,74 +266,73 @@ describe('ConsultTransferPopoverComponent Snapshots', () => { describe('Interactions', () => { it('should render component after switching to queues tab', async () => { - let screen; + let renderResult; await act(async () => { - screen = render(); + renderResult = render(); }); - const queueTab = screen.container.querySelector('.queue-tab'); - fireEvent.click(queueTab); + const queuesButton1 = Array.from(renderResult.container.querySelectorAll('button')).find((btn) => { + const el = btn as HTMLButtonElement; + return el && el.textContent === 'Queues'; + }) as HTMLButtonElement | undefined; + if (queuesButton1) { + fireEvent.click(queuesButton1); + } - const container = screen.container.querySelector('.agent-popover-content'); - mockUIDProps(container); - expect(container).toMatchSnapshot(); + await waitFor(() => expect(renderResult.container.querySelector('[aria-label="Queue One"]')).toBeInTheDocument()); }); it('should render component after agent selection', async () => { - let screen; + let renderResult; await act(async () => { - screen = render(); + renderResult = render(); }); - const agentItems = screen.container.querySelectorAll('.call-control-list-item'); - if (agentItems.length > 0) { - fireEvent.click(agentItems[0]); - } - - const container = screen.container.querySelector('.agent-popover-content'); - mockUIDProps(container); - expect(container).toMatchSnapshot(); + const firstAgentButton = renderResult.container.querySelectorAll('.call-control-list-item button')[0]; + if (firstAgentButton) fireEvent.click(firstAgentButton); + expect(mockOnAgentSelect).toHaveBeenCalled(); }); it('should render component after queue selection', async () => { - let screen; + let renderResult; await act(async () => { - screen = render(); + renderResult = render(); }); // Switch to queues tab first - const queueTab = screen.container.querySelector('.queue-tab'); - fireEvent.click(queueTab); - - // Then click on a queue - const queueItems = screen.container.querySelectorAll('.call-control-list-item'); - if (queueItems.length > 0) { - fireEvent.click(queueItems[0]); + const queuesButton2 = Array.from(renderResult.container.querySelectorAll('button')).find((btn) => { + const el = btn as HTMLButtonElement; + return el && el.textContent === 'Queues'; + }) as HTMLButtonElement | undefined; + if (queuesButton2) { + fireEvent.click(queuesButton2); } - const container = screen.container.querySelector('.agent-popover-content'); - mockUIDProps(container); - expect(container).toMatchSnapshot(); + // Then click on a queue's action button + await waitFor(() => + expect(renderResult.container.querySelectorAll('.call-control-list-item button').length).toBeGreaterThan(0) + ); + const firstQueueButton = renderResult.container.querySelectorAll('.call-control-list-item button')[0]; + if (firstQueueButton) fireEvent.click(firstQueueButton); + expect(mockOnQueueSelect).toHaveBeenCalled(); }); }); describe('State Management', () => { it('should update when heading changes', async () => { - let screen; + let renderResult; await act(async () => { - screen = render(); + renderResult = render(); }); - screen.rerender(); - const container = screen.container.querySelector('.agent-popover-content'); - mockUIDProps(container); - expect(container).toMatchSnapshot(); + renderResult.rerender(); + expect(renderResult.getByText('New Heading')).toBeInTheDocument(); }); it('should update when agents list changes', async () => { - let screen; + let renderResult; await act(async () => { - screen = render(); + renderResult = render(); }); const newAgents = [ @@ -317,23 +345,34 @@ describe('ConsultTransferPopoverComponent Snapshots', () => { siteId: 'site2', }, ]; - screen.rerender(); - const container = screen.container.querySelector('.agent-popover-content'); - mockUIDProps(container); - expect(container).toMatchSnapshot(); + renderResult.rerender(); + const updated = renderResult.container.querySelector('[aria-label="New Agent One"]'); + expect(updated).toBeInTheDocument(); }); it('should update when queues list changes', async () => { - let screen; + let renderResult; await act(async () => { - screen = render(); + renderResult = render(); }); - const newQueues = [{id: 'newQueue1', name: 'New Queue One'} as ContactServiceQueue]; - screen.rerender(); - const container = screen.container.querySelector('.agent-popover-content'); - mockUIDProps(container); - expect(container).toMatchSnapshot(); + const newQueues = [buildQueue('newQueue1', 'New Queue One')]; + renderResult.rerender( + ({data: newQueues, meta: {page: 0, totalPages: 1}})} + /> + ); + const queuesButton = Array.from(renderResult.container.querySelectorAll('button')).find((btn) => { + const el = btn as HTMLButtonElement; + return el && el.textContent === 'Queues'; + }) as HTMLButtonElement | undefined; + if (queuesButton) fireEvent.click(queuesButton); + await waitFor(() => + expect(renderResult.container.querySelector('[aria-label="New Queue One"]').textContent).toBeDefined() + ); + const newQueue = renderResult.container.querySelector('[aria-label="New Queue One"]'); + expect(newQueue).toBeInTheDocument(); }); }); }); diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx index b8dab3d96..f6b4e94da 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {render, fireEvent} from '@testing-library/react'; +import {render, fireEvent, waitFor} from '@testing-library/react'; import '@testing-library/jest-dom'; import ConsultTransferPopoverComponent from '../../../../../src/components/task/CallControl/CallControlCustom/consult-transfer-popover'; import {ContactServiceQueue} from '@webex/cc-store'; @@ -46,16 +46,17 @@ describe('ConsultTransferPopoverComponent', () => { siteId: 'site1', }, ], - queues: [ - {id: 'queue1', name: 'Queue One'} as ContactServiceQueue, - {id: 'queue2', name: 'Queue Two'} as ContactServiceQueue, - ], + getQueues: async () => ({ + data: [ + {id: 'queue1', name: 'Queue One'} as ContactServiceQueue, + {id: 'queue2', name: 'Queue Two'} as ContactServiceQueue, + ], + meta: {page: 0, totalPages: 1}, + }), onAgentSelect: mockOnAgentSelect, onQueueSelect: mockOnQueueSelect, allowConsultToQueue: true, logger: loggerMock, - showTabs: true, - emptyMessage: 'No agents or queues available', }; beforeEach(() => { @@ -77,26 +78,16 @@ describe('ConsultTransferPopoverComponent', () => { expect(heading).toHaveAttribute('type', 'body-large-bold'); // Verify tabs container - const tabList = screen.container.querySelector('.agent-tablist'); - expect(tabList).toBeInTheDocument(); - expect(tabList).toHaveAttribute('role', 'tablist'); - expect(tabList).toHaveAttribute('aria-label', 'Tabs'); - expect(tabList).toHaveAttribute('data-orientation', 'horizontal'); + const buttons = Array.from(screen.container.querySelectorAll('button')).map( + (b) => (b as HTMLButtonElement).textContent + ); + expect(buttons).toEqual(expect.arrayContaining(['Agents', 'Queues', 'Dial Number', 'Entry Point'])); // Verify Agents tab (active by default) - const agentTab = screen.container.querySelector('.agent-tab'); - expect(agentTab).toBeInTheDocument(); - expect(agentTab).toHaveAttribute('role', 'tab'); - expect(agentTab).toHaveAttribute('data-active', 'true'); - expect(agentTab).toHaveTextContent('Agents'); + expect(screen.getByText('Agents')).toBeInTheDocument(); // Verify Queues tab (inactive by default) - const queueTab = screen.container.querySelector('.queue-tab'); - expect(queueTab).toBeInTheDocument(); - expect(queueTab).toHaveAttribute('role', 'tab'); - expect(queueTab).toHaveAttribute('data-active', 'false'); - expect(queueTab).toHaveAttribute('tabindex', '-1'); - expect(queueTab).toHaveTextContent('Queues'); + expect(screen.getByText('Queues')).toBeInTheDocument(); // Verify agent list const agentList = screen.container.querySelector('.agent-list'); @@ -126,10 +117,12 @@ describe('ConsultTransferPopoverComponent', () => { fireEvent.mouseDown(listItemContainer!); // Test tab switching - const queueTab = screen.container.querySelector('.queue-tab'); - fireEvent.click(queueTab!); + fireEvent.click(screen.getByText('Queues')); // Test queue selection after switching tabs - click on the button inside the first queue item + await waitFor(() => + expect(screen.container.querySelectorAll('.call-control-list-item button').length).toBeGreaterThan(0) + ); const firstQueueButton = screen.container.querySelectorAll('.call-control-list-item button')[0]; fireEvent.click(firstQueueButton); expect(mockOnQueueSelect).toHaveBeenCalledWith('queue1', 'Queue One'); @@ -140,13 +133,12 @@ describe('ConsultTransferPopoverComponent', () => { const emptyProps = { ...baseProps, buddyAgents: [], - queues: [], + getQueues: async () => ({data: [], meta: {page: 0, totalPages: 0}}), }; let screen = await render(); - expect(screen.container.querySelector('.agent-tablist')).toBeInTheDocument(); - expect(screen.container.querySelector('.consult-empty-state')).toBeInTheDocument(); - expect(screen.container.querySelector('.consult-empty-message')).toBeInTheDocument(); + expect(screen.getByText('No data available for consult transfer.')).toBeInTheDocument(); + screen.unmount(); // Test empty agents tab with tabs showing (covers lines 94-96) const emptyAgentsProps = { @@ -155,23 +147,29 @@ describe('ConsultTransferPopoverComponent', () => { }; screen = await render(); - expect(screen.container.querySelector('.agent-tablist')).toBeInTheDocument(); + const buttons = Array.from(screen.container.querySelectorAll('button')).map( + (b) => (b as HTMLButtonElement).textContent + ); + expect(buttons).toEqual(expect.arrayContaining(['Agents', 'Queues', 'Dial Number', 'Entry Point'])); expect(screen.container.querySelector('.consult-empty-state')).toBeInTheDocument(); + expect(screen.getByText('No data available for consult transfer.')).toBeInTheDocument(); + screen.unmount(); // Test empty queues tab when switched to queues (covers lines 98-100) const emptyQueuesProps = { ...baseProps, - queues: [], + getQueues: async () => ({data: [], meta: {page: 0, totalPages: 0}}), }; screen = await render(); // Switch to queues tab - const queueTab = screen.container.querySelector('.queue-tab'); - fireEvent.click(queueTab!); + fireEvent.click(screen.getByText('Queues')); - // Should show empty state for queues (covers lines 98-100) - expect(screen.container.querySelector('.consult-empty-state')).toBeInTheDocument(); + // With empty queues, the list should not render and no items should be present + expect(screen.container.querySelector('.agent-list')).toBeNull(); + expect(screen.container.querySelectorAll('.call-control-list-item').length).toBe(0); + screen.unmount(); // Test hidden queue tab when allowConsultToQueue is false const propsWithoutQueue = { @@ -180,14 +178,15 @@ describe('ConsultTransferPopoverComponent', () => { }; screen = await render(); - const hiddenQueueTab = screen.container.querySelector('.queue-tab'); - expect(hiddenQueueTab).toHaveStyle('display: none'); + const queuesButton = Array.from(screen.container.querySelectorAll('button')).find( + (btn) => (btn as HTMLButtonElement).textContent === 'Queues' + ) as HTMLButtonElement | undefined; + expect(queuesButton?.disabled).toBe(true); }); it('covers edge case for empty items in renderList (line 50)', async () => { // Mock isAgentsEmpty to return false even with empty array to force renderList call const mockIsAgentsEmpty = jest.spyOn(utils, 'isAgentsEmpty').mockReturnValue(false); - const mockShouldShowTabs = jest.spyOn(utils, 'shouldShowTabs').mockReturnValue(true); try { const propsWithEmptyAgents = { @@ -197,12 +196,11 @@ describe('ConsultTransferPopoverComponent', () => { const screen = await render(); - // Should render the "No agents found" text (line 50) + // Should render the "No agents found" text via empty list state expect(screen.getByText('No agents found')).toBeInTheDocument(); } finally { // Restore original functions mockIsAgentsEmpty.mockRestore(); - mockShouldShowTabs.mockRestore(); } }); }); diff --git a/packages/contact-center/store/package.json b/packages/contact-center/store/package.json index 98315460a..0146bf5b0 100644 --- a/packages/contact-center/store/package.json +++ b/packages/contact-center/store/package.json @@ -22,7 +22,7 @@ "test:styles": "eslint" }, "dependencies": { - "@webex/contact-center": "3.9.0-next.4", + "@webex/contact-center": "3.9.0-next.10", "mobx": "6.13.5", "typescript": "5.6.3" }, @@ -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/store/src/store.types.ts b/packages/contact-center/store/src/store.types.ts index f85239ed1..9a3eb4d3e 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -8,6 +8,15 @@ import { BuddyAgentsResponse, StateChange, Logout, + EntryPointRecord, + EntryPointListResponse, + EntryPointSearchParams, + AddressBookEntry, + AddressBookEntriesResponse, + AddressBookEntrySearchParams, + ContactServiceQueuesResponse, + ContactServiceQueueSearchParams, + AddressBook, } from '@webex/contact-center'; import {DestinationType} from 'node_modules/@webex/contact-center/dist/types/services/task/types'; import { @@ -36,7 +45,9 @@ interface IContactCenter { getAllTasks: () => Record; }; getBuddyAgents(data: BuddyAgents): Promise; - getQueues(search?: string, filter?: string, page?: number, pageSize?: number): Promise; + getQueues(params?: ContactServiceQueueSearchParams): Promise; + getEntryPoints(params?: EntryPointSearchParams): Promise; + addressBook: AddressBook; agentConfig?: { regexUS: RegExp | string; agentId: string; @@ -119,6 +130,14 @@ interface IStoreWrapper extends IStore { onErrorCallback?: (widgetName: string, error: Error) => void; setCurrentTask(task: ITask): void; refreshTaskList(): void; + getBuddyAgents(mediaType?: string): Promise; + getQueues(mediaType?: string, params?: ContactServiceQueueSearchParams): Promise; + getQueuesPaginated( + mediaType?: string, + params?: ContactServiceQueueSearchParams + ): Promise; + getEntryPoints(params?: EntryPointSearchParams): Promise; + getAddressBookEntries(params?: AddressBookEntrySearchParams): Promise; setDeviceType(option: string): void; setDialNumber(input: string): void; setCurrentState(state: string): void; @@ -249,6 +268,14 @@ export type { BuddyDetails, ContactServiceQueue, AgentLoginProfile, + EntryPointRecord, + EntryPointListResponse, + EntryPointSearchParams, + AddressBookEntry, + AddressBookEntriesResponse, + AddressBookEntrySearchParams, + ContactServiceQueuesResponse, + ContactServiceQueueSearchParams, IWebex, }; diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index 19d3a03f8..d5b72c78d 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -14,6 +14,11 @@ import { ENGAGED_LABEL, ENGAGED_USERNAME, ContactServiceQueue, + ContactServiceQueueSearchParams, + EntryPointListResponse, + EntryPointSearchParams, + AddressBookEntriesResponse, + AddressBookEntrySearchParams, Profile, AgentLoginProfile, ERROR_TRIGGERING_IDLE_CODES, @@ -670,19 +675,63 @@ class StoreWrapper implements IStoreWrapper { }; getQueues = async ( - mediaType: string = this.currentTask.data.interaction.mediaType ?? 'TELEPHONY' + mediaType: string = this.currentTask.data.interaction.mediaType ?? 'TELEPHONY', + params?: ContactServiceQueueSearchParams ): Promise> => { try { const upperMediaType = mediaType.toUpperCase(); - let queueList = await this.store.cc.getQueues(); - queueList = queueList.filter((queue) => queue.channelType === upperMediaType); - return queueList; + const response = await this.store.cc.getQueues(params); + + const toQueueArray = (value: {data: ContactServiceQueue[]}): ContactServiceQueue[] => value.data; + + const queues: ContactServiceQueue[] = Array.isArray(response) ? response : toQueueArray(response); + + const filteredQueues = queues.filter((queue) => queue.channelType === upperMediaType); + return filteredQueues; } catch (error) { console.error('Error fetching queues:', error); return Promise.reject(error); } }; + getQueuesPaginated = async ( + mediaType: string = this.currentTask.data.interaction.mediaType ?? 'TELEPHONY', + params?: ContactServiceQueueSearchParams + ) => { + try { + const upperMediaType = mediaType.toUpperCase(); + const response = await this.store.cc.getQueues(params); + const data = Array.isArray(response) ? response : response.data; + const filtered = data.filter((queue) => queue.channelType === upperMediaType); + return Array.isArray(response) + ? {data: filtered, meta: {page: 0, pageSize: filtered.length, total: filtered.length, totalPages: 1}} + : {...response, data: filtered}; + } catch (error) { + console.error('Error fetching paginated queues:', error); + return Promise.reject(error); + } + }; + + getEntryPoints = async (params?: EntryPointSearchParams): Promise => { + try { + const response: EntryPointListResponse = await this.store.cc.getEntryPoints(params); + return response; + } catch (error) { + console.error('Error fetching entry points:', error); + return Promise.reject(error); + } + }; + + getAddressBookEntries = async (params?: AddressBookEntrySearchParams): Promise => { + try { + const response: AddressBookEntriesResponse = await this.store.cc.addressBook.getEntries(params ?? {}); + return response; + } catch (error) { + console.error('Error fetching address book entries:', error); + return Promise.reject(error); + } + }; + cleanUpStore = () => { this.store.logger.info('CC-Widgets: cleanUpStore(): resetting store on logout', { module: 'storeEventsWrapper.ts', diff --git a/packages/contact-center/store/tests/storeEventsWrapper.ts b/packages/contact-center/store/tests/storeEventsWrapper.ts index 79d6c862f..55775fc9b 100644 --- a/packages/contact-center/store/tests/storeEventsWrapper.ts +++ b/packages/contact-center/store/tests/storeEventsWrapper.ts @@ -24,7 +24,7 @@ console.log = jest.fn(); import {CC_EVENTS, TASK_EVENTS} from '../src/store.types'; import storeWrapper from '../src/storeEventsWrapper'; -import {ITask} from '@webex/contact-center'; +import {ITask, AddressBook} from '@webex/contact-center'; import {mockCC, mockTask as mockTaskFixture} from '@webex/test-fixtures'; jest.mock('../src/store', () => ({ @@ -819,6 +819,21 @@ describe('storeEventsWrapper', () => { await expect(storeWrapper.getQueues('telephony')).rejects.toThrow('queue error'); }); + it('should return contact service queues list when SDK returns paginated response', async () => { + const queueList = [ + {id: 'queue1', name: 'Queue 1', channelType: 'TELEPHONY'}, + {id: 'queue2', name: 'Queue 2', channelType: 'CHAT'}, + ]; + storeWrapper['store'].cc.getQueues = jest + .fn() + .mockResolvedValue({data: queueList, meta: {page: 1, pageSize: 50, total: 2}}); + + const result = await storeWrapper.getQueues('telephony'); + + expect(result).toEqual([{id: 'queue1', name: 'Queue 1', channelType: 'TELEPHONY'}]); + expect(storeWrapper['store'].cc.getQueues).toHaveBeenCalled(); + }); + it('should handle consultQueueCancelled event', () => { const consultInitiatedSpy = jest.spyOn(storeWrapper, 'setConsultInitiated'); const isQueueConsultInProgressSpy = jest.spyOn(storeWrapper, 'setIsQueueConsultInProgress'); @@ -832,6 +847,39 @@ describe('storeEventsWrapper', () => { expect(currentConsultQueueIdSpy).toHaveBeenCalledWith(null); expect(consultStartTimeStampSpy).toHaveBeenCalledWith(null); }); + + it('should fetch entry points successfully', async () => { + const mockResponse = {data: [{id: 'ep1', name: 'Entry 1'}], meta: {page: 0, totalPages: 1}}; + storeWrapper['store'].cc.getEntryPoints = jest.fn().mockResolvedValue(mockResponse); + + const result = await storeWrapper.getEntryPoints({page: 0, pageSize: 25}); + expect(storeWrapper['store'].cc.getEntryPoints).toHaveBeenCalledWith({page: 0, pageSize: 25}); + expect(result).toEqual(mockResponse); + }); + + it('should handle error while fetching entry points', async () => { + storeWrapper['store'].cc.getEntryPoints = jest.fn().mockRejectedValue(new Error('ep error')); + await expect(storeWrapper.getEntryPoints({page: 0, pageSize: 25})).rejects.toThrow('ep error'); + }); + + it('should fetch address book entries successfully', async () => { + const mockResponse = {data: [{id: 'ab1', name: 'Alice'}], meta: {page: 0, totalPages: 1}}; + // Fix: Use type assertion to 'unknown' first to avoid TS error about missing properties + storeWrapper['store'].cc.addressBook = { + getEntries: jest.fn().mockResolvedValue(mockResponse), + } as unknown as AddressBook; + + const result = await storeWrapper.getAddressBookEntries({page: 0, pageSize: 25}); + expect(storeWrapper['store'].cc.addressBook.getEntries).toHaveBeenCalledWith({page: 0, pageSize: 25}); + expect(result).toEqual(mockResponse); + }); + + it('should handle error while fetching address book entries', async () => { + storeWrapper['store'].cc.addressBook = { + getEntries: jest.fn().mockRejectedValue(new Error('ab error')), + } as unknown as AddressBook; + await expect(storeWrapper.getAddressBookEntries({page: 0, pageSize: 25})).rejects.toThrow('ab error'); + }); }); describe('storeEventsWrapper events reactions', () => { diff --git a/packages/contact-center/task/src/helper.ts b/packages/contact-center/task/src/helper.ts index cf8c79238..1d9011f37 100644 --- a/packages/contact-center/task/src/helper.ts +++ b/packages/contact-center/task/src/helper.ts @@ -421,6 +421,51 @@ export const useCallControl = (props: useCallControlProps) => { } }, [logger]); + const getAddressBookEntries = useCallback( + async ({page, pageSize, search}: {page: number; pageSize: number; search?: string}) => { + try { + return await store.getAddressBookEntries({page, pageSize, search}); + } catch (error) { + logger?.error(`CC-Widgets: Task: Error fetching address book entries - ${error.message || error}`, { + module: 'useCallControl', + method: 'getAddressBookEntries', + }); + return {data: [], meta: {page: 0, totalPages: 0}}; + } + }, + [logger] + ); + + const getEntryPoints = useCallback( + async ({page, pageSize, search}: {page: number; pageSize: number; search?: string}) => { + try { + return await store.getEntryPoints({page, pageSize, search}); + } catch (error) { + logger?.error(`CC-Widgets: Task: Error fetching entry points - ${error.message || error}`, { + module: 'useCallControl', + method: 'getEntryPoints', + }); + return {data: [], meta: {page: 0, totalPages: 0}}; + } + }, + [logger] + ); + + const getQueuesFetcher = useCallback( + async ({page, pageSize, search}: {page: number; pageSize: number; search?: string}) => { + try { + return await store.getQueuesPaginated('TELEPHONY', {page, pageSize, search}); + } catch (error) { + logger?.error(`CC-Widgets: Task: Error fetching queues (paginated) - ${error.message || error}`, { + module: 'useCallControl', + method: 'getQueuesFetcher', + }); + return {data: [], meta: {page: 0, totalPages: 0}}; + } + }, + [logger] + ); + const holdCallback = () => { try { setIsHeld(true); @@ -791,7 +836,6 @@ export const useCallControl = (props: useCallControlProps) => { logger.error('CC-Widgets: CallControl: Error initializing auto wrap-up timer', { module: 'widget-cc-task#helper.ts', method: 'useCallControl#autoWrapupTimer', - //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 error, }); } @@ -836,6 +880,9 @@ export const useCallControl = (props: useCallControlProps) => { controlVisibility, secondsUntilAutoWrapup, cancelAutoWrapup, + getAddressBookEntries, + getEntryPoints, + getQueuesFetcher, }; }; diff --git a/packages/contact-center/task/tests/CallControl/index.tsx b/packages/contact-center/task/tests/CallControl/index.tsx index afd21a7b1..03678a22e 100644 --- a/packages/contact-center/task/tests/CallControl/index.tsx +++ b/packages/contact-center/task/tests/CallControl/index.tsx @@ -67,6 +67,9 @@ describe('CallControl Component', () => { cancelAutoWrapup: jest.fn(), toggleMute: jest.fn(), isMuted: false, + getAddressBookEntries: jest.fn().mockResolvedValue({data: [], meta: {page: 0, totalPages: 0}}), + getEntryPoints: jest.fn().mockResolvedValue({data: [], meta: {page: 0, totalPages: 0}}), + getQueuesFetcher: jest.fn().mockResolvedValue({data: [], meta: {page: 0, totalPages: 0}}), }); render( diff --git a/packages/contact-center/task/tests/helper.ts b/packages/contact-center/task/tests/helper.ts index 4d6d4d971..4e5b490d1 100644 --- a/packages/contact-center/task/tests/helper.ts +++ b/packages/contact-center/task/tests/helper.ts @@ -1,7 +1,12 @@ import {renderHook, act, waitFor} from '@testing-library/react'; import {useIncomingTask, useTaskList, useCallControl, useOutdialCall} from '../src/helper'; import * as taskUtils from '../src/Utils/task-util'; -import {TASK_EVENTS} from '@webex/cc-store'; +import { + AddressBookEntriesResponse, + EntryPointListResponse, + ContactServiceQueuesResponse, + TASK_EVENTS, +} from '@webex/cc-store'; import {mockAgents, mockCC, mockQueueDetails, mockTask} from '@webex/test-fixtures'; import store from '@webex/cc-store'; import React from 'react'; @@ -2189,6 +2194,66 @@ describe('useCallControl', () => { getQueuesSpy.mockRestore(); }); + it('should get address book entries via getAddressBookEntries', async () => { + const mockResponse = {data: [{id: '1', name: 'Alice', number: '123'}], meta: {page: 0, totalPages: 1}}; + jest.spyOn(store, 'getAddressBookEntries').mockResolvedValue(mockResponse as AddressBookEntriesResponse); + + const {result} = renderHook(() => + useCallControl({ + currentTask: mockCurrentTask, + logger: mockLogger, + featureFlags: store.featureFlags, + deviceType: store.deviceType, + consultInitiated: false, + isMuted: false, + }) + ); + + const res = await result.current.getAddressBookEntries({page: 0, pageSize: 25}); + expect(res).toEqual(mockResponse); + }); + + it('should get entry points via getEntryPoints', async () => { + const mockResponse = {data: [{id: 'ep1', name: 'Entry 1'}], meta: {page: 0, totalPages: 1}}; + jest.spyOn(store, 'getEntryPoints').mockResolvedValue(mockResponse as EntryPointListResponse); + + const {result} = renderHook(() => + useCallControl({ + currentTask: mockCurrentTask, + logger: mockLogger, + featureFlags: store.featureFlags, + deviceType: store.deviceType, + consultInitiated: false, + isMuted: false, + }) + ); + + const res = await result.current.getEntryPoints({page: 0, pageSize: 25}); + expect(res).toEqual(mockResponse); + }); + + it('should get queues via getQueuesFetcher (paginated)', async () => { + const mockResponse = { + data: [{id: 'q1', name: 'Queue 1', channelType: 'TELEPHONY'}], + meta: {page: 0, totalPages: 1}, + }; + jest.spyOn(store, 'getQueuesPaginated').mockResolvedValue(mockResponse as ContactServiceQueuesResponse); + + const {result} = renderHook(() => + useCallControl({ + currentTask: mockCurrentTask, + logger: mockLogger, + featureFlags: store.featureFlags, + deviceType: store.deviceType, + consultInitiated: false, + isMuted: false, + }) + ); + + const res = await result.current.getQueuesFetcher({page: 0, pageSize: 25}); + expect(res).toEqual(mockResponse); + }); + it('should call cancelAutoWrapup successfully', async () => { const mockOnRecordingToggle = jest.fn(); diff --git a/packages/contact-center/test-fixtures/src/fixtures.ts b/packages/contact-center/test-fixtures/src/fixtures.ts index 4404fa420..a94af052e 100644 --- a/packages/contact-center/test-fixtures/src/fixtures.ts +++ b/packages/contact-center/test-fixtures/src/fixtures.ts @@ -1,4 +1,4 @@ -import {ITask, Profile, TaskData, TaskResponse} from '@webex/contact-center'; +import {ITask, Profile, TaskData, TaskResponse, AddressBook} from '@webex/contact-center'; import {IContactCenter} from '@webex/cc-store'; const mockProfile: Profile = { @@ -83,7 +83,11 @@ const mockCC: IContactCenter = { getAllTasks: jest.fn().mockReturnValue({}), }, getBuddyAgents: jest.fn().mockResolvedValue([]), - getQueues: jest.fn().mockResolvedValue([]), + getQueues: jest.fn().mockResolvedValue({data: [], meta: {page: 0, pageSize: 25, total: 0, totalPages: 1}}), + getEntryPoints: jest.fn().mockResolvedValue({data: [], meta: {page: 0, pageSize: 25, total: 0, totalPages: 1}}), + addressBook: { + getEntries: jest.fn().mockResolvedValue({data: [], meta: {page: 0, pageSize: 25, total: 0, totalPages: 1}}), + } as unknown as AddressBook, setAgentState: jest.fn().mockResolvedValue({}), }; diff --git a/yarn.lock b/yarn.lock index f47418a9f..e082b1721 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9374,13 +9374,13 @@ __metadata: languageName: node linkType: hard -"@webex/calling@npm:3.9.0-next.2": - version: 3.9.0-next.2 - resolution: "@webex/calling@npm:3.9.0-next.2" +"@webex/calling@npm:3.9.0-next.5": + version: 3.9.0-next.5 + resolution: "@webex/calling@npm:3.9.0-next.5" dependencies: "@types/platform": "npm:1.3.4" "@webex/internal-media-core": "npm:2.18.5" - "@webex/internal-plugin-metrics": "npm:3.9.0-next.1" + "@webex/internal-plugin-metrics": "npm:3.9.0-next.3" "@webex/media-helpers": "npm:3.8.1-next.16" async-mutex: "npm:0.4.0" buffer: "npm:6.0.3" @@ -9388,7 +9388,7 @@ __metadata: platform: "npm:1.3.6" uuid: "npm:8.3.2" xstate: "npm:4.30.6" - checksum: 10c0/d8f0c18cdb9cdf0f580cd0fca38fd0513cd4007053ba885470ffa5b6dd90a25b648c7668c5bba093c1f37849b3b6154f23dd225fea3344f60713f8e7acec8dd5 + checksum: 10c0/2326aeea8441907bfb778a2f27148e6777d545226a241d165552ad44ec041275b62a5ecfb12d26ecdaf1eb4839424bb1a6021b87a5225354211eff4a29ce8329 languageName: node linkType: hard @@ -9500,7 +9500,7 @@ __metadata: "@testing-library/react": "npm:16.0.1" "@types/jest": "npm:29.5.14" "@types/react-test-renderer": "npm:18" - "@webex/contact-center": "npm:3.9.0-next.4" + "@webex/contact-center": "npm:3.9.0-next.10" "@webex/test-fixtures": "workspace:*" babel-jest: "npm:29.7.0" babel-loader: "npm:9.2.1" @@ -9839,21 +9839,21 @@ __metadata: languageName: node linkType: hard -"@webex/contact-center@npm:3.9.0-next.4": - version: 3.9.0-next.4 - resolution: "@webex/contact-center@npm:3.9.0-next.4" +"@webex/contact-center@npm:3.9.0-next.10": + version: 3.9.0-next.10 + resolution: "@webex/contact-center@npm:3.9.0-next.10" dependencies: "@types/platform": "npm:1.3.4" - "@webex/calling": "npm:3.9.0-next.2" - "@webex/internal-plugin-mercury": "npm:3.9.0-next.1" - "@webex/internal-plugin-metrics": "npm:3.9.0-next.1" - "@webex/internal-plugin-support": "npm:3.9.0-next.1" - "@webex/plugin-authorization": "npm:3.9.0-next.1" - "@webex/plugin-logger": "npm:3.9.0-next.1" - "@webex/webex-core": "npm:3.9.0-next.1" + "@webex/calling": "npm:3.9.0-next.5" + "@webex/internal-plugin-mercury": "npm:3.9.0-next.3" + "@webex/internal-plugin-metrics": "npm:3.9.0-next.3" + "@webex/internal-plugin-support": "npm:3.9.0-next.4" + "@webex/plugin-authorization": "npm:3.9.0-next.3" + "@webex/plugin-logger": "npm:3.9.0-next.3" + "@webex/webex-core": "npm:3.9.0-next.3" jest-html-reporters: "npm:3.0.11" lodash: "npm:^4.17.21" - checksum: 10c0/3039bd5f0e78cf4642cef7b824e38fd49c76f6105621ae00e737d3900843189ce52031c9bc1e4884e0383f525eab53612d2445b44460084f28aa5975fa4b4f40 + checksum: 10c0/2fcc6b4d5c68ff130759c41517021d188c5cc1ed3f4629d5ce14024ea15a7ef772eb5213b45e9dba7c6b0932c9499b3c190c441fa12a50e70035ee44aae4d8ab languageName: node linkType: hard @@ -9870,16 +9870,16 @@ __metadata: languageName: node linkType: hard -"@webex/event-dictionary-ts@npm:^1.0.1819": - version: 1.0.1896 - resolution: "@webex/event-dictionary-ts@npm:1.0.1896" +"@webex/event-dictionary-ts@npm:^1.0.1930": + version: 1.0.1941 + resolution: "@webex/event-dictionary-ts@npm:1.0.1941" 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/cfa5bd5854376e15fac4324a222d028b2221b223a69b91aaf120f6044301302f410ad2cc6a07961d001f89d9b598d9778ad8daf449c226a8cb873d0d51da6dd4 + checksum: 10c0/cb7a34a7fcf760a2ea68e8d786f8fec4a0cfd498aa9ce7fc37d5151c05fa8bd879408bf3c6682b28feeef0e6e51e326bb21ff4073f860366848fec3c8cb4e27d languageName: node linkType: hard @@ -10241,21 +10241,21 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-conversation@npm:3.9.0-next.1": - version: 3.9.0-next.1 - resolution: "@webex/internal-plugin-conversation@npm:3.9.0-next.1" +"@webex/internal-plugin-conversation@npm:3.9.0-next.3": + version: 3.9.0-next.3 + resolution: "@webex/internal-plugin-conversation@npm:3.9.0-next.3" dependencies: "@webex/common": "npm:3.8.1-next.11" "@webex/helper-html": "npm:3.8.1-next.11" "@webex/helper-image": "npm:3.8.1-next.11" - "@webex/internal-plugin-encryption": "npm:3.9.0-next.1" - "@webex/internal-plugin-user": "npm:3.9.0-next.1" - "@webex/webex-core": "npm:3.9.0-next.1" + "@webex/internal-plugin-encryption": "npm:3.9.0-next.3" + "@webex/internal-plugin-user": "npm:3.9.0-next.3" + "@webex/webex-core": "npm:3.9.0-next.3" crypto-js: "npm:^4.1.1" lodash: "npm:^4.17.21" node-scr: "npm:^0.3.0" uuid: "npm:^3.3.2" - checksum: 10c0/0ec59ce9403e2ebf7d939d3cc4338e7444eeb0ce73005eb87bd1286c8d6bb319946962569863648b791d78be1785e4afbb56a2e3dc3d07a778d2db592ff48a0d + checksum: 10c0/be33259eb95ada91c9ea41970a215bc6287ae78bc9f724adc2c319b65e13d0322027d9196abb478f511d464fde59ca5657ebbe97de8c9a8f4531dd08ca5151c3 languageName: node linkType: hard @@ -10308,20 +10308,20 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-device@npm:3.9.0-next.1": - version: 3.9.0-next.1 - resolution: "@webex/internal-plugin-device@npm:3.9.0-next.1" +"@webex/internal-plugin-device@npm:3.9.0-next.3": + version: 3.9.0-next.3 + resolution: "@webex/internal-plugin-device@npm:3.9.0-next.3" dependencies: "@webex/common": "npm:3.8.1-next.11" "@webex/common-timers": "npm:3.8.1-next.11" "@webex/http-core": "npm:3.8.1-next.11" - "@webex/internal-plugin-metrics": "npm:3.9.0-next.1" - "@webex/webex-core": "npm:3.9.0-next.1" + "@webex/internal-plugin-metrics": "npm:3.9.0-next.3" + "@webex/webex-core": "npm:3.9.0-next.3" ampersand-collection: "npm:^2.0.2" ampersand-state: "npm:^5.0.3" lodash: "npm:^4.17.21" uuid: "npm:^3.3.2" - checksum: 10c0/4abd1341141000f41b6ea0723c83eee73582e0ba7ab331cce9d23371daeda1c0ad105577070bcf2826f42026345bca74169ff8966312efd8cb6f28ac06c9177b + checksum: 10c0/7383d13dc95c2e9fcdec101e80838a625862fc834b741798a2594c61919fccc6c19e153a61fafb614a6e50b8fa30dc61a1a39b08786e609e56c9bfecfc6a565d languageName: node linkType: hard @@ -10403,17 +10403,17 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-encryption@npm:3.9.0-next.1": - version: 3.9.0-next.1 - resolution: "@webex/internal-plugin-encryption@npm:3.9.0-next.1" +"@webex/internal-plugin-encryption@npm:3.9.0-next.3": + version: 3.9.0-next.3 + resolution: "@webex/internal-plugin-encryption@npm:3.9.0-next.3" dependencies: "@webex/common": "npm:3.8.1-next.11" "@webex/common-timers": "npm:3.8.1-next.11" "@webex/http-core": "npm:3.8.1-next.11" - "@webex/internal-plugin-device": "npm:3.9.0-next.1" - "@webex/internal-plugin-mercury": "npm:3.9.0-next.1" + "@webex/internal-plugin-device": "npm:3.9.0-next.3" + "@webex/internal-plugin-mercury": "npm:3.9.0-next.3" "@webex/test-helper-file": "npm:3.8.1-next.11" - "@webex/webex-core": "npm:3.9.0-next.1" + "@webex/webex-core": "npm:3.9.0-next.3" asn1js: "npm:^2.0.26" debug: "npm:^4.3.4" isomorphic-webcrypto: "npm:^2.3.8" @@ -10425,7 +10425,7 @@ __metadata: safe-buffer: "npm:^5.2.0" uuid: "npm:^3.3.2" valid-url: "npm:^1.0.9" - checksum: 10c0/0fd1b8875ec8dfce221ec246429c079bfc34eb70d063efbc26e1728d94542513c9c3c0fcae8dbd9764294d340ba29a3f70ee14393381eefec4f42a425d1d4f3a + checksum: 10c0/b594ac9ce995208c02ccbf91c6f0b0cafe43dedf7a956c374b9f37f1b3a5b55695e21bff159e893ad23e32e3e0546d9cdc4afbc813ca66a1d864cb6917c4379a languageName: node linkType: hard @@ -10464,14 +10464,14 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-feature@npm:3.9.0-next.1": - version: 3.9.0-next.1 - resolution: "@webex/internal-plugin-feature@npm:3.9.0-next.1" +"@webex/internal-plugin-feature@npm:3.9.0-next.3": + version: 3.9.0-next.3 + resolution: "@webex/internal-plugin-feature@npm:3.9.0-next.3" dependencies: - "@webex/internal-plugin-device": "npm:3.9.0-next.1" - "@webex/webex-core": "npm:3.9.0-next.1" + "@webex/internal-plugin-device": "npm:3.9.0-next.3" + "@webex/webex-core": "npm:3.9.0-next.3" lodash: "npm:^4.17.21" - checksum: 10c0/111ba44e51cf6222567b4682adbfcdca1a7523f59165dcb671217416b1ca8b89d079b99641570f4d895b95bb4c1f34d8754420eff272a6dacc81d0f1ab35df94 + checksum: 10c0/dd5a6a124b5e5f2b56cabc567b64f78aaf8c87a2a9e6c8c9f4e735dbebab1ee4eb63a47fdb4536486f60eb7c4fcd4ec0d39df523a1999b78df176cf818a2f243 languageName: node linkType: hard @@ -10649,27 +10649,27 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-mercury@npm:3.9.0-next.1": - version: 3.9.0-next.1 - resolution: "@webex/internal-plugin-mercury@npm:3.9.0-next.1" +"@webex/internal-plugin-mercury@npm:3.9.0-next.3": + version: 3.9.0-next.3 + resolution: "@webex/internal-plugin-mercury@npm:3.9.0-next.3" dependencies: "@webex/common": "npm:3.8.1-next.11" "@webex/common-timers": "npm:3.8.1-next.11" - "@webex/internal-plugin-device": "npm:3.9.0-next.1" - "@webex/internal-plugin-feature": "npm:3.9.0-next.1" - "@webex/internal-plugin-metrics": "npm:3.9.0-next.1" + "@webex/internal-plugin-device": "npm:3.9.0-next.3" + "@webex/internal-plugin-feature": "npm:3.9.0-next.3" + "@webex/internal-plugin-metrics": "npm:3.9.0-next.3" "@webex/test-helper-chai": "npm:3.8.1-next.11" "@webex/test-helper-mocha": "npm:3.8.1-next.11" "@webex/test-helper-mock-web-socket": "npm:3.8.1-next.11" "@webex/test-helper-mock-webex": "npm:3.8.1-next.11" "@webex/test-helper-refresh-callback": "npm:3.8.1-next.11" "@webex/test-helper-test-users": "npm:3.8.1-next.11" - "@webex/webex-core": "npm:3.9.0-next.1" + "@webex/webex-core": "npm:3.9.0-next.3" backoff: "npm:^2.5.0" lodash: "npm:^4.17.21" uuid: "npm:^3.3.2" ws: "npm:^8.17.1" - checksum: 10c0/48af16b7b619df7a287cf76460978142a070633932df1888b38f204f01da1ad128635a83b610acce0d9d05f24b812d059ac64306f66e66c4e218bd8b7440bd58 + checksum: 10c0/2c712e2e635a5b7e5f32c131502161f40fd7fb438318f95573005a07a2388230ebeae69079f004e7cff3814ed153c86108ae722fad0cd4f60903f834434edaee languageName: node linkType: hard @@ -10717,20 +10717,20 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-metrics@npm:3.9.0-next.1": - version: 3.9.0-next.1 - resolution: "@webex/internal-plugin-metrics@npm:3.9.0-next.1" +"@webex/internal-plugin-metrics@npm:3.9.0-next.3": + version: 3.9.0-next.3 + resolution: "@webex/internal-plugin-metrics@npm:3.9.0-next.3" dependencies: "@webex/common": "npm:3.8.1-next.11" "@webex/common-timers": "npm:3.8.1-next.11" - "@webex/event-dictionary-ts": "npm:^1.0.1819" + "@webex/event-dictionary-ts": "npm:^1.0.1930" "@webex/test-helper-chai": "npm:3.8.1-next.11" "@webex/test-helper-mock-webex": "npm:3.8.1-next.11" - "@webex/webex-core": "npm:3.9.0-next.1" + "@webex/webex-core": "npm:3.9.0-next.3" ip-anonymize: "npm:^0.1.0" lodash: "npm:^4.17.21" uuid: "npm:^3.3.2" - checksum: 10c0/cf7e845c5928c2c22f672ef80ebd269a072d4b4522cdd139319243eaa19503864fd5d70e9532da0427719952e283d84536556730d912bcb0575ce4b6fb9e1d40 + checksum: 10c0/ad7fe8f9daf8e891e5f2ed9d3fc1044a9c9ddcfe9a86c3edd28d2e59be934c82cbc99ec2f4f78ab231a7b1f95efa2b17383a9bebc9c37f21aaa7d2232c4c5a65 languageName: node linkType: hard @@ -10827,18 +10827,18 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-search@npm:3.9.0-next.1": - version: 3.9.0-next.1 - resolution: "@webex/internal-plugin-search@npm:3.9.0-next.1" +"@webex/internal-plugin-search@npm:3.9.0-next.3": + version: 3.9.0-next.3 + resolution: "@webex/internal-plugin-search@npm:3.9.0-next.3" dependencies: "@webex/common": "npm:3.8.1-next.11" - "@webex/internal-plugin-conversation": "npm:3.9.0-next.1" - "@webex/internal-plugin-device": "npm:3.9.0-next.1" - "@webex/internal-plugin-encryption": "npm:3.9.0-next.1" - "@webex/webex-core": "npm:3.9.0-next.1" + "@webex/internal-plugin-conversation": "npm:3.9.0-next.3" + "@webex/internal-plugin-device": "npm:3.9.0-next.3" + "@webex/internal-plugin-encryption": "npm:3.9.0-next.3" + "@webex/webex-core": "npm:3.9.0-next.3" lodash: "npm:^4.17.21" uuid: "npm:^3.3.2" - checksum: 10c0/64c478bfca7dd323544fcb76f5f27ddab78ca9b6b58aeb3a25d0c24ed21eb0c151944e4e7ba3926c24bfedfeb3a4e383856b64b103232779d8b075038fffb06f + checksum: 10c0/f3951822334b32cf48c0801fc1e514f0765ad5af8eb4418b6d09e832f2870827f6a9ffaddd0fbe5202b6ab040e72f364f6d927c7f71393115ef270e71279d459 languageName: node linkType: hard @@ -10893,20 +10893,20 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-support@npm:3.9.0-next.1": - version: 3.9.0-next.1 - resolution: "@webex/internal-plugin-support@npm:3.9.0-next.1" +"@webex/internal-plugin-support@npm:3.9.0-next.4": + version: 3.9.0-next.4 + resolution: "@webex/internal-plugin-support@npm:3.9.0-next.4" dependencies: - "@webex/internal-plugin-device": "npm:3.9.0-next.1" - "@webex/internal-plugin-search": "npm:3.9.0-next.1" + "@webex/internal-plugin-device": "npm:3.9.0-next.3" + "@webex/internal-plugin-search": "npm:3.9.0-next.3" "@webex/test-helper-chai": "npm:3.8.1-next.11" "@webex/test-helper-file": "npm:3.8.1-next.11" "@webex/test-helper-mock-webex": "npm:3.8.1-next.11" "@webex/test-helper-test-users": "npm:3.8.1-next.11" - "@webex/webex-core": "npm:3.9.0-next.1" + "@webex/webex-core": "npm:3.9.0-next.3" lodash: "npm:^4.17.21" uuid: "npm:^3.3.2" - checksum: 10c0/e06e5bcf48fef1c416317993903dccc2bbef2c084e1b7e39df2aa3e691465043a4d80de49df76a28acd19ed7aeb239225ff8bd3efed495f80bcdc886b0c5048c + checksum: 10c0/85e2dd86bb15df963e8621ebd7b542405bf0b306344e8ddcf227aa140e6c4eea4e1a4b3ca83d79e8623665055811bcb37dfefc891e0c7b10855271aeda76128e languageName: node linkType: hard @@ -10958,19 +10958,19 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-user@npm:3.9.0-next.1": - version: 3.9.0-next.1 - resolution: "@webex/internal-plugin-user@npm:3.9.0-next.1" +"@webex/internal-plugin-user@npm:3.9.0-next.3": + version: 3.9.0-next.3 + resolution: "@webex/internal-plugin-user@npm:3.9.0-next.3" dependencies: "@webex/common": "npm:3.8.1-next.11" - "@webex/internal-plugin-device": "npm:3.9.0-next.1" + "@webex/internal-plugin-device": "npm:3.9.0-next.3" "@webex/test-helper-chai": "npm:3.8.1-next.11" "@webex/test-helper-mock-webex": "npm:3.8.1-next.11" "@webex/test-helper-test-users": "npm:3.8.1-next.11" - "@webex/webex-core": "npm:3.9.0-next.1" + "@webex/webex-core": "npm:3.9.0-next.3" lodash: "npm:^4.17.21" uuid: "npm:^3.3.2" - checksum: 10c0/bc52b599464d3e25c700deac6c28b867adabfcefce950e754ad314a577a47056d7459ee417c2c967b65b300b8552861e73a6717d5598dcdb43397aeff74eb604 + checksum: 10c0/fd7af0ad43ebea809caf7e6baa15fff31103b2d770df6b1ec8f68d9a33f6e071f5fdcf23782a76abd327e3c6da460e83486582c2c8d9f032182b37c205e743a1 languageName: node linkType: hard @@ -11165,20 +11165,20 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-authorization-browser@npm:3.9.0-next.1": - version: 3.9.0-next.1 - resolution: "@webex/plugin-authorization-browser@npm:3.9.0-next.1" +"@webex/plugin-authorization-browser@npm:3.9.0-next.3": + version: 3.9.0-next.3 + resolution: "@webex/plugin-authorization-browser@npm:3.9.0-next.3" dependencies: "@webex/common": "npm:3.8.1-next.11" - "@webex/internal-plugin-device": "npm:3.9.0-next.1" - "@webex/plugin-authorization-node": "npm:3.9.0-next.1" - "@webex/storage-adapter-local-storage": "npm:3.9.0-next.1" + "@webex/internal-plugin-device": "npm:3.9.0-next.3" + "@webex/plugin-authorization-node": "npm:3.9.0-next.3" + "@webex/storage-adapter-local-storage": "npm:3.9.0-next.3" "@webex/storage-adapter-spec": "npm:3.8.1-next.11" - "@webex/webex-core": "npm:3.9.0-next.1" + "@webex/webex-core": "npm:3.9.0-next.3" jsonwebtoken: "npm:^9.0.2" lodash: "npm:^4.17.21" uuid: "npm:^3.3.2" - checksum: 10c0/9e21845d5ff3af1b6e42a149e04b3b7deefc69c53f81d205717b4bbb98d163d034aff733e2bfe9f087094b42bb736499d751cc90dcbbef29a3ebf1e0af8975a9 + checksum: 10c0/843fc3457fb284e7a904d05c1426f2f2fd39a25832a83fb0eea8143cb736785fd0156bcdb49af9cabae08fb33cf45b0be274706ccbcb2267c6c6d6de1230969d languageName: node linkType: hard @@ -11221,16 +11221,16 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-authorization-node@npm:3.9.0-next.1": - version: 3.9.0-next.1 - resolution: "@webex/plugin-authorization-node@npm:3.9.0-next.1" +"@webex/plugin-authorization-node@npm:3.9.0-next.3": + version: 3.9.0-next.3 + resolution: "@webex/plugin-authorization-node@npm:3.9.0-next.3" dependencies: "@webex/common": "npm:3.8.1-next.11" - "@webex/internal-plugin-device": "npm:3.9.0-next.1" - "@webex/webex-core": "npm:3.9.0-next.1" + "@webex/internal-plugin-device": "npm:3.9.0-next.3" + "@webex/webex-core": "npm:3.9.0-next.3" jsonwebtoken: "npm:^9.0.0" uuid: "npm:^3.3.2" - checksum: 10c0/b52a593e11a4034ad4580ef040da8eff0f804142404cc052aeaeeb245a9d9537b20b65537616ac852e547b1a13c149f9c079b675599578bd2b4895b0154cfac0 + checksum: 10c0/057440bf0c5ccaee6c7803faebcd1b81eaa6a72505bf61a6e04a94d76cfe260487ec0127b754aaca372c4831ae54673fd1f51e0c13b38e038ac174a72e332d34 languageName: node linkType: hard @@ -11264,13 +11264,13 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-authorization@npm:3.9.0-next.1": - version: 3.9.0-next.1 - resolution: "@webex/plugin-authorization@npm:3.9.0-next.1" +"@webex/plugin-authorization@npm:3.9.0-next.3": + version: 3.9.0-next.3 + resolution: "@webex/plugin-authorization@npm:3.9.0-next.3" dependencies: - "@webex/plugin-authorization-browser": "npm:3.9.0-next.1" - "@webex/plugin-authorization-node": "npm:3.9.0-next.1" - checksum: 10c0/ead3a65be83d824bd51f3625466b07207df06ecdbe79f08ff675dfdd08d23a60dc95f3745056cdccb892d8e0f9ce4f96125df04ed1c97dda34c40cb24bc253db + "@webex/plugin-authorization-browser": "npm:3.9.0-next.3" + "@webex/plugin-authorization-node": "npm:3.9.0-next.3" + checksum: 10c0/b71184ee1b5cf4ef895ea14632803a5b3701f120a705b6421e94914f472d49d70da439484a9ab311f603c8f6b4a2adbd6039fd70fb3b6b8d80f93edfadc6a080 languageName: node linkType: hard @@ -11398,17 +11398,17 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-logger@npm:3.9.0-next.1": - version: 3.9.0-next.1 - resolution: "@webex/plugin-logger@npm:3.9.0-next.1" +"@webex/plugin-logger@npm:3.9.0-next.3": + version: 3.9.0-next.3 + resolution: "@webex/plugin-logger@npm:3.9.0-next.3" dependencies: "@webex/common": "npm:3.8.1-next.11" "@webex/test-helper-chai": "npm:3.8.1-next.11" "@webex/test-helper-mocha": "npm:3.8.1-next.11" "@webex/test-helper-mock-webex": "npm:3.8.1-next.11" - "@webex/webex-core": "npm:3.9.0-next.1" + "@webex/webex-core": "npm:3.9.0-next.3" lodash: "npm:^4.17.21" - checksum: 10c0/5937a116e57d7d63aacec25747c119a599cefec4ca3327165e3a9f23d79b12467501c93d6c92b50590614d6d80b75135a6e4e21a7f1d158342e50b5543b7027b + checksum: 10c0/81ea37b4ade7fe5b25eeafd7b93beec1a66d5e88d38e724f5fe7256b5520973799655247a61ce74f78aa03ad37f8a46d9d7649eb78edabaf8c8ebc8d6f6259fb languageName: node linkType: hard @@ -11888,14 +11888,14 @@ __metadata: languageName: node linkType: hard -"@webex/storage-adapter-local-storage@npm:3.9.0-next.1": - version: 3.9.0-next.1 - resolution: "@webex/storage-adapter-local-storage@npm:3.9.0-next.1" +"@webex/storage-adapter-local-storage@npm:3.9.0-next.3": + version: 3.9.0-next.3 + resolution: "@webex/storage-adapter-local-storage@npm:3.9.0-next.3" dependencies: "@webex/storage-adapter-spec": "npm:3.8.1-next.11" "@webex/test-helper-mocha": "npm:3.8.1-next.11" - "@webex/webex-core": "npm:3.9.0-next.1" - checksum: 10c0/22c19a4d768841ab08db9d3e0ec8b6063f9614dca1ed77c352b6b6e47a15f3f2b600ffa96b59c48128fb622cfa57d9389be9326c4d153210926c688b855830bb + "@webex/webex-core": "npm:3.9.0-next.3" + checksum: 10c0/024963fd645e62236823b0437e504c22b566ce31f9712dee3def054c0b3a70cdd4a285f9b247d75515d5443847f77bb8172a844e6867a80dc0dfd37df4d83a51 languageName: node linkType: hard @@ -12606,9 +12606,9 @@ __metadata: languageName: node linkType: hard -"@webex/webex-core@npm:3.9.0-next.1": - version: 3.9.0-next.1 - resolution: "@webex/webex-core@npm:3.9.0-next.1" +"@webex/webex-core@npm:3.9.0-next.3": + version: 3.9.0-next.3 + resolution: "@webex/webex-core@npm:3.9.0-next.3" dependencies: "@webex/common": "npm:3.8.1-next.11" "@webex/common-timers": "npm:3.8.1-next.11" @@ -12622,7 +12622,7 @@ __metadata: jsonwebtoken: "npm:^9.0.0" lodash: "npm:^4.17.21" uuid: "npm:^3.3.2" - checksum: 10c0/ee499fa7cbf5d6ded9284b5d982ac8a70dfa6bd9c634b24b206a1e33e8717db4c7c5ed53850fc59787a331c98211db4efb2e6da84aa5c156220e919786277bb7 + checksum: 10c0/bbe29938dd464232278a7edbb6bb8d7d826f0df0f5795db767faeb69330865b2c7a3c2aa69b8a36923be0d8df117dbd9d452084016b0ce2746e9d1d3a1916977 languageName: node linkType: hard From 976eec0ceaa694aea19ddafbfbdf43d0626443ba Mon Sep 17 00:00:00 2001 From: adhmenon Date: Fri, 3 Oct 2025 19:13:22 +0530 Subject: [PATCH 02/20] feat(cc-wudgets): added-snaps --- ...consult-transfer-popover.snapshot.tsx.snap | 3332 ++++++++++++++++- .../consult-transfer-popover.snapshot.tsx | 43 + 2 files changed, 3298 insertions(+), 77 deletions(-) diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap index a1df70cd8..6f428a6fa 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with agents having different states 1`] = ` +exports[`ConsultTransferPopoverComponent Snapshots Interactions should render component after agent selection 1`] = `
@@ -46,9 +46,9 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem alt="" aria-labelledby="" class="md-button md-button--36" - data-md-event-key="md-button-37" + data-md-event-key="md-button-45" data-md-keyboard-key="agents" - id="md-button-37" + id="md-button-45" tabindex="0" type="button" variant="primary" @@ -64,9 +64,9 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem alt="" aria-labelledby="" class="md-button md-button--36" - data-md-event-key="md-button-38" + data-md-event-key="md-button-46" data-md-keyboard-key="queues" - id="md-button-38" + id="md-button-46" tabindex="0" type="button" variant="secondary" @@ -82,9 +82,9 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem alt="" aria-labelledby="" class="md-button md-button--36" - data-md-event-key="md-button-39" + data-md-event-key="md-button-47" data-md-keyboard-key="dial-number" - id="md-button-39" + id="md-button-47" tabindex="0" type="button" variant="secondary" @@ -100,9 +100,9 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem alt="" aria-labelledby="" class="md-button md-button--36" - data-md-event-key="md-button-40" + data-md-event-key="md-button-48" data-md-keyboard-key="entry-point" - id="md-button-40" + id="md-button-48" tabindex="0" type="button" variant="secondary" @@ -122,7 +122,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem style="cursor: pointer; pointer-events: auto;" >
@@ -162,7 +162,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem tagname="p" type="body-large-regular" > - Available Agent + Agent One
@@ -236,7 +236,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem tagname="p" type="body-large-regular" > - Busy Agent + Agent Two
+ + +`; + +exports[`ConsultTransferPopoverComponent Snapshots Interactions should render component after queue selection 1`] = ` +
+ + Select an Agent + +
-
  • - +
    +
    +
  • +
    + + + + +
    +
    +
      +
      +
    • -
    -
    - - Idle Agent - -
    -
    + + Queue One + +
    - + +
    -
    - + + + + + +`; + +exports[`ConsultTransferPopoverComponent Snapshots Interactions should render component after switching to queues tab 1`] = ` +
    + + Select an Agent + +
    +
    +
    + +
    +
    - +
    +
    + + + + +
    +
    +
      +
      +
    • +
      + +
      +
      + + Queue One + +
      +
      +
      + +
      +
      +
    • +
      +
    +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render the component with heading and category buttons 1`] = ` +
    + + Select an Agent + +
    +
    +
    + +
    +
    +
    +
    +
    + + + + +
    +
      +
      +
    • +
      + +
      +
      + + Agent One + +
      +
      +
      + +
      +
      +
    • +
      +
      +
    • +
      + +
      +
      + + Agent Two + +
      +
      +
      + +
      +
      +
    • +
      +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with agents having different states 1`] = ` +
    + + Select an Agent + +
    +
    +
    + +
    +
    +
    +
    +
    + + + + +
    +
      +
      +
    • +
      + +
      +
      + + Available Agent + +
      +
      +
      + +
      +
      +
    • +
      +
      +
    • +
      + +
      +
      + + Busy Agent + +
      +
      +
      + +
      +
      +
    • +
      +
      +
    • +
      + +
      +
      + + Idle Agent + +
      +
      +
      + +
      +
      +
    • +
      +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with allowConsultToQueue false 1`] = ` +
    + + Select an Agent + +
    +
    +
    + +
    +
    +
    +
    +
    + + + + +
    +
      +
      +
    • +
      + +
      +
      + + Agent One + +
      +
      +
      + +
      +
      +
    • +
      +
      +
    • +
      + +
      +
      + + Agent Two + +
      +
      +
      + +
      +
      +
    • +
      +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with custom empty message 1`] = ` +
    + + Select an Agent + +
    +
    +
    + +
    +
    +
    +
    +
    + + + + +
    +
    +
    +
    + No data available for consult transfer. +
    +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with different heading 1`] = ` +
    + + Choose Transfer Target + +
    +
    +
    + +
    +
    +
    +
    +
    + + + + +
    +
      +
      +
    • +
      + +
      +
      + + Agent One + +
      +
      +
      + +
      +
      +
    • +
      +
      +
    • +
      + +
      +
      + + Agent Two + +
      +
      +
      + +
      +
      +
    • +
      +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with empty agents list 1`] = ` +
    + + Select an Agent + +
    +
    +
    + +
    +
    +
    +
    +
    + + + + +
    +
    +
    +
    + No data available for consult transfer. +
    +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with empty queues list 1`] = ` +
    + + Select an Agent + +
    +
    +
    + +
    +
    +
    +
    +
    + + + + +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with single agent 1`] = ` +
    + + Select an Agent + +
    +
    +
    + +
    +
    +
    +
    +
    + + + + +
    +
      +
      +
    • +
      + +
      +
      + + Single Agent + +
      +
      +
      + +
      +
      +
    • +
      +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with single queue 1`] = ` +
    + + Select an Agent + +
    +
    +
    + +
    +
    +
    +
    +
    + + + + +
    +
    +
      +
      +
    • +
      + +
      +
      + + Single Queue + +
      +
      +
      + +
      +
      +
    • +
      +
    +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render without errors when rendered with minimal props 1`] = ` +
    + + Select an Agent + +
    +
    +
    + +
    +
    +
    +
    +
    + + + + +
    +
      +
      +
    • +
      + +
      +
      + + Agent One + +
      +
      +
      + +
      +
      +
    • +
      +
      +
    • +
      + +
      +
      + + Agent Two + +
      +
      +
      + +
      +
      +
    • +
      +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots State Management should update when agents list changes 1`] = ` +
    + + Select an Agent + +
    +
    +
    + +
    +
    +
    +
    +
    + + + + +
    +
      +
      +
    • +
      + +
      +
      + + New Agent One + +
      +
      +
      + +
      +
      +
    • +
      +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots State Management should update when heading changes 1`] = ` +
    + + New Heading + +
    +
    +
    + +
    +
    +
    +
    +
    + + + + +
    +
      +
      +
    • +
      + +
      +
      + + Agent One + +
      +
      +
      + +
      +
      +
    • +
      +
      +
    • +
      + +
      +
      + + Agent Two + +
      +
      +
      + +
      +
      +
    • +
      +
    +
    +`; + +exports[`ConsultTransferPopoverComponent Snapshots State Management should update when queues list changes 1`] = ` +
    + + Select an Agent + +
    +
    +
    + +
    +
    +
    +
    +
    + + + + +
    +
    +
      +
      +
    • +
      + +
      +
      + + New Queue One + +
      +
      +
      + +
      +
      +
    • +
      +
    +
    `; diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.snapshot.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.snapshot.tsx index 0d4e1b054..57b1cfa23 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.snapshot.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.snapshot.tsx @@ -98,6 +98,9 @@ describe('ConsultTransferPopoverComponent Snapshots', () => { (b) => (b as HTMLButtonElement).textContent ); expect(btns).toEqual(expect.arrayContaining(['Agents', 'Queues', 'Dial Number', 'Entry Point'])); + const container = screen.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); }); it('should render without errors when rendered with minimal props', async () => { @@ -108,6 +111,7 @@ describe('ConsultTransferPopoverComponent Snapshots', () => { const container = screen.container.querySelector('.agent-popover-content'); mockUIDProps(container); expect(container).toBeInTheDocument(); + expect(container).toMatchSnapshot(); }); it('should render with different heading', async () => { @@ -117,6 +121,9 @@ describe('ConsultTransferPopoverComponent Snapshots', () => { screen = render(); }); expect(screen.getByText('Choose Transfer Target')).toBeInTheDocument(); + const container = screen.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); }); it('should render with empty agents list', async () => { @@ -127,6 +134,9 @@ describe('ConsultTransferPopoverComponent Snapshots', () => { }); const emptyState = screen.container.querySelector('.consult-empty-message'); expect(emptyState?.textContent).toBe('No data available for consult transfer.'); + const container = screen.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); }); it('should render with empty queues list', async () => { @@ -146,6 +156,9 @@ describe('ConsultTransferPopoverComponent Snapshots', () => { const list = screen.container.querySelector('.agent-list'); expect(list).toBeNull(); expect(screen.container.querySelectorAll('.call-control-list-item').length).toBe(0); + const container = screen.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); }); it('should render with allowConsultToQueue false', async () => { @@ -161,6 +174,9 @@ describe('ConsultTransferPopoverComponent Snapshots', () => { }) as HTMLButtonElement | undefined; expect(queuesButton).toBeDefined(); expect(queuesButton?.disabled).toBe(true); + const container = screen.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); }); it('should render with custom empty message', async () => { @@ -177,6 +193,9 @@ describe('ConsultTransferPopoverComponent Snapshots', () => { const emptyState = screen.container.querySelector('.consult-empty-state'); expect(emptyState).toBeInTheDocument(); expect(screen.getByText('No data available for consult transfer.')).toBeInTheDocument(); + const container = screen.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); }); it('should render with single agent', async () => { @@ -200,6 +219,9 @@ describe('ConsultTransferPopoverComponent Snapshots', () => { const singleAgentItem = screen.container.querySelector('[aria-label="Single Agent"]'); expect(singleAgentItem).toBeInTheDocument(); + const container = screen.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); }); it('should render with single queue', async () => { @@ -221,6 +243,9 @@ describe('ConsultTransferPopoverComponent Snapshots', () => { }) as HTMLButtonElement | undefined; if (queuesButton) fireEvent.click(queuesButton); await waitFor(() => expect(screen.container.querySelector('[aria-label="Single Queue"]')).toBeInTheDocument()); + const container = screen.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); }); it('should render with agents having different states', async () => { @@ -280,6 +305,9 @@ describe('ConsultTransferPopoverComponent Snapshots', () => { } await waitFor(() => expect(renderResult.container.querySelector('[aria-label="Queue One"]')).toBeInTheDocument()); + const container = renderResult.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); }); it('should render component after agent selection', async () => { @@ -291,6 +319,9 @@ describe('ConsultTransferPopoverComponent Snapshots', () => { const firstAgentButton = renderResult.container.querySelectorAll('.call-control-list-item button')[0]; if (firstAgentButton) fireEvent.click(firstAgentButton); expect(mockOnAgentSelect).toHaveBeenCalled(); + const container = renderResult.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); }); it('should render component after queue selection', async () => { @@ -315,6 +346,9 @@ describe('ConsultTransferPopoverComponent Snapshots', () => { const firstQueueButton = renderResult.container.querySelectorAll('.call-control-list-item button')[0]; if (firstQueueButton) fireEvent.click(firstQueueButton); expect(mockOnQueueSelect).toHaveBeenCalled(); + const container = renderResult.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); }); }); @@ -327,6 +361,9 @@ describe('ConsultTransferPopoverComponent Snapshots', () => { renderResult.rerender(); expect(renderResult.getByText('New Heading')).toBeInTheDocument(); + const container = renderResult.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); }); it('should update when agents list changes', async () => { @@ -348,6 +385,9 @@ describe('ConsultTransferPopoverComponent Snapshots', () => { renderResult.rerender(); const updated = renderResult.container.querySelector('[aria-label="New Agent One"]'); expect(updated).toBeInTheDocument(); + const container = renderResult.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); }); it('should update when queues list changes', async () => { @@ -373,6 +413,9 @@ describe('ConsultTransferPopoverComponent Snapshots', () => { ); const newQueue = renderResult.container.querySelector('[aria-label="New Queue One"]'); expect(newQueue).toBeInTheDocument(); + const container = renderResult.container.querySelector('.agent-popover-content'); + mockUIDProps(container); + expect(container).toMatchSnapshot(); }); }); }); From 04222942d7eb903c3ab8a097852055127948ef79 Mon Sep 17 00:00:00 2001 From: adhmenon Date: Fri, 3 Oct 2025 20:24:58 +0530 Subject: [PATCH 03/20] feat(cc-wudgets): adjusted-queues-method --- .../contact-center/cc-components/package.json | 2 +- .../contact-center/cc-widgets/package.json | 2 +- .../contact-center/station-login/package.json | 2 +- .../contact-center/store/src/store.types.ts | 6 +--- .../store/src/storeEventsWrapper.ts | 35 ++++++++----------- .../store/tests/storeEventsWrapper.ts | 4 +-- packages/contact-center/task/package.json | 2 +- packages/contact-center/task/src/helper.ts | 11 ++++-- packages/contact-center/task/tests/helper.ts | 16 ++++----- .../contact-center/ui-logging/package.json | 2 +- .../contact-center/user-state/package.json | 2 +- 11 files changed, 38 insertions(+), 46 deletions(-) diff --git a/packages/contact-center/cc-components/package.json b/packages/contact-center/cc-components/package.json index a6cad049c..3245c3901 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-widgets/package.json b/packages/contact-center/cc-widgets/package.json index 6f74d2d25..c66fdfc8c 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 3f158afbc..93b32c000 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/src/store.types.ts b/packages/contact-center/store/src/store.types.ts index 9a3eb4d3e..9791904a3 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -131,11 +131,7 @@ interface IStoreWrapper extends IStore { setCurrentTask(task: ITask): void; refreshTaskList(): void; getBuddyAgents(mediaType?: string): Promise; - getQueues(mediaType?: string, params?: ContactServiceQueueSearchParams): Promise; - getQueuesPaginated( - mediaType?: string, - params?: ContactServiceQueueSearchParams - ): Promise; + getQueues(mediaType?: string, params?: ContactServiceQueueSearchParams): Promise; getEntryPoints(params?: EntryPointSearchParams): Promise; getAddressBookEntries(params?: AddressBookEntrySearchParams): Promise; setDeviceType(option: string): void; diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index d5b72c78d..b06ba7a9f 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -677,39 +677,34 @@ class StoreWrapper implements IStoreWrapper { getQueues = async ( mediaType: string = this.currentTask.data.interaction.mediaType ?? 'TELEPHONY', params?: ContactServiceQueueSearchParams - ): Promise> => { + ): Promise<{ + data: ContactServiceQueue[]; + meta: {page: number; pageSize: number; total: number; totalPages: number}; + }> => { try { const upperMediaType = mediaType.toUpperCase(); const response = await this.store.cc.getQueues(params); - - const toQueueArray = (value: {data: ContactServiceQueue[]}): ContactServiceQueue[] => value.data; - - const queues: ContactServiceQueue[] = Array.isArray(response) ? response : toQueueArray(response); - - const filteredQueues = queues.filter((queue) => queue.channelType === upperMediaType); - return filteredQueues; + const data = Array.isArray(response) ? response : response.data; + const filtered = data.filter((queue) => queue.channelType === upperMediaType); + const page = Array.isArray(response) ? 0 : (response.meta?.page ?? 0); + const totalPages = Array.isArray(response) ? 1 : (response.meta?.totalPages ?? 1); + const pageSize = Array.isArray(response) ? filtered.length : (response.meta?.pageSize ?? filtered.length); + const total = Array.isArray(response) + ? filtered.length + : ((response as {meta?: {total?: number}}).meta?.total ?? filtered.length); + return {data: filtered, meta: {page, pageSize, total, totalPages}}; } catch (error) { console.error('Error fetching queues:', error); return Promise.reject(error); } }; + // Deprecated: Use getQueues (paginated) instead getQueuesPaginated = async ( mediaType: string = this.currentTask.data.interaction.mediaType ?? 'TELEPHONY', params?: ContactServiceQueueSearchParams ) => { - try { - const upperMediaType = mediaType.toUpperCase(); - const response = await this.store.cc.getQueues(params); - const data = Array.isArray(response) ? response : response.data; - const filtered = data.filter((queue) => queue.channelType === upperMediaType); - return Array.isArray(response) - ? {data: filtered, meta: {page: 0, pageSize: filtered.length, total: filtered.length, totalPages: 1}} - : {...response, data: filtered}; - } catch (error) { - console.error('Error fetching paginated queues:', error); - return Promise.reject(error); - } + return this.getQueues(mediaType, params); }; getEntryPoints = async (params?: EntryPointSearchParams): Promise => { diff --git a/packages/contact-center/store/tests/storeEventsWrapper.ts b/packages/contact-center/store/tests/storeEventsWrapper.ts index 55775fc9b..524d558b9 100644 --- a/packages/contact-center/store/tests/storeEventsWrapper.ts +++ b/packages/contact-center/store/tests/storeEventsWrapper.ts @@ -806,7 +806,7 @@ describe('storeEventsWrapper', () => { const result = await storeWrapper.getQueues('telephony'); - expect(result).toEqual([ + expect(result.data).toEqual([ {id: 'queue1', name: 'Queue 1', channelType: 'TELEPHONY'}, {id: 'queue2', name: 'Queue 2', channelType: 'TELEPHONY'}, ]); @@ -830,7 +830,7 @@ describe('storeEventsWrapper', () => { const result = await storeWrapper.getQueues('telephony'); - expect(result).toEqual([{id: 'queue1', name: 'Queue 1', channelType: 'TELEPHONY'}]); + expect(result.data).toEqual([{id: 'queue1', name: 'Queue 1', channelType: 'TELEPHONY'}]); expect(storeWrapper['store'].cc.getQueues).toHaveBeenCalled(); }); diff --git a/packages/contact-center/task/package.json b/packages/contact-center/task/package.json index 601be34b2..db4381ead 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 1d9011f37..f48c5c345 100644 --- a/packages/contact-center/task/src/helper.ts +++ b/packages/contact-center/task/src/helper.ts @@ -410,8 +410,13 @@ export const useCallControl = (props: useCallControlProps) => { const loadQueues = useCallback(async () => { try { - const queues = await store.getQueues(); - setQueues(queues); + const response = (await store.getQueues()) as unknown as + | ContactServiceQueue[] + | { + data: ContactServiceQueue[]; + }; + const data = Array.isArray(response) ? response : (response as {data: ContactServiceQueue[]}).data; + setQueues(data); } catch (error) { logger?.error(`CC-Widgets: Task: Error loading queues - ${error.message || error}`, { module: 'useCallControl', @@ -454,7 +459,7 @@ export const useCallControl = (props: useCallControlProps) => { const getQueuesFetcher = useCallback( async ({page, pageSize, search}: {page: number; pageSize: number; search?: string}) => { try { - return await store.getQueuesPaginated('TELEPHONY', {page, pageSize, search}); + return await store.getQueues('TELEPHONY', {page, pageSize, search}); } catch (error) { logger?.error(`CC-Widgets: Task: Error fetching queues (paginated) - ${error.message || error}`, { module: 'useCallControl', diff --git a/packages/contact-center/task/tests/helper.ts b/packages/contact-center/task/tests/helper.ts index 4e5b490d1..e4323893b 100644 --- a/packages/contact-center/task/tests/helper.ts +++ b/packages/contact-center/task/tests/helper.ts @@ -1,12 +1,7 @@ import {renderHook, act, waitFor} from '@testing-library/react'; import {useIncomingTask, useTaskList, useCallControl, useOutdialCall} from '../src/helper'; import * as taskUtils from '../src/Utils/task-util'; -import { - AddressBookEntriesResponse, - EntryPointListResponse, - ContactServiceQueuesResponse, - TASK_EVENTS, -} from '@webex/cc-store'; +import {AddressBookEntriesResponse, EntryPointListResponse, TASK_EVENTS, IContactCenter} from '@webex/cc-store'; import {mockAgents, mockCC, mockQueueDetails, mockTask} from '@webex/test-fixtures'; import store from '@webex/cc-store'; import React from 'react'; @@ -48,13 +43,13 @@ beforeAll(() => { webRtcEnabled: true, }; store.store.cc = { - ...store.store.cc, // Keep other properties if they exist + ...mockCC, taskManager: { getAllTasks: jest.fn().mockReturnValue({ [taskMock.data.interactionId]: taskMock, }), }, - }; + } as IContactCenter; }); describe('useIncomingTask Hook', () => { @@ -2235,9 +2230,10 @@ describe('useCallControl', () => { it('should get queues via getQueuesFetcher (paginated)', async () => { const mockResponse = { data: [{id: 'q1', name: 'Queue 1', channelType: 'TELEPHONY'}], - meta: {page: 0, totalPages: 1}, + meta: {page: 0, pageSize: 25, total: 1, totalPages: 1}, }; - jest.spyOn(store, 'getQueuesPaginated').mockResolvedValue(mockResponse as ContactServiceQueuesResponse); + // Use a broad cast to satisfy the spy return type regardless of meta shape + jest.spyOn(store, 'getQueues').mockResolvedValue(mockResponse as unknown as ReturnType); const {result} = renderHook(() => useCallControl({ diff --git a/packages/contact-center/ui-logging/package.json b/packages/contact-center/ui-logging/package.json index 9f3ec708d..c1d3e2620 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 ebf0eb38b..ff04bafa2 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 +} From 7f1703199a4aeaf4c12a5079aba3be10f2259ef3 Mon Sep 17 00:00:00 2001 From: adhmenon Date: Fri, 3 Oct 2025 20:33:31 +0530 Subject: [PATCH 04/20] feat(cc-wudgets): removed-whitespaces --- packages/contact-center/cc-components/package.json | 2 +- packages/contact-center/station-login/package.json | 2 +- packages/contact-center/store/package.json | 2 +- packages/contact-center/task/package.json | 2 +- packages/contact-center/test-fixtures/package.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/contact-center/cc-components/package.json b/packages/contact-center/cc-components/package.json index 3245c3901..a6cad049c 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/station-login/package.json b/packages/contact-center/station-login/package.json index 93b32c000..3f158afbc 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 0146bf5b0..63d2119cb 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 db4381ead..601be34b2 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/test-fixtures/package.json b/packages/contact-center/test-fixtures/package.json index 13918a997..09ee00cc4 100644 --- a/packages/contact-center/test-fixtures/package.json +++ b/packages/contact-center/test-fixtures/package.json @@ -47,4 +47,4 @@ "webpack-cli": "5.1.4", "webpack-merge": "6.0.1" } -} +} \ No newline at end of file From 5c169263493f6f7d1f0621a92efa24f2adefb34e Mon Sep 17 00:00:00 2001 From: adhmenon Date: Mon, 6 Oct 2025 10:18:28 +0530 Subject: [PATCH 05/20] feat(cc-wudgets): added-consult-transfer-props --- .../consult-transfer-popover.tsx | 50 +++++++++++-------- .../task/CallControl/call-control.tsx | 2 + .../src/components/task/task.types.ts | 18 +++++++ .../contact-center/cc-components/src/wc.ts | 1 + .../consult-transfer-popover.tsx | 48 ++++++++++++++++++ .../task/CallControl/call-control.tsx | 33 ++++++++++++ .../task/src/CallControl/index.tsx | 3 +- .../task/src/CallControlCAD/index.tsx | 2 + .../contact-center/task/src/task.types.ts | 1 + 9 files changed, 135 insertions(+), 23 deletions(-) diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx index 9a2dd6c13..70053df90 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx @@ -25,8 +25,10 @@ const ConsultTransferPopoverComponent: React.FC { + const {showDialNumberTab = true, showEntryPointTab = true} = consultTransferOptions || {}; const [selectedCategory, setSelectedCategory] = useState('Agents'); const [searchQuery, setSearchQuery] = useState(''); const loadMoreRef = useRef(null); @@ -199,9 +201,9 @@ const ConsultTransferPopoverComponent: React.FC { - if (selectedCategory === 'Dial Number' && dialNumbers.length === 0) { + if (selectedCategory === 'Dial Number' && showDialNumberTab && dialNumbers.length === 0) { loadDialNumbers(0, '', true); - } else if (selectedCategory === 'Entry Point' && entryPoints.length === 0) { + } else if (selectedCategory === 'Entry Point' && showEntryPointTab && entryPoints.length === 0) { loadEntryPoints(0, '', true); } else if (selectedCategory === 'Queues' && queuesData.length === 0) { loadQueues(0, '', true); @@ -209,8 +211,8 @@ const ConsultTransferPopoverComponent: React.FC Queues - - + {showDialNumberTab && ( + + )} + {showEntryPointTab && ( + + )}
    {!hasAnyData && } @@ -308,7 +314,7 @@ const ConsultTransferPopoverComponent: React.FC )} - {selectedCategory === 'Dial Number' && !noDialNumbers && ( + {showDialNumberTab && selectedCategory === 'Dial Number' && !noDialNumbers && (
    {renderList( dialNumbers.map((d) => ({id: d.id, name: d.name, number: d.number})), @@ -345,7 +351,7 @@ const ConsultTransferPopoverComponent: React.FC )} - {selectedCategory === 'Entry Point' && !noEntryPoints && ( + {showEntryPointTab && selectedCategory === 'Entry Point' && !noEntryPoints && (
    {renderList( entryPoints.map((e) => ({id: e.id, name: e.name})), 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 09bf0ef44..babcfa2fa 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 @@ -64,6 +64,7 @@ function CallControlComponent(props: CallControlComponentProps) { getAddressBookEntries, getEntryPoints, getQueuesFetcher, + consultTransferOptions, } = props; useEffect(() => { @@ -219,6 +220,7 @@ function CallControlComponent(props: CallControlComponentProps) { } onDialNumberSelect={(dialNumber) => handleTargetSelect(dialNumber, dialNumber, 'dialNumber')} allowConsultToQueue={allowConsultToQueue} + consultTransferOptions={consultTransferOptions} logger={logger} /> ) : null} diff --git a/packages/contact-center/cc-components/src/components/task/task.types.ts b/packages/contact-center/cc-components/src/components/task/task.types.ts index cd2d280c5..64b9ebb32 100644 --- a/packages/contact-center/cc-components/src/components/task/task.types.ts +++ b/packages/contact-center/cc-components/src/components/task/task.types.ts @@ -418,6 +418,11 @@ export interface ControlProps { getEntryPoints?: FetchFunction; /** Fetch paginated queues (filtered by media type in store) */ getQueuesFetcher?: FetchFunction; + + /** + * Options to configure consult/transfer popover behavior. + */ + consultTransferOptions?: ConsultTransferOptions; } export type CallControlComponentProps = Pick< @@ -466,6 +471,7 @@ export type CallControlComponentProps = Pick< | 'getAddressBookEntries' | 'getEntryPoints' | 'getQueuesFetcher' + | 'consultTransferOptions' >; /** @@ -529,6 +535,8 @@ export interface ConsultTransferPopoverComponentProps { onEntryPointSelect?: (entryPointId: string, entryPointName: string) => void; onDialNumberSelect?: (dialNumber: string) => void; allowConsultToQueue: boolean; + /** Options governing popover visibility/behavior */ + consultTransferOptions?: ConsultTransferOptions; logger: ILogger; } @@ -693,3 +701,13 @@ export type FetchFunction = (params: { * Type for transform function */ export type TransformFunction = (item: T, page: number, index: number) => U; + +/** + * Options to configure Consult/Transfer popover behavior and visibility. + */ +export interface ConsultTransferOptions { + /** Show the Dial Number tab. Defaults to true. */ + showDialNumberTab?: boolean; + /** Show the Entry Point tab. Defaults to true. */ + showEntryPointTab?: boolean; +} diff --git a/packages/contact-center/cc-components/src/wc.ts b/packages/contact-center/cc-components/src/wc.ts index eb786815c..1553aab77 100644 --- a/packages/contact-center/cc-components/src/wc.ts +++ b/packages/contact-center/cc-components/src/wc.ts @@ -58,6 +58,7 @@ const commonPropsForCallControl: Record { expect(mockOnQueueSelect).toHaveBeenCalledWith('queue1', 'Queue One'); }); + it('hides Dial Number tab when consultTransferOptions.showDialNumberTab is false', async () => { + const screen = await render( + + ); + + const buttons = Array.from(screen.container.querySelectorAll('button')).map( + (b) => (b as HTMLButtonElement).textContent + ); + + expect(buttons).toEqual(expect.arrayContaining(['Agents', 'Queues'])); + expect(buttons).not.toEqual(expect.arrayContaining(['Dial Number'])); + }); + + it('hides Entry Point tab when consultTransferOptions.showEntryPointTab is false', async () => { + const screen = await render( + + ); + + const buttons = Array.from(screen.container.querySelectorAll('button')).map( + (b) => (b as HTMLButtonElement).textContent + ); + + expect(buttons).toEqual(expect.arrayContaining(['Agents', 'Queues', 'Dial Number'])); + expect(buttons).not.toEqual(expect.arrayContaining(['Entry Point'])); + }); + + it('hides both tabs when both flags are false and shows empty state if no other data', async () => { + const screen = await render( + ({data: [], meta: {page: 0, totalPages: 0}})} + consultTransferOptions={{showDialNumberTab: false, showEntryPointTab: false}} + /> + ); + + const buttons = Array.from(screen.container.querySelectorAll('button')).map( + (b) => (b as HTMLButtonElement).textContent + ); + + expect(buttons).toEqual(expect.arrayContaining(['Agents', 'Queues'])); + expect(buttons).not.toEqual(expect.arrayContaining(['Dial Number'])); + expect(buttons).not.toEqual(expect.arrayContaining(['Entry Point'])); + + // With no agents/queues and both tabs hidden, should show the empty state + expect(screen.getByText('No data available for consult transfer.')).toBeInTheDocument(); + }); + it('handles edge cases and conditional rendering', async () => { // Test empty state when both lists are completely empty const emptyProps = { diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx index 5907c47b4..c67cad330 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx @@ -490,5 +490,38 @@ describe('CallControlComponent', () => { expect(consultButtonElement).toBeInTheDocument(); expect(consultButtonElement).toHaveClass('call-control-button'); }); + + it('passes consultTransferOptions to popover and hides tabs accordingly', async () => { + jest.spyOn(callControlUtils, 'filterButtonsForConsultation').mockReturnValue([ + { + id: 'consult', + icon: 'consult', + tooltip: 'Consult', + className: 'call-control-button', + disabled: false, + menuType: 'Consult', + isVisible: true, + dataTestId: 'consult-button', + }, + ]); + + const screen = await render( + + ); + + // Act: open the consult popover + const consultButton = screen.getByLabelText('Consult'); + fireEvent.click(consultButton); + + // Assert: popover content should not include Dial Number tab + const buttons = Array.from(screen.container.querySelectorAll('button')).map( + (b) => (b as HTMLButtonElement).textContent + ); + expect(buttons).not.toEqual(expect.arrayContaining(['Dial Number'])); + }); }); }); diff --git a/packages/contact-center/task/src/CallControl/index.tsx b/packages/contact-center/task/src/CallControl/index.tsx index 03c8ddfa5..41412f7d5 100644 --- a/packages/contact-center/task/src/CallControl/index.tsx +++ b/packages/contact-center/task/src/CallControl/index.tsx @@ -8,7 +8,7 @@ import {CallControlProps} from '../task.types'; import {CallControlComponent} from '@webex/cc-components'; const CallControlInternal: React.FunctionComponent = observer( - ({onHoldResume, onEnd, onWrapUp, onRecordingToggle, onToggleMute}) => { + ({onHoldResume, onEnd, onWrapUp, onRecordingToggle, onToggleMute, consultTransferOptions}) => { const { logger, currentTask, @@ -48,6 +48,7 @@ const CallControlInternal: React.FunctionComponent = observer( isEndConsultEnabled, allowConsultToQueue, logger, + consultTransferOptions, }; return ; diff --git a/packages/contact-center/task/src/CallControlCAD/index.tsx b/packages/contact-center/task/src/CallControlCAD/index.tsx index d4ea6b8bb..caa9d736d 100644 --- a/packages/contact-center/task/src/CallControlCAD/index.tsx +++ b/packages/contact-center/task/src/CallControlCAD/index.tsx @@ -16,6 +16,7 @@ const CallControlCADInternal: React.FunctionComponent = observ onToggleMute, callControlClassName, callControlConsultClassName, + consultTransferOptions, }) => { const { logger, @@ -57,6 +58,7 @@ const CallControlCADInternal: React.FunctionComponent = observ isEndConsultEnabled, allowConsultToQueue, logger, + consultTransferOptions, }; return ; diff --git a/packages/contact-center/task/src/task.types.ts b/packages/contact-center/task/src/task.types.ts index a1b3d510b..04b79adf7 100644 --- a/packages/contact-center/task/src/task.types.ts +++ b/packages/contact-center/task/src/task.types.ts @@ -20,6 +20,7 @@ export type CallControlProps = Partial< | 'callControlClassName' | 'callControlConsultClassName' | 'onToggleMute' + | 'consultTransferOptions' > >; From 975000ad6000510a8e58fcf811d8196df5d481f2 Mon Sep 17 00:00:00 2001 From: adhmenon Date: Mon, 6 Oct 2025 11:40:47 +0530 Subject: [PATCH 06/20] feat(cc-wudgets): fixed-styles --- .../consult-transfer-list-item.tsx | 22 +- .../task/CallControl/call-control.styles.scss | 51 ++++ ...nsult-transfer-list-item.snapshot.tsx.snap | 235 +++++++++--------- ...consult-transfer-popover.snapshot.tsx.snap | 229 +++++++++-------- .../consult-transfer-list-item.tsx | 43 ++-- 5 files changed, 315 insertions(+), 265 deletions(-) diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-list-item.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-list-item.tsx index aced9fa80..3440f3cc4 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-list-item.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-list-item.tsx @@ -16,32 +16,22 @@ const ConsultTransferListComponent: React.FC return ( - + - - + + {title} {subtitle && ( - + {subtitle} )} - +
    - +
    diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss index 2ea24b6d6..faca4092a 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss +++ b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss @@ -244,6 +244,57 @@ pointer-events: auto; } +.call-control-list-item { + padding: 12px 16px; + margin: 2px 0; + border-radius: 8px; + transition: background-color 0.2s ease; +} + +.call-control-list-item-start { + margin-right: 12px; + display: flex; + align-items: center; +} + +.call-control-list-item-middle { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + min-width: 0; + overflow: hidden; + padding-right: 8px; +} + +.call-control-list-item-title { + margin: 0; + margin-bottom: 2px; + line-height: 1.3; + font-weight: 500; + font-size: 14px; + color: #323130; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.call-control-list-item-subtitle { + margin: 0; + line-height: 1.2; + font-size: 12px; + color: #605e5c; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.call-control-list-item-end { + display: flex; + align-items: center; + margin-left: 8px; +} + .custom-text { margin: 0 !important; line-height: 1 !important; diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-list-item.snapshot.tsx.snap b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-list-item.snapshot.tsx.snap index 0eba3b4df..8ed318e08 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-list-item.snapshot.tsx.snap +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-list-item.snapshot.tsx.snap @@ -14,6 +14,7 @@ exports[`ConsultTransferListComponent Snapshots Interactions should render compo tabindex="0" >
    John Doe Manager
    @@ -95,6 +95,7 @@ exports[`ConsultTransferListComponent Snapshots Interactions should render compo tabindex="0" >
    John Doe Manager
    @@ -176,6 +176,7 @@ exports[`ConsultTransferListComponent Snapshots Interactions should render compo tabindex="0" >
    John Doe Manager
    @@ -257,6 +257,7 @@ exports[`ConsultTransferListComponent Snapshots Rendering - Tests for UI element tabindex="0" >
    John Doe Manager
    @@ -338,6 +338,7 @@ exports[`ConsultTransferListComponent Snapshots Rendering - Tests for UI element tabindex="0" >
    John Doe Manager
    @@ -419,6 +419,7 @@ exports[`ConsultTransferListComponent Snapshots Rendering - Tests for UI element tabindex="0" >
    John Doe Manager
    @@ -500,6 +500,7 @@ exports[`ConsultTransferListComponent Snapshots Rendering - Tests for UI element tabindex="0" >
    Alexander Benjamin Christopher Manager
    @@ -581,6 +581,7 @@ exports[`ConsultTransferListComponent Snapshots Rendering - Tests for UI element tabindex="0" >
    John Doe Senior Manager of Customer Relations and Business Development
    @@ -662,6 +662,7 @@ exports[`ConsultTransferListComponent Snapshots Rendering - Tests for UI element tabindex="0" >
    John Manager
    @@ -743,6 +743,7 @@ exports[`ConsultTransferListComponent Snapshots Rendering - Tests for UI element tabindex="0" >
    John O'Connor & Jane Manager
    @@ -824,6 +824,7 @@ exports[`ConsultTransferListComponent Snapshots Rendering - Tests for UI element tabindex="0" >
    John Doe Manager
    @@ -905,6 +905,7 @@ exports[`ConsultTransferListComponent Snapshots Rendering - Tests for UI element tabindex="0" >
    John Doe
    @@ -978,6 +979,7 @@ exports[`ConsultTransferListComponent Snapshots State Management should update w tabindex="0" >
    John Doe Manager
    @@ -1059,6 +1060,7 @@ exports[`ConsultTransferListComponent Snapshots State Management should update w tabindex="0" >
    John Doe Senior Manager
    @@ -1140,6 +1141,7 @@ exports[`ConsultTransferListComponent Snapshots State Management should update w tabindex="0" >
    Jane Smith Manager
    diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap index 6f428a6fa..bcea85e20 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap @@ -134,6 +134,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Interactions should render co tabindex="-1" >
    Agent One
    @@ -208,6 +209,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Interactions should render co tabindex="-1" >
    Agent Two
    @@ -405,6 +407,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Interactions should render co tabindex="-1" >
    Queue One
    @@ -603,6 +606,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Interactions should render co tabindex="-1" >
    Queue One
    @@ -800,6 +804,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem tabindex="-1" >
    Agent One
    @@ -874,6 +879,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem tabindex="-1" >
    Agent Two
    @@ -1070,6 +1076,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem tabindex="-1" >
    Available Agent
    @@ -1144,6 +1151,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem tabindex="-1" >
    Busy Agent
    @@ -1218,6 +1226,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem tabindex="-1" >
    Idle Agent
    @@ -1414,6 +1423,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem tabindex="-1" >
    Agent One
    @@ -1488,6 +1498,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem tabindex="-1" >
    Agent Two
    @@ -1814,6 +1825,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem tabindex="-1" >
    Agent One
    @@ -1888,6 +1900,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem tabindex="-1" >
    Agent Two
    @@ -2332,6 +2345,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem tabindex="-1" >
    Single Agent
    @@ -2529,6 +2543,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem tabindex="-1" >
    Single Queue
    @@ -2726,6 +2741,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem tabindex="-1" >
    Agent One
    @@ -2800,6 +2816,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem tabindex="-1" >
    Agent Two
    @@ -2996,6 +3013,7 @@ exports[`ConsultTransferPopoverComponent Snapshots State Management should updat tabindex="-1" >
    New Agent One
    @@ -3188,6 +3206,7 @@ exports[`ConsultTransferPopoverComponent Snapshots State Management should updat tabindex="-1" >
    Agent One
    @@ -3262,6 +3281,7 @@ exports[`ConsultTransferPopoverComponent Snapshots State Management should updat tabindex="-1" >
    Agent Two
    @@ -3459,6 +3479,7 @@ exports[`ConsultTransferPopoverComponent Snapshots State Management should updat tabindex="-1" >
    New Queue One
    diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-list-item.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-list-item.tsx index 95603e202..11652b518 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-list-item.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-list-item.tsx @@ -67,36 +67,23 @@ describe('CallControlListItemPresentational', () => { expect(initialsSpan).toHaveTextContent('JD'); expect(initialsSpan).toHaveAttribute('aria-hidden', 'true'); - // Verify middle section with title and subtitle + // Verify middle section with title and subtitle (class-based layout) const middleSection = screen.container.querySelector('[data-position="middle"]'); expect(middleSection).toBeInTheDocument(); - expect(middleSection).toHaveStyle({ - flex: '1', - display: 'flex', - flexDirection: 'column', - marginLeft: '8px', - minWidth: '0', - overflow: 'hidden', - }); + expect(middleSection).toHaveClass('call-control-list-item-middle'); // Verify title text - const titleElements = screen.container.querySelectorAll('mdc-text'); - expect(titleElements[0]).toHaveTextContent('John Doe'); - expect(titleElements[0]).toHaveAttribute('tagname', 'p'); - expect(titleElements[0]).toHaveAttribute('type', 'body-large-regular'); - expect(titleElements[0]).toHaveStyle({ - margin: '0px', - lineHeight: '1.2', - }); + const textElements = screen.container.querySelectorAll('mdc-text'); + expect(textElements[0]).toHaveTextContent('John Doe'); + expect(textElements[0]).toHaveAttribute('tagname', 'div'); + expect(textElements[0]).toHaveAttribute('type', 'body-large-regular'); + expect(textElements[0]).toHaveClass('call-control-list-item-title'); // Verify subtitle text - expect(titleElements[1]).toHaveTextContent('Manager'); - expect(titleElements[1]).toHaveAttribute('tagname', 'p'); - expect(titleElements[1]).toHaveAttribute('type', 'body-midsize-regular'); - expect(titleElements[1]).toHaveStyle({ - margin: '0px', - lineHeight: '1.2', - }); + expect(textElements[1]).toHaveTextContent('Manager'); + expect(textElements[1]).toHaveAttribute('tagname', 'div'); + expect(textElements[1]).toHaveAttribute('type', 'body-midsize-regular'); + expect(textElements[1]).toHaveClass('call-control-list-item-subtitle'); // Verify end section with button const endSection = screen.container.querySelector('[data-position="end"]'); @@ -110,7 +97,7 @@ describe('CallControlListItemPresentational', () => { const button = screen.container.querySelector('button'); expect(button).toBeInTheDocument(); expect(button).toHaveAttribute('data-color', 'join'); - expect(button).toHaveAttribute('data-size', '28'); + expect(button).toHaveAttribute('data-size', '32'); expect(button).toHaveAttribute('data-disabled', 'false'); expect(button).toHaveAttribute('tabindex', '-1'); expect(button).toHaveAttribute('type', 'button'); @@ -141,8 +128,8 @@ describe('CallControlListItemPresentational', () => { const screen = await render(); // Should only have one mdc-text element (title only) - const textElements = screen.container.querySelectorAll('mdc-text'); - expect(textElements).toHaveLength(1); - expect(textElements[0]).toHaveTextContent('John Doe'); + const titleOnlyElements = screen.container.querySelectorAll('mdc-text'); + expect(titleOnlyElements).toHaveLength(1); + expect(titleOnlyElements[0]).toHaveTextContent('John Doe'); }); }); From 92a1f836ec9dd235b0d74ea3bd1df1277fe5da30 Mon Sep 17 00:00:00 2001 From: adhmenon Date: Tue, 7 Oct 2025 18:56:41 +0530 Subject: [PATCH 07/20] feat(cc-widgets): added-basic-playwright-for-address-book --- playwright/Utils/advancedTaskControlUtils.ts | 98 ++++++++++++++----- playwright/Utils/helperUtils.ts | 28 ++++++ .../tests/advanced-task-controls-test.spec.ts | 87 ++++++++++++++-- playwright/tests/tasklist-test.spec.ts | 11 ++- 4 files changed, 193 insertions(+), 31 deletions(-) diff --git a/playwright/Utils/advancedTaskControlUtils.ts b/playwright/Utils/advancedTaskControlUtils.ts index 127fc1373..5f4bc08f6 100644 --- a/playwright/Utils/advancedTaskControlUtils.ts +++ b/playwright/Utils/advancedTaskControlUtils.ts @@ -1,4 +1,5 @@ -import {Page} from '@playwright/test'; +import {Page, expect} from '@playwright/test'; +import {dismissOverlays} from './helperUtils'; import {AWAIT_TIMEOUT, FORM_FIELD_TIMEOUT} from '../constants'; /** @@ -112,12 +113,14 @@ export function verifyConsultTransferredLogs(): void { */ export async function consultOrTransfer( page: Page, - type: 'agent' | 'queue' | 'dialNumber', + type: 'agent' | 'queue' | 'dialNumber' | 'entryPoint', action: 'consult' | 'transfer', value: string ): Promise { // Determine which button to click for consult or transfer if (action === 'consult') { + // Close any backdrops that might intercept clicks + await dismissOverlays(page); await page.getByTestId('call-control:consult').nth(1).click({timeout: AWAIT_TIMEOUT}); } else { await page @@ -126,47 +129,96 @@ export async function consultOrTransfer( .click({timeout: AWAIT_TIMEOUT}); } - // Navigate to the correct tab and perform the action - if (type === 'agent' || type === 'queue') { - const tabName = type === 'agent' ? 'Agents' : 'Queues'; - await page.getByRole('tab', {name: tabName}).click({timeout: AWAIT_TIMEOUT}); - const listItem = page.getByRole('listitem', {name: value, exact: true}); + // Popover universal search should be visible (inside popover container) + const popover = page.locator('.agent-popover-content'); + await expect(popover.locator('#consult-search')).toBeVisible({timeout: FORM_FIELD_TIMEOUT}); + + // Navigate to the correct category and perform the action + const clickCategory = async (name: 'Agents' | 'Queues' | 'Dial Number' | 'Entry Point') => { + // New UI uses buttons instead of tabs + const button = popover.getByRole('button', {name}); + await button.click({timeout: AWAIT_TIMEOUT}); + await page.waitForTimeout(200); + }; + + const clickListItemPrimaryButton = async (categoryLabel: string) => { + // Prefer exact aria-label match within the consult/transfer popover to avoid strict-mode collisions + const listItem = popover.locator(`[role="listitem"][aria-label="${value}"]`).first(); await listItem.waitFor({state: 'visible', timeout: AWAIT_TIMEOUT}); + // Some UIs reveal the action button on hover; hover to ensure visibility + await listItem.hover(); await listItem.scrollIntoViewIfNeeded(); - await page.waitForTimeout(300); - const button = listItem.getByRole('button'); - await button.waitFor({state: 'visible', timeout: AWAIT_TIMEOUT}); - await button.scrollIntoViewIfNeeded(); - await button.evaluate((el) => { + await page.waitForTimeout(200); + const primaryButton = listItem.getByRole('button'); + await primaryButton.waitFor({state: 'visible', timeout: AWAIT_TIMEOUT}); + await primaryButton.scrollIntoViewIfNeeded(); + await primaryButton.evaluate((el) => { if (el.hasAttribute('disabled')) { - throw new Error(`${tabName.slice(0, -1)} button is disabled`); + throw new Error(`${categoryLabel} button is disabled`); } }); let lastError; for (let i = 0; i < 3; i++) { try { - await button.click({timeout: AWAIT_TIMEOUT, force: true}); + await primaryButton.click({timeout: AWAIT_TIMEOUT, force: true}); lastError = undefined; break; - } catch (e) { - lastError = e; - await page.waitForTimeout(400); + } catch (err) { + lastError = err; + await page.waitForTimeout(300); } } if (lastError) { throw lastError; } + await page + .locator('.md-popover-backdrop') + .waitFor({state: 'hidden', timeout: 3000}) + .catch(() => {}); + }; + + if (type === 'agent') { + await clickCategory('Agents'); + await clickListItemPrimaryButton('Agent'); + } else if (type === 'queue') { + await clickCategory('Queues'); + await clickListItemPrimaryButton('Queue'); } else if (type === 'dialNumber') { - await page.getByRole('tab', {name: 'Dial Number'}).click({timeout: AWAIT_TIMEOUT}); - const inputLocator = page.getByTestId('consult-transfer-dial-number-input').locator('input'); - await inputLocator.waitFor({state: 'visible', timeout: AWAIT_TIMEOUT}); - await inputLocator.click({timeout: AWAIT_TIMEOUT}); - await inputLocator.fill(value, {timeout: AWAIT_TIMEOUT}); - await page.getByTestId('dial-number-btn').click({timeout: AWAIT_TIMEOUT}); + await clickCategory('Dial Number'); + // Require a valid name to avoid strict-mode ambiguous matches + if (!value || value.trim() === '') { + throw new Error( + 'PW_DIAL_NUMBER_NAME is not set. Please provide the Dial Number list item name (e.g., cypher_pstn).' + ); + } + // List-based selection only (no legacy input fallback) + const search = popover.locator('#consult-search'); + if (value && (await search.isVisible({timeout: 500}).catch(() => false))) { + await search.fill(value, {timeout: AWAIT_TIMEOUT}); + await page.waitForTimeout(300); + } + const listItem = popover.getByRole('listitem', {name: value, exact: true}); + await listItem.waitFor({state: 'visible', timeout: FORM_FIELD_TIMEOUT}); + await clickListItemPrimaryButton('Dial Number'); + } else if (type === 'entryPoint') { + await clickCategory('Entry Point'); + // Optional: filter via universal search (only if value provided) + if (value) { + const search = popover.locator('#consult-search'); + if (await search.isVisible({timeout: 500}).catch(() => false)) { + await search.fill(value, {timeout: AWAIT_TIMEOUT}); + await page.waitForTimeout(300); + } + } + await clickListItemPrimaryButton('Entry Point'); } // Wait a moment for the action to be processed await page.waitForTimeout(2000); + if (action === 'consult') { + // Confirm consult UI is present + await expect(page.getByTestId('cancel-consult-btn')).toBeVisible({timeout: FORM_FIELD_TIMEOUT}); + } } /** diff --git a/playwright/Utils/helperUtils.ts b/playwright/Utils/helperUtils.ts index e8ecd1c8f..2fab9161d 100644 --- a/playwright/Utils/helperUtils.ts +++ b/playwright/Utils/helperUtils.ts @@ -505,3 +505,31 @@ export const pageSetup = async ( await page.getByTestId('state-select').waitFor({state: 'visible', timeout: 30000}); }; + +/** + * Dismisses any visible popover/tooltips/backdrops that might intercept pointer events. + * Attempts ESC presses and quick background clicks. + */ +export async function dismissOverlays(page: Page): Promise { + for (let i = 0; i < 3; i++) { + // If a Momentum popover backdrop is visible, try ESC to close + const backdropVisible = await page + .locator('.md-popover-backdrop') + .isVisible() + .catch(() => false); + const tippyVisible = await page + .locator('[id^="tippy-"]') + .first() + .isVisible() + .catch(() => false); + if (!backdropVisible && !tippyVisible) return; + try { + await page.keyboard.press('Escape'); + } catch {} + // Small click near top-left to blur active elements + try { + await page.mouse.click(5, 5); + } catch {} + await page.waitForTimeout(200); + } +} diff --git a/playwright/tests/advanced-task-controls-test.spec.ts b/playwright/tests/advanced-task-controls-test.spec.ts index 7eaa55f32..ef30e4afb 100644 --- a/playwright/tests/advanced-task-controls-test.spec.ts +++ b/playwright/tests/advanced-task-controls-test.spec.ts @@ -139,8 +139,7 @@ export default function createAdvancedTaskControlsTests() { }); test('Call Blind Transferred to DialNumber', async () => { - // First transfer from Agent 1 to Agent 2 - await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'transfer', process.env.PW_DIAL_NUMBER!); + await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'transfer', process.env.PW_DIAL_NUMBER_NAME); //DialNumber accepts the transfer await acceptExtensionCall(testManager.dialNumberPage); @@ -178,6 +177,33 @@ export default function createAdvancedTaskControlsTests() { // ============================================================================= describe('Consult and Consult Transfer Scenarios', () => { + test('Entry Point Consult: visible and functional only for supported users (no blind transfer)', async ({}, testInfo) => { + if (testManager.projectName !== 'SET_5') { + test.skip(true, 'Entrypoint consult is only enabled for SET_5 (user23/user24).'); + } + + await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); + await changeUserState(testManager.agent2Page, USER_STATES.MEETING); + await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!); + const incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); + await incomingTaskDiv.waitFor({state: 'visible', timeout: 80000}); + await acceptExtensionCall(testManager.agent1ExtensionPage); + await testManager.agent1Page.waitForTimeout(3000); + await verifyCurrentState(testManager.agent1Page, USER_STATES.ENGAGED); + + // Ensure consult UI and Entry Point tab exists + await testManager.agent1Page.getByTestId('call-control:consult').nth(1).click(); + await expect(testManager.agent1Page.locator('#consult-search')).toBeVisible(); + await testManager.agent1Page.getByRole('button', {name: 'Entry Point'}).click(); + + console.log('PW_ENTRYPOINT_NAME:', process.env.PW_ENTRYPOINT_NAME); + await consultOrTransfer(testManager.agent1Page, 'entryPoint', 'consult', process.env.PW_ENTRYPOINT_NAME!); + await expect(testManager.agent1Page.getByTestId('cancel-consult-btn')).toBeVisible(); + await testManager.agent1Page.waitForTimeout(1000); + await cancelConsult(testManager.agent1Page); + await testManager.agent1Page.waitForTimeout(1000); + await verifyCurrentState(testManager.agent1Page, USER_STATES.ENGAGED); + }); test('Agent Consult Transfer: cancel, decline, timeout, and transfer scenarios are handled correctly in sequence', async () => { // ...existing code for Agent Consult Transfer test... await changeUserState(testManager.agent2Page, USER_STATES.MEETING); @@ -380,7 +406,6 @@ export default function createAdvancedTaskControlsTests() { }); test('Dial Number Consult: cancel, decline, accept/end, and transfer scenarios are handled correctly in sequence', async () => { - // ...existing code for Dial Number Consult test... await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); // Setup: create call and get to engaged state @@ -396,7 +421,7 @@ export default function createAdvancedTaskControlsTests() { // 1. Cancel consult clearAdvancedCapturedLogs(); - await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'consult', process.env.PW_DIAL_NUMBER!); + await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'consult', process.env.PW_DIAL_NUMBER_NAME); await expect(testManager.agent1Page.getByTestId('cancel-consult-btn')).toBeVisible(); await testManager.agent1Page.waitForTimeout(2000); await cancelConsult(testManager.agent1Page); @@ -405,7 +430,7 @@ export default function createAdvancedTaskControlsTests() { // 2. Decline consult clearAdvancedCapturedLogs(); - await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'consult', process.env.PW_DIAL_NUMBER!); + await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'consult', process.env.PW_DIAL_NUMBER_NAME); await declineExtensionCall(testManager.dialNumberPage); await testManager.agent1Page.waitForTimeout(2000); await cancelConsult(testManager.agent1Page); // still needs to cancel even if declined @@ -418,7 +443,7 @@ export default function createAdvancedTaskControlsTests() { // 3. Accept consult and end clearAdvancedCapturedLogs(); - await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'consult', process.env.PW_DIAL_NUMBER!); + await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'consult', process.env.PW_DIAL_NUMBER_NAME); await testManager.agent1Page.waitForTimeout(2000); verifyConsultStartSuccessLogs(); await acceptExtensionCall(testManager.dialNumberPage); @@ -432,7 +457,7 @@ export default function createAdvancedTaskControlsTests() { // 4. Consult transfer clearAdvancedCapturedLogs(); - await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'consult', process.env.PW_DIAL_NUMBER!); + await consultOrTransfer(testManager.agent1Page, 'dialNumber', 'consult', process.env.PW_DIAL_NUMBER_NAME); await acceptExtensionCall(testManager.dialNumberPage); await testManager.agent1Page.waitForTimeout(3000); await testManager.agent1Page.getByTestId('transfer-consult-btn').click(); @@ -443,5 +468,53 @@ export default function createAdvancedTaskControlsTests() { verifyConsultTransferredLogs(); await endCallTask(testManager.dialNumberPage); }); + + test('Dial Number search filters list to the matching entry (local search)', async () => { + if (testManager.projectName !== 'SET_5') { + test.skip(true, 'Dial Number search validation runs only for SET_5 (user23/user24).'); + } + + const searchTerm = process.env.PW_DIAL_NUMBER_NAME!; + + // Setup: create call and get to engaged state + await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); + await changeUserState(testManager.agent2Page, USER_STATES.MEETING); + await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!); + const incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); + await incomingTaskDiv.waitFor({state: 'visible', timeout: 80000}); + await acceptExtensionCall(testManager.agent1ExtensionPage); + await testManager.agent1Page.waitForTimeout(3000); + await verifyCurrentState(testManager.agent1Page, USER_STATES.ENGAGED); + + // Open consult popover and switch to Dial Number + await testManager.agent1Page.getByTestId('call-control:consult').nth(1).click(); + const popover = testManager.agent1Page.locator('.agent-popover-content'); + await expect(popover).toBeVisible({timeout: 10000}); + await popover.getByRole('button', {name: 'Dial Number'}).click(); + + // Perform search and wait for local filtering to reflect + await popover.locator('#consult-search').fill(searchTerm); + await testManager.agent1Page.waitForTimeout(4000); + + // Read visible list item titles (aria-labels) and validate only the searched item remains + const labels = await popover + .locator('[role="listitem"]') + .evaluateAll((nodes) => nodes.map((n) => n.getAttribute('aria-label'))); + expect(labels).toContain(searchTerm); + expect(labels.filter(Boolean).length).toBe(1); + + // Close the popover to avoid overlay blocking further actions + await testManager.agent1Page.keyboard.press('Escape'); + await testManager.agent1Page + .locator('.md-popover-backdrop') + .waitFor({state: 'hidden', timeout: 3000}) + .catch(() => {}); + + // End call and complete wrapup to clean up for next tests + await endTask(testManager.agent1Page); + await testManager.agent1Page.waitForTimeout(2000); + await submitWrapup(testManager.agent1Page, WRAPUP_REASONS.SALE); + await testManager.agent1Page.waitForTimeout(1000); + }); }); } diff --git a/playwright/tests/tasklist-test.spec.ts b/playwright/tests/tasklist-test.spec.ts index 5b594c09a..5cab6ecca 100644 --- a/playwright/tests/tasklist-test.spec.ts +++ b/playwright/tests/tasklist-test.spec.ts @@ -108,7 +108,16 @@ async function waitForConsoleLogs( const start = Date.now(); while (Date.now() - start < timeoutMs) { - if (logs.some((log) => pattern.test(log))) return; + // Allow partial match in case of trailing debug suffixes + if ( + logs.some( + (log) => + log.includes(`onTaskSelected invoked for task with title : ${title}`) && + log.includes(`, and mediaType : ${mediaType}`) + ) + ) { + return; + } await new Promise((r) => setTimeout(r, intervalMs)); } From 87ea791585612f9e197d10cf53892e1610d7df68 Mon Sep 17 00:00:00 2001 From: adhmenon Date: Wed, 8 Oct 2025 12:00:02 +0530 Subject: [PATCH 08/20] feat(cc-widgets): resolved-comments --- playwright/Utils/advancedTaskControlUtils.ts | 191 ++++++++++-------- playwright/Utils/helperUtils.ts | 22 +- .../tests/advanced-task-controls-test.spec.ts | 6 - playwright/tests/tasklist-test.spec.ts | 11 +- 4 files changed, 124 insertions(+), 106 deletions(-) diff --git a/playwright/Utils/advancedTaskControlUtils.ts b/playwright/Utils/advancedTaskControlUtils.ts index 5f4bc08f6..69bb206ef 100644 --- a/playwright/Utils/advancedTaskControlUtils.ts +++ b/playwright/Utils/advancedTaskControlUtils.ts @@ -117,9 +117,28 @@ export async function consultOrTransfer( action: 'consult' | 'transfer', value: string ): Promise { - // Determine which button to click for consult or transfer + await openConsultOrTransferMenu(page, action); + const popover = await getPopover(page); + + if (type === 'agent') { + await performAgentSelection(page, popover, value); + } else if (type === 'queue') { + await performQueueSelection(page, popover, value); + } else if (type === 'dialNumber') { + await performDialNumberSelection(page, popover, value); + } else if (type === 'entryPoint') { + await performEntryPointSelection(page, popover, value); + } + + await page.waitForTimeout(2000); + if (action === 'consult') { + await expect(page.getByTestId('cancel-consult-btn')).toBeVisible({timeout: FORM_FIELD_TIMEOUT}); + } +} + +// ===== Internal helper functions ===== +async function openConsultOrTransferMenu(page: Page, action: 'consult' | 'transfer'): Promise { if (action === 'consult') { - // Close any backdrops that might intercept clicks await dismissOverlays(page); await page.getByTestId('call-control:consult').nth(1).click({timeout: AWAIT_TIMEOUT}); } else { @@ -128,97 +147,109 @@ export async function consultOrTransfer( .getByLabel('Transfer Call') .click({timeout: AWAIT_TIMEOUT}); } +} - // Popover universal search should be visible (inside popover container) +async function getPopover(page: Page) { const popover = page.locator('.agent-popover-content'); await expect(popover.locator('#consult-search')).toBeVisible({timeout: FORM_FIELD_TIMEOUT}); + return popover; +} - // Navigate to the correct category and perform the action - const clickCategory = async (name: 'Agents' | 'Queues' | 'Dial Number' | 'Entry Point') => { - // New UI uses buttons instead of tabs - const button = popover.getByRole('button', {name}); - await button.click({timeout: AWAIT_TIMEOUT}); - await page.waitForTimeout(200); - }; +async function clickCategory( + page: Page, + popover: ReturnType, + name: 'Agents' | 'Queues' | 'Dial Number' | 'Entry Point' +): Promise { + const button = popover.getByRole('button', {name}); + await button.click({timeout: AWAIT_TIMEOUT}); + await page.waitForTimeout(200); +} - const clickListItemPrimaryButton = async (categoryLabel: string) => { - // Prefer exact aria-label match within the consult/transfer popover to avoid strict-mode collisions - const listItem = popover.locator(`[role="listitem"][aria-label="${value}"]`).first(); - await listItem.waitFor({state: 'visible', timeout: AWAIT_TIMEOUT}); - // Some UIs reveal the action button on hover; hover to ensure visibility - await listItem.hover(); - await listItem.scrollIntoViewIfNeeded(); - await page.waitForTimeout(200); - const primaryButton = listItem.getByRole('button'); - await primaryButton.waitFor({state: 'visible', timeout: AWAIT_TIMEOUT}); - await primaryButton.scrollIntoViewIfNeeded(); - await primaryButton.evaluate((el) => { - if (el.hasAttribute('disabled')) { - throw new Error(`${categoryLabel} button is disabled`); - } - }); - let lastError; - for (let i = 0; i < 3; i++) { - try { - await primaryButton.click({timeout: AWAIT_TIMEOUT, force: true}); - lastError = undefined; - break; - } catch (err) { - lastError = err; - await page.waitForTimeout(300); - } +async function clickListItemPrimaryButton( + page: Page, + popover: ReturnType, + value: string, + categoryLabel: string +): Promise { + const listItem = popover.locator(`[role="listitem"][aria-label="${value}"]`).first(); + await listItem.waitFor({state: 'visible', timeout: AWAIT_TIMEOUT}); + await listItem.hover(); + await listItem.scrollIntoViewIfNeeded(); + await page.waitForTimeout(200); + const primaryButton = listItem.getByRole('button'); + await primaryButton.waitFor({state: 'visible', timeout: AWAIT_TIMEOUT}); + await primaryButton.scrollIntoViewIfNeeded(); + await primaryButton.evaluate((el) => { + if (el.hasAttribute('disabled')) { + throw new Error(`${categoryLabel} button is disabled`); } - if (lastError) { - throw lastError; + }); + let lastError; + for (let i = 0; i < 3; i++) { + try { + await primaryButton.click({timeout: AWAIT_TIMEOUT, force: true}); + lastError = undefined; + break; + } catch (err) { + lastError = err; + await page.waitForTimeout(300); } - await page - .locator('.md-popover-backdrop') - .waitFor({state: 'hidden', timeout: 3000}) - .catch(() => {}); - }; + } + if (lastError) { + throw lastError; + } + await page + .locator('.md-popover-backdrop') + .waitFor({state: 'hidden', timeout: 3000}) + .catch(() => {}); +} - if (type === 'agent') { - await clickCategory('Agents'); - await clickListItemPrimaryButton('Agent'); - } else if (type === 'queue') { - await clickCategory('Queues'); - await clickListItemPrimaryButton('Queue'); - } else if (type === 'dialNumber') { - await clickCategory('Dial Number'); - // Require a valid name to avoid strict-mode ambiguous matches - if (!value || value.trim() === '') { - throw new Error( - 'PW_DIAL_NUMBER_NAME is not set. Please provide the Dial Number list item name (e.g., cypher_pstn).' - ); - } - // List-based selection only (no legacy input fallback) +async function performAgentSelection(page: Page, popover: ReturnType, value: string): Promise { + await clickCategory(page, popover, 'Agents'); + await clickListItemPrimaryButton(page, popover, value, 'Agent'); +} + +async function performQueueSelection(page: Page, popover: ReturnType, value: string): Promise { + await clickCategory(page, popover, 'Queues'); + await clickListItemPrimaryButton(page, popover, value, 'Queue'); +} + +async function performDialNumberSelection( + page: Page, + popover: ReturnType, + value: string +): Promise { + if (!value || value.trim() === '') { + throw new Error( + 'PW_DIAL_NUMBER_NAME is not set. Please provide the Dial Number list item name (e.g., cypher_pstn).' + ); + } + await clickCategory(page, popover, 'Dial Number'); + const search = popover.locator('#consult-search'); + if (await search.isVisible({timeout: 500}).catch(() => false)) { + await search.fill(value, {timeout: AWAIT_TIMEOUT}); + await page.waitForTimeout(300); + } + await popover + .getByRole('listitem', {name: value, exact: true}) + .waitFor({state: 'visible', timeout: FORM_FIELD_TIMEOUT}); + await clickListItemPrimaryButton(page, popover, value, 'Dial Number'); +} + +async function performEntryPointSelection( + page: Page, + popover: ReturnType, + value: string +): Promise { + await clickCategory(page, popover, 'Entry Point'); + if (value) { const search = popover.locator('#consult-search'); - if (value && (await search.isVisible({timeout: 500}).catch(() => false))) { + if (await search.isVisible({timeout: 500}).catch(() => false)) { await search.fill(value, {timeout: AWAIT_TIMEOUT}); await page.waitForTimeout(300); } - const listItem = popover.getByRole('listitem', {name: value, exact: true}); - await listItem.waitFor({state: 'visible', timeout: FORM_FIELD_TIMEOUT}); - await clickListItemPrimaryButton('Dial Number'); - } else if (type === 'entryPoint') { - await clickCategory('Entry Point'); - // Optional: filter via universal search (only if value provided) - if (value) { - const search = popover.locator('#consult-search'); - if (await search.isVisible({timeout: 500}).catch(() => false)) { - await search.fill(value, {timeout: AWAIT_TIMEOUT}); - await page.waitForTimeout(300); - } - } - await clickListItemPrimaryButton('Entry Point'); - } - - // Wait a moment for the action to be processed - await page.waitForTimeout(2000); - if (action === 'consult') { - // Confirm consult UI is present - await expect(page.getByTestId('cancel-consult-btn')).toBeVisible({timeout: FORM_FIELD_TIMEOUT}); } + await clickListItemPrimaryButton(page, popover, value, 'Entry Point'); } /** diff --git a/playwright/Utils/helperUtils.ts b/playwright/Utils/helperUtils.ts index 2fab9161d..047fe3462 100644 --- a/playwright/Utils/helperUtils.ts +++ b/playwright/Utils/helperUtils.ts @@ -511,17 +511,19 @@ export const pageSetup = async ( * Attempts ESC presses and quick background clicks. */ export async function dismissOverlays(page: Page): Promise { + const isVisibleWithin = async (locator: any, timeoutMs: number = 500): Promise => { + try { + await locator.waitFor({state: 'visible', timeout: timeoutMs}); + return true; + } catch { + return false; + } + }; + for (let i = 0; i < 3; i++) { - // If a Momentum popover backdrop is visible, try ESC to close - const backdropVisible = await page - .locator('.md-popover-backdrop') - .isVisible() - .catch(() => false); - const tippyVisible = await page - .locator('[id^="tippy-"]') - .first() - .isVisible() - .catch(() => false); + // If a Momentum popover backdrop is visible, try ESC to close (with bounded timeout) + const backdropVisible = await isVisibleWithin(page.locator('.md-popover-backdrop'), 500); + const tippyVisible = await isVisibleWithin(page.locator('[id^="tippy-"]').first(), 500); if (!backdropVisible && !tippyVisible) return; try { await page.keyboard.press('Escape'); diff --git a/playwright/tests/advanced-task-controls-test.spec.ts b/playwright/tests/advanced-task-controls-test.spec.ts index ef30e4afb..e43d4909d 100644 --- a/playwright/tests/advanced-task-controls-test.spec.ts +++ b/playwright/tests/advanced-task-controls-test.spec.ts @@ -178,12 +178,7 @@ export default function createAdvancedTaskControlsTests() { describe('Consult and Consult Transfer Scenarios', () => { test('Entry Point Consult: visible and functional only for supported users (no blind transfer)', async ({}, testInfo) => { - if (testManager.projectName !== 'SET_5') { - test.skip(true, 'Entrypoint consult is only enabled for SET_5 (user23/user24).'); - } - await changeUserState(testManager.agent1Page, USER_STATES.AVAILABLE); - await changeUserState(testManager.agent2Page, USER_STATES.MEETING); await createCallTask(testManager.callerPage!, process.env[`${testManager.projectName}_ENTRY_POINT`]!); const incomingTaskDiv = testManager.agent1Page.getByTestId('samples:incoming-task-telephony').first(); await incomingTaskDiv.waitFor({state: 'visible', timeout: 80000}); @@ -196,7 +191,6 @@ export default function createAdvancedTaskControlsTests() { await expect(testManager.agent1Page.locator('#consult-search')).toBeVisible(); await testManager.agent1Page.getByRole('button', {name: 'Entry Point'}).click(); - console.log('PW_ENTRYPOINT_NAME:', process.env.PW_ENTRYPOINT_NAME); await consultOrTransfer(testManager.agent1Page, 'entryPoint', 'consult', process.env.PW_ENTRYPOINT_NAME!); await expect(testManager.agent1Page.getByTestId('cancel-consult-btn')).toBeVisible(); await testManager.agent1Page.waitForTimeout(1000); diff --git a/playwright/tests/tasklist-test.spec.ts b/playwright/tests/tasklist-test.spec.ts index 5cab6ecca..5b594c09a 100644 --- a/playwright/tests/tasklist-test.spec.ts +++ b/playwright/tests/tasklist-test.spec.ts @@ -108,16 +108,7 @@ async function waitForConsoleLogs( const start = Date.now(); while (Date.now() - start < timeoutMs) { - // Allow partial match in case of trailing debug suffixes - if ( - logs.some( - (log) => - log.includes(`onTaskSelected invoked for task with title : ${title}`) && - log.includes(`, and mediaType : ${mediaType}`) - ) - ) { - return; - } + if (logs.some((log) => pattern.test(log))) return; await new Promise((r) => setTimeout(r, intervalMs)); } From 7f3a2834c1609dc152779a0741b1be1030792cb2 Mon Sep 17 00:00:00 2001 From: adhmenon Date: Wed, 8 Oct 2025 14:39:55 +0530 Subject: [PATCH 09/20] feat(cc-widgets): moved-hooks --- .../call-control-custom.utils.ts | 6 +- .../consult-transfer-popover-hooks.ts | 199 +++++++++++ .../consult-transfer-popover.tsx | 244 +++---------- .../task/CallControl/call-control.styles.scss | 37 +- .../src/components/task/task.types.ts | 16 +- ...consult-transfer-popover.snapshot.tsx.snap | 330 +++++++----------- .../consult-transfer-popover.tsx | 118 ++++++- .../store/src/storeEventsWrapper.ts | 8 - 8 files changed, 531 insertions(+), 427 deletions(-) create mode 100644 packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover-hooks.ts 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 f497a38b0..2f1f714bc 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 @@ -1,7 +1,7 @@ import {BuddyDetails, ContactServiceQueue, ILogger} from '@webex/cc-store'; import {useState, useCallback} from 'react'; import {MUTE_CALL, UNMUTE_CALL} from '../../constants'; -import {FetchFunction, TransformFunction} from '../../task.types'; +import {FetchPaginatedList, TransformPaginatedData} from '../../task.types'; /** * Interface for button configuration @@ -659,8 +659,8 @@ export const debounce = unknown>( }; export const usePaginatedData = ( - fetchFunction: FetchFunction | undefined, - transformFunction: TransformFunction, + fetchFunction: FetchPaginatedList | undefined, + transformFunction: TransformPaginatedData, categoryName: string, logger? ) => { diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover-hooks.ts b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover-hooks.ts new file mode 100644 index 000000000..779e879ab --- /dev/null +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover-hooks.ts @@ -0,0 +1,199 @@ +import {useCallback, useEffect, useRef, useState} from 'react'; +import {AddressBookEntry, ContactServiceQueue, EntryPointRecord, ILogger} from '@webex/cc-store'; +import {FetchPaginatedList} from '../../task.types'; +import {debounce, usePaginatedData} from './call-control-custom.utils'; + +export type CategoryType = 'Agents' | 'Queues' | 'Dial Number' | 'Entry Point'; + +type UseConsultTransferParams = { + showDialNumberTab: boolean; + showEntryPointTab: boolean; + getAddressBookEntries?: FetchPaginatedList; + getEntryPoints?: FetchPaginatedList; + getQueues?: FetchPaginatedList; + logger?: ILogger; +}; + +export function useConsultTransferPopover({ + showDialNumberTab, + showEntryPointTab, + getAddressBookEntries, + getEntryPoints, + getQueues, + logger, +}: UseConsultTransferParams) { + const [selectedCategory, setSelectedCategory] = useState('Agents'); + const [searchQuery, setSearchQuery] = useState(''); + const loadMoreRef = useRef(null); + + const { + data: dialNumbers, + page: dialNumbersPage, + hasMore: hasMoreDialNumbers, + loading: loadingDialNumbers, + loadData: loadDialNumbers, + reset: resetDialNumbers, + } = usePaginatedData( + getAddressBookEntries, + (entry) => ({ + id: entry.id, + name: entry.name, + number: entry.number, + organizationId: entry.organizationId, + version: entry.version, + createdTime: entry.createdTime, + lastUpdatedTime: entry.lastUpdatedTime, + }), + 'Dial Numbers', + logger + ); + + const { + data: entryPoints, + page: entryPointsPage, + hasMore: hasMoreEntryPoints, + loading: loadingEntryPoints, + loadData: loadEntryPoints, + reset: resetEntryPoints, + } = usePaginatedData( + getEntryPoints, + (entry) => ({id: entry.id, name: entry.name}), + 'Entry Points', + logger + ); + + const { + data: queuesData, + page: queuesPage, + hasMore: hasMoreQueues, + loading: loadingQueues, + loadData: loadQueues, + reset: resetQueues, + } = usePaginatedData( + getQueues, + (entry) => ({id: entry.id, name: entry.name, description: entry.description}), + 'Queues', + logger + ); + + const loadNextPage = useCallback(() => { + if (selectedCategory === 'Dial Number' && hasMoreDialNumbers && !loadingDialNumbers) { + loadDialNumbers(dialNumbersPage + 1, searchQuery); + } else if (selectedCategory === 'Entry Point' && hasMoreEntryPoints && !loadingEntryPoints) { + loadEntryPoints(entryPointsPage + 1, searchQuery); + } else if (selectedCategory === 'Queues' && hasMoreQueues && !loadingQueues) { + loadQueues(queuesPage + 1, searchQuery); + } + }, [ + selectedCategory, + hasMoreDialNumbers, + hasMoreEntryPoints, + hasMoreQueues, + loadingDialNumbers, + loadingEntryPoints, + loadingQueues, + dialNumbersPage, + entryPointsPage, + queuesPage, + searchQuery, + loadDialNumbers, + loadEntryPoints, + loadQueues, + ]); + + const debouncedSearchRef = useRef>(); + if (!debouncedSearchRef.current) { + debouncedSearchRef.current = debounce((query: string, category: CategoryType) => { + if (query.length === 0 || query.length >= 2) { + if (category === 'Dial Number') { + loadDialNumbers(0, query, true); + } else if (category === 'Entry Point') { + loadEntryPoints(0, query, true); + } else if (category === 'Queues') { + loadQueues(0, query, true); + } + } + }, 500); + } + + useEffect(() => { + return () => { + debouncedSearchRef.current = undefined; + }; + }, []); + + const handleSearchChange = useCallback( + (value: string) => { + setSearchQuery(value); + if (selectedCategory !== 'Agents') { + debouncedSearchRef.current?.(value, selectedCategory); + } + }, + [selectedCategory] + ); + + const handleCategoryChange = useCallback( + (category: CategoryType) => { + setSelectedCategory(category); + setSearchQuery(''); + resetDialNumbers(); + resetEntryPoints(); + resetQueues(); + }, + [resetDialNumbers, resetEntryPoints, resetQueues] + ); + + const createCategoryClickHandler = (category: CategoryType) => () => handleCategoryChange(category); + const handleAgentsClick = createCategoryClickHandler('Agents'); + const handleQueuesClick = createCategoryClickHandler('Queues'); + const handleDialNumberClick = createCategoryClickHandler('Dial Number'); + const handleEntryPointClick = createCategoryClickHandler('Entry Point'); + + useEffect(() => { + const loadMoreElement = loadMoreRef.current; + if (!loadMoreElement) return; + const observer = new IntersectionObserver( + (entries) => { + const [entry] = entries; + if (entry.isIntersecting) { + loadNextPage(); + } + }, + {threshold: 1.0} + ); + observer.observe(loadMoreElement); + return () => { + observer.unobserve(loadMoreElement); + }; + }, [loadNextPage]); + + useEffect(() => { + if (selectedCategory === 'Dial Number' && showDialNumberTab && dialNumbers.length === 0) { + loadDialNumbers(0, '', true); + } else if (selectedCategory === 'Entry Point' && showEntryPointTab && entryPoints.length === 0) { + loadEntryPoints(0, '', true); + } else if (selectedCategory === 'Queues' && queuesData.length === 0) { + loadQueues(0, '', true); + } + }, [selectedCategory]); + + return { + selectedCategory, + searchQuery, + loadMoreRef, + dialNumbers, + hasMoreDialNumbers, + loadingDialNumbers, + entryPoints, + hasMoreEntryPoints, + loadingEntryPoints, + queuesData, + hasMoreQueues, + loadingQueues, + handleSearchChange, + handleAgentsClick, + handleQueuesClick, + handleDialNumberClick, + handleEntryPointClick, + }; +} diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx index 70053df90..4c1607a6e 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx @@ -1,17 +1,10 @@ -import React, {useState, useRef, useEffect, useCallback} from 'react'; +import React from 'react'; import {Text, ListNext, TextInput, Button} from '@momentum-ui/react-collaboration'; import ConsultTransferListComponent from './consult-transfer-list-item'; import {ConsultTransferPopoverComponentProps} from '../../task.types'; -import {AddressBookEntry, EntryPointRecord, ContactServiceQueue} from '@webex/cc-store'; import ConsultTransferEmptyState from './consult-transfer-empty-state'; -import { - isAgentsEmpty, - handleAgentSelection, - handleQueueSelection, - debounce, - usePaginatedData, -} from './call-control-custom.utils'; -type CategoryType = 'Agents' | 'Queues' | 'Dial Number' | 'Entry Point'; +import {isAgentsEmpty, handleAgentSelection, handleQueueSelection} from './call-control-custom.utils'; +import {useConsultTransferPopover} from './consult-transfer-popover-hooks'; const ConsultTransferPopoverComponent: React.FC = ({ heading, @@ -29,9 +22,32 @@ const ConsultTransferPopoverComponent: React.FC { const {showDialNumberTab = true, showEntryPointTab = true} = consultTransferOptions || {}; - const [selectedCategory, setSelectedCategory] = useState('Agents'); - const [searchQuery, setSearchQuery] = useState(''); - const loadMoreRef = useRef(null); + const { + selectedCategory, + searchQuery, + loadMoreRef, + dialNumbers, + hasMoreDialNumbers, + loadingDialNumbers, + entryPoints, + hasMoreEntryPoints, + loadingEntryPoints, + queuesData, + hasMoreQueues, + loadingQueues, + handleSearchChange, + handleAgentsClick, + handleQueuesClick, + handleDialNumberClick, + handleEntryPointClick, + } = useConsultTransferPopover({ + showDialNumberTab, + showEntryPointTab, + getAddressBookEntries, + getEntryPoints, + getQueues, + logger, + }); const noAgents = isAgentsEmpty(buddyAgents, logger); @@ -41,7 +57,7 @@ const ConsultTransferPopoverComponent: React.FC ( {items.map((item) => ( -
    e.stopPropagation()} style={{cursor: 'pointer', pointerEvents: 'auto'}}> +
    e.stopPropagation()} className="consult-list-item-wrapper"> ); - const { - data: dialNumbers, - page: dialNumbersPage, - hasMore: hasMoreDialNumbers, - loading: loadingDialNumbers, - loadData: loadDialNumbers, - reset: resetDialNumbers, - } = usePaginatedData( - getAddressBookEntries, - (entry) => ({ - id: entry.id, - name: entry.name, - number: entry.number, - organizationId: entry.organizationId, - version: entry.version, - createdTime: entry.createdTime, - lastUpdatedTime: entry.lastUpdatedTime, - }), - 'Dial Numbers', - logger - ); - - const { - data: entryPoints, - page: entryPointsPage, - hasMore: hasMoreEntryPoints, - loading: loadingEntryPoints, - loadData: loadEntryPoints, - reset: resetEntryPoints, - } = usePaginatedData( - getEntryPoints, - (entry) => ({id: entry.id, name: entry.name}), - 'Entry Points', - logger - ); - - const { - data: queuesData, - page: queuesPage, - hasMore: hasMoreQueues, - loading: loadingQueues, - loadData: loadQueues, - reset: resetQueues, - } = usePaginatedData( - getQueues, - (entry) => ({id: entry.id, name: entry.name, description: entry.description}), - 'Queues', - logger - ); - - const loadNextPage = useCallback(() => { - if (selectedCategory === 'Dial Number' && hasMoreDialNumbers && !loadingDialNumbers) { - loadDialNumbers(dialNumbersPage + 1, searchQuery); - } else if (selectedCategory === 'Entry Point' && hasMoreEntryPoints && !loadingEntryPoints) { - loadEntryPoints(entryPointsPage + 1, searchQuery); - } else if (selectedCategory === 'Queues' && hasMoreQueues && !loadingQueues) { - loadQueues(queuesPage + 1, searchQuery); - } - }, [ - selectedCategory, - hasMoreDialNumbers, - hasMoreEntryPoints, - hasMoreQueues, - loadingDialNumbers, - loadingEntryPoints, - loadingQueues, - dialNumbersPage, - entryPointsPage, - queuesPage, - searchQuery, - loadDialNumbers, - loadEntryPoints, - loadQueues, - ]); - - const debouncedSearchRef = useRef>(); - if (!debouncedSearchRef.current) { - debouncedSearchRef.current = debounce((query: string, category: CategoryType) => { - if (query.length === 0 || query.length >= 2) { - if (category === 'Dial Number') { - loadDialNumbers(0, query, true); - } else if (category === 'Entry Point') { - loadEntryPoints(0, query, true); - } else if (category === 'Queues') { - loadQueues(0, query, true); - } - } - }, 500); - } - - useEffect(() => { - return () => { - debouncedSearchRef.current = undefined; - }; - }, []); - - const handleSearchChange = useCallback( - (value: string) => { - setSearchQuery(value); - if (selectedCategory !== 'Agents') { - debouncedSearchRef.current?.(value, selectedCategory); - } - }, - [selectedCategory] - ); - - const handleCategoryChange = useCallback( - (category: CategoryType) => { - setSelectedCategory(category); - setSearchQuery(''); - resetDialNumbers(); - resetEntryPoints(); - resetQueues(); - }, - [resetDialNumbers, resetEntryPoints, resetQueues] - ); - - const createCategoryClickHandler = (category: CategoryType) => () => handleCategoryChange(category); - const handleAgentsClick = createCategoryClickHandler('Agents'); - const handleQueuesClick = createCategoryClickHandler('Queues'); - const handleDialNumberClick = createCategoryClickHandler('Dial Number'); - const handleEntryPointClick = createCategoryClickHandler('Entry Point'); - - useEffect(() => { - const loadMoreElement = loadMoreRef.current; - if (!loadMoreElement) return; - const observer = new IntersectionObserver( - (entries) => { - const [entry] = entries; - if (entry.isIntersecting) { - loadNextPage(); - } - }, - {threshold: 1.0} - ); - observer.observe(loadMoreElement); - return () => { - observer.unobserve(loadMoreElement); - }; - }, [loadNextPage]); - - useEffect(() => { - if (selectedCategory === 'Dial Number' && showDialNumberTab && dialNumbers.length === 0) { - loadDialNumbers(0, '', true); - } else if (selectedCategory === 'Entry Point' && showEntryPointTab && entryPoints.length === 0) { - loadEntryPoints(0, '', true); - } else if (selectedCategory === 'Queues' && queuesData.length === 0) { - loadQueues(0, '', true); - } - }, [selectedCategory]); - const noQueues = queuesData.length === 0; const noDialNumbers = !showDialNumberTab || dialNumbers.length === 0; const noEntryPoints = !showEntryPointTab || entryPoints.length === 0; @@ -218,11 +83,11 @@ const ConsultTransferPopoverComponent: React.FC - + {heading} -
    +
    handleSearchChange(value)} clearAriaLabel="Clear search" aria-labelledby="consult-search-label" - style={{width: '100%'}} + className="consult-search-input" />
    -
    +
    @@ -248,7 +113,7 @@ const ConsultTransferPopoverComponent: React.FC Queues @@ -257,7 +122,7 @@ const ConsultTransferPopoverComponent: React.FC Dial Number @@ -267,7 +132,7 @@ const ConsultTransferPopoverComponent: React.FC Entry Point @@ -290,16 +155,7 @@ const ConsultTransferPopoverComponent: React.FC handleQueueSelection(item.id, item.name, onQueueSelect, logger) )} {hasMoreQueues && ( -
    +
    {loadingQueues ? ( Loading more queues... @@ -327,16 +183,7 @@ const ConsultTransferPopoverComponent: React.FC +
    {loadingDialNumbers ? ( Loading more dial numbers... @@ -362,16 +209,7 @@ const ConsultTransferPopoverComponent: React.FC +
    {loadingEntryPoints ? ( Loading more entry points... diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss index faca4092a..1cafccb01 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss +++ b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss @@ -211,6 +211,39 @@ background-color: var(--mds-color-theme-background-secondary-hover); } } + + .consult-list-item-wrapper { + cursor: pointer; + pointer-events: auto; + } + + .consult-search-input { + width: 100%; + } + + .consult-category-buttons { + margin: 0.5rem 0; + display: flex; + flex-direction: row; + gap: 0.5rem; + flex-wrap: wrap; + } + + .consult-category-button-standard { + min-width: 5rem; + } + + .consult-category-button-wide { + min-width: 6.25rem; + } + + .consult-load-more { + padding: 0.5rem; + text-align: center; + display: flex; + justify-content: center; + align-items: center; + } } .agent-tablist { @@ -273,7 +306,7 @@ line-height: 1.3; font-weight: 500; font-size: 14px; - color: #323130; + color: var(--mds-color-theme-text-primary-normal); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -283,7 +316,7 @@ margin: 0; line-height: 1.2; font-size: 12px; - color: #605e5c; + color: var(--mds-color-theme-text-secondary-normal); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/packages/contact-center/cc-components/src/components/task/task.types.ts b/packages/contact-center/cc-components/src/components/task/task.types.ts index 64b9ebb32..70b3e3aad 100644 --- a/packages/contact-center/cc-components/src/components/task/task.types.ts +++ b/packages/contact-center/cc-components/src/components/task/task.types.ts @@ -413,11 +413,11 @@ export interface ControlProps { cancelAutoWrapup: () => void; /** Fetch paginated address book entries for dial numbers */ - getAddressBookEntries?: FetchFunction; + getAddressBookEntries?: FetchPaginatedList; /** Fetch paginated entry points */ - getEntryPoints?: FetchFunction; + getEntryPoints?: FetchPaginatedList; /** Fetch paginated queues (filtered by media type in store) */ - getQueuesFetcher?: FetchFunction; + getQueuesFetcher?: FetchPaginatedList; /** * Options to configure consult/transfer popover behavior. @@ -527,9 +527,9 @@ export interface ConsultTransferPopoverComponentProps { heading: string; buttonIcon: string; buddyAgents: BuddyDetails[]; - getAddressBookEntries?: FetchFunction; - getEntryPoints?: FetchFunction; - getQueues?: FetchFunction; + getAddressBookEntries?: FetchPaginatedList; + getEntryPoints?: FetchPaginatedList; + getQueues?: FetchPaginatedList; onAgentSelect?: (agentId: string, agentName: string) => void; onQueueSelect?: (queueId: string, queueName: string) => void; onEntryPointSelect?: (entryPointId: string, entryPointName: string) => void; @@ -691,7 +691,7 @@ export interface TimerUIState { /** * Type for fetch function */ -export type FetchFunction = (params: { +export type FetchPaginatedList = (params: { page: number; pageSize: number; search?: string; @@ -700,7 +700,7 @@ export type FetchFunction = (params: { /** * Type for transform function */ -export type TransformFunction = (item: T, page: number, index: number) => U; +export type TransformPaginatedData = (item: T, page: number, index: number) => U; /** * Options to configure Consult/Transfer popover behavior and visibility. diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap index bcea85e20..0481e450a 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap @@ -6,20 +6,16 @@ exports[`ConsultTransferPopoverComponent Snapshots Interactions should render co > Select an Agent -
    +
    @@ -113,7 +115,9 @@ const ConsultTransferPopoverComponent: React.FC Queues @@ -122,7 +126,9 @@ const ConsultTransferPopoverComponent: React.FC Dial Number @@ -132,7 +138,9 @@ const ConsultTransferPopoverComponent: React.FC Entry Point diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss index 1cafccb01..e5356463c 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss +++ b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss @@ -237,6 +237,19 @@ min-width: 6.25rem; } + .consult-category-button-standard:not(.consult-category-button-active), + .consult-category-button-wide:not(.consult-category-button-active) { + background-color: transparent; + border-color: transparent; + box-shadow: none; + } + + .consult-category-button-standard:not(.consult-category-button-active):hover, + .consult-category-button-wide:not(.consult-category-button-active):hover { + background-color: var(--mds-color-theme-background-secondary-hover); + border-color: transparent; + } + .consult-load-more { padding: 0.5rem; text-align: center; diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap index 0481e450a..6b4be453d 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap @@ -41,7 +41,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Interactions should render co
    @@ -194,11 +203,11 @@ const ConsultTransferPopoverComponent: React.FC {loadingDialNumbers ? ( - Loading more dial numbers... + {LOADING_MORE_DIAL_NUMBERS} ) : ( - Scroll to load more + {SCROLL_TO_LOAD_MORE} )}
    @@ -220,11 +229,11 @@ const ConsultTransferPopoverComponent: React.FC {loadingEntryPoints ? ( - Loading more entry points... + {LOADING_MORE_ENTRY_POINTS} ) : ( - Scroll to load more + {SCROLL_TO_LOAD_MORE} )}
    diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss index e5356463c..f560902d8 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss +++ b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss @@ -291,14 +291,14 @@ } .call-control-list-item { - padding: 12px 16px; - margin: 2px 0; - border-radius: 8px; + padding: 0.75rem 1rem; + margin: 0.125rem 0; + border-radius: 0.5rem; transition: background-color 0.2s ease; } .call-control-list-item-start { - margin-right: 12px; + margin-right: 0.75rem; display: flex; align-items: center; } @@ -310,15 +310,15 @@ justify-content: center; min-width: 0; overflow: hidden; - padding-right: 8px; + padding-right: 0.5rem; } .call-control-list-item-title { margin: 0; - margin-bottom: 2px; + margin-bottom: 0.125rem; line-height: 1.3; font-weight: 500; - font-size: 14px; + font-size: 0.875rem; color: var(--mds-color-theme-text-primary-normal); white-space: nowrap; overflow: hidden; @@ -328,7 +328,7 @@ .call-control-list-item-subtitle { margin: 0; line-height: 1.2; - font-size: 12px; + font-size: 0.75rem; color: var(--mds-color-theme-text-secondary-normal); white-space: nowrap; overflow: hidden; @@ -338,7 +338,7 @@ .call-control-list-item-end { display: flex; align-items: center; - margin-left: 8px; + margin-left: 0.5rem; } .custom-text { @@ -359,7 +359,7 @@ .consult-agent-name { line-height: 0; - margin-top: -8px; + margin-top: -0.5rem; } .consult-buttons-container { @@ -388,7 +388,7 @@ flex-direction: column; align-items: center; justify-content: center; - min-height: 320px; + min-height: 20rem; padding: 2rem 0 1rem 0; text-align: center; } diff --git a/packages/contact-center/cc-components/src/components/task/constants.ts b/packages/contact-center/cc-components/src/components/task/constants.ts index 39620fcaa..ff01108b1 100644 --- a/packages/contact-center/cc-components/src/components/task/constants.ts +++ b/packages/contact-center/cc-components/src/components/task/constants.ts @@ -18,6 +18,14 @@ export const UNMUTE_CALL = 'Unmute'; export const DIAL_NUMBER = 'DialNumber'; export const AGENTS = 'Agents'; export const QUEUES = 'Queues'; +// Generic UI strings +export const SEARCH_PLACEHOLDER = 'Search...'; +export const CLEAR_SEARCH = 'Clear search'; +export const SCROLL_TO_LOAD_MORE = 'Scroll to load more'; +export const LOADING_MORE_QUEUES = 'Loading more queues...'; +export const LOADING_MORE_DIAL_NUMBERS = 'Loading more dial numbers...'; +export const LOADING_MORE_ENTRY_POINTS = 'Loading more entry points...'; +export const NO_DATA_AVAILABLE_CONSULT_TRANSFER = 'No data available for consult transfer.'; // Pagination export const DEFAULT_PAGE_SIZE = 25; // CallControlCAD constants diff --git a/packages/contact-center/cc-components/src/components/task/task.types.ts b/packages/contact-center/cc-components/src/components/task/task.types.ts index a67765056..c19ce4d2c 100644 --- a/packages/contact-center/cc-components/src/components/task/task.types.ts +++ b/packages/contact-center/cc-components/src/components/task/task.types.ts @@ -8,6 +8,7 @@ import { ContactServiceQueue, AddressBookEntry, EntryPointRecord, + FetchPaginatedList, } from '@webex/cc-store'; type Enum> = T[keyof T]; @@ -414,8 +415,10 @@ export interface ControlProps { /** Fetch paginated address book entries for dial numbers */ getAddressBookEntries?: FetchPaginatedList; + /** Fetch paginated entry points */ getEntryPoints?: FetchPaginatedList; + /** Fetch paginated queues (filtered by media type in store) */ getQueuesFetcher?: FetchPaginatedList; @@ -688,24 +691,6 @@ export interface TimerUIState { formattedTime: string; } -/** - * Type for fetch function - */ -export type PaginatedListParams = { - page: number; - pageSize: number; - search?: string; -}; - -export type FetchPaginatedList = ( - params: PaginatedListParams -) => Promise<{data: T[]; meta?: {page?: number; totalPages?: number}}>; - -/** - * Type for transform function - */ -export type TransformPaginatedData = (item: T, page: number, index: number) => U; - /** * Categories displayed in Consult/Transfer popover. */ diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx index 1fccd1771..704c816f7 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx @@ -88,10 +88,14 @@ describe('ConsultTransferPopoverComponent', () => { expect(buttons).toEqual(expect.arrayContaining(['Agents', 'Queues', 'Dial Number', 'Entry Point'])); // Verify Agents tab (active by default) - expect(screen.getByText('Agents')).toBeInTheDocument(); + const agentsButton = screen.getByRole('button', {name: 'Agents'}); + expect(agentsButton).toBeInTheDocument(); + expect(agentsButton).toHaveTextContent('Agents'); // Verify Queues tab (inactive by default) - expect(screen.getByText('Queues')).toBeInTheDocument(); + const queuesButton = screen.getByRole('button', {name: 'Queues'}); + expect(queuesButton).toBeInTheDocument(); + expect(queuesButton).toHaveTextContent('Queues'); // Verify agent list const agentList = screen.container.querySelector('.agent-list'); @@ -180,60 +184,53 @@ describe('ConsultTransferPopoverComponent', () => { expect(screen.getByText('No data available for consult transfer.')).toBeInTheDocument(); }); - it('handles edge cases and conditional rendering', async () => { - // Test empty state when both lists are completely empty + it('shows empty state when both lists are completely empty', async () => { const emptyProps = { ...baseProps, buddyAgents: [], getQueues: async () => ({data: [], meta: {page: 0, totalPages: 0}}), }; - let screen = await render(); + const screen = await render(); expect(screen.getByText('No data available for consult transfer.')).toBeInTheDocument(); - screen.unmount(); + }); - // Test empty agents tab with tabs showing (covers lines 94-96) + it('shows tabs and empty agents message when no agents are available', async () => { const emptyAgentsProps = { ...baseProps, buddyAgents: [], }; - screen = await render(); + const screen = await render(); const buttons = Array.from(screen.container.querySelectorAll('button')).map( (b) => (b as HTMLButtonElement).textContent ); expect(buttons).toEqual(expect.arrayContaining(['Agents', 'Queues', 'Dial Number', 'Entry Point'])); expect(screen.container.querySelector('.consult-empty-state')).toBeInTheDocument(); expect(screen.getByText('No data available for consult transfer.')).toBeInTheDocument(); - screen.unmount(); + }); - // Test empty queues tab when switched to queues (covers lines 98-100) + it('shows no items when queues are empty after switching to queues', async () => { const emptyQueuesProps = { ...baseProps, getQueues: async () => ({data: [], meta: {page: 0, totalPages: 0}}), }; - screen = await render(); - - // Switch to queues tab - fireEvent.click(screen.getByText('Queues')); - - // With empty queues, the list should not render and no items should be present + const screen = await render(); + fireEvent.click(screen.getByRole('button', {name: 'Queues'})); expect(screen.container.querySelector('.agent-list')).toBeNull(); expect(screen.container.querySelectorAll('.call-control-list-item').length).toBe(0); - screen.unmount(); + }); - // Test hidden queue tab when allowConsultToQueue is false + it('disables queue selection when allowConsultToQueue is false', async () => { const propsWithoutQueue = { ...baseProps, allowConsultToQueue: false, }; - screen = await render(); - const queuesButton = Array.from(screen.container.querySelectorAll('button')).find( - (btn) => (btn as HTMLButtonElement).textContent === 'Queues' - ) as HTMLButtonElement | undefined; - expect(queuesButton?.disabled).toBe(true); + const screen = await render(); + const queuesButton = screen.getByRole('button', {name: 'Queues'}) as HTMLButtonElement; + expect(queuesButton.disabled).toBe(true); }); it('covers edge case for empty items in renderList (line 50)', async () => { diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx index c67cad330..45fd7089d 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx @@ -513,15 +513,13 @@ describe('CallControlComponent', () => { /> ); - // Act: open the consult popover const consultButton = screen.getByLabelText('Consult'); fireEvent.click(consultButton); - // Assert: popover content should not include Dial Number tab - const buttons = Array.from(screen.container.querySelectorAll('button')).map( - (b) => (b as HTMLButtonElement).textContent - ); - expect(buttons).not.toEqual(expect.arrayContaining(['Dial Number'])); + await screen.findByRole('button', {name: 'Agents'}); + expect(screen.getByRole('button', {name: 'Queues'})).toBeInTheDocument(); + expect(screen.getByRole('button', {name: 'Entry Point'})).toBeInTheDocument(); + expect(screen.queryByRole('button', {name: 'Dial Number'})).not.toBeInTheDocument(); }); }); }); diff --git a/packages/contact-center/store/src/store.types.ts b/packages/contact-center/store/src/store.types.ts index 43ccb8057..484b413a1 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -226,6 +226,21 @@ type AgentLoginProfile = { }; }; +// Generic pagination params for list-fetching APIs +type PaginatedListParams = { + page: number; + pageSize: number; + search?: string; +}; + +// Generic fetch/transform helpers for paginated APIs +type FetchPaginatedList = ( + params: PaginatedListParams +) => Promise<{data: T[]; meta?: {page?: number; totalPages?: number}}>; + +// Generic transform function for paginated APIs +type TransformPaginatedData = (item: T, page: number, index: number) => U; + // Utility consts const DIALNUMBER: string = 'AGENT_DN'; const EXTENSION: string = 'EXTENSION'; @@ -252,9 +267,6 @@ const ERROR_TRIGGERING_IDLE_CODES = { RONA: 'RONA', }; -// Module identifiers for logger context -const STORE_EVENTS_WRAPPER_MODULE = 'storeEventsWrapper.ts'; - export type { IContactCenter, ITask, @@ -282,6 +294,9 @@ export type { ContactServiceQueuesResponse, ContactServiceQueueSearchParams, IWebex, + PaginatedListParams, + FetchPaginatedList, + TransformPaginatedData, }; export { @@ -298,5 +313,4 @@ export { AGENT_STATE_AVAILABLE, LoginOptions, ERROR_TRIGGERING_IDLE_CODES, - STORE_EVENTS_WRAPPER_MODULE, }; diff --git a/packages/contact-center/task/src/helper.ts b/packages/contact-center/task/src/helper.ts index 4a58319c0..130fd01a0 100644 --- a/packages/contact-center/task/src/helper.ts +++ b/packages/contact-center/task/src/helper.ts @@ -1,14 +1,13 @@ import {useEffect, useCallback, useState, useRef, useMemo} from 'react'; import {ITask} from '@webex/contact-center'; -import { - useCallControlProps, - UseTaskListProps, - UseTaskProps, - Participant, - useOutdialCallProps, +import {useCallControlProps, UseTaskListProps, UseTaskProps, Participant, useOutdialCallProps} from './task.types'; +import store, { + TASK_EVENTS, + BuddyDetails, + DestinationType, + ContactServiceQueue, PaginatedListParams, -} from './task.types'; -import store, {TASK_EVENTS, BuddyDetails, DestinationType, ContactServiceQueue} from '@webex/cc-store'; +} from '@webex/cc-store'; import {findHoldTimestamp, getControlsVisibility} from './Utils/task-util'; const ENGAGED_LABEL = 'ENGAGED'; diff --git a/packages/contact-center/task/src/task.types.ts b/packages/contact-center/task/src/task.types.ts index 9a976d24d..04b79adf7 100644 --- a/packages/contact-center/task/src/task.types.ts +++ b/packages/contact-center/task/src/task.types.ts @@ -1,11 +1,5 @@ import {TaskProps, ControlProps, OutdialCallProps} from '@webex/cc-components'; -export type PaginatedListParams = { - page: number; - pageSize: number; - search?: string; -}; - export type UseTaskProps = Pick & Partial>; From 03165025e6539b5a53581c6c695cab093c972204 Mon Sep 17 00:00:00 2001 From: adhmenon Date: Wed, 15 Oct 2025 13:41:59 +0530 Subject: [PATCH 16/20] feat(cc-widgets): removed-unknowns --- packages/contact-center/task/src/helper.ts | 12 ++++-------- packages/contact-center/task/tests/helper.ts | 13 ++++++++----- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/contact-center/task/src/helper.ts b/packages/contact-center/task/src/helper.ts index 130fd01a0..22b26d571 100644 --- a/packages/contact-center/task/src/helper.ts +++ b/packages/contact-center/task/src/helper.ts @@ -416,12 +416,7 @@ export const useCallControl = (props: useCallControlProps) => { const loadQueues = useCallback(async () => { try { - const response = (await store.getQueues()) as unknown as - | ContactServiceQueue[] - | { - data: ContactServiceQueue[]; - }; - const data = Array.isArray(response) ? response : (response as {data: ContactServiceQueue[]}).data; + const {data} = await store.getQueues(); setQueues(data); } catch (error) { logger?.error(`CC-Widgets: Task: Error loading queues - ${error.message || error}`, { @@ -465,7 +460,8 @@ export const useCallControl = (props: useCallControlProps) => { const getQueuesFetcher = useCallback( async ({page, pageSize, search}: PaginatedListParams) => { try { - return await store.getQueues('TELEPHONY', {page, pageSize, search}); + const mediaType = currentTask?.data?.interaction?.mediaType; + return await store.getQueues(mediaType, {page, pageSize, search}); } catch (error) { logger?.error(`CC-Widgets: Task: Error fetching queues (paginated) - ${error.message || error}`, { module: 'useCallControl', @@ -474,7 +470,7 @@ export const useCallControl = (props: useCallControlProps) => { return {data: [], meta: {page: 0, totalPages: 0}}; } }, - [logger] + [logger, currentTask] ); const holdCallback = () => { diff --git a/packages/contact-center/task/tests/helper.ts b/packages/contact-center/task/tests/helper.ts index e4323893b..57a1a478d 100644 --- a/packages/contact-center/task/tests/helper.ts +++ b/packages/contact-center/task/tests/helper.ts @@ -2168,7 +2168,11 @@ describe('useCallControl', () => { }); it('should load queues successfully', async () => { - const getQueuesSpy = jest.spyOn(store, 'getQueues').mockResolvedValue(mockQueueDetails); + const getQueuesResponse: Awaited> = { + data: mockQueueDetails, + meta: {page: 0, pageSize: mockQueueDetails.length, total: mockQueueDetails.length, totalPages: 1}, + }; + const getQueuesSpy = jest.spyOn(store, 'getQueues').mockResolvedValue(getQueuesResponse); const {result} = renderHook(() => useCallControl({ @@ -2228,12 +2232,11 @@ describe('useCallControl', () => { }); it('should get queues via getQueuesFetcher (paginated)', async () => { - const mockResponse = { - data: [{id: 'q1', name: 'Queue 1', channelType: 'TELEPHONY'}], + const mockResponse: Awaited> = { + data: [mockQueueDetails[0]], meta: {page: 0, pageSize: 25, total: 1, totalPages: 1}, }; - // Use a broad cast to satisfy the spy return type regardless of meta shape - jest.spyOn(store, 'getQueues').mockResolvedValue(mockResponse as unknown as ReturnType); + jest.spyOn(store, 'getQueues').mockResolvedValue(mockResponse); const {result} = renderHook(() => useCallControl({ From a4ee310d1e6b4bc18aaba0b6a51ed620c1d3c6f2 Mon Sep 17 00:00:00 2001 From: adhmenon Date: Wed, 15 Oct 2025 13:51:47 +0530 Subject: [PATCH 17/20] feat(cc-widgets): fixed-digital-list --- .../task/CallControl/call-control.tsx | 10 +++++- .../task/CallControl/call-control.tsx | 32 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) 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 babcfa2fa..ae4214fe8 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 @@ -220,7 +220,15 @@ function CallControlComponent(props: CallControlComponentProps) { } onDialNumberSelect={(dialNumber) => handleTargetSelect(dialNumber, dialNumber, 'dialNumber')} allowConsultToQueue={allowConsultToQueue} - consultTransferOptions={consultTransferOptions} + consultTransferOptions={ + isTelephony + ? consultTransferOptions + : { + ...consultTransferOptions, + showDialNumberTab: false, + showEntryPointTab: false, + } + } logger={logger} /> ) : null} diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx index 45fd7089d..db1c9a2ae 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx @@ -521,5 +521,37 @@ describe('CallControlComponent', () => { expect(screen.getByRole('button', {name: 'Entry Point'})).toBeInTheDocument(); expect(screen.queryByRole('button', {name: 'Dial Number'})).not.toBeInTheDocument(); }); + + it('hides Dial Number and Entry Point tabs for non-telephony media', async () => { + jest.spyOn(callControlUtils, 'filterButtonsForConsultation').mockReturnValue([ + { + id: 'consult', + icon: 'consult', + tooltip: 'Consult', + className: 'call-control-button', + disabled: false, + menuType: 'Consult', + isVisible: true, + dataTestId: 'consult-button', + }, + ]); + + isTelephonyMediaTypeSpy.mockReturnValue(false); + + const screen = await render( + + ); + + const consultButton = screen.getByLabelText('Consult'); + fireEvent.click(consultButton); + + await screen.findByRole('button', {name: 'Agents'}); + expect(screen.getByRole('button', {name: 'Queues'})).toBeInTheDocument(); + expect(screen.queryByRole('button', {name: 'Dial Number'})).not.toBeInTheDocument(); + expect(screen.queryByRole('button', {name: 'Entry Point'})).not.toBeInTheDocument(); + }); }); }); From f925eaca37557f8d7f2862b77c07f585ecc198d4 Mon Sep 17 00:00:00 2001 From: adhmenon Date: Wed, 15 Oct 2025 14:47:02 +0530 Subject: [PATCH 18/20] feat(cc-widgets): added-test-fixtures --- .../call-control-custom.util.tsx | 175 ++++++++---------- .../store/tests/storeEventsWrapper.ts | 38 ++-- .../test-fixtures/src/fixtures.ts | 55 +++++- 3 files changed, 155 insertions(+), 113 deletions(-) diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/call-control-custom.util.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/call-control-custom.util.tsx index 1dbac8cd0..d7e457f9c 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/call-control-custom.util.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/call-control-custom.util.tsx @@ -1,5 +1,6 @@ import '@testing-library/jest-dom'; -import {BuddyDetails, ContactServiceQueue} from '@webex/cc-store'; +import {BuddyDetails} from '@webex/cc-store'; +import {mockAgents, mockQueueDetails} from '@webex/test-fixtures'; import { createConsultButtons, getVisibleButtons, @@ -41,75 +42,6 @@ const loggerMock = { trace: jest.fn(), }; -// Build a fully typed ContactServiceQueue with explicit required inputs -const buildQueue = (id: string, name: string, description: string = 'Queue'): ContactServiceQueue => ({ - organizationId: 'org-1', - id, - version: 1, - name, - description, - queueType: 'INBOUND', - checkAgentAvailability: true, - channelType: 'TELEPHONY', - serviceLevelThreshold: 20, - maxActiveContacts: 25, - maxTimeInQueue: 600, - defaultMusicInQueueMediaFileId: 'media-1', - active: true, - monitoringPermitted: true, - parkingPermitted: true, - recordingPermitted: true, - recordingAllCallsPermitted: true, - pauseRecordingPermitted: true, - controlFlowScriptUrl: 'https://example.com/control-flow', - ivrRequeueUrl: 'https://example.com/requeue', - routingType: 'LONGEST_AVAILABLE_AGENT', - queueRoutingType: 'TEAM_BASED', - callDistributionGroups: [], -}); - -const mockBuddyAgents: BuddyDetails[] = [ - { - agentId: 'agent1', - agentName: 'John Doe', - state: 'Available', - teamId: 'team1', - dn: 'dn1', - siteId: 'site1', - }, - { - agentId: 'agent2', - agentName: 'Jane Smith', - state: 'Available', - teamId: 'team1', - dn: 'dn2', - siteId: 'site1', - }, - { - agentId: 'agent3', - agentName: '', - state: 'Available', - teamId: 'team1', - dn: 'dn3', - siteId: 'site1', - }, - { - agentId: '', - agentName: 'Invalid Agent', - state: 'Available', - teamId: 'team1', - dn: 'dn4', - siteId: 'site1', - }, -]; - -const mockQueues: ContactServiceQueue[] = [ - buildQueue('queue1', 'Support Queue', 'Support Queue Description'), - buildQueue('queue2', 'Sales Queue', 'Sales Queue Description'), - buildQueue('queue3', '', 'Empty Name Queue'), - buildQueue('', 'Invalid Queue', 'Invalid Queue Description'), -]; - describe('Call Control Custom Utils', () => { beforeEach(() => { jest.clearAllMocks(); @@ -417,15 +349,39 @@ describe('Call Control Custom Utils', () => { describe('shouldShowTabs', () => { it('should return true when agents exist', () => { - expect(shouldShowTabs(mockBuddyAgents, [])).toBe(true); + expect( + shouldShowTabs( + mockAgents.map((a) => ({ + agentId: a.agentId, + agentName: a.agentName, + state: a.state, + teamId: a.teamId, + dn: a.dn, + siteId: a.siteId, + })), + [] + ) + ).toBe(true); }); it('should return true when queues exist', () => { - expect(shouldShowTabs([], mockQueues)).toBe(true); + expect(shouldShowTabs([], mockQueueDetails)).toBe(true); }); it('should return true when both exist', () => { - expect(shouldShowTabs(mockBuddyAgents, mockQueues)).toBe(true); + expect( + shouldShowTabs( + mockAgents.map((a) => ({ + agentId: a.agentId, + agentName: a.agentName, + state: a.state, + teamId: a.teamId, + dn: a.dn, + siteId: a.siteId, + })), + mockQueueDetails + ) + ).toBe(true); }); it('should return false when both are empty', () => { @@ -439,7 +395,18 @@ describe('Call Control Custom Utils', () => { describe('isAgentsEmpty', () => { it('should return false when agents exist', () => { - expect(isAgentsEmpty(mockBuddyAgents)).toBe(false); + expect( + isAgentsEmpty( + mockAgents.map((a) => ({ + agentId: a.agentId, + agentName: a.agentName, + state: a.state, + teamId: a.teamId, + dn: a.dn, + siteId: a.siteId, + })) + ) + ).toBe(false); }); it('should return true when agents array is empty', () => { @@ -457,7 +424,7 @@ describe('Call Control Custom Utils', () => { describe('isQueuesEmpty', () => { it('should return false when queues exist', () => { - expect(isQueuesEmpty(mockQueues)).toBe(false); + expect(isQueuesEmpty(mockQueueDetails)).toBe(false); }); it('should return true when queues array is empty', () => { @@ -560,11 +527,20 @@ describe('Call Control Custom Utils', () => { describe('createAgentListData', () => { it('should transform buddy agents to list data', () => { - const result = createAgentListData(mockBuddyAgents); + const result = createAgentListData( + mockAgents.map((a) => ({ + agentId: a.agentId, + agentName: a.agentName, + state: a.state, + teamId: a.teamId, + dn: a.dn, + siteId: a.siteId, + })) + ); - expect(result).toHaveLength(4); - expect(result[0]).toEqual({id: 'agent1', name: 'John Doe'}); - expect(result[1]).toEqual({id: 'agent2', name: 'Jane Smith'}); + expect(result).toHaveLength(2); + expect(result[0]).toEqual({id: mockAgents[0].agentId, name: mockAgents[0].agentName}); + expect(result[1]).toEqual({id: mockAgents[1].agentId, name: mockAgents[1].agentName}); }); it('should handle empty array', () => { @@ -575,11 +551,11 @@ describe('Call Control Custom Utils', () => { describe('createQueueListData', () => { it('should transform queues to list data', () => { - const result = createQueueListData(mockQueues); + const result = createQueueListData(mockQueueDetails); - expect(result).toHaveLength(4); - expect(result[0]).toEqual({id: 'queue1', name: 'Support Queue'}); - expect(result[1]).toEqual({id: 'queue2', name: 'Sales Queue'}); + expect(result).toHaveLength(2); + expect(result[0]).toEqual({id: mockQueueDetails[0].id, name: mockQueueDetails[0].name}); + expect(result[1]).toEqual({id: mockQueueDetails[1].id, name: mockQueueDetails[1].name}); }); it('should handle empty array', () => { @@ -799,19 +775,19 @@ describe('Call Control Custom Utils', () => { describe('isQueueAvailable', () => { it('should return true for valid queue', () => { - expect(isQueueAvailable(buildQueue('queue1', 'Support Queue'))).toBe(true); + expect(isQueueAvailable({...mockQueueDetails[0], id: 'queue1', name: 'Support Queue'})).toBe(true); }); it('should return false for missing id', () => { - expect(isQueueAvailable(buildQueue('', 'Support Queue'))).toBeFalsy(); + expect(isQueueAvailable({...mockQueueDetails[0], id: '', name: 'Support Queue'})).toBeFalsy(); }); it('should return false for missing name', () => { - expect(isQueueAvailable(buildQueue('queue1', ''))).toBeFalsy(); + expect(isQueueAvailable({...mockQueueDetails[0], id: 'queue1', name: ''})).toBeFalsy(); }); it('should return false for whitespace-only name', () => { - expect(isQueueAvailable(buildQueue('queue1', ' '))).toBeFalsy(); + expect(isQueueAvailable({...mockQueueDetails[0], id: 'queue1', name: ' '})).toBeFalsy(); }); it('should return false for null queue', () => { @@ -825,10 +801,23 @@ describe('Call Control Custom Utils', () => { describe('filterAvailableAgents', () => { it('should filter out invalid agents', () => { - const result = filterAvailableAgents(mockBuddyAgents); + const result = filterAvailableAgents( + mockAgents.map((a) => ({ + agentId: a.agentId, + agentName: a.agentName, + state: a.state, + teamId: a.teamId, + dn: a.dn, + siteId: a.siteId, + })) + ); expect(result).toHaveLength(2); - expect(result[0]).toEqual(expect.objectContaining({agentId: 'agent1', agentName: 'John Doe'})); - expect(result[1]).toEqual(expect.objectContaining({agentId: 'agent2', agentName: 'Jane Smith'})); + expect(result[0]).toEqual( + expect.objectContaining({agentId: mockAgents[0].agentId, agentName: mockAgents[0].agentName}) + ); + expect(result[1]).toEqual( + expect.objectContaining({agentId: mockAgents[1].agentId, agentName: mockAgents[1].agentName}) + ); }); it('should return empty array for null input', () => { @@ -842,10 +831,10 @@ describe('Call Control Custom Utils', () => { describe('filterAvailableQueues', () => { it('should filter out invalid queues', () => { - const result = filterAvailableQueues(mockQueues); + const result = filterAvailableQueues(mockQueueDetails); expect(result).toHaveLength(2); - expect(result[0]).toEqual(expect.objectContaining({id: 'queue1', name: 'Support Queue'})); - expect(result[1]).toEqual(expect.objectContaining({id: 'queue2', name: 'Sales Queue'})); + expect(result[0]).toEqual(expect.objectContaining({id: mockQueueDetails[0].id, name: mockQueueDetails[0].name})); + expect(result[1]).toEqual(expect.objectContaining({id: mockQueueDetails[1].id, name: mockQueueDetails[1].name})); }); it('should return empty array for null input', () => { diff --git a/packages/contact-center/store/tests/storeEventsWrapper.ts b/packages/contact-center/store/tests/storeEventsWrapper.ts index 524d558b9..a5c328a44 100644 --- a/packages/contact-center/store/tests/storeEventsWrapper.ts +++ b/packages/contact-center/store/tests/storeEventsWrapper.ts @@ -24,8 +24,14 @@ console.log = jest.fn(); import {CC_EVENTS, TASK_EVENTS} from '../src/store.types'; import storeWrapper from '../src/storeEventsWrapper'; -import {ITask, AddressBook} from '@webex/contact-center'; -import {mockCC, mockTask as mockTaskFixture} from '@webex/test-fixtures'; +import {ITask} from '@webex/contact-center'; +import { + mockCC, + mockTask as mockTaskFixture, + mockEntryPointsResponse, + mockAddressBookEntriesResponse, + mockQueueDetails, +} from '@webex/test-fixtures'; jest.mock('../src/store', () => ({ getInstance: jest.fn().mockReturnValue({ @@ -34,6 +40,9 @@ jest.mock('../src/store', () => ({ cc: { on: jest.fn(), off: jest.fn(), + addressBook: { + getEntries: jest.fn(), + }, taskManager: { getAllTasks: jest.fn().mockReturnValue({ interaction1: { @@ -821,16 +830,16 @@ describe('storeEventsWrapper', () => { it('should return contact service queues list when SDK returns paginated response', async () => { const queueList = [ - {id: 'queue1', name: 'Queue 1', channelType: 'TELEPHONY'}, - {id: 'queue2', name: 'Queue 2', channelType: 'CHAT'}, + {...mockQueueDetails[0], channelType: 'TELEPHONY'}, + {...mockQueueDetails[1], channelType: 'CHAT'}, ]; storeWrapper['store'].cc.getQueues = jest .fn() - .mockResolvedValue({data: queueList, meta: {page: 1, pageSize: 50, total: 2}}); + .mockResolvedValue({data: queueList, meta: {page: 1, pageSize: 50, total: 2, totalPages: 1}}); const result = await storeWrapper.getQueues('telephony'); - expect(result.data).toEqual([{id: 'queue1', name: 'Queue 1', channelType: 'TELEPHONY'}]); + expect(result.data).toEqual([{...mockQueueDetails[0], channelType: 'TELEPHONY'}]); expect(storeWrapper['store'].cc.getQueues).toHaveBeenCalled(); }); @@ -849,12 +858,11 @@ describe('storeEventsWrapper', () => { }); it('should fetch entry points successfully', async () => { - const mockResponse = {data: [{id: 'ep1', name: 'Entry 1'}], meta: {page: 0, totalPages: 1}}; - storeWrapper['store'].cc.getEntryPoints = jest.fn().mockResolvedValue(mockResponse); + storeWrapper['store'].cc.getEntryPoints = jest.fn().mockResolvedValue(mockEntryPointsResponse); const result = await storeWrapper.getEntryPoints({page: 0, pageSize: 25}); expect(storeWrapper['store'].cc.getEntryPoints).toHaveBeenCalledWith({page: 0, pageSize: 25}); - expect(result).toEqual(mockResponse); + expect(result).toEqual(mockEntryPointsResponse); }); it('should handle error while fetching entry points', async () => { @@ -863,21 +871,15 @@ describe('storeEventsWrapper', () => { }); it('should fetch address book entries successfully', async () => { - const mockResponse = {data: [{id: 'ab1', name: 'Alice'}], meta: {page: 0, totalPages: 1}}; - // Fix: Use type assertion to 'unknown' first to avoid TS error about missing properties - storeWrapper['store'].cc.addressBook = { - getEntries: jest.fn().mockResolvedValue(mockResponse), - } as unknown as AddressBook; + jest.spyOn(storeWrapper['store'].cc.addressBook, 'getEntries').mockResolvedValue(mockAddressBookEntriesResponse); const result = await storeWrapper.getAddressBookEntries({page: 0, pageSize: 25}); expect(storeWrapper['store'].cc.addressBook.getEntries).toHaveBeenCalledWith({page: 0, pageSize: 25}); - expect(result).toEqual(mockResponse); + expect(result).toEqual(mockAddressBookEntriesResponse); }); it('should handle error while fetching address book entries', async () => { - storeWrapper['store'].cc.addressBook = { - getEntries: jest.fn().mockRejectedValue(new Error('ab error')), - } as unknown as AddressBook; + jest.spyOn(storeWrapper['store'].cc.addressBook, 'getEntries').mockRejectedValue(new Error('ab error')); await expect(storeWrapper.getAddressBookEntries({page: 0, pageSize: 25})).rejects.toThrow('ab error'); }); }); diff --git a/packages/contact-center/test-fixtures/src/fixtures.ts b/packages/contact-center/test-fixtures/src/fixtures.ts index a94af052e..7b21edb73 100644 --- a/packages/contact-center/test-fixtures/src/fixtures.ts +++ b/packages/contact-center/test-fixtures/src/fixtures.ts @@ -1,4 +1,12 @@ -import {ITask, Profile, TaskData, TaskResponse, AddressBook} from '@webex/contact-center'; +import { + ITask, + Profile, + TaskData, + TaskResponse, + AddressBook, + EntryPointListResponse, + AddressBookEntriesResponse, +} from '@webex/contact-center'; import {IContactCenter} from '@webex/cc-store'; const mockProfile: Profile = { @@ -91,6 +99,40 @@ const mockCC: IContactCenter = { setAgentState: jest.fn().mockResolvedValue({}), }; +const mockEntryPointsResponse: EntryPointListResponse = { + data: [ + { + id: 'ep1', + name: 'Entry 1', + type: 'Voice', + isActive: true, + orgId: 'org1', + }, + ], + meta: {page: 0, pageSize: 25, totalPages: 1}, +}; + +const mockAddressBookEntriesResponse: AddressBookEntriesResponse = { + data: [ + { + id: 'ab1', + name: 'Alice', + number: '123', + organizationId: 'org1', + version: 1, + createdTime: 1704067200000, + lastUpdatedTime: 1704067200000, + }, + ], + meta: {page: 0, pageSize: 25, totalPages: 1}, +}; + +// Factory to create a typed AddressBook mock without spreading unknown in tests +const makeMockAddressBook = (getEntriesMock?: jest.Mock): AddressBook => + ({ + getEntries: getEntriesMock || jest.fn().mockResolvedValue(mockAddressBookEntriesResponse), + }) as unknown as AddressBook; + const mockTask: ITask = { data: { interaction: { @@ -384,4 +426,13 @@ const mockAgents = [ }, ]; -export {mockProfile, mockCC, mockTask, mockQueueDetails, mockAgents}; +export { + mockProfile, + mockCC, + mockTask, + mockQueueDetails, + mockAgents, + mockEntryPointsResponse, + mockAddressBookEntriesResponse, + makeMockAddressBook, +}; From aee0b462871d0bcc265e6ee508204eff6ecd5c64 Mon Sep 17 00:00:00 2001 From: adhmenon Date: Wed, 15 Oct 2025 15:59:55 +0530 Subject: [PATCH 19/20] feat(cc-widgets): removed-unknowns --- .../test-fixtures/src/fixtures.ts | 119 ++++++++++++------ 1 file changed, 80 insertions(+), 39 deletions(-) diff --git a/packages/contact-center/test-fixtures/src/fixtures.ts b/packages/contact-center/test-fixtures/src/fixtures.ts index 7b21edb73..5fe27340e 100644 --- a/packages/contact-center/test-fixtures/src/fixtures.ts +++ b/packages/contact-center/test-fixtures/src/fixtures.ts @@ -1,4 +1,4 @@ -import { +import type { ITask, Profile, TaskData, @@ -6,6 +6,7 @@ import { AddressBook, EntryPointListResponse, AddressBookEntriesResponse, + ContactServiceQueuesResponse, } from '@webex/contact-center'; import {IContactCenter} from '@webex/cc-store'; @@ -72,33 +73,6 @@ const mockProfile: Profile = { lastIdleCodeChangeTimestamp: 123456789, }; -const mockCC: IContactCenter = { - on: jest.fn(), - off: jest.fn(), - updateAgentProfile: jest.fn(), - stationLogin: jest.fn(), - deregister: jest.fn(), - stationLogout: jest.fn(), - LoggerProxy: { - log: jest.fn(), - error: jest.fn(), - warn: jest.fn(), - info: jest.fn(), - trace: jest.fn(), - }, - register: jest.fn(), - taskManager: { - getAllTasks: jest.fn().mockReturnValue({}), - }, - getBuddyAgents: jest.fn().mockResolvedValue([]), - getQueues: jest.fn().mockResolvedValue({data: [], meta: {page: 0, pageSize: 25, total: 0, totalPages: 1}}), - getEntryPoints: jest.fn().mockResolvedValue({data: [], meta: {page: 0, pageSize: 25, total: 0, totalPages: 1}}), - addressBook: { - getEntries: jest.fn().mockResolvedValue({data: [], meta: {page: 0, pageSize: 25, total: 0, totalPages: 1}}), - } as unknown as AddressBook, - setAgentState: jest.fn().mockResolvedValue({}), -}; - const mockEntryPointsResponse: EntryPointListResponse = { data: [ { @@ -127,11 +101,12 @@ const mockAddressBookEntriesResponse: AddressBookEntriesResponse = { meta: {page: 0, pageSize: 25, totalPages: 1}, }; -// Factory to create a typed AddressBook mock without spreading unknown in tests -const makeMockAddressBook = (getEntriesMock?: jest.Mock): AddressBook => +const makeMockAddressBook = (getEntriesMock?: AddressBook['getEntries']): AddressBook => ({ getEntries: getEntriesMock || jest.fn().mockResolvedValue(mockAddressBookEntriesResponse), - }) as unknown as AddressBook; + }) as {} as AddressBook; + +const mockAddressBook = makeMockAddressBook(); const mockTask: ITask = { data: { @@ -185,9 +160,9 @@ const mockQueueDetails = [ id: 'q1', name: 'Queue1', description: 'Test Queue 1', - queueType: 'inbound', + queueType: 'INBOUND' as const, checkAgentAvailability: true, - channelType: 'telephony', + channelType: 'TELEPHONY' as const, skillProfileId: 'skill1', siteId: 'site1', version: 1, @@ -265,8 +240,8 @@ const mockQueueDetails = [ queueStatisticsEnabled: true, controlFlowScriptUrl: 'https://example.com/script1.js', ivrRequeueUrl: 'https://example.com/requeue1', - routingType: 'skill_based', - queueRoutingType: 'priority', + routingType: 'SKILLS_BASED' as const, + queueRoutingType: 'SKILL_BASED' as const, callParkingUrl: 'https://example.com/parking1', overflowQueueId: 'overflow1', queueContactLimit: 100, @@ -293,9 +268,9 @@ const mockQueueDetails = [ id: 'q2', name: 'Queue2', description: 'Test Queue 2', - queueType: 'inbound', + queueType: 'INBOUND' as const, checkAgentAvailability: true, - channelType: 'telephony', + channelType: 'TELEPHONY' as const, skillProfileId: 'skill2', siteId: 'site1', version: 1, @@ -373,8 +348,8 @@ const mockQueueDetails = [ queueStatisticsEnabled: true, controlFlowScriptUrl: 'https://example.com/script2.js', ivrRequeueUrl: 'https://example.com/requeue2', - routingType: 'skill_based', - queueRoutingType: 'priority', + routingType: 'SKILLS_BASED' as const, + queueRoutingType: 'SKILL_BASED' as const, callParkingUrl: 'https://example.com/parking2', overflowQueueId: 'overflow2', queueContactLimit: 100, @@ -426,6 +401,72 @@ const mockAgents = [ }, ]; +const mockQueuesResponse: ContactServiceQueuesResponse = { + data: mockQueueDetails.map((q) => ({ + id: q.id, + version: q.version, + name: q.name, + description: q.description, + queueType: q.queueType, + checkAgentAvailability: q.checkAgentAvailability, + channelType: q.channelType, + serviceLevelThreshold: q.serviceLevelThreshold, + maxActiveContacts: q.maxActiveContacts, + maxTimeInQueue: q.maxTimeInQueue, + defaultMusicInQueueMediaFileId: q.defaultMusicInQueueMediaFileId, + timezone: q.timezone, + active: q.active, + outdialCampaignEnabled: q.outdialCampaignEnabled, + monitoringPermitted: q.monitoringPermitted, + parkingPermitted: q.parkingPermitted, + recordingPermitted: q.recordingPermitted, + recordingAllCallsPermitted: q.recordingAllCallsPermitted, + pauseRecordingPermitted: q.pauseRecordingPermitted, + recordingPauseDuration: q.recordingPauseDuration, + controlFlowScriptUrl: q.controlFlowScriptUrl, + ivrRequeueUrl: q.ivrRequeueUrl, + overflowNumber: q.overflowQueueId, + routingType: q.routingType, + queueRoutingType: q.queueRoutingType, + queueSkillRequirements: q.queueSkillRequirements?.map((s) => ({ + skillId: s.id, + skillName: s.name, + condition: 'EQUALS', + skillValue: '1', + })), + agents: q.agents?.map((a) => ({id: a.id})), + callDistributionGroups: q.callDistributionGroups, + createdTime: typeof q.createdTime === 'number' ? q.createdTime : Date.parse(String(q.createdTime)), + lastUpdatedTime: typeof q.lastUpdatedTime === 'number' ? q.lastUpdatedTime : Date.parse(String(q.lastUpdatedTime)), + })), + meta: {page: 0, pageSize: 25, totalPages: 1}, +}; + +const mockCC: IContactCenter = { + on: jest.fn(), + off: jest.fn(), + updateAgentProfile: jest.fn(), + stationLogin: jest.fn(), + deregister: jest.fn(), + stationLogout: jest.fn(), + LoggerProxy: { + log: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + trace: jest.fn(), + }, + register: jest.fn(), + taskManager: { + getAllTasks: jest.fn().mockReturnValue({}), + }, + getBuddyAgents: jest.fn().mockResolvedValue(mockAgents), + getQueues: jest.fn().mockResolvedValue(mockQueuesResponse), + getEntryPoints: jest.fn().mockResolvedValue(mockEntryPointsResponse), + addressBook: mockAddressBook, + setAgentState: jest.fn().mockResolvedValue({}), +}; + export { mockProfile, mockCC, From 84936b80cf42b38be699398f56fcc7b4255f7e4f Mon Sep 17 00:00:00 2001 From: adhmenon Date: Wed, 15 Oct 2025 16:01:00 +0530 Subject: [PATCH 20/20] feat(cc-widgets): removed-dead-code --- packages/contact-center/test-fixtures/src/fixtures.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/contact-center/test-fixtures/src/fixtures.ts b/packages/contact-center/test-fixtures/src/fixtures.ts index 5fe27340e..2b786e9f7 100644 --- a/packages/contact-center/test-fixtures/src/fixtures.ts +++ b/packages/contact-center/test-fixtures/src/fixtures.ts @@ -104,6 +104,8 @@ const mockAddressBookEntriesResponse: AddressBookEntriesResponse = { const makeMockAddressBook = (getEntriesMock?: AddressBook['getEntries']): AddressBook => ({ getEntries: getEntriesMock || jest.fn().mockResolvedValue(mockAddressBookEntriesResponse), + // Need to add this only for AddressBook type + // eslint-disable-next-line @typescript-eslint/no-empty-object-type }) as {} as AddressBook; const mockAddressBook = makeMockAddressBook();