@webex/contact-center 3.12.0-next.16 → 3.12.0-next.161
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/.sdd/manifest.json +887 -0
- package/AGENTS.md +94 -0
- package/ai-docs/ARCHITECTURE.md +168 -0
- package/ai-docs/CONTRACTS.md +62 -0
- package/ai-docs/GETTING_STARTED.md +168 -0
- package/ai-docs/GLOSSARY.md +43 -0
- package/ai-docs/README.md +138 -0
- package/ai-docs/REVIEW_CHECKLIST.md +41 -0
- package/ai-docs/RULES.md +444 -0
- package/ai-docs/SECURITY.md +52 -0
- package/ai-docs/SERVICE_STATE.md +52 -0
- package/ai-docs/SPEC_INDEX.md +65 -0
- package/ai-docs/adr/0001-spec-source-policy.md +55 -0
- package/ai-docs/adr/README.md +8 -0
- package/ai-docs/adr/_adr-template.md +31 -0
- package/ai-docs/contact-center-spec.md +385 -0
- package/ai-docs/features/consult-transfer-list-policy/spec/feature-spec.md +362 -0
- package/ai-docs/features/contact-owner-change-refactor.md +78 -0
- package/ai-docs/features/generated-spec-conformance-fidelity-remediation/spec/feature-spec.md +117 -0
- package/ai-docs/features/residual-warning-coverage-completion/spec/feature-spec.md +203 -0
- package/ai-docs/features/validator-code-fidelity-drift-fix/spec/feature-spec.md +315 -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 +888 -49
- package/dist/cc.js.map +1 -1
- package/dist/config.js +13 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.js +38 -3
- package/dist/constants.js.map +1 -1
- package/dist/index.js +54 -17
- package/dist/index.js.map +1 -1
- package/dist/metrics/MetricsManager.js +16 -0
- package/dist/metrics/MetricsManager.js.map +1 -1
- package/dist/metrics/behavioral-events.js +185 -0
- package/dist/metrics/behavioral-events.js.map +1 -1
- package/dist/metrics/constants.js +54 -4
- package/dist/metrics/constants.js.map +1 -1
- package/dist/services/AddressBook.js +18 -15
- package/dist/services/AddressBook.js.map +1 -1
- package/dist/services/AnswerCallOnWebexService.js +202 -0
- package/dist/services/AnswerCallOnWebexService.js.map +1 -0
- package/dist/services/ApiAiAssistant.js +369 -14
- package/dist/services/ApiAiAssistant.js.map +1 -1
- package/dist/services/EntryPoint.js +46 -68
- package/dist/services/EntryPoint.js.map +1 -1
- package/dist/services/Queue.js +27 -22
- package/dist/services/Queue.js.map +1 -1
- package/dist/services/UserPreference.js +427 -0
- package/dist/services/UserPreference.js.map +1 -0
- package/dist/services/WebCallingService.js +10 -2
- package/dist/services/WebCallingService.js.map +1 -1
- package/dist/services/WebexCrossClientService.js +225 -0
- package/dist/services/WebexCrossClientService.js.map +1 -0
- package/dist/services/WxAppTelephonyMercurySync.js +111 -0
- package/dist/services/WxAppTelephonyMercurySync.js.map +1 -0
- package/dist/services/config/Util.js +48 -48
- package/dist/services/config/Util.js.map +1 -1
- package/dist/services/config/constants.js +56 -78
- package/dist/services/config/constants.js.map +1 -1
- package/dist/services/config/index.js +100 -226
- package/dist/services/config/index.js.map +1 -1
- package/dist/services/config/types.js +93 -24
- package/dist/services/config/types.js.map +1 -1
- package/dist/services/constants.js +11 -1
- package/dist/services/constants.js.map +1 -1
- package/dist/services/core/Err.js.map +1 -1
- package/dist/services/core/Utils.js +131 -32
- package/dist/services/core/Utils.js.map +1 -1
- package/dist/services/core/WebexRequest.js +6 -2
- package/dist/services/core/WebexRequest.js.map +1 -1
- package/dist/services/core/aqm-reqs.js +17 -11
- package/dist/services/core/aqm-reqs.js.map +1 -1
- package/dist/services/core/constants.js +9 -1
- package/dist/services/core/constants.js.map +1 -1
- package/dist/services/core/types.js.map +1 -1
- package/dist/services/core/websocket/WebSocketManager.js +19 -2
- 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 -1
- package/dist/services/index.js.map +1 -1
- package/dist/services/task/Task.js +955 -0
- package/dist/services/task/Task.js.map +1 -0
- package/dist/services/task/TaskFactory.js +50 -0
- package/dist/services/task/TaskFactory.js.map +1 -0
- package/dist/services/task/TaskManager.js +1272 -605
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/services/task/TaskUtils.js +221 -27
- package/dist/services/task/TaskUtils.js.map +1 -1
- package/dist/services/task/WebexCallingUtils.js +70 -0
- package/dist/services/task/WebexCallingUtils.js.map +1 -0
- package/dist/services/task/constants.js +19 -2
- package/dist/services/task/constants.js.map +1 -1
- package/dist/services/task/contact.js +29 -0
- package/dist/services/task/contact.js.map +1 -1
- package/dist/services/task/digital/Digital.js +78 -0
- package/dist/services/task/digital/Digital.js.map +1 -0
- package/dist/services/task/state-machine/TaskStateMachine.js +982 -0
- package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -0
- package/dist/services/task/state-machine/actions.js +572 -0
- package/dist/services/task/state-machine/actions.js.map +1 -0
- package/dist/services/task/state-machine/constants.js +161 -0
- package/dist/services/task/state-machine/constants.js.map +1 -0
- package/dist/services/task/state-machine/guards.js +379 -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 +703 -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 +215 -5
- package/dist/services/task/types.js.map +1 -1
- package/dist/services/task/voice/Voice.js +1504 -0
- package/dist/services/task/voice/Voice.js.map +1 -0
- package/dist/services/task/voice/WebRTC.js +152 -0
- package/dist/services/task/voice/WebRTC.js.map +1 -0
- package/dist/services/task/voice/wxAppOfferObservability.js +145 -0
- package/dist/services/task/voice/wxAppOfferObservability.js.map +1 -0
- package/dist/services/task/voice/wxAppVoiceMethods.js +395 -0
- package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -0
- package/dist/services/wxAppDiagnosticLogging.js +159 -0
- package/dist/services/wxAppDiagnosticLogging.js.map +1 -0
- package/dist/services/wxAppTelephonyUtils.js +19 -0
- package/dist/services/wxAppTelephonyUtils.js.map +1 -0
- package/dist/types/cc.d.ts +152 -9
- package/dist/types/config.d.ts +13 -0
- package/dist/types/constants.d.ts +36 -1
- package/dist/types/index.d.ts +54 -20
- package/dist/types/metrics/MetricsManager.d.ts +6 -0
- package/dist/types/metrics/constants.d.ts +48 -1
- package/dist/types/services/AddressBook.d.ts +2 -1
- package/dist/types/services/AnswerCallOnWebexService.d.ts +38 -0
- package/dist/types/services/ApiAiAssistant.d.ts +67 -5
- package/dist/types/services/EntryPoint.d.ts +7 -5
- package/dist/types/services/Queue.d.ts +5 -3
- package/dist/types/services/UserPreference.d.ts +118 -0
- package/dist/types/services/WebexCrossClientService.d.ts +32 -0
- package/dist/types/services/WxAppTelephonyMercurySync.d.ts +28 -0
- package/dist/types/services/config/Util.d.ts +4 -8
- package/dist/types/services/config/constants.d.ts +49 -76
- package/dist/types/services/config/index.d.ts +16 -64
- package/dist/types/services/config/types.d.ts +240 -114
- package/dist/types/services/constants.d.ts +9 -0
- package/dist/types/services/core/Err.d.ts +2 -0
- package/dist/types/services/core/Utils.d.ts +43 -13
- package/dist/types/services/core/WebexRequest.d.ts +1 -0
- package/dist/types/services/core/constants.d.ts +7 -0
- package/dist/types/services/core/types.d.ts +2 -0
- package/dist/types/services/core/websocket/WebSocketManager.d.ts +1 -0
- 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 +186 -0
- package/dist/types/services/task/TaskFactory.d.ts +14 -0
- package/dist/types/services/task/TaskUtils.d.ts +54 -2
- package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
- package/dist/types/services/task/constants.d.ts +17 -0
- package/dist/types/services/task/contact.d.ts +4 -0
- package/dist/types/services/task/digital/Digital.d.ts +23 -0
- package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +1442 -0
- package/dist/types/services/task/state-machine/actions.d.ts +10 -0
- package/dist/types/services/task/state-machine/constants.d.ts +107 -0
- package/dist/types/services/task/state-machine/guards.d.ts +103 -0
- package/dist/types/services/task/state-machine/index.d.ts +13 -0
- package/dist/types/services/task/state-machine/types.d.ts +277 -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 +738 -66
- package/dist/types/services/task/voice/Voice.d.ts +241 -0
- package/dist/types/services/task/voice/WebRTC.d.ts +54 -0
- package/dist/types/services/task/voice/wxAppOfferObservability.d.ts +39 -0
- package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +68 -0
- package/dist/types/services/wxAppDiagnosticLogging.d.ts +63 -0
- package/dist/types/services/wxAppTelephonyUtils.d.ts +5 -0
- package/dist/types/types.d.ts +213 -11
- package/dist/types/utils/PageCache.d.ts +20 -3
- package/dist/types/webex.d.ts +1 -0
- package/dist/types.js +168 -6
- package/dist/types.js.map +1 -1
- package/dist/utils/PageCache.js +19 -5
- package/dist/utils/PageCache.js.map +1 -1
- package/dist/webex.js +22 -2
- package/dist/webex.js.map +1 -1
- package/package.json +16 -12
- package/src/cc.ts +1137 -67
- package/src/config.ts +13 -0
- package/src/constants.ts +39 -1
- package/src/index.ts +62 -15
- package/src/metrics/MetricsManager.ts +16 -0
- package/src/metrics/ai-docs/AGENTS.md +350 -0
- package/src/metrics/ai-docs/ARCHITECTURE.md +338 -0
- package/src/metrics/ai-docs/metrics-spec.md +971 -0
- package/src/metrics/behavioral-events.ts +190 -0
- package/src/metrics/constants.ts +59 -4
- package/src/services/AddressBook.ts +17 -6
- package/src/services/AnswerCallOnWebexService.ts +239 -0
- package/src/services/ApiAiAssistant.ts +494 -15
- package/src/services/EntryPoint.ts +59 -60
- package/src/services/Queue.ts +29 -12
- package/src/services/UserPreference.ts +509 -0
- package/src/services/WebCallingService.ts +16 -2
- package/src/services/WebexCrossClientService.ts +298 -0
- package/src/services/WxAppTelephonyMercurySync.ts +142 -0
- package/src/services/agent/ai-docs/AGENTS.md +240 -0
- package/src/services/agent/ai-docs/ARCHITECTURE.md +304 -0
- package/src/services/agent/ai-docs/agent-spec.md +505 -0
- package/src/services/ai-docs/AGENTS.md +386 -0
- package/src/services/ai-docs/services-spec.md +512 -0
- package/src/services/config/Util.ts +54 -84
- package/src/services/config/ai-docs/AGENTS.md +250 -0
- package/src/services/config/ai-docs/ARCHITECTURE.md +413 -0
- package/src/services/config/ai-docs/config-spec.md +669 -0
- package/src/services/config/constants.ts +63 -85
- package/src/services/config/index.ts +116 -277
- package/src/services/config/types.ts +229 -125
- package/src/services/constants.ts +10 -0
- package/src/services/core/Err.ts +1 -0
- package/src/services/core/Utils.ts +151 -37
- package/src/services/core/WebexRequest.ts +3 -1
- package/src/services/core/ai-docs/AGENTS.md +381 -0
- package/src/services/core/ai-docs/ARCHITECTURE.md +698 -0
- package/src/services/core/ai-docs/core-spec.md +797 -0
- package/src/services/core/aqm-reqs.ts +18 -12
- package/src/services/core/constants.ts +8 -0
- package/src/services/core/types.ts +33 -1
- package/src/services/core/websocket/WebSocketManager.ts +28 -2
- package/src/services/core/websocket/types.ts +1 -1
- package/src/services/index.ts +1 -1
- package/src/services/task/Task.ts +1211 -0
- package/src/services/task/TaskFactory.ts +82 -0
- package/src/services/task/TaskManager.ts +1585 -724
- package/src/services/task/TaskUtils.ts +316 -25
- package/src/services/task/WebexCallingUtils.ts +136 -0
- package/src/services/task/ai-docs/AGENTS.md +457 -0
- package/src/services/task/ai-docs/ARCHITECTURE.md +595 -0
- package/src/services/task/ai-docs/task-spec.md +1501 -0
- package/src/services/task/constants.ts +18 -0
- package/src/services/task/contact.ts +30 -0
- package/src/services/task/digital/Digital.ts +102 -0
- package/src/services/task/state-machine/TaskStateMachine.ts +1320 -0
- package/src/services/task/state-machine/actions.ts +741 -0
- package/src/services/task/state-machine/ai-docs/AGENTS.md +460 -0
- package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +1156 -0
- package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +2247 -0
- package/src/services/task/state-machine/constants.ts +172 -0
- package/src/services/task/state-machine/guards.ts +442 -0
- package/src/services/task/state-machine/index.ts +28 -0
- package/src/services/task/state-machine/types.ts +258 -0
- package/src/services/task/state-machine/uiControlsComputer.ts +1135 -0
- package/src/services/task/taskDataNormalizer.ts +137 -0
- package/src/services/task/types.ts +918 -70
- package/src/services/task/voice/Voice.ts +1878 -0
- package/src/services/task/voice/WebRTC.ts +194 -0
- package/src/services/task/voice/wxAppOfferObservability.ts +204 -0
- package/src/services/task/voice/wxAppVoiceMethods.ts +575 -0
- package/src/services/wxAppDiagnosticLogging.ts +286 -0
- package/src/services/wxAppTelephonyUtils.ts +14 -0
- package/src/types.ts +272 -13
- package/src/utils/AGENTS.md +289 -0
- package/src/utils/PageCache.ts +38 -5
- package/src/utils/ai-docs/utils-spec.md +391 -0
- package/src/webex.js +10 -0
- package/test/unit/fixtures/aiSummaryTestUtils.ts +40 -0
- package/test/unit/spec/cc.ts +3553 -107
- package/test/unit/spec/index.ts +885 -0
- package/test/unit/spec/logger-proxy.ts +70 -0
- package/test/unit/spec/metrics/MetricsManager.ts +25 -0
- package/test/unit/spec/metrics/behavioral-events.ts +60 -0
- package/test/unit/spec/services/AddressBook.ts +37 -6
- package/test/unit/spec/services/AnswerCallOnWebexService.ts +264 -0
- package/test/unit/spec/services/ApiAiAssistant.ts +442 -23
- package/test/unit/spec/services/EntryPoint.ts +87 -40
- package/test/unit/spec/services/Queue.ts +123 -12
- package/test/unit/spec/services/UserPreference.ts +401 -0
- package/test/unit/spec/services/WebCallingService.ts +45 -1
- package/test/unit/spec/services/WebexCrossClientService.ts +509 -0
- package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +136 -0
- package/test/unit/spec/services/config/Util.ts +501 -0
- package/test/unit/spec/services/config/index.ts +317 -547
- package/test/unit/spec/services/core/Utils.ts +447 -8
- package/test/unit/spec/services/core/WebexRequest.ts +8 -7
- package/test/unit/spec/services/core/aqm-reqs.ts +142 -1
- package/test/unit/spec/services/core/websocket/WebSocketManager.ts +129 -53
- package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
- package/test/unit/spec/services/task/Task.ts +2723 -0
- package/test/unit/spec/services/task/TaskFactory.ts +116 -0
- package/test/unit/spec/services/task/TaskManager.ts +4734 -1769
- package/test/unit/spec/services/task/TaskUtils.ts +307 -0
- package/test/unit/spec/services/task/WebexCallingUtils.ts +153 -0
- package/test/unit/spec/services/task/contact.ts +33 -0
- package/test/unit/spec/services/task/digital/Digital.ts +105 -0
- package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +4060 -0
- package/test/unit/spec/services/task/state-machine/guards.ts +839 -0
- package/test/unit/spec/services/task/state-machine/types.ts +18 -0
- package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +3101 -0
- package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
- package/test/unit/spec/services/task/voice/Voice.ts +2355 -0
- package/test/unit/spec/services/task/voice/WebRTC.ts +235 -0
- package/test/unit/spec/services/task/voice/wxAppOfferObservability.ts +361 -0
- package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +719 -0
- package/test/unit/spec/services/wxAppDiagnosticLogging.ts +179 -0
- package/test/unit/spec/wellnessSampleUtils.ts +288 -0
- package/typedoc.md +1 -1
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
- package/dist/services/task/index.js +0 -1530
- 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 -1806
- package/test/unit/spec/services/task/index.ts +0 -2205
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import {WebSocketManager} from '../../../../../../src/services/core/websocket/WebSocketManager';
|
|
3
3
|
import {WebexSDK, SubscribeRequest} from '../../../../../../src/types';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
RTD_SUBSCRIBE_API,
|
|
6
|
+
SUBSCRIBE_API,
|
|
7
|
+
WCC_API_GATEWAY,
|
|
8
|
+
} from '../../../../../../src/services/constants';
|
|
5
9
|
import {WEB_SOCKET_MANAGER_FILE} from '../../../../../../src/constants';
|
|
6
10
|
import LoggerProxy from '../../../../../../src/logger-proxy';
|
|
7
11
|
|
|
@@ -18,10 +22,10 @@ jest.mock('../../../../../../src/logger-proxy', () => ({
|
|
|
18
22
|
|
|
19
23
|
class MockWebSocket {
|
|
20
24
|
static inst: MockWebSocket;
|
|
21
|
-
onopen: () => void = () => {
|
|
22
|
-
onerror: (event: any) => void = () => {
|
|
23
|
-
onclose: (event: any) => void = () => {
|
|
24
|
-
onmessage: (msg: any) => void = () => {
|
|
25
|
+
onopen: () => void = () => {};
|
|
26
|
+
onerror: (event: any) => void = () => {};
|
|
27
|
+
onclose: (event: any) => void = () => {};
|
|
28
|
+
onmessage: (msg: any) => void = () => {};
|
|
25
29
|
close = jest.fn();
|
|
26
30
|
send = jest.fn();
|
|
27
31
|
|
|
@@ -37,7 +41,7 @@ class MockWebSocket {
|
|
|
37
41
|
class MockCustomEvent<T> extends Event {
|
|
38
42
|
detail: T;
|
|
39
43
|
|
|
40
|
-
constructor(event: string, params: {
|
|
44
|
+
constructor(event: string, params: {detail: T}) {
|
|
41
45
|
super(event);
|
|
42
46
|
this.detail = params.detail;
|
|
43
47
|
}
|
|
@@ -49,7 +53,7 @@ global.CustomEvent = MockCustomEvent as any;
|
|
|
49
53
|
class MockMessageEvent extends Event {
|
|
50
54
|
data: any;
|
|
51
55
|
|
|
52
|
-
constructor(type: string, eventInitDict: {
|
|
56
|
+
constructor(type: string, eventInitDict: {data: any}) {
|
|
53
57
|
super(type);
|
|
54
58
|
this.data = eventInitDict.data;
|
|
55
59
|
}
|
|
@@ -90,21 +94,23 @@ describe('WebSocketManager', () => {
|
|
|
90
94
|
};
|
|
91
95
|
|
|
92
96
|
global.Worker = jest.fn(() => mockWorker) as any;
|
|
93
|
-
global.WebSocket =
|
|
97
|
+
global.WebSocket = jest.fn(function (this: any, ...args: any[]) {
|
|
98
|
+
return new MockWebSocket(...(args as []));
|
|
99
|
+
}) as any;
|
|
94
100
|
|
|
95
101
|
global.Blob = function (content: any[], options: any) {
|
|
96
|
-
return {
|
|
102
|
+
return {content, options};
|
|
97
103
|
} as any;
|
|
98
104
|
|
|
99
105
|
global.URL.createObjectURL = function (blob: Blob) {
|
|
100
106
|
return 'blob:http://localhost:3000/12345';
|
|
101
107
|
};
|
|
102
108
|
|
|
103
|
-
webSocketManager = new WebSocketManager({
|
|
109
|
+
webSocketManager = new WebSocketManager({webex: mockWebex});
|
|
104
110
|
|
|
105
111
|
setTimeout(() => {
|
|
106
112
|
MockWebSocket.inst.onopen();
|
|
107
|
-
MockWebSocket.inst.onmessage({
|
|
113
|
+
MockWebSocket.inst.onmessage({data: JSON.stringify({type: 'Welcome'})});
|
|
108
114
|
}, 1);
|
|
109
115
|
|
|
110
116
|
console.log = jest.fn();
|
|
@@ -115,10 +121,58 @@ describe('WebSocketManager', () => {
|
|
|
115
121
|
expect(webSocketManager).toBeDefined();
|
|
116
122
|
});
|
|
117
123
|
|
|
124
|
+
describe('webSocketUrl validation', () => {
|
|
125
|
+
it('rejects a non-wss webSocketUrl and does not construct a socket', async () => {
|
|
126
|
+
const subscribeResponse = {
|
|
127
|
+
body: {
|
|
128
|
+
webSocketUrl: 'http://fake-url.cisco.com',
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
133
|
+
|
|
134
|
+
await expect(
|
|
135
|
+
webSocketManager.initWebSocket({body: fakeSubscribeRequest, resource: SUBSCRIBE_API})
|
|
136
|
+
).rejects.toBeDefined();
|
|
137
|
+
|
|
138
|
+
expect(global.WebSocket).not.toHaveBeenCalled();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('rejects a wss webSocketUrl with a non-allow-listed host and does not construct a socket', async () => {
|
|
142
|
+
const subscribeResponse = {
|
|
143
|
+
body: {
|
|
144
|
+
webSocketUrl: 'wss://evil.attacker.com',
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
149
|
+
|
|
150
|
+
await expect(
|
|
151
|
+
webSocketManager.initWebSocket({body: fakeSubscribeRequest, resource: SUBSCRIBE_API})
|
|
152
|
+
).rejects.toBeDefined();
|
|
153
|
+
|
|
154
|
+
expect(global.WebSocket).not.toHaveBeenCalled();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('connects for a valid wss allow-listed webSocketUrl', async () => {
|
|
158
|
+
const subscribeResponse = {
|
|
159
|
+
body: {
|
|
160
|
+
webSocketUrl: 'wss://notifs.wxcc-us1.cisco.com',
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
165
|
+
|
|
166
|
+
await webSocketManager.initWebSocket({body: fakeSubscribeRequest, resource: SUBSCRIBE_API});
|
|
167
|
+
|
|
168
|
+
expect(global.WebSocket).toHaveBeenCalledWith('wss://notifs.wxcc-us1.cisco.com');
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
118
172
|
it('should register and connect to WebSocket with X-ORGANIZATION-ID header for INT environment', async () => {
|
|
119
173
|
const subscribeResponse = {
|
|
120
174
|
body: {
|
|
121
|
-
webSocketUrl: 'wss://fake-url',
|
|
175
|
+
webSocketUrl: 'wss://fake-url.cisco.com',
|
|
122
176
|
},
|
|
123
177
|
};
|
|
124
178
|
|
|
@@ -126,7 +180,7 @@ describe('WebSocketManager', () => {
|
|
|
126
180
|
(mockWebex.internal.services.isIntegrationEnvironment as jest.Mock).mockReturnValue(true);
|
|
127
181
|
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
128
182
|
|
|
129
|
-
await webSocketManager.initWebSocket({
|
|
183
|
+
await webSocketManager.initWebSocket({body: fakeSubscribeRequest, resource: SUBSCRIBE_API});
|
|
130
184
|
|
|
131
185
|
expect(mockWebex.request).toHaveBeenCalledWith({
|
|
132
186
|
service: WCC_API_GATEWAY,
|
|
@@ -137,13 +191,35 @@ describe('WebSocketManager', () => {
|
|
|
137
191
|
});
|
|
138
192
|
});
|
|
139
193
|
|
|
140
|
-
it('should
|
|
194
|
+
it('should connect rtd websocket', async () => {
|
|
141
195
|
const subscribeResponse = {
|
|
142
196
|
body: {
|
|
143
|
-
webSocketUrl: 'wss://fake-url',
|
|
197
|
+
webSocketUrl: 'wss://fake-url.cisco.com',
|
|
144
198
|
},
|
|
145
199
|
};
|
|
146
200
|
|
|
201
|
+
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
202
|
+
|
|
203
|
+
await webSocketManager.initWebSocket({
|
|
204
|
+
body: fakeSubscribeRequest,
|
|
205
|
+
resource: RTD_SUBSCRIBE_API,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
expect(mockWebex.request).toHaveBeenCalledWith({
|
|
209
|
+
service: WCC_API_GATEWAY,
|
|
210
|
+
resource: RTD_SUBSCRIBE_API,
|
|
211
|
+
method: 'POST',
|
|
212
|
+
body: fakeSubscribeRequest,
|
|
213
|
+
headers: {'X-ORGANIZATION-ID': 'test-org-id'},
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should register and connect to WebSocket without X-ORGANIZATION-ID header for production environment', async () => {
|
|
218
|
+
const subscribeResponse = {
|
|
219
|
+
body: {
|
|
220
|
+
webSocketUrl: 'wss://fake-url.cisco.com',
|
|
221
|
+
},
|
|
222
|
+
};
|
|
147
223
|
// Mock production environment (services.isIntegrationEnvironment returns false)
|
|
148
224
|
(mockWebex.internal.services.isIntegrationEnvironment as jest.Mock).mockReturnValue(false);
|
|
149
225
|
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
@@ -170,7 +246,7 @@ describe('WebSocketManager', () => {
|
|
|
170
246
|
it('should not send X-ORGANIZATION-ID header when services.isIntegrationEnvironment is not available', async () => {
|
|
171
247
|
const subscribeResponse = {
|
|
172
248
|
body: {
|
|
173
|
-
webSocketUrl: 'wss://fake-url',
|
|
249
|
+
webSocketUrl: 'wss://fake-url.cisco.com',
|
|
174
250
|
},
|
|
175
251
|
};
|
|
176
252
|
|
|
@@ -219,54 +295,54 @@ describe('WebSocketManager', () => {
|
|
|
219
295
|
it('should close WebSocket connection', async () => {
|
|
220
296
|
const subscribeResponse = {
|
|
221
297
|
body: {
|
|
222
|
-
webSocketUrl: 'wss://fake-url',
|
|
298
|
+
webSocketUrl: 'wss://fake-url.cisco.com',
|
|
223
299
|
},
|
|
224
300
|
};
|
|
225
301
|
|
|
226
302
|
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
227
303
|
|
|
228
|
-
await webSocketManager.initWebSocket({
|
|
304
|
+
await webSocketManager.initWebSocket({body: fakeSubscribeRequest, resource: SUBSCRIBE_API});
|
|
229
305
|
|
|
230
306
|
webSocketManager.close(true, 'Test reason');
|
|
231
307
|
|
|
232
308
|
expect(MockWebSocket.inst.close).toHaveBeenCalled();
|
|
233
|
-
expect(mockWorker.postMessage).toHaveBeenCalledWith({
|
|
309
|
+
expect(mockWorker.postMessage).toHaveBeenCalledWith({type: 'terminate'});
|
|
234
310
|
});
|
|
235
311
|
|
|
236
312
|
it('should handle WebSocket keepalive messages', async () => {
|
|
237
313
|
const subscribeResponse = {
|
|
238
314
|
body: {
|
|
239
|
-
webSocketUrl: 'wss://fake-url',
|
|
315
|
+
webSocketUrl: 'wss://fake-url.cisco.com',
|
|
240
316
|
},
|
|
241
317
|
};
|
|
242
318
|
|
|
243
319
|
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
244
320
|
|
|
245
|
-
await webSocketManager.initWebSocket({
|
|
321
|
+
await webSocketManager.initWebSocket({body: fakeSubscribeRequest, resource: SUBSCRIBE_API});
|
|
246
322
|
|
|
247
323
|
setTimeout(() => {
|
|
248
324
|
MockWebSocket.inst.onopen();
|
|
249
|
-
MockWebSocket.inst.onmessage({
|
|
325
|
+
MockWebSocket.inst.onmessage({data: JSON.stringify({type: 'keepalive'})});
|
|
250
326
|
mockWorker.onmessage({
|
|
251
327
|
data: {
|
|
252
|
-
type: 'keepalive'
|
|
253
|
-
}
|
|
328
|
+
type: 'keepalive',
|
|
329
|
+
},
|
|
254
330
|
});
|
|
255
331
|
}, 1);
|
|
256
332
|
|
|
257
|
-
expect(MockWebSocket.inst.send).toHaveBeenCalledWith(JSON.stringify({
|
|
333
|
+
expect(MockWebSocket.inst.send).toHaveBeenCalledWith(JSON.stringify({keepalive: 'true'}));
|
|
258
334
|
});
|
|
259
335
|
|
|
260
336
|
it('should handle WebSocket close due to network issue', async () => {
|
|
261
337
|
const subscribeResponse = {
|
|
262
338
|
body: {
|
|
263
|
-
webSocketUrl: 'wss://fake-url',
|
|
339
|
+
webSocketUrl: 'wss://fake-url.cisco.com',
|
|
264
340
|
},
|
|
265
341
|
};
|
|
266
342
|
|
|
267
343
|
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
268
344
|
|
|
269
|
-
await webSocketManager.initWebSocket({
|
|
345
|
+
await webSocketManager.initWebSocket({body: fakeSubscribeRequest, resource: SUBSCRIBE_API});
|
|
270
346
|
|
|
271
347
|
// Mock navigator.onLine to simulate network issue
|
|
272
348
|
Object.defineProperty(global, 'navigator', {
|
|
@@ -289,10 +365,10 @@ describe('WebSocketManager', () => {
|
|
|
289
365
|
// Wait for the close event to be handled
|
|
290
366
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
291
367
|
|
|
292
|
-
expect(mockWorker.postMessage).toHaveBeenCalledWith({
|
|
368
|
+
expect(mockWorker.postMessage).toHaveBeenCalledWith({type: 'terminate'});
|
|
293
369
|
expect(LoggerProxy.error).toHaveBeenCalledWith(
|
|
294
370
|
'[WebSocketStatus] | event=webSocketClose | WebSocket connection closed REASON: network issue',
|
|
295
|
-
{
|
|
371
|
+
{module: WEB_SOCKET_MANAGER_FILE, method: 'webSocketOnCloseHandler'}
|
|
296
372
|
);
|
|
297
373
|
|
|
298
374
|
// Restore navigator.onLine to true
|
|
@@ -307,59 +383,59 @@ describe('WebSocketManager', () => {
|
|
|
307
383
|
it('should handle WebSocket error event', async () => {
|
|
308
384
|
const subscribeResponse = {
|
|
309
385
|
body: {
|
|
310
|
-
webSocketUrl: 'wss://fake-url',
|
|
386
|
+
webSocketUrl: 'wss://fake-url.cisco.com',
|
|
311
387
|
},
|
|
312
388
|
};
|
|
313
389
|
|
|
314
390
|
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
315
391
|
|
|
316
|
-
await webSocketManager.initWebSocket({
|
|
392
|
+
await webSocketManager.initWebSocket({body: fakeSubscribeRequest, resource: SUBSCRIBE_API});
|
|
317
393
|
|
|
318
394
|
const errorEvent = new Event('error');
|
|
319
395
|
MockWebSocket.inst.onerror(errorEvent);
|
|
320
396
|
|
|
321
397
|
expect(LoggerProxy.error).toHaveBeenCalledWith(
|
|
322
398
|
'[WebSocketStatus] | event=socketConnectionFailed | WebSocket connection failed [object Event]',
|
|
323
|
-
{
|
|
399
|
+
{module: WEB_SOCKET_MANAGER_FILE, method: 'connect'}
|
|
324
400
|
);
|
|
325
401
|
});
|
|
326
402
|
|
|
327
403
|
it('should handle WebSocket message event with AGENT_MULTI_LOGIN', async () => {
|
|
328
404
|
const subscribeResponse = {
|
|
329
405
|
body: {
|
|
330
|
-
webSocketUrl: 'wss://fake-url',
|
|
406
|
+
webSocketUrl: 'wss://fake-url.cisco.com',
|
|
331
407
|
},
|
|
332
408
|
};
|
|
333
409
|
|
|
334
410
|
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
335
411
|
|
|
336
|
-
await webSocketManager.initWebSocket({
|
|
412
|
+
await webSocketManager.initWebSocket({body: fakeSubscribeRequest, resource: SUBSCRIBE_API});
|
|
337
413
|
|
|
338
414
|
const messageEvent = new MessageEvent('message', {
|
|
339
|
-
data: JSON.stringify({
|
|
415
|
+
data: JSON.stringify({type: 'AGENT_MULTI_LOGIN'}),
|
|
340
416
|
});
|
|
341
417
|
MockWebSocket.inst.onmessage(messageEvent);
|
|
342
418
|
|
|
343
419
|
expect(MockWebSocket.inst.close).toHaveBeenCalled();
|
|
344
420
|
expect(LoggerProxy.error).toHaveBeenCalledWith(
|
|
345
421
|
'[WebSocketStatus] | event=agentMultiLogin | WebSocket connection closed by agent multiLogin',
|
|
346
|
-
{
|
|
422
|
+
{module: WEB_SOCKET_MANAGER_FILE, method: 'connect'}
|
|
347
423
|
);
|
|
348
424
|
});
|
|
349
425
|
|
|
350
426
|
it('should handle WebSocket message event with Welcome', async () => {
|
|
351
427
|
const subscribeResponse = {
|
|
352
428
|
body: {
|
|
353
|
-
webSocketUrl: 'wss://fake-url',
|
|
429
|
+
webSocketUrl: 'wss://fake-url.cisco.com',
|
|
354
430
|
},
|
|
355
431
|
};
|
|
356
432
|
|
|
357
433
|
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
358
434
|
|
|
359
|
-
await webSocketManager.initWebSocket({
|
|
435
|
+
await webSocketManager.initWebSocket({body: fakeSubscribeRequest, resource: SUBSCRIBE_API});
|
|
360
436
|
|
|
361
437
|
const messageEvent = new MessageEvent('message', {
|
|
362
|
-
data: JSON.stringify({
|
|
438
|
+
data: JSON.stringify({type: 'Welcome', data: {someData: 'data'}}),
|
|
363
439
|
});
|
|
364
440
|
MockWebSocket.inst.onmessage(messageEvent);
|
|
365
441
|
|
|
@@ -369,13 +445,13 @@ describe('WebSocketManager', () => {
|
|
|
369
445
|
it('should handle WebSocket close with forceCloseWebSocketOnTimeout', async () => {
|
|
370
446
|
const subscribeResponse = {
|
|
371
447
|
body: {
|
|
372
|
-
webSocketUrl: 'wss://fake-url',
|
|
448
|
+
webSocketUrl: 'wss://fake-url.cisco.com',
|
|
373
449
|
},
|
|
374
450
|
};
|
|
375
451
|
|
|
376
452
|
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
377
453
|
|
|
378
|
-
await webSocketManager.initWebSocket({
|
|
454
|
+
await webSocketManager.initWebSocket({body: fakeSubscribeRequest, resource: SUBSCRIBE_API});
|
|
379
455
|
|
|
380
456
|
webSocketManager['forceCloseWebSocketOnTimeout'] = true;
|
|
381
457
|
|
|
@@ -394,23 +470,23 @@ describe('WebSocketManager', () => {
|
|
|
394
470
|
// Wait for the close event to be handled
|
|
395
471
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
396
472
|
|
|
397
|
-
expect(mockWorker.postMessage).toHaveBeenCalledWith({
|
|
473
|
+
expect(mockWorker.postMessage).toHaveBeenCalledWith({type: 'terminate'});
|
|
398
474
|
expect(LoggerProxy.error).toHaveBeenCalledWith(
|
|
399
475
|
'[WebSocketStatus] | event=webSocketClose | WebSocket connection closed REASON: WebSocket auto close timed out. Forcefully closed websocket.',
|
|
400
|
-
{
|
|
476
|
+
{module: WEB_SOCKET_MANAGER_FILE, method: 'webSocketOnCloseHandler'}
|
|
401
477
|
);
|
|
402
478
|
});
|
|
403
479
|
|
|
404
480
|
it('should handle WebSocket close without reconnect', async () => {
|
|
405
481
|
const subscribeResponse = {
|
|
406
482
|
body: {
|
|
407
|
-
webSocketUrl: 'wss://fake-url',
|
|
483
|
+
webSocketUrl: 'wss://fake-url.cisco.com',
|
|
408
484
|
},
|
|
409
485
|
};
|
|
410
486
|
|
|
411
487
|
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
412
488
|
|
|
413
|
-
await webSocketManager.initWebSocket({
|
|
489
|
+
await webSocketManager.initWebSocket({body: fakeSubscribeRequest, resource: SUBSCRIBE_API});
|
|
414
490
|
webSocketManager.shouldReconnect = false;
|
|
415
491
|
// Simulate the WebSocket close event
|
|
416
492
|
setTimeout(() => {
|
|
@@ -421,26 +497,26 @@ describe('WebSocketManager', () => {
|
|
|
421
497
|
target: MockWebSocket.inst,
|
|
422
498
|
});
|
|
423
499
|
}, 1);
|
|
424
|
-
|
|
500
|
+
|
|
425
501
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
426
502
|
|
|
427
|
-
expect(mockWorker.postMessage).toHaveBeenCalledWith({
|
|
503
|
+
expect(mockWorker.postMessage).toHaveBeenCalledWith({type: 'terminate'});
|
|
428
504
|
expect(LoggerProxy.error).not.toHaveBeenCalledWith(
|
|
429
505
|
'[WebSocketStatus] | event=webSocketClose | WebSocket connection closed REASON: no reconnect',
|
|
430
|
-
{
|
|
506
|
+
{module: WEB_SOCKET_MANAGER_FILE, method: 'webSocketOnCloseHandler'}
|
|
431
507
|
);
|
|
432
508
|
});
|
|
433
509
|
|
|
434
510
|
it('should handle WebSocket close with clean close', async () => {
|
|
435
511
|
const subscribeResponse = {
|
|
436
512
|
body: {
|
|
437
|
-
webSocketUrl: 'wss://fake-url',
|
|
513
|
+
webSocketUrl: 'wss://fake-url.cisco.com',
|
|
438
514
|
},
|
|
439
515
|
};
|
|
440
516
|
|
|
441
517
|
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
442
518
|
|
|
443
|
-
await webSocketManager.initWebSocket({
|
|
519
|
+
await webSocketManager.initWebSocket({body: fakeSubscribeRequest, resource: SUBSCRIBE_API});
|
|
444
520
|
|
|
445
521
|
// Simulate the WebSocket close event
|
|
446
522
|
setTimeout(() => {
|
|
@@ -455,10 +531,10 @@ describe('WebSocketManager', () => {
|
|
|
455
531
|
// Wait for the close event to be handled
|
|
456
532
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
457
533
|
|
|
458
|
-
expect(mockWorker.postMessage).toHaveBeenCalledWith({
|
|
534
|
+
expect(mockWorker.postMessage).toHaveBeenCalledWith({type: 'terminate'});
|
|
459
535
|
expect(LoggerProxy.error).not.toHaveBeenCalledWith(
|
|
460
536
|
'[WebSocketStatus] | event=webSocketClose | WebSocket connection closed REASON: clean close',
|
|
461
|
-
{
|
|
537
|
+
{module: WEB_SOCKET_MANAGER_FILE, method: 'webSocketOnCloseHandler'}
|
|
462
538
|
);
|
|
463
539
|
});
|
|
464
|
-
});
|
|
540
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import AutoWrapup from '../../../../../src/services/task/AutoWrapup';
|
|
2
|
+
|
|
3
|
+
describe('AutoWrapup', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
jest.useFakeTimers();
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
jest.useRealTimers();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('starts, reports running, and completes', () => {
|
|
13
|
+
const onComplete = jest.fn();
|
|
14
|
+
const timer = new AutoWrapup(1000, true);
|
|
15
|
+
|
|
16
|
+
expect(timer.allowCancelAutoWrapup).toBe(true);
|
|
17
|
+
expect(timer.isRunning()).toBe(false);
|
|
18
|
+
|
|
19
|
+
timer.start(onComplete);
|
|
20
|
+
expect(timer.isRunning()).toBe(true);
|
|
21
|
+
expect(timer.getTimeLeft()).toBeGreaterThan(0);
|
|
22
|
+
expect(timer.getTimeLeftSeconds()).toBeGreaterThan(0);
|
|
23
|
+
|
|
24
|
+
jest.advanceTimersByTime(1000);
|
|
25
|
+
expect(onComplete).toHaveBeenCalledTimes(1);
|
|
26
|
+
expect(timer.isRunning()).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('clear() is safe when not running and cancels an active timer', () => {
|
|
30
|
+
const onComplete = jest.fn();
|
|
31
|
+
const timer = new AutoWrapup(1000);
|
|
32
|
+
|
|
33
|
+
timer.clear(); // no-op
|
|
34
|
+
expect(timer.isRunning()).toBe(false);
|
|
35
|
+
|
|
36
|
+
timer.start(onComplete);
|
|
37
|
+
expect(timer.isRunning()).toBe(true);
|
|
38
|
+
|
|
39
|
+
timer.clear();
|
|
40
|
+
expect(timer.isRunning()).toBe(false);
|
|
41
|
+
|
|
42
|
+
jest.advanceTimersByTime(1000);
|
|
43
|
+
expect(onComplete).not.toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('start() replaces an existing timer', () => {
|
|
47
|
+
const onComplete1 = jest.fn();
|
|
48
|
+
const onComplete2 = jest.fn();
|
|
49
|
+
const timer = new AutoWrapup(1000);
|
|
50
|
+
|
|
51
|
+
timer.start(onComplete1);
|
|
52
|
+
jest.advanceTimersByTime(500);
|
|
53
|
+
|
|
54
|
+
timer.start(onComplete2);
|
|
55
|
+
jest.advanceTimersByTime(500);
|
|
56
|
+
expect(onComplete1).not.toHaveBeenCalled();
|
|
57
|
+
expect(onComplete2).not.toHaveBeenCalled();
|
|
58
|
+
|
|
59
|
+
jest.advanceTimersByTime(500);
|
|
60
|
+
expect(onComplete2).toHaveBeenCalledTimes(1);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|