diff --git a/docs/samples/contact-center/app.js b/docs/samples/contact-center/app.js
index 1ee0d9551b1..05c04cceaa3 100644
--- a/docs/samples/contact-center/app.js
+++ b/docs/samples/contact-center/app.js
@@ -1,15 +1,3 @@
-/* eslint-disable no-underscore-dangle */
-/* eslint-env browser */
-
-/* global Webex */
-
-/* eslint-disable require-jsdoc */
-/* eslint-disable no-unused-vars */
-/* eslint-disable no-console */
-/* eslint-disable no-global-assign */
-/* eslint-disable no-multi-assign */
-/* eslint-disable max-len */
-
// Globals
let webex;
let sdk;
@@ -21,6 +9,7 @@ let agentId;
let taskControl;
let task;
let taskId;
+let wrapupCodes = []; // Add this to store wrapup codes
const authTypeElm = document.querySelector('#auth-type');
const credentialsFormElm = document.querySelector('#credentials');
@@ -42,6 +31,12 @@ const incomingDetailsElm = document.querySelector('#incoming-call');
const answerElm = document.querySelector('#answer');
const declineElm = document.querySelector('#decline');
const callControlListener = document.querySelector('#callcontrolsection');
+const holdResumeElm = document.querySelector('#hold-resume');
+const pauseResumeRecordingElm = document.querySelector('#pause-resume-recording');
+const endElm = document.querySelector('#end');
+const wrapupElm = document.querySelector('#wrapup');
+const wrapupCodesDropdownElm = document.querySelector('#wrapupCodesDropdown');
+const autoResumeCheckboxElm = document.querySelector('#auto-resume-checkbox'); // Add this
// Store and Grab `access-token` from sessionStorage
if (sessionStorage.getItem('date') > new Date().getTime()) {
@@ -84,14 +79,32 @@ const taskEvents = new CustomEvent('task:incoming', {
},
});
-// TODO: Activate the call control buttons once the call is accepted and refctor this
+function updateButtonsPostEndCall() {
+ holdResumeElm.disabled = true;
+ endElm.disabled = true;
+ pauseResumeRecordingElm.disabled = true;
+ wrapupElm.disabled = false;
+ wrapupCodesDropdownElm.disabled = false;
+}
+
function registerTaskListeners(task) {
task.on('task:assigned', (task) => {
- console.log('Call has been accepted for task: ', task.data.interactionId);
- })
+ console.info('Call has been accepted for task: ', task.data.interactionId);
+ holdResumeElm.disabled = false;
+ holdResumeElm.innerText = 'Hold';
+ pauseResumeRecordingElm.disabled = false;
+ pauseResumeRecordingElm.innerText = 'Pause Recording';
+ endElm.disabled = false;
+ });
task.on('task:media', (track) => {
document.getElementById('remote-audio').srcObject = new MediaStream([track]);
- })
+ });
+ task.on('task:end', (task) => {
+ if (!endElm.disabled) {
+ console.info('Call ended successfully by the external user');
+ updateButtonsPostEndCall();
+ }
+ });
}
function generateWebexConfig({credentials}) {
@@ -146,6 +159,8 @@ function register() {
teamsDropdown.innerHTML = ''; // Clear previously selected option on teamsDropdown
const listTeams = agentProfile.teams;
agentId = agentProfile.agentId;
+ wrapupCodes = agentProfile.wrapupCodes;
+ populateWrapupCodesDropdown();
listTeams.forEach((team) => {
const option = document.createElement('option');
option.value = team.id;
@@ -166,6 +181,7 @@ function register() {
});
if (agentProfile.isAgentLoggedIn) {
+ loginAgentElm.disabled = true;
logoutAgentElm.classList.remove('hidden');
}
@@ -189,7 +205,17 @@ function register() {
taskEvents.detail.task = task;
incomingCallListener.dispatchEvent(taskEvents);
- })
+ })
+}
+
+function populateWrapupCodesDropdown() {
+ wrapupCodesDropdownElm.innerHTML = ''; // Clear previous options
+ wrapupCodes.forEach((code) => {
+ const option = document.createElement('option');
+ option.text = code.name;
+ option.value = code.id;
+ wrapupCodesDropdownElm.add(option);
+ });
}
async function handleAgentLogin(e) {
@@ -366,3 +392,79 @@ function expandAll() {
});
}
+function holdResumeCall() {
+ if (holdResumeElm.innerText === 'Hold') {
+ holdResumeElm.disabled = true;
+ task.hold().then(() => {
+ console.info('Call held successfully');
+ holdResumeElm.innerText = 'Resume';
+ holdResumeElm.disabled = false;
+ }).catch((error) => {
+ console.error('Failed to hold the call', error);
+ holdResumeElm.disabled = false;
+ });
+ } else {
+ holdResumeElm.disabled = true;
+ task.resume().then(() => {
+ console.info('Call resumed successfully');
+ holdResumeElm.innerText = 'Hold';
+ holdResumeElm.disabled = false;
+ }).catch((error) => {
+ console.error('Failed to resume the call', error);
+ holdResumeElm.disabled = false;
+ });
+ }
+}
+
+function togglePauseResumeRecording() {
+ const autoResumed = autoResumeCheckboxElm.checked;
+ if (pauseResumeRecordingElm.innerText === 'Pause Recording') {
+ pauseResumeRecordingElm.disabled = true;
+ task.pauseRecording().then(() => {
+ console.info('Recording paused successfully');
+ pauseResumeRecordingElm.innerText = 'Resume Recording';
+ pauseResumeRecordingElm.disabled = false;
+ autoResumeCheckboxElm.disabled = false;
+ }).catch((error) => {
+ console.error('Failed to pause recording', error);
+ pauseResumeRecordingElm.disabled = false;
+ });
+ } else {
+ pauseResumeRecordingElm.disabled = true;
+ task.resumeRecording({ autoResumed: autoResumed }).then(() => {
+ console.info('Recording resumed successfully');
+ pauseResumeRecordingElm.innerText = 'Pause Recording';
+ pauseResumeRecordingElm.disabled = false;
+ autoResumeCheckboxElm.disabled = true;
+ }).catch((error) => {
+ console.error('Failed to resume recording', error);
+ pauseResumeRecordingElm.disabled = false;
+ });
+ }
+}
+
+function endCall() {
+ endElm.disabled = true;
+ task.end().then(() => {
+ console.log('Call ended successfully by agent');
+ updateButtonsPostEndCall();
+ }).catch((error) => {
+ console.error('Failed to end the call', error);
+ endElm.disabled = false;
+ });
+}
+
+function wrapupCall() {
+ wrapupElm.disabled = true;
+ const wrapupReason = wrapupCodesDropdownElm.options[wrapupCodesDropdownElm.selectedIndex].text;
+ const auxCodeId = wrapupCodesDropdownElm.options[wrapupCodesDropdownElm.selectedIndex].value;
+ task.wrapup({wrapUpReason: wrapupReason, auxCodeId: auxCodeId}).then(() => {
+ console.info('Call wrapped up successfully');
+ holdResumeElm.disabled = true;
+ endElm.disabled = true;
+ wrapupCodesDropdownElm.disabled = true;
+ }).catch((error) => {
+ console.error('Failed to wrap up the call', error);
+ wrapupElm.disabled = false;
+ });
+}
\ No newline at end of file
diff --git a/docs/samples/contact-center/index.html b/docs/samples/contact-center/index.html
index 0d99a068d8f..e106e4fb140 100644
--- a/docs/samples/contact-center/index.html
+++ b/docs/samples/contact-center/index.html
@@ -150,7 +150,24 @@
diff --git a/packages/@webex/plugin-cc/src/cc.ts b/packages/@webex/plugin-cc/src/cc.ts
index 9ba322eeedf..11baf533edd 100644
--- a/packages/@webex/plugin-cc/src/cc.ts
+++ b/packages/@webex/plugin-cc/src/cc.ts
@@ -347,9 +347,9 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
} catch (error) {
const {reason, error: detailedError} = getErrorDetails(error, 'silentReLogin', CC_FILE);
if (reason === 'AGENT_NOT_FOUND') {
- LoggerProxy.info('Agent not found during re-login, handling silently', {
+ LoggerProxy.log('Agent not found during re-login, handling silently', {
module: CC_FILE,
- method: this.silentRelogin.name,
+ method: 'silentRelogin',
});
return;
diff --git a/packages/@webex/plugin-cc/src/constants.ts b/packages/@webex/plugin-cc/src/constants.ts
index 744b898e6cb..8f63fd52671 100644
--- a/packages/@webex/plugin-cc/src/constants.ts
+++ b/packages/@webex/plugin-cc/src/constants.ts
@@ -10,3 +10,4 @@ export const CC_FILE = 'cc';
export const CONNECTION_SERVICE_FILE = 'connection-service';
export const WEB_SOCKET_MANAGER_FILE = 'WebSocketManager';
export const AQM_REQS_FILE = 'aqm-reqs';
+export const TASK_MANAGER_FILE = 'TaskManager';
diff --git a/packages/@webex/plugin-cc/src/services/core/Utils.ts b/packages/@webex/plugin-cc/src/services/core/Utils.ts
index 907f543f4f2..163be2b9ea8 100644
--- a/packages/@webex/plugin-cc/src/services/core/Utils.ts
+++ b/packages/@webex/plugin-cc/src/services/core/Utils.ts
@@ -13,10 +13,12 @@ const getCommonErrorDetails = (errObj: WebexRequestPayload) => {
export const getErrorDetails = (error: any, methodName: string, moduleName: string) => {
const failure = error.details as Failure;
const reason = failure?.data?.reason ?? `Error while performing ${methodName}`;
- LoggerProxy.error(`${methodName} failed with trackingId: ${failure?.trackingId}`, {
- module: moduleName,
- method: methodName,
- });
+ if (!(reason === 'AGENT_NOT_FOUND' && methodName === 'silentReLogin')) {
+ LoggerProxy.error(`${methodName} failed with trackingId: ${failure?.trackingId}`, {
+ module: moduleName,
+ method: methodName,
+ });
+ }
return {
error: new Error(reason ?? `Error while performing ${methodName}`),
diff --git a/packages/@webex/plugin-cc/src/services/core/aqm-reqs.ts b/packages/@webex/plugin-cc/src/services/core/aqm-reqs.ts
index 76bf780fb52..0b175c42a3e 100644
--- a/packages/@webex/plugin-cc/src/services/core/aqm-reqs.ts
+++ b/packages/@webex/plugin-cc/src/services/core/aqm-reqs.ts
@@ -98,12 +98,12 @@ export default class AqmReqs {
if ('errId' in notifFail) {
LoggerProxy.log(`Routing request failed: ${msg}`, {
module: AQM_REQS_FILE,
- method: this.createPromise.name,
+ method: 'createPromise',
});
const eerr = new Err.Details(notifFail.errId, msg as any);
LoggerProxy.log(`Routing request failed: ${eerr}`, {
module: AQM_REQS_FILE,
- method: this.createPromise.name,
+ method: 'createPromise',
});
reject(eerr);
} else {
diff --git a/packages/@webex/plugin-cc/src/services/core/websocket/connection-service.ts b/packages/@webex/plugin-cc/src/services/core/websocket/connection-service.ts
index f222d81071e..f9e42edba80 100644
--- a/packages/@webex/plugin-cc/src/services/core/websocket/connection-service.ts
+++ b/packages/@webex/plugin-cc/src/services/core/websocket/connection-service.ts
@@ -53,9 +53,9 @@ export class ConnectionService extends EventEmitter {
isKeepAlive: this.isKeepAlive,
};
this.webSocketManager.handleConnectionLost(event);
- LoggerProxy.log(`Dispatching connection lost event`, {
+ LoggerProxy.log(`Dispatching connection event`, {
module: CONNECTION_SERVICE_FILE,
- method: this.dispatchConnectionEvent.name,
+ method: 'dispatchConnectionEvent',
});
this.emit('connectionLost', event);
}
@@ -119,7 +119,7 @@ export class ConnectionService extends EventEmitter {
private handleSocketClose = async (): Promise => {
LoggerProxy.info(`event=socketConnectionRetry | Trying to reconnect to websocket`, {
module: CONNECTION_SERVICE_FILE,
- method: this.handleSocketClose.name,
+ method: 'handleSocketClose',
});
const onlineStatus = navigator.onLine;
if (onlineStatus) {
diff --git a/packages/@webex/plugin-cc/src/services/task/TaskManager.ts b/packages/@webex/plugin-cc/src/services/task/TaskManager.ts
index 085b56c9bd1..deb89ea9290 100644
--- a/packages/@webex/plugin-cc/src/services/task/TaskManager.ts
+++ b/packages/@webex/plugin-cc/src/services/task/TaskManager.ts
@@ -4,8 +4,10 @@ import {WebSocketManager} from '../core/websocket/WebSocketManager';
import routingContact from './contact';
import WebCallingService from '../WebCallingService';
import {ITask, TASK_EVENTS, TaskId} from './types';
+import {TASK_MANAGER_FILE} from '../../constants';
import {CC_EVENTS} from '../config/types';
import {LoginOption} from '../../types';
+import LoggerProxy from '../../logger-proxy';
import Task from '.';
export default class TaskManager extends EventEmitter {
@@ -68,7 +70,7 @@ export default class TaskManager extends EventEmitter {
break;
case CC_EVENTS.AGENT_CONTACT_ASSIGNED:
this.currentTask = this.currentTask.updateTaskData(payload.data);
- this.emit(TASK_EVENTS.TASK_ASSIGNED, this.currentTask);
+ this.currentTask.emit(TASK_EVENTS.TASK_ASSIGNED, this.currentTask);
break;
case CC_EVENTS.CONTACT_ENDED:
this.emit(TASK_EVENTS.TASK_UNASSIGNED, this.currentTask);
@@ -77,6 +79,13 @@ export default class TaskManager extends EventEmitter {
this.webCallingService.unregisterCallListeners();
}
break;
+ case CC_EVENTS.AGENT_WRAPUP:
+ this.currentTask = this.currentTask.updateTaskData(payload.data);
+ this.currentTask.emit(TASK_EVENTS.TASK_END, this.currentTask);
+ break;
+ case CC_EVENTS.AGENT_WRAPPEDUP:
+ this.removeCurrentTaskFromCollection();
+ break;
default:
break;
}
@@ -84,6 +93,16 @@ export default class TaskManager extends EventEmitter {
});
}
+ private removeCurrentTaskFromCollection() {
+ if (this.currentTask && this.currentTask.data && this.currentTask.data.interactionId) {
+ delete this.taskCollection[this.currentTask.data.interactionId];
+ LoggerProxy.info(`Task removed from collection: ${this.currentTask.data.interactionId}`, {
+ module: TASK_MANAGER_FILE,
+ method: 'removeCurrentTaskFromCollection',
+ });
+ }
+ }
+
/**
* @param taskId - Unique identifier for each task
*/
diff --git a/packages/@webex/plugin-cc/src/services/task/index.ts b/packages/@webex/plugin-cc/src/services/task/index.ts
index bad5fb65dee..e7312fcd029 100644
--- a/packages/@webex/plugin-cc/src/services/task/index.ts
+++ b/packages/@webex/plugin-cc/src/services/task/index.ts
@@ -5,7 +5,15 @@ import {getErrorDetails} from '../core/Utils';
import {LoginOption} from '../../types';
import {CC_FILE} from '../../constants';
import routingContact from './contact';
-import {ITask, TaskResponse, TaskData, TaskId, TASK_EVENTS} from './types';
+import {
+ ITask,
+ TaskResponse,
+ TaskData,
+ TaskId,
+ TASK_EVENTS,
+ WrapupPayLoad,
+ ResumeRecordingPayload,
+} from './types';
import WebCallingService from '../WebCallingService';
export default class Task extends EventEmitter implements ITask {
@@ -101,5 +109,136 @@ export default class Task extends EventEmitter implements ITask {
}
}
- // TODO: Hold/resume, recording pause/resume, consult and transfer public methods to be implemented here
+ /**
+ * This is used to hold the task.
+ * @returns Promise
+ * @throws Error
+ * @example
+ * ```typescript
+ * task.hold().then(()=>{}).catch(()=>{})
+ * ```
+ * */
+ public async hold(): Promise {
+ try {
+ return this.contact.hold({
+ interactionId: this.data.interactionId,
+ data: {mediaResourceId: this.data.mediaResourceId},
+ });
+ } catch (error) {
+ const {error: detailedError} = getErrorDetails(error, 'hold', CC_FILE);
+ throw detailedError;
+ }
+ }
+
+ /**
+ * This is used to resume the task.
+ * @returns Promise
+ * @throws Error
+ * @example
+ * ```typescript
+ * task.resume().then(()=>{}).catch(()=>{})
+ * ```
+ */
+ public async resume(): Promise {
+ try {
+ return this.contact.unHold({
+ interactionId: this.data.interactionId,
+ data: {mediaResourceId: this.data.mediaResourceId},
+ });
+ } catch (error) {
+ const {error: detailedError} = getErrorDetails(error, 'resume', CC_FILE);
+ throw detailedError;
+ }
+ }
+
+ /**
+ * This is used to end the task.
+ * @returns Promise
+ * @throws Error
+ * @example
+ * ```typescript
+ * task.end().then(()=>{}).catch(()=>{})
+ * ```
+ */
+ public async end(): Promise {
+ try {
+ return this.contact.end({interactionId: this.data.interactionId});
+ } catch (error) {
+ const {error: detailedError} = getErrorDetails(error, 'end', CC_FILE);
+ throw detailedError;
+ }
+ }
+
+ /**
+ * This is used to wrap up the task.
+ * @param wrapupPayload - WrapupPayLoad
+ * @returns Promise
+ * @throws Error
+ * @example
+ * ```typescript
+ * task.wrapup(wrapupPayload).then(()=>{}).catch(()=>{})
+ * ```
+ */
+ public async wrapup(wrapupPayload: WrapupPayLoad): Promise {
+ try {
+ if (!this.data) {
+ throw new Error('No task data available');
+ }
+ if (!wrapupPayload.auxCodeId || wrapupPayload.auxCodeId.length === 0) {
+ throw new Error('AuxCodeId is required');
+ }
+ if (!wrapupPayload.wrapUpReason || wrapupPayload.wrapUpReason.length === 0) {
+ throw new Error('WrapUpReason is required');
+ }
+
+ return this.contact.wrapup({interactionId: this.data.interactionId, data: wrapupPayload});
+ } catch (error) {
+ const {error: detailedError} = getErrorDetails(error, 'wrapup', CC_FILE);
+ throw detailedError;
+ }
+ }
+
+ /**
+ * This is used to pause the call recording
+ * @returns Promise
+ * @throws Error
+ * @example
+ * ```typescript
+ * task.pauseRecording().then(()=>{}).catch(()=>{});
+ * ```
+ */
+ public async pauseRecording(): Promise {
+ try {
+ return this.contact.pauseRecording({interactionId: this.data.interactionId});
+ } catch (error) {
+ const {error: detailedError} = getErrorDetails(error, 'pauseRecording', CC_FILE);
+ throw detailedError;
+ }
+ }
+
+ /**
+ * This is used to pause the call recording
+ * @param resumeRecordingPayload
+ * @returns Promise
+ * @throws Error
+ * @example
+ * ```typescript
+ * task.resumeRecording(resumeRecordingPayload).then(()=>{}).catch(()=>{});
+ * ```
+ */
+ public async resumeRecording(
+ resumeRecordingPayload: ResumeRecordingPayload
+ ): Promise {
+ try {
+ return this.contact.resumeRecording({
+ interactionId: this.data.interactionId,
+ data: resumeRecordingPayload,
+ });
+ } catch (error) {
+ const {error: detailedError} = getErrorDetails(error, 'resumeRecording', CC_FILE);
+ throw detailedError;
+ }
+ }
+
+ // TODO: consult and transfer public methods to be implemented here
}
diff --git a/packages/@webex/plugin-cc/src/services/task/types.ts b/packages/@webex/plugin-cc/src/services/task/types.ts
index a731403c204..ea38cc6e2c1 100644
--- a/packages/@webex/plugin-cc/src/services/task/types.ts
+++ b/packages/@webex/plugin-cc/src/services/task/types.ts
@@ -1,4 +1,5 @@
import {CallId} from '@webex/calling/dist/types/common/types';
+import EventEmitter from 'events';
import {Msg} from '../core/GlobalTypes';
export type TaskId = string;
@@ -327,7 +328,7 @@ export type TaskResponse = AgentContact | Error | void;
/**
* Represents an interface for managing task related operations.
*/
-export interface ITask {
+export interface ITask extends EventEmitter {
/**
* Event data received in the CC events
*/
@@ -362,5 +363,61 @@ export interface ITask {
* ```
*/
decline(taskId: TaskId): Promise;
- // TODO: Add the remianing public methods
+ /**
+ * This is used to hold the task.
+ * @param taskId
+ * @returns Promise
+ * @example
+ * ```
+ * task.hold();
+ * ```
+ */
+ hold(): Promise;
+ /**
+ * This is used to resume the task.
+ * @returns Promise
+ * @example
+ * ```
+ * task.resume();
+ * ```
+ */
+ resume(): Promise;
+ /**
+ * This is used to end the task.
+ * @returns Promise
+ * @example
+ * ```
+ * task.end();
+ * ```
+ */
+ end(): Promise;
+ /**
+ * This is used to wrap up the task.
+ * @param wrapupPayload
+ * @returns Promise
+ * @example
+ * ```
+ * task.wrapup(data);
+ * ```
+ */
+ wrapup(wrapupPayload: WrapupPayLoad): Promise;
+ /**
+ * This is used to pause the call recording.
+ * @returns Promise
+ * @example
+ * ```
+ * task.wrapup();
+ * ```
+ */
+ pauseRecording(): Promise;
+ /**
+ * This is used to resume the call recording.
+ * @param resumeRecordingPayload
+ * @returns Promise
+ * @example
+ * ```
+ * task.resumeRecording();
+ * ```
+ */
+ resumeRecording(resumeRecordingPayload: ResumeRecordingPayload): Promise;
}
diff --git a/packages/@webex/plugin-cc/test/unit/spec/cc.ts b/packages/@webex/plugin-cc/test/unit/spec/cc.ts
index 9632f1c8aa4..cd944df02ad 100644
--- a/packages/@webex/plugin-cc/test/unit/spec/cc.ts
+++ b/packages/@webex/plugin-cc/test/unit/spec/cc.ts
@@ -766,7 +766,7 @@ describe('webex.cc', () => {
jest.spyOn(webex.cc.services.agent, 'reload').mockRejectedValue(error);
await webex.cc['silentRelogin']();
- expect(LoggerProxy.info).toHaveBeenCalledWith(
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
'Agent not found during re-login, handling silently',
{module: CC_FILE, method: 'silentRelogin'}
);
diff --git a/packages/@webex/plugin-cc/test/unit/spec/services/task/TaskManager.ts b/packages/@webex/plugin-cc/test/unit/spec/services/task/TaskManager.ts
index 375f0d51343..894e4cd965c 100644
--- a/packages/@webex/plugin-cc/test/unit/spec/services/task/TaskManager.ts
+++ b/packages/@webex/plugin-cc/test/unit/spec/services/task/TaskManager.ts
@@ -13,7 +13,8 @@ import config from '../../../../../src/config';
jest.mock('./../../../../../src/services/task', () => {
return jest.fn().mockImplementation(() => {
return {
- updateTaskData: jest.fn(),
+ updateTaskData: jest.fn().mockReturnThis(),
+ emit: jest.fn()
};
});
});
@@ -147,11 +148,11 @@ describe('TaskManager', () => {
},
};
- const taskAssignedSpy = jest.spyOn(taskManager, 'emit');
+ const currentTaskAssignedSpy = jest.spyOn(taskManager.currentTask, 'emit');
webSocketManagerMock.emit('message', JSON.stringify(assignedPayload));
- expect(taskAssignedSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, taskManager.currentTask);
+ expect(currentTaskAssignedSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, taskManager.currentTask);
});
it('should handle WebSocket message for AGENT_CONTACT_RESERVED and emit task:incoming for extension case', () => {
@@ -292,4 +293,74 @@ describe('TaskManager', () => {
taskManager.unregisterIncomingCallEvent();
expect(webCallingServiceOffSpy).toHaveBeenCalledWith(LINE_EVENTS.INCOMING_CALL, webCallingServiceOffSpy.mock.calls[0][1]);
});
+
+ it('should emit TASK_END event on AGENT_WRAPUP event', () => {
+ // Need to setup the task with current task
+ const firstPayload = {
+ data: {
+ type: CC_EVENTS.AGENT_CONTACT_RESERVED,
+ agentId: "723a8ffb-a26e-496d-b14a-ff44fb83b64f",
+ eventTime: 1733211616959,
+ eventType: "RoutingMessage",
+ interaction: {},
+ interactionId: "0ae913a4-c857-4705-8d49-76dd3dde75e4",
+ orgId: "6ecef209-9a34-4ed1-a07a-7ddd1dbe925a",
+ trackingId: "575c0ec2-618c-42af-a61c-53aeb0a221ee",
+ mediaResourceId: '0ae913a4-c857-4705-8d49-76dd3dde75e4',
+ destAgentId: 'ebeb893b-ba67-4f36-8418-95c7492b28c2',
+ owner: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
+ queueMgr: 'aqm',
+ },
+ };
+ webSocketManagerMock.emit('message', JSON.stringify(firstPayload));
+
+ const wrapupPayload = {
+ data: {
+ type: CC_EVENTS.AGENT_WRAPUP,
+ agentId: "723a8ffb-a26e-496d-b14a-ff44fb83b64f",
+ eventTime: 1733211616959,
+ eventType: "RoutingMessage",
+ interaction: {},
+ interactionId: taskId,
+ orgId: "6ecef209-9a34-4ed1-a07a-7ddd1dbe925a",
+ trackingId: "575c0ec2-618c-42af-a61c-53aeb0a221ee",
+ mediaResourceId: '0ae913a4-c857-4705-8d49-76dd3dde75e4',
+ destAgentId: 'ebeb893b-ba67-4f36-8418-95c7492b28c2',
+ owner: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
+ queueMgr: 'aqm',
+ },
+ };
+
+ const taskEmitSpy = jest.spyOn(taskManager.currentTask, 'emit');
+
+ webSocketManagerMock.emit('message', JSON.stringify(wrapupPayload));
+
+ expect(taskManager.currentTask.updateTaskData).toHaveBeenCalledWith(wrapupPayload.data);
+ expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, taskManager.currentTask);
+ });
+
+ it('should remove currentTask from taskCollection on AGENT_WRAPPEDUP event', () => {
+ const payload = {
+ data: {
+ type: CC_EVENTS.AGENT_WRAPPEDUP,
+ agentId: "723a8ffb-a26e-496d-b14a-ff44fb83b64f",
+ eventTime: 1733211616959,
+ eventType: "RoutingMessage",
+ interaction: {},
+ interactionId: taskId,
+ orgId: "6ecef209-9a34-4ed1-a07a-7ddd1dbe925a",
+ trackingId: "575c0ec2-618c-42af-a61c-53aeb0a221ee",
+ mediaResourceId: '0ae913a4-c857-4705-8d49-76dd3dde75e4',
+ destAgentId: 'ebeb893b-ba67-4f36-8418-95c7492b28c2',
+ owner: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
+ queueMgr: 'aqm',
+ },
+ };
+
+ taskManager.taskCollection[taskId] = taskManager.currentTask;
+
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
+
+ expect(taskManager.getTask(taskId)).toBeUndefined();
+ });
});
\ No newline at end of file
diff --git a/packages/@webex/plugin-cc/test/unit/spec/services/task/index.ts b/packages/@webex/plugin-cc/test/unit/spec/services/task/index.ts
index c54b37cea0f..fb0f6602716 100644
--- a/packages/@webex/plugin-cc/test/unit/spec/services/task/index.ts
+++ b/packages/@webex/plugin-cc/test/unit/spec/services/task/index.ts
@@ -7,7 +7,7 @@ import * as Utils from '../../../../../src/services/core/Utils';
import { CC_EVENTS } from '../../../../../src/services/config/types';
import config from '../../../../../src/config';
import WebCallingService from '../../../../../src/services/WebCallingService';
-import { TASK_EVENTS } from '../../../../../src/services/task/types';
+import { TASK_EVENTS, TaskResponse, AgentContact } from '../../../../../src/services/task/types';
jest.mock('@webex/calling');
@@ -39,6 +39,12 @@ describe('Task', () => {
contactMock = {
accept: jest.fn().mockResolvedValue({}),
+ hold: jest.fn().mockResolvedValue({}),
+ unHold: jest.fn().mockResolvedValue({}),
+ end: jest.fn().mockResolvedValue({}),
+ wrapup: jest.fn().mockResolvedValue({}),
+ pauseRecording: jest.fn().mockResolvedValue({}),
+ resumeRecording: jest.fn().mockResolvedValue({})
};
webCallingService = new WebCallingService(
@@ -182,4 +188,179 @@ describe('Task', () => {
await expect(task.decline()).rejects.toThrow(new Error(error.details.data.reason));
expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'decline', CC_FILE);
});
+
+ it('should hold the task and return the expected response', async () => {
+ const expectedResponse: TaskResponse = { data: { interactionId: taskId } } as AgentContact;
+ contactMock.hold.mockResolvedValue(expectedResponse);
+
+ const response = await task.hold();
+
+ expect(contactMock.hold).toHaveBeenCalledWith({ interactionId: taskId, data: { mediaResourceId: taskDataMock.mediaResourceId } });
+ expect(response).toEqual(expectedResponse);
+ });
+
+ it('should handle errors in hold method', async () => {
+ const error = {
+ details: {
+ trackingId: '1234',
+ data: {
+ reason: 'Hold Failed',
+ },
+ },
+ };
+ contactMock.hold.mockImplementation(() => { throw error; });
+
+ await expect(task.hold()).rejects.toThrow(error.details.data.reason);
+ expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'hold', CC_FILE);
+ });
+
+ it('should resume the task and return the expected response', async () => {
+ const expectedResponse: TaskResponse = { data: { interactionId: taskId } } as AgentContact;
+ contactMock.unHold.mockResolvedValue(expectedResponse);
+
+ const response = await task.resume();
+
+ expect(contactMock.unHold).toHaveBeenCalledWith({ interactionId: taskId, data: { mediaResourceId: taskDataMock.mediaResourceId } });
+ expect(response).toEqual(expectedResponse);
+ });
+
+ it('should handle errors in resume method', async () => {
+ const error = {
+ details: {
+ trackingId: '1234',
+ data: {
+ reason: 'Resume Failed',
+ },
+ },
+ };
+ contactMock.unHold.mockImplementation(() => { throw error; });
+
+ await expect(task.resume()).rejects.toThrow(error.details.data.reason);
+ expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'resume', CC_FILE);
+ });
+
+ it('should end the task and return the expected response', async () => {
+ const expectedResponse: TaskResponse = { data: { interactionId: taskId } } as AgentContact;
+ contactMock.end.mockResolvedValue(expectedResponse);
+
+ const response = await task.end();
+
+ expect(contactMock.end).toHaveBeenCalledWith({ interactionId: taskId });
+ expect(response).toEqual(expectedResponse);
+ });
+
+ it('should handle errors in end method', async () => {
+ const error = {
+ details: {
+ trackingId: '1234',
+ data: {
+ reason: 'End Failed',
+ },
+ },
+ };
+ contactMock.end.mockImplementation(() => { throw error; });
+
+ await expect(task.end()).rejects.toThrow(error.details.data.reason);
+ expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'end', CC_FILE);
+ });
+
+ it('should wrap up the task and return the expected response', async () => {
+ const expectedResponse: TaskResponse = { data: { interactionId: taskId } } as AgentContact;
+ const wrapupPayload = {
+ wrapUpReason: 'Customer request',
+ auxCodeId: 'auxCodeId123'
+ };
+ contactMock.wrapup.mockResolvedValue(expectedResponse);
+
+ const response = await task.wrapup(wrapupPayload);
+
+ expect(contactMock.wrapup).toHaveBeenCalledWith({ interactionId: taskId, data: wrapupPayload });
+ expect(response).toEqual(expectedResponse);
+ });
+
+ it('should handle errors in wrapup method', async () => {
+ const error = {
+ details: {
+ trackingId: '1234',
+ data: {
+ reason: 'Wrapup Failed',
+ },
+ },
+ };
+ contactMock.wrapup.mockImplementation(() => { throw error; });
+
+ const wrapupPayload = {
+ wrapUpReason: 'Customer request',
+ auxCodeId: 'auxCodeId123'
+ };
+
+ await expect(task.wrapup(wrapupPayload)).rejects.toThrow(error.details.data.reason);
+ expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'wrapup', CC_FILE);
+ });
+
+ it('should throw an error if auxCodeId is missing in wrapup method', async () => {
+ const wrapupPayload = {
+ wrapUpReason: 'Customer request',
+ auxCodeId: ''
+ };
+ await expect(task.wrapup(wrapupPayload)).rejects.toThrow();
+ });
+
+ it('should throw an error if wrapUpReason is missing in wrapup method', async () => {
+ const wrapupPayload = {
+ wrapUpReason: '',
+ auxCodeId: 'auxCodeId123'
+ };
+ await expect(task.wrapup(wrapupPayload)).rejects.toThrow();
+ });
+
+ it('should pause the recording of the task', async () => {
+ await task.pauseRecording();
+
+ expect(contactMock.pauseRecording).toHaveBeenCalledWith({ interactionId: taskId });
+ });
+
+ it('should handle errors in pauseRecording method', async () => {
+ const error = {
+ details: {
+ trackingId: '1234',
+ data: {
+ reason: 'Pause Recording Failed',
+ },
+ },
+ };
+ contactMock.pauseRecording.mockImplementation(() => { throw error; });
+
+ await expect(task.pauseRecording()).rejects.toThrow(error.details.data.reason);
+ expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'pauseRecording', CC_FILE);
+ });
+
+ it('should resume the recording of the task', async () => {
+ const resumePayload = {
+ autoResumed: true
+ };
+
+ await task.resumeRecording(resumePayload);
+
+ expect(contactMock.resumeRecording).toHaveBeenCalledWith({ interactionId: taskId, data: resumePayload });
+ });
+
+ it('should handle errors in resumeRecording method', async () => {
+ const error = {
+ details: {
+ trackingId: '1234',
+ data: {
+ reason: 'Resume Recording Failed',
+ },
+ },
+ };
+ contactMock.resumeRecording.mockImplementation(() => { throw error; });
+
+ const resumePayload = {
+ autoResumed: true
+ };
+
+ await expect(task.resumeRecording(resumePayload)).rejects.toThrow(error.details.data.reason);
+ expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'resumeRecording', CC_FILE);
+ });
});
\ No newline at end of file