@webex/contact-center 3.12.0-next.9 → 3.12.0-task-refactor.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +438 -0
- package/ai-docs/README.md +131 -0
- package/ai-docs/RULES.md +455 -0
- package/ai-docs/patterns/event-driven-patterns.md +485 -0
- package/ai-docs/patterns/testing-patterns.md +480 -0
- package/ai-docs/patterns/typescript-patterns.md +365 -0
- package/ai-docs/templates/README.md +102 -0
- package/ai-docs/templates/documentation/create-agents-md.md +240 -0
- package/ai-docs/templates/documentation/create-architecture-md.md +295 -0
- package/ai-docs/templates/existing-service/bug-fix.md +254 -0
- package/ai-docs/templates/existing-service/feature-enhancement.md +450 -0
- package/ai-docs/templates/new-method/00-master.md +80 -0
- package/ai-docs/templates/new-method/01-requirements.md +232 -0
- package/ai-docs/templates/new-method/02-implementation.md +295 -0
- package/ai-docs/templates/new-method/03-tests.md +201 -0
- package/ai-docs/templates/new-method/04-validation.md +141 -0
- package/ai-docs/templates/new-service/00-master.md +109 -0
- package/ai-docs/templates/new-service/01-pre-questions.md +159 -0
- package/ai-docs/templates/new-service/02-code-generation.md +346 -0
- package/ai-docs/templates/new-service/03-integration.md +178 -0
- package/ai-docs/templates/new-service/04-test-generation.md +205 -0
- package/ai-docs/templates/new-service/05-validation.md +145 -0
- package/dist/cc.js +65 -123
- package/dist/cc.js.map +1 -1
- package/dist/constants.js +13 -2
- package/dist/constants.js.map +1 -1
- package/dist/index.js +13 -5
- package/dist/index.js.map +1 -1
- package/dist/metrics/behavioral-events.js +26 -13
- package/dist/metrics/behavioral-events.js.map +1 -1
- package/dist/metrics/constants.js +7 -6
- package/dist/metrics/constants.js.map +1 -1
- package/dist/services/ApiAiAssistant.js +0 -3
- package/dist/services/ApiAiAssistant.js.map +1 -1
- package/dist/services/config/Util.js +2 -3
- package/dist/services/config/Util.js.map +1 -1
- package/dist/services/config/types.js +16 -14
- package/dist/services/config/types.js.map +1 -1
- package/dist/services/constants.js +0 -1
- package/dist/services/constants.js.map +1 -1
- package/dist/services/core/Err.js.map +1 -1
- package/dist/services/core/Utils.js +79 -55
- package/dist/services/core/Utils.js.map +1 -1
- package/dist/services/core/aqm-reqs.js +17 -92
- package/dist/services/core/aqm-reqs.js.map +1 -1
- package/dist/services/core/websocket/WebSocketManager.js +5 -25
- package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
- package/dist/services/core/websocket/types.js.map +1 -1
- package/dist/services/index.js +1 -2
- package/dist/services/index.js.map +1 -1
- package/dist/services/task/Task.js +644 -0
- package/dist/services/task/Task.js.map +1 -0
- package/dist/services/task/TaskFactory.js +45 -0
- package/dist/services/task/TaskFactory.js.map +1 -0
- package/dist/services/task/TaskManager.js +556 -532
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/services/task/TaskUtils.js +132 -28
- package/dist/services/task/TaskUtils.js.map +1 -1
- package/dist/services/task/constants.js +7 -6
- package/dist/services/task/constants.js.map +1 -1
- package/dist/services/task/dialer.js +0 -51
- package/dist/services/task/dialer.js.map +1 -1
- package/dist/services/task/digital/Digital.js +77 -0
- package/dist/services/task/digital/Digital.js.map +1 -0
- package/dist/services/task/state-machine/TaskStateMachine.js +634 -0
- package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -0
- package/dist/services/task/state-machine/actions.js +366 -0
- package/dist/services/task/state-machine/actions.js.map +1 -0
- package/dist/services/task/state-machine/constants.js +139 -0
- package/dist/services/task/state-machine/constants.js.map +1 -0
- package/dist/services/task/state-machine/guards.js +256 -0
- package/dist/services/task/state-machine/guards.js.map +1 -0
- package/dist/services/task/state-machine/index.js +53 -0
- package/dist/services/task/state-machine/index.js.map +1 -0
- package/dist/services/task/state-machine/types.js +54 -0
- package/dist/services/task/state-machine/types.js.map +1 -0
- package/dist/services/task/state-machine/uiControlsComputer.js +369 -0
- package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -0
- package/dist/services/task/taskDataNormalizer.js +99 -0
- package/dist/services/task/taskDataNormalizer.js.map +1 -0
- package/dist/services/task/types.js +157 -18
- package/dist/services/task/types.js.map +1 -1
- package/dist/services/task/voice/Voice.js +1031 -0
- package/dist/services/task/voice/Voice.js.map +1 -0
- package/dist/services/task/voice/WebRTC.js +149 -0
- package/dist/services/task/voice/WebRTC.js.map +1 -0
- package/dist/types/cc.d.ts +4 -33
- package/dist/types/constants.d.ts +13 -2
- package/dist/types/index.d.ts +11 -5
- package/dist/types/metrics/constants.d.ts +5 -3
- package/dist/types/services/ApiAiAssistant.d.ts +1 -1
- package/dist/types/services/config/types.d.ts +97 -25
- package/dist/types/services/core/Err.d.ts +0 -2
- package/dist/types/services/core/Utils.d.ts +25 -23
- package/dist/types/services/core/aqm-reqs.d.ts +0 -49
- package/dist/types/services/core/websocket/WebSocketManager.d.ts +1 -1
- package/dist/types/services/core/websocket/connection-service.d.ts +0 -1
- package/dist/types/services/core/websocket/types.d.ts +1 -1
- package/dist/types/services/index.d.ts +1 -1
- package/dist/types/services/task/Task.d.ts +146 -0
- package/dist/types/services/task/TaskFactory.d.ts +12 -0
- package/dist/types/services/task/TaskUtils.d.ts +39 -8
- package/dist/types/services/task/constants.d.ts +5 -4
- package/dist/types/services/task/dialer.d.ts +0 -15
- package/dist/types/services/task/digital/Digital.d.ts +22 -0
- package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +906 -0
- package/dist/types/services/task/state-machine/actions.d.ts +8 -0
- package/dist/types/services/task/state-machine/constants.d.ts +91 -0
- package/dist/types/services/task/state-machine/guards.d.ts +78 -0
- package/dist/types/services/task/state-machine/index.d.ts +13 -0
- package/dist/types/services/task/state-machine/types.d.ts +256 -0
- package/dist/types/services/task/state-machine/uiControlsComputer.d.ts +9 -0
- package/dist/types/services/task/taskDataNormalizer.d.ts +10 -0
- package/dist/types/services/task/types.d.ts +539 -88
- package/dist/types/services/task/voice/Voice.d.ts +183 -0
- package/dist/types/services/task/voice/WebRTC.d.ts +53 -0
- package/dist/types/types.d.ts +68 -0
- package/dist/types/webex.d.ts +1 -0
- package/dist/types.js +70 -0
- package/dist/types.js.map +1 -1
- package/dist/webex.js +14 -2
- package/dist/webex.js.map +1 -1
- package/package.json +14 -11
- package/src/cc.ts +91 -177
- package/src/constants.ts +13 -2
- package/src/index.ts +14 -5
- package/src/metrics/ai-docs/AGENTS.md +348 -0
- package/src/metrics/ai-docs/ARCHITECTURE.md +336 -0
- package/src/metrics/behavioral-events.ts +28 -14
- package/src/metrics/constants.ts +7 -8
- package/src/services/ApiAiAssistant.ts +2 -4
- package/src/services/agent/ai-docs/AGENTS.md +238 -0
- package/src/services/agent/ai-docs/ARCHITECTURE.md +302 -0
- package/src/services/ai-docs/AGENTS.md +384 -0
- package/src/services/config/Util.ts +2 -3
- package/src/services/config/ai-docs/AGENTS.md +253 -0
- package/src/services/config/ai-docs/ARCHITECTURE.md +424 -0
- package/src/services/config/types.ts +108 -20
- package/src/services/constants.ts +0 -1
- package/src/services/core/Err.ts +0 -1
- package/src/services/core/Utils.ts +90 -67
- package/src/services/core/ai-docs/AGENTS.md +379 -0
- package/src/services/core/ai-docs/ARCHITECTURE.md +696 -0
- package/src/services/core/aqm-reqs.ts +22 -100
- package/src/services/core/websocket/WebSocketManager.ts +4 -23
- package/src/services/core/websocket/types.ts +1 -1
- package/src/services/index.ts +1 -2
- package/src/services/task/Task.ts +785 -0
- package/src/services/task/TaskFactory.ts +55 -0
- package/src/services/task/TaskManager.ts +567 -633
- package/src/services/task/TaskUtils.ts +175 -31
- package/src/services/task/ai-docs/AGENTS.md +448 -0
- package/src/services/task/ai-docs/ARCHITECTURE.md +573 -0
- package/src/services/task/constants.ts +5 -4
- package/src/services/task/dialer.ts +1 -56
- package/src/services/task/digital/Digital.ts +95 -0
- package/src/services/task/state-machine/TaskStateMachine.ts +793 -0
- package/src/services/task/state-machine/actions.ts +409 -0
- package/src/services/task/state-machine/ai-docs/AGENTS.md +495 -0
- package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +1135 -0
- package/src/services/task/state-machine/constants.ts +150 -0
- package/src/services/task/state-machine/guards.ts +295 -0
- package/src/services/task/state-machine/index.ts +28 -0
- package/src/services/task/state-machine/types.ts +228 -0
- package/src/services/task/state-machine/uiControlsComputer.ts +529 -0
- package/src/services/task/taskDataNormalizer.ts +137 -0
- package/src/services/task/types.ts +641 -95
- package/src/services/task/voice/Voice.ts +1255 -0
- package/src/services/task/voice/WebRTC.ts +187 -0
- package/src/types.ts +88 -5
- package/src/utils/AGENTS.md +276 -0
- package/src/webex.js +2 -0
- package/test/unit/spec/cc.ts +59 -142
- package/test/unit/spec/logger-proxy.ts +70 -0
- package/test/unit/spec/services/ApiAiAssistant.ts +17 -0
- package/test/unit/spec/services/config/index.ts +26 -55
- package/test/unit/spec/services/core/Utils.ts +103 -52
- package/test/unit/spec/services/core/websocket/WebSocketManager.ts +48 -112
- package/test/unit/spec/services/core/websocket/connection-service.ts +5 -4
- package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
- package/test/unit/spec/services/task/Task.ts +416 -0
- package/test/unit/spec/services/task/TaskFactory.ts +62 -0
- package/test/unit/spec/services/task/TaskManager.ts +781 -1735
- package/test/unit/spec/services/task/TaskUtils.ts +125 -0
- package/test/unit/spec/services/task/dialer.ts +112 -198
- package/test/unit/spec/services/task/digital/Digital.ts +105 -0
- package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +473 -0
- package/test/unit/spec/services/task/state-machine/guards.ts +288 -0
- package/test/unit/spec/services/task/state-machine/types.ts +18 -0
- package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +147 -0
- package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
- package/test/unit/spec/services/task/voice/Voice.ts +587 -0
- package/test/unit/spec/services/task/voice/WebRTC.ts +242 -0
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
- package/dist/services/task/index.js +0 -1525
- package/dist/services/task/index.js.map +0 -1
- package/dist/types/services/task/index.d.ts +0 -650
- package/src/services/task/index.ts +0 -1801
- package/test/unit/spec/services/task/index.ts +0 -2184
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import Task from '../../../../../src/services/task/Task';
|
|
2
|
+
import {
|
|
3
|
+
TaskData,
|
|
4
|
+
DESTINATION_TYPE,
|
|
5
|
+
TASK_EVENTS,
|
|
6
|
+
TASK_CHANNEL_TYPE,
|
|
7
|
+
VOICE_VARIANT,
|
|
8
|
+
} from '../../../../../src/services/task/types';
|
|
9
|
+
import {TaskEvent} from '../../../../../src/services/task/state-machine';
|
|
10
|
+
import LoggerProxy from '../../../../../src/logger-proxy';
|
|
11
|
+
import {createTaskData} from './taskTestUtils';
|
|
12
|
+
|
|
13
|
+
class DummyTask extends Task {
|
|
14
|
+
constructor(contact: any, data: TaskData) {
|
|
15
|
+
super(contact, data, {
|
|
16
|
+
channelType: 'voice',
|
|
17
|
+
isEndTaskEnabled: true,
|
|
18
|
+
isEndConsultEnabled: true,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public accept() {
|
|
23
|
+
return Promise.resolve({} as any);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class SpyAcceptTask extends Task {
|
|
28
|
+
public acceptMock: jest.Mock;
|
|
29
|
+
|
|
30
|
+
constructor(contact: any, data: TaskData, configOverrides: any = {}) {
|
|
31
|
+
super(contact, data, {
|
|
32
|
+
channelType: TASK_CHANNEL_TYPE.VOICE,
|
|
33
|
+
isEndTaskEnabled: true,
|
|
34
|
+
isEndConsultEnabled: true,
|
|
35
|
+
...configOverrides,
|
|
36
|
+
});
|
|
37
|
+
this.acceptMock = jest.fn().mockResolvedValue({} as any);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public accept() {
|
|
41
|
+
return this.acceptMock();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const flushPromises = () => new Promise((resolve) => setImmediate(resolve));
|
|
46
|
+
|
|
47
|
+
jest.mock('../../../../../src/logger-proxy', () => ({
|
|
48
|
+
__esModule: true,
|
|
49
|
+
default: {
|
|
50
|
+
log: jest.fn(),
|
|
51
|
+
error: jest.fn(),
|
|
52
|
+
info: jest.fn(),
|
|
53
|
+
initialize: jest.fn(),
|
|
54
|
+
},
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
jest.mock('../../../../../src/services/core/WebexRequest', () => ({
|
|
58
|
+
__esModule: true,
|
|
59
|
+
default: {
|
|
60
|
+
getInstance: jest.fn().mockReturnValue({uploadLogs: jest.fn()}),
|
|
61
|
+
},
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
describe('Task (base class)', () => {
|
|
65
|
+
const dummyContact = {} as any;
|
|
66
|
+
const initialData = {
|
|
67
|
+
foo: 'bar',
|
|
68
|
+
nested: {a: 1, b: 2},
|
|
69
|
+
} as unknown as TaskData;
|
|
70
|
+
|
|
71
|
+
let task: DummyTask;
|
|
72
|
+
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
task = new DummyTask(dummyContact, initialData);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('merges updateTaskData when shouldOverwrite is false', () => {
|
|
78
|
+
const updated = {foo: 'baz', nested: {b: 3}} as unknown as TaskData;
|
|
79
|
+
task.updateTaskData(updated);
|
|
80
|
+
expect(task.data.foo).toBe('baz');
|
|
81
|
+
// nested.a remains, nested.b updated
|
|
82
|
+
expect((task.data as any).nested).toEqual({a: 1, b: 3});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('overwrites data when shouldOverwrite is true', () => {
|
|
86
|
+
const updated = {x: 42} as unknown as TaskData;
|
|
87
|
+
task.updateTaskData(updated, true);
|
|
88
|
+
expect((task.data as any).x).toBe(42);
|
|
89
|
+
expect((task.data as any).foo).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('getUIControls returns default controls shape for idle voice task', () => {
|
|
93
|
+
const controls = task.uiControls;
|
|
94
|
+
const mainControls = controls.main;
|
|
95
|
+
|
|
96
|
+
// IDLE state: no active call, ALL controls should be hidden AND disabled
|
|
97
|
+
expect(mainControls.accept.isVisible).toBe(false);
|
|
98
|
+
expect(mainControls.accept.isEnabled).toBe(false);
|
|
99
|
+
expect(mainControls.decline.isVisible).toBe(false);
|
|
100
|
+
expect(mainControls.decline.isEnabled).toBe(false);
|
|
101
|
+
expect(mainControls.end.isVisible).toBe(false);
|
|
102
|
+
expect(mainControls.end.isEnabled).toBe(false);
|
|
103
|
+
expect(mainControls.transfer.isVisible).toBe(false);
|
|
104
|
+
expect(mainControls.transfer.isEnabled).toBe(false);
|
|
105
|
+
expect(mainControls.hold.isVisible).toBe(false);
|
|
106
|
+
expect(mainControls.hold.isEnabled).toBe(false);
|
|
107
|
+
expect(mainControls.mute.isVisible).toBe(false);
|
|
108
|
+
expect(mainControls.mute.isEnabled).toBe(false);
|
|
109
|
+
expect(mainControls.consult.isVisible).toBe(false);
|
|
110
|
+
expect(mainControls.consult.isEnabled).toBe(false);
|
|
111
|
+
expect(mainControls.consultTransfer.isVisible).toBe(false);
|
|
112
|
+
expect(mainControls.consultTransfer.isEnabled).toBe(false);
|
|
113
|
+
expect(mainControls.endConsult.isVisible).toBe(false);
|
|
114
|
+
expect(mainControls.endConsult.isEnabled).toBe(false);
|
|
115
|
+
expect(mainControls.recording.isVisible).toBe(false);
|
|
116
|
+
expect(mainControls.recording.isEnabled).toBe(false);
|
|
117
|
+
expect(mainControls.conference.isVisible).toBe(false);
|
|
118
|
+
expect(mainControls.conference.isEnabled).toBe(false);
|
|
119
|
+
expect(mainControls.wrapup.isVisible).toBe(false);
|
|
120
|
+
expect(mainControls.wrapup.isEnabled).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('calls updateUiControls when updateTaskData is invoked', () => {
|
|
124
|
+
const spy = jest.spyOn(task as any, 'updateUiControls');
|
|
125
|
+
task.updateTaskData({foo: 'new'} as TaskData);
|
|
126
|
+
expect(spy).toHaveBeenCalled();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('logs state transitions using locally tracked previous state', () => {
|
|
130
|
+
const logSpy = jest.spyOn(LoggerProxy, 'log');
|
|
131
|
+
const statefulData = createTaskData();
|
|
132
|
+
const transitionTask = new DummyTask(dummyContact, statefulData);
|
|
133
|
+
|
|
134
|
+
logSpy.mockClear();
|
|
135
|
+
|
|
136
|
+
transitionTask.stateMachineService?.send({
|
|
137
|
+
type: TaskEvent.TASK_INCOMING,
|
|
138
|
+
taskData: statefulData,
|
|
139
|
+
});
|
|
140
|
+
transitionTask.stateMachineService?.send({type: TaskEvent.ASSIGN, taskData: statefulData});
|
|
141
|
+
|
|
142
|
+
const transitionMessages = logSpy.mock.calls
|
|
143
|
+
.filter(
|
|
144
|
+
([msg]) => typeof msg === 'string' && (msg as string).startsWith('State machine transition')
|
|
145
|
+
)
|
|
146
|
+
.map(([msg]) => msg);
|
|
147
|
+
|
|
148
|
+
expect(transitionMessages).toEqual([
|
|
149
|
+
'State machine transition: IDLE -> OFFERED',
|
|
150
|
+
'State machine transition: OFFERED -> CONNECTED',
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
transitionTask.stateMachineService?.stop();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('emits task:wrapup when wrap-up is required', () => {
|
|
157
|
+
const overrides = (task as any).getStateMachineActionOverrides();
|
|
158
|
+
const emitSpy = jest.spyOn(task, 'emit');
|
|
159
|
+
|
|
160
|
+
task.updateTaskData(createTaskData({wrapUpRequired: true}) as TaskData);
|
|
161
|
+
overrides.emitTaskWrapup({event: {type: TaskEvent.TASK_WRAPUP}});
|
|
162
|
+
|
|
163
|
+
expect(emitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_WRAPUP, task);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('does not emit task:wrapup when wrap-up is not required', () => {
|
|
167
|
+
const overrides = (task as any).getStateMachineActionOverrides();
|
|
168
|
+
const emitSpy = jest.spyOn(task, 'emit');
|
|
169
|
+
|
|
170
|
+
task.updateTaskData(createTaskData({wrapUpRequired: false}) as TaskData);
|
|
171
|
+
overrides.emitTaskWrapup({event: {type: TaskEvent.TASK_WRAPUP}});
|
|
172
|
+
|
|
173
|
+
expect(emitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_WRAPUP, task);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('throws for unsupported voice operations in the base class', async () => {
|
|
177
|
+
const fullData = createTaskData();
|
|
178
|
+
const voiceTask = new DummyTask(dummyContact, fullData);
|
|
179
|
+
|
|
180
|
+
const cases: Array<() => Promise<unknown>> = [
|
|
181
|
+
() => voiceTask.decline(),
|
|
182
|
+
() => voiceTask.pauseRecording(),
|
|
183
|
+
() => voiceTask.resumeRecording({} as any),
|
|
184
|
+
() => voiceTask.consult({} as any),
|
|
185
|
+
() => voiceTask.endConsult({} as any),
|
|
186
|
+
() => voiceTask.consultTransfer({} as any),
|
|
187
|
+
() => voiceTask.consultConference(),
|
|
188
|
+
() => voiceTask.exitConference(),
|
|
189
|
+
() => voiceTask.transferConference(),
|
|
190
|
+
() => voiceTask.toggleMute(),
|
|
191
|
+
() => voiceTask.hold(),
|
|
192
|
+
() => voiceTask.resume(),
|
|
193
|
+
() => voiceTask.holdResume(),
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
for (const fn of cases) {
|
|
197
|
+
await expect(fn()).rejects.toThrow('Unsupported operation');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
expect(() => voiceTask.unregisterWebCallListeners()).not.toThrow();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('syncs task.data from CONTACT_UPDATED', () => {
|
|
204
|
+
const fullData = createTaskData({foo: 'old'} as any);
|
|
205
|
+
const voiceTask = new DummyTask(dummyContact, fullData);
|
|
206
|
+
|
|
207
|
+
voiceTask.sendStateMachineEvent({
|
|
208
|
+
type: TaskEvent.CONTACT_UPDATED,
|
|
209
|
+
taskData: {...fullData, foo: 'new'} as any,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
expect((voiceTask.data as any).foo).toBe('new');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('stopStateMachine clears state snapshot access', () => {
|
|
216
|
+
const fullData = createTaskData();
|
|
217
|
+
const voiceTask = new DummyTask(dummyContact, fullData);
|
|
218
|
+
|
|
219
|
+
expect((voiceTask as any).getCurrentState()).toBeDefined();
|
|
220
|
+
(voiceTask as any).stopStateMachine();
|
|
221
|
+
expect((voiceTask as any).getCurrentState()).toBeUndefined();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('auto-answers on offer when supported and flagged', async () => {
|
|
225
|
+
const data = createTaskData({isAutoAnswering: true});
|
|
226
|
+
const webrtcTask = new SpyAcceptTask(dummyContact, data, {voiceVariant: VOICE_VARIANT.WEBRTC});
|
|
227
|
+
const autoAnsweredSpy = jest.fn();
|
|
228
|
+
webrtcTask.on(TASK_EVENTS.TASK_AUTO_ANSWERED, autoAnsweredSpy);
|
|
229
|
+
|
|
230
|
+
webrtcTask.sendStateMachineEvent({type: TaskEvent.TASK_INCOMING, taskData: data});
|
|
231
|
+
webrtcTask.sendStateMachineEvent({
|
|
232
|
+
type: TaskEvent.TASK_OFFERED,
|
|
233
|
+
taskData: {...data, isAutoAnswering: true} as any,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
await flushPromises();
|
|
237
|
+
expect(webrtcTask.acceptMock).toHaveBeenCalled();
|
|
238
|
+
expect(autoAnsweredSpy).toHaveBeenCalledWith(webrtcTask);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('does not auto-answer when isAutoAnswering is false', async () => {
|
|
242
|
+
const data = createTaskData({isAutoAnswering: false});
|
|
243
|
+
const webrtcTask = new SpyAcceptTask(dummyContact, data, {voiceVariant: VOICE_VARIANT.WEBRTC});
|
|
244
|
+
|
|
245
|
+
webrtcTask.sendStateMachineEvent({type: TaskEvent.TASK_INCOMING, taskData: data});
|
|
246
|
+
webrtcTask.sendStateMachineEvent({type: TaskEvent.TASK_OFFERED, taskData: data});
|
|
247
|
+
|
|
248
|
+
await flushPromises();
|
|
249
|
+
expect(webrtcTask.acceptMock).not.toHaveBeenCalled();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('does not auto-answer for voice tasks when variant is not WebRTC', async () => {
|
|
253
|
+
const data = createTaskData({isAutoAnswering: true});
|
|
254
|
+
const pstnTask = new SpyAcceptTask(dummyContact, data, {voiceVariant: VOICE_VARIANT.PSTN});
|
|
255
|
+
|
|
256
|
+
pstnTask.sendStateMachineEvent({type: TaskEvent.TASK_INCOMING, taskData: data});
|
|
257
|
+
pstnTask.sendStateMachineEvent({type: TaskEvent.TASK_OFFERED, taskData: data});
|
|
258
|
+
|
|
259
|
+
await flushPromises();
|
|
260
|
+
expect(pstnTask.acceptMock).not.toHaveBeenCalled();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('clears isAutoAnswering when auto-answer fails', async () => {
|
|
264
|
+
const data = createTaskData({isAutoAnswering: true});
|
|
265
|
+
const webrtcTask = new SpyAcceptTask(dummyContact, data, {voiceVariant: VOICE_VARIANT.WEBRTC});
|
|
266
|
+
webrtcTask.acceptMock.mockRejectedValue(new Error('fail'));
|
|
267
|
+
|
|
268
|
+
webrtcTask.sendStateMachineEvent({type: TaskEvent.TASK_INCOMING, taskData: data});
|
|
269
|
+
webrtcTask.sendStateMachineEvent({type: TaskEvent.TASK_OFFERED, taskData: data});
|
|
270
|
+
|
|
271
|
+
await flushPromises();
|
|
272
|
+
expect(webrtcTask.data.isAutoAnswering).toBe(false);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('emits task:cleanup (non-removal) on CONTACT_ENDED when wrap-up is required', () => {
|
|
276
|
+
const cleanupSpy = jest.fn();
|
|
277
|
+
const base = createTaskData({
|
|
278
|
+
wrapUpRequired: true,
|
|
279
|
+
interaction: {state: 'connected'},
|
|
280
|
+
} as any);
|
|
281
|
+
const webrtcTask = new SpyAcceptTask(dummyContact, base, {voiceVariant: VOICE_VARIANT.WEBRTC});
|
|
282
|
+
webrtcTask.on(TASK_EVENTS.TASK_CLEANUP, cleanupSpy);
|
|
283
|
+
|
|
284
|
+
webrtcTask.sendStateMachineEvent({type: TaskEvent.TASK_INCOMING, taskData: base});
|
|
285
|
+
webrtcTask.sendStateMachineEvent({type: TaskEvent.TASK_OFFERED, taskData: base});
|
|
286
|
+
webrtcTask.sendStateMachineEvent({type: TaskEvent.ASSIGN, taskData: base});
|
|
287
|
+
webrtcTask.sendStateMachineEvent({type: TaskEvent.CONTACT_ENDED, taskData: base});
|
|
288
|
+
|
|
289
|
+
expect(cleanupSpy).toHaveBeenCalledWith(webrtcTask, {removeFromCollection: false});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('emits task:cleanup (removal) when entering a final state', () => {
|
|
293
|
+
const cleanupSpy = jest.fn();
|
|
294
|
+
const base = createTaskData({wrapUpRequired: false});
|
|
295
|
+
const webrtcTask = new SpyAcceptTask(dummyContact, base, {voiceVariant: VOICE_VARIANT.WEBRTC});
|
|
296
|
+
webrtcTask.on(TASK_EVENTS.TASK_CLEANUP, cleanupSpy);
|
|
297
|
+
|
|
298
|
+
webrtcTask.sendStateMachineEvent({type: TaskEvent.TASK_INCOMING, taskData: base});
|
|
299
|
+
webrtcTask.sendStateMachineEvent({type: TaskEvent.RONA, taskData: base, reason: 'RONA'} as any);
|
|
300
|
+
|
|
301
|
+
expect(cleanupSpy).toHaveBeenCalledWith(webrtcTask, {removeFromCollection: true});
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe('Task common methods', () => {
|
|
306
|
+
let contact: any;
|
|
307
|
+
let task: DummyTask;
|
|
308
|
+
const taskData = {interactionId: '123', foo: 'bar', nested: {a: 1, b: 2}} as TaskData;
|
|
309
|
+
|
|
310
|
+
beforeEach(() => {
|
|
311
|
+
contact = {
|
|
312
|
+
vteamTransfer: jest.fn().mockResolvedValue({result: 'vt'}),
|
|
313
|
+
blindTransfer: jest.fn().mockResolvedValue({result: 'bt'}),
|
|
314
|
+
end: jest.fn().mockResolvedValue({result: 'end'}),
|
|
315
|
+
wrapup: jest.fn().mockResolvedValue({result: 'wrap'}),
|
|
316
|
+
};
|
|
317
|
+
task = new DummyTask(contact, taskData);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('transfer uses blindTransfer for agent destinations', async () => {
|
|
321
|
+
const payload = {to: 'dest', destinationType: DESTINATION_TYPE.AGENT} as any;
|
|
322
|
+
const result = await task.transfer(payload);
|
|
323
|
+
expect(contact.blindTransfer).toHaveBeenCalledWith({
|
|
324
|
+
interactionId: taskData.interactionId,
|
|
325
|
+
data: payload,
|
|
326
|
+
});
|
|
327
|
+
expect(result).toEqual({result: 'bt'});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('transfer uses vteamTransfer for queue destinations', async () => {
|
|
331
|
+
const payload = {to: 'queue1', destinationType: DESTINATION_TYPE.QUEUE} as any;
|
|
332
|
+
const result = await task.transfer(payload);
|
|
333
|
+
expect(contact.vteamTransfer).toHaveBeenCalledWith({
|
|
334
|
+
interactionId: taskData.interactionId,
|
|
335
|
+
data: payload,
|
|
336
|
+
});
|
|
337
|
+
expect(result).toEqual({result: 'vt'});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('end invokes contact.end and returns its response', async () => {
|
|
341
|
+
const result = await task.end();
|
|
342
|
+
expect(contact.end).toHaveBeenCalledWith({
|
|
343
|
+
interactionId: taskData.interactionId,
|
|
344
|
+
});
|
|
345
|
+
expect(result).toEqual({result: 'end'});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('wrapup invokes contact.wrapup with proper args', async () => {
|
|
349
|
+
const payload = {auxCodeId: 'code1', wrapUpReason: 'reason1'} as any;
|
|
350
|
+
const result = await task.wrapup(payload);
|
|
351
|
+
expect(contact.wrapup).toHaveBeenCalledWith({
|
|
352
|
+
interactionId: taskData.interactionId,
|
|
353
|
+
data: payload,
|
|
354
|
+
});
|
|
355
|
+
expect(result).toEqual({result: 'wrap'});
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe('Task failure scenarios', () => {
|
|
360
|
+
let contact: any;
|
|
361
|
+
let task: DummyTask;
|
|
362
|
+
const taskData = {interactionId: '123', foo: 'bar', nested: {a: 1, b: 2}} as TaskData;
|
|
363
|
+
|
|
364
|
+
beforeEach(() => {
|
|
365
|
+
contact = {
|
|
366
|
+
vteamTransfer: jest.fn(),
|
|
367
|
+
blindTransfer: jest.fn(),
|
|
368
|
+
end: jest.fn(),
|
|
369
|
+
wrapup: jest.fn(),
|
|
370
|
+
};
|
|
371
|
+
task = new DummyTask(contact, taskData);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('transfer rejects when blindTransfer throws', async () => {
|
|
375
|
+
const payload = {to: 'dest', destinationType: DESTINATION_TYPE.AGENT} as any;
|
|
376
|
+
const err = new Error('Error while performing transfer');
|
|
377
|
+
contact.blindTransfer.mockRejectedValue(err);
|
|
378
|
+
|
|
379
|
+
await expect(task.transfer(payload)).rejects.toThrow('Error while performing transfer');
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('transfer rejects when vteamTransfer throws', async () => {
|
|
383
|
+
const payload = {to: 'queue1', destinationType: DESTINATION_TYPE.QUEUE} as any;
|
|
384
|
+
const err = new Error('Error while performing transfer');
|
|
385
|
+
contact.vteamTransfer.mockRejectedValue(err);
|
|
386
|
+
|
|
387
|
+
await expect(task.transfer(payload)).rejects.toThrow('Error while performing transfer');
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('end rejects when contact.end throws', async () => {
|
|
391
|
+
const err = new Error('Error while performing end');
|
|
392
|
+
contact.end.mockRejectedValue(err);
|
|
393
|
+
|
|
394
|
+
await expect(task.end()).rejects.toThrow('Error while performing end');
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('wrapup throws when auxCodeId is missing', async () => {
|
|
398
|
+
await expect(task.wrapup({auxCodeId: '', wrapUpReason: 'reason1'} as any)).rejects.toThrow(
|
|
399
|
+
'Error while performing wrapup'
|
|
400
|
+
);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('wrapup throws when wrapUpReason is missing', async () => {
|
|
404
|
+
await expect(task.wrapup({auxCodeId: 'code1', wrapUpReason: ''} as any)).rejects.toThrow(
|
|
405
|
+
'Error while performing wrapup'
|
|
406
|
+
);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('wrapup rejects when contact.wrapup throws', async () => {
|
|
410
|
+
const payload = {auxCodeId: 'code1', wrapUpReason: 'reason1'} as any;
|
|
411
|
+
const err = new Error('Error while performing wrapup');
|
|
412
|
+
contact.wrapup.mockRejectedValue(err);
|
|
413
|
+
|
|
414
|
+
await expect(task.wrapup(payload)).rejects.toThrow('Error while performing wrapup');
|
|
415
|
+
});
|
|
416
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import 'jsdom-global/register';
|
|
2
|
+
import TaskFactory from '../../../../../src/services/task/TaskFactory';
|
|
3
|
+
import {MEDIA_CHANNEL, TaskData} from '../../../../../src/services/task/types';
|
|
4
|
+
import {LoginOption} from '../../../../../src/types';
|
|
5
|
+
import WebCallingService from '../../../../../src/services/WebCallingService';
|
|
6
|
+
import {ConfigFlags} from '../../../../../src/types';
|
|
7
|
+
import register from '@babel/register';
|
|
8
|
+
|
|
9
|
+
describe('TaskFactory', () => {
|
|
10
|
+
const dummyContact = {} as any;
|
|
11
|
+
const baseData: Partial<TaskData> = {
|
|
12
|
+
interactionId: 'id',
|
|
13
|
+
interaction: {mediaType: MEDIA_CHANNEL.TELEPHONY},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const makeSvc = (loginOption: LoginOption) =>
|
|
17
|
+
({
|
|
18
|
+
loginOption,
|
|
19
|
+
on: jest.fn(),
|
|
20
|
+
off: jest.fn?.(),
|
|
21
|
+
} as unknown) as WebCallingService;
|
|
22
|
+
|
|
23
|
+
const configFlags: ConfigFlags = {
|
|
24
|
+
isEndTaskEnabled: true,
|
|
25
|
+
isEndConsultEnabled: true,
|
|
26
|
+
webRtcEnabled: true,
|
|
27
|
+
autoWrapup: false,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
it('creates WebRTC for TELEPHONY + BROWSER', () => {
|
|
31
|
+
const svc = makeSvc(LoginOption.BROWSER);
|
|
32
|
+
const task = TaskFactory.createTask(dummyContact, svc, baseData as TaskData, configFlags);
|
|
33
|
+
expect(task.constructor.name).toBe('WebRTC');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('creates Voice for TELEPHONY + EXTENSION', () => {
|
|
37
|
+
const svc = makeSvc(LoginOption.EXTENSION);
|
|
38
|
+
const task = TaskFactory.createTask(dummyContact, svc, baseData as TaskData, configFlags);
|
|
39
|
+
expect(task.constructor.name).toBe('Voice');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('creates Digital for CHAT, EMAIL, SOCIAL', () => {
|
|
43
|
+
const svc = makeSvc(LoginOption.BROWSER);
|
|
44
|
+
for (const type of [MEDIA_CHANNEL.CHAT, MEDIA_CHANNEL.EMAIL, MEDIA_CHANNEL.SOCIAL]) {
|
|
45
|
+
const data = {...baseData, interaction: {mediaType: type}} as TaskData;
|
|
46
|
+
const task = TaskFactory.createTask(dummyContact, svc, data, configFlags);
|
|
47
|
+
expect(task.constructor.name).toBe('Digital');
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('defaults undefined mediaType to TELEPHONY', () => {
|
|
52
|
+
const svcBrowser = makeSvc(LoginOption.BROWSER);
|
|
53
|
+
const svcExt = makeSvc(LoginOption.EXTENSION);
|
|
54
|
+
const data = {interactionId: 'id', interaction: {}} as TaskData;
|
|
55
|
+
|
|
56
|
+
const t1 = TaskFactory.createTask(dummyContact, svcBrowser, data, configFlags);
|
|
57
|
+
expect(t1.constructor.name).toBe('WebRTC');
|
|
58
|
+
|
|
59
|
+
const t2 = TaskFactory.createTask(dummyContact, svcExt, data, configFlags);
|
|
60
|
+
expect(t2.constructor.name).toBe('Voice');
|
|
61
|
+
});
|
|
62
|
+
});
|