@webex/contact-center 3.12.0-next.9 → 3.12.0-task-refactor.2
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 +570 -535
- 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 +372 -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 +263 -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 +377 -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 +579 -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 +422 -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 +303 -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 +542 -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
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as Utils from '../../../../../src/services/core/Utils';
|
|
2
|
-
import {FALLBACK_DIAL_NUMBER_REGEX} from '../../../../../src/services/core/Utils';
|
|
3
2
|
import LoggerProxy from '../../../../../src/logger-proxy';
|
|
4
3
|
import WebexRequest from '../../../../../src/services/core/WebexRequest';
|
|
5
4
|
import {LoginOption, WebexRequestPayload} from '../../../../../src/types';
|
|
@@ -245,7 +244,7 @@ describe('Utils', () => {
|
|
|
245
244
|
const result = Utils.getStationLoginErrorData(failure, LoginOption.AGENT_DN);
|
|
246
245
|
expect(result).toEqual({
|
|
247
246
|
message:
|
|
248
|
-
'Enter a valid dial number. For help, reach out to your administrator or support team.',
|
|
247
|
+
'Enter a valid US dial number. For help, reach out to your administrator or support team.',
|
|
249
248
|
fieldName: LoginOption.AGENT_DN,
|
|
250
249
|
});
|
|
251
250
|
});
|
|
@@ -531,7 +530,7 @@ describe('Utils', () => {
|
|
|
531
530
|
};
|
|
532
531
|
|
|
533
532
|
const result = Utils.calculateDestAgentId(interaction, currentAgentId);
|
|
534
|
-
expect(result).
|
|
533
|
+
expect(result).toBe('');
|
|
535
534
|
});
|
|
536
535
|
|
|
537
536
|
it('should handle CBT scenario when phone number is not a direct participant key', () => {
|
|
@@ -558,65 +557,117 @@ describe('Utils', () => {
|
|
|
558
557
|
});
|
|
559
558
|
});
|
|
560
559
|
|
|
561
|
-
describe('
|
|
562
|
-
const
|
|
563
|
-
name: 'Any Format',
|
|
564
|
-
prefix: '',
|
|
565
|
-
regex: '([0-9a-zA-Z]+[-._])*[0-9a-zA-Z]+',
|
|
566
|
-
strippedChars: '( )-',
|
|
567
|
-
};
|
|
568
|
-
|
|
569
|
-
const usOnlyEntry = {
|
|
570
|
-
name: 'US',
|
|
571
|
-
prefix: '1',
|
|
572
|
-
regex: FALLBACK_DIAL_NUMBER_REGEX.source,
|
|
573
|
-
strippedChars: '( )-',
|
|
574
|
-
};
|
|
575
|
-
|
|
576
|
-
describe('with multiple dial plan entries (Any Format + US)', () => {
|
|
577
|
-
const dialPlanEntries = [anyFormatEntry, usOnlyEntry];
|
|
578
|
-
|
|
579
|
-
it('should return true for a valid US phone number', () => {
|
|
580
|
-
const result = Utils.isValidDialNumber('12223334567', dialPlanEntries);
|
|
581
|
-
expect(result).toBe(true);
|
|
582
|
-
});
|
|
560
|
+
describe('calculateDestType', () => {
|
|
561
|
+
const currentAgentId = 'current-agent-123';
|
|
583
562
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
563
|
+
it('should return DIALNUMBER when pType is DN', () => {
|
|
564
|
+
const interaction: any = {
|
|
565
|
+
media: {
|
|
566
|
+
consult: {
|
|
567
|
+
mType: 'consult',
|
|
568
|
+
participants: [currentAgentId, 'dest-agent-456'],
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
participants: {
|
|
572
|
+
[currentAgentId]: {type: 'Agent', pType: 'Agent'},
|
|
573
|
+
'dest-agent-456': {type: 'Agent', pType: 'DN', id: 'dest-agent-456'},
|
|
574
|
+
},
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const result = Utils.calculateDestType(interaction, currentAgentId);
|
|
578
|
+
expect(result).toBe('dialNumber');
|
|
588
579
|
});
|
|
589
580
|
|
|
590
|
-
|
|
591
|
-
const
|
|
581
|
+
it('should return ENTRYPOINT when pType is EP-DN', () => {
|
|
582
|
+
const interaction: any = {
|
|
583
|
+
media: {
|
|
584
|
+
consult: {
|
|
585
|
+
mType: 'consult',
|
|
586
|
+
participants: [currentAgentId, 'dest-agent-456'],
|
|
587
|
+
},
|
|
588
|
+
},
|
|
589
|
+
participants: {
|
|
590
|
+
[currentAgentId]: {type: 'Agent', pType: 'Agent'},
|
|
591
|
+
'dest-agent-456': {type: 'Agent', pType: 'EP-DN', id: 'dest-agent-456'},
|
|
592
|
+
},
|
|
593
|
+
};
|
|
592
594
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
});
|
|
595
|
+
const result = Utils.calculateDestType(interaction, currentAgentId);
|
|
596
|
+
expect(result).toBe('entryPoint');
|
|
597
|
+
});
|
|
597
598
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
599
|
+
it('should return lowercase pType for other types', () => {
|
|
600
|
+
const interaction: any = {
|
|
601
|
+
media: {
|
|
602
|
+
consult: {
|
|
603
|
+
mType: 'consult',
|
|
604
|
+
participants: [currentAgentId, 'dest-agent-456'],
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
participants: {
|
|
608
|
+
[currentAgentId]: {type: 'Agent', pType: 'Agent'},
|
|
609
|
+
'dest-agent-456': {type: 'Agent', pType: 'Agent', id: 'dest-agent-456'},
|
|
610
|
+
},
|
|
611
|
+
};
|
|
602
612
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
expect(result).toBe(false);
|
|
606
|
-
});
|
|
613
|
+
const result = Utils.calculateDestType(interaction, currentAgentId);
|
|
614
|
+
expect(result).toBe('agent');
|
|
607
615
|
});
|
|
608
616
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
617
|
+
it('should return agent when no destination agent found', () => {
|
|
618
|
+
const interaction: any = {
|
|
619
|
+
media: {},
|
|
620
|
+
participants: {
|
|
621
|
+
[currentAgentId]: {type: 'Agent', pType: 'Agent'},
|
|
622
|
+
},
|
|
623
|
+
};
|
|
614
624
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
expect(result).toBe(false);
|
|
618
|
-
});
|
|
625
|
+
const result = Utils.calculateDestType(interaction, currentAgentId);
|
|
626
|
+
expect(result).toBe('agent');
|
|
619
627
|
});
|
|
620
628
|
});
|
|
621
629
|
|
|
630
|
+
describe('buildConsultConferenceParamData', () => {
|
|
631
|
+
it('maps entryPoint destinationType correctly', () => {
|
|
632
|
+
const result = Utils.buildConsultConferenceParamData(
|
|
633
|
+
{
|
|
634
|
+
agentId: 'agent1',
|
|
635
|
+
destinationType: 'entryPoint',
|
|
636
|
+
destAgentId: 'ep123',
|
|
637
|
+
},
|
|
638
|
+
'interaction123'
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
expect(result).toEqual({
|
|
642
|
+
interactionId: 'interaction123',
|
|
643
|
+
data: {
|
|
644
|
+
agentId: 'agent1',
|
|
645
|
+
to: 'ep123',
|
|
646
|
+
destinationType: 'entryPoint',
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it('maps EP-DN/EP_DN destinationType variants to entryPoint', () => {
|
|
652
|
+
const hyphenResult = Utils.buildConsultConferenceParamData(
|
|
653
|
+
{
|
|
654
|
+
agentId: 'agent1',
|
|
655
|
+
destinationType: 'EP-DN',
|
|
656
|
+
destAgentId: 'ep123',
|
|
657
|
+
},
|
|
658
|
+
'interaction123'
|
|
659
|
+
);
|
|
660
|
+
const underscoreResult = Utils.buildConsultConferenceParamData(
|
|
661
|
+
{
|
|
662
|
+
agentId: 'agent1',
|
|
663
|
+
destinationType: 'EP_DN',
|
|
664
|
+
destAgentId: 'ep123',
|
|
665
|
+
},
|
|
666
|
+
'interaction123'
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
expect(hyphenResult.data.destinationType).toBe('entryPoint');
|
|
670
|
+
expect(underscoreResult.data.destinationType).toBe('entryPoint');
|
|
671
|
+
});
|
|
672
|
+
});
|
|
622
673
|
});
|
|
@@ -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
|
}
|
|
@@ -74,14 +78,6 @@ describe('WebSocketManager', () => {
|
|
|
74
78
|
|
|
75
79
|
mockWebex = {
|
|
76
80
|
request: jest.fn(),
|
|
77
|
-
credentials: {
|
|
78
|
-
getOrgId: jest.fn().mockReturnValue('test-org-id'),
|
|
79
|
-
},
|
|
80
|
-
internal: {
|
|
81
|
-
services: {
|
|
82
|
-
isIntegrationEnvironment: jest.fn().mockReturnValue(true), // INT environment by default
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
81
|
} as unknown as WebexSDK;
|
|
86
82
|
|
|
87
83
|
mockWorker = {
|
|
@@ -93,18 +89,18 @@ describe('WebSocketManager', () => {
|
|
|
93
89
|
global.WebSocket = MockWebSocket as any;
|
|
94
90
|
|
|
95
91
|
global.Blob = function (content: any[], options: any) {
|
|
96
|
-
return {
|
|
92
|
+
return {content, options};
|
|
97
93
|
} as any;
|
|
98
94
|
|
|
99
95
|
global.URL.createObjectURL = function (blob: Blob) {
|
|
100
96
|
return 'blob:http://localhost:3000/12345';
|
|
101
97
|
};
|
|
102
98
|
|
|
103
|
-
webSocketManager = new WebSocketManager({
|
|
99
|
+
webSocketManager = new WebSocketManager({webex: mockWebex});
|
|
104
100
|
|
|
105
101
|
setTimeout(() => {
|
|
106
102
|
MockWebSocket.inst.onopen();
|
|
107
|
-
MockWebSocket.inst.onmessage({
|
|
103
|
+
MockWebSocket.inst.onmessage({data: JSON.stringify({type: 'Welcome'})});
|
|
108
104
|
}, 1);
|
|
109
105
|
|
|
110
106
|
console.log = jest.fn();
|
|
@@ -115,107 +111,47 @@ describe('WebSocketManager', () => {
|
|
|
115
111
|
expect(webSocketManager).toBeDefined();
|
|
116
112
|
});
|
|
117
113
|
|
|
118
|
-
it('should register and connect to WebSocket
|
|
114
|
+
it('should register and connect to WebSocket', async () => {
|
|
119
115
|
const subscribeResponse = {
|
|
120
116
|
body: {
|
|
121
117
|
webSocketUrl: 'wss://fake-url',
|
|
122
118
|
},
|
|
123
119
|
};
|
|
124
120
|
|
|
125
|
-
// Mock INT environment (services.isIntegrationEnvironment returns true)
|
|
126
|
-
(mockWebex.internal.services.isIntegrationEnvironment as jest.Mock).mockReturnValue(true);
|
|
127
121
|
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
128
122
|
|
|
129
|
-
await webSocketManager.initWebSocket({
|
|
123
|
+
await webSocketManager.initWebSocket({body: fakeSubscribeRequest, resource: SUBSCRIBE_API});
|
|
130
124
|
|
|
131
125
|
expect(mockWebex.request).toHaveBeenCalledWith({
|
|
132
126
|
service: WCC_API_GATEWAY,
|
|
133
127
|
resource: SUBSCRIBE_API,
|
|
134
128
|
method: 'POST',
|
|
135
129
|
body: fakeSubscribeRequest,
|
|
136
|
-
headers: {'X-ORGANIZATION-ID': 'test-org-id'},
|
|
137
130
|
});
|
|
138
131
|
});
|
|
139
132
|
|
|
140
|
-
it('should
|
|
133
|
+
it('should connect rtd websocket', async () => {
|
|
141
134
|
const subscribeResponse = {
|
|
142
135
|
body: {
|
|
143
136
|
webSocketUrl: 'wss://fake-url',
|
|
144
137
|
},
|
|
145
138
|
};
|
|
146
139
|
|
|
147
|
-
// Mock production environment (services.isIntegrationEnvironment returns false)
|
|
148
|
-
(mockWebex.internal.services.isIntegrationEnvironment as jest.Mock).mockReturnValue(false);
|
|
149
140
|
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
150
141
|
|
|
151
|
-
|
|
152
|
-
webSocketManager = new WebSocketManager({ webex: mockWebex });
|
|
153
|
-
|
|
154
|
-
setTimeout(() => {
|
|
155
|
-
MockWebSocket.inst.onopen();
|
|
156
|
-
MockWebSocket.inst.onmessage({ data: JSON.stringify({ type: "Welcome" }) });
|
|
157
|
-
}, 1);
|
|
158
|
-
|
|
159
|
-
await webSocketManager.initWebSocket({ body: fakeSubscribeRequest, resource: SUBSCRIBE_API });
|
|
160
|
-
|
|
161
|
-
expect(mockWebex.request).toHaveBeenCalledWith({
|
|
162
|
-
service: WCC_API_GATEWAY,
|
|
163
|
-
resource: SUBSCRIBE_API,
|
|
164
|
-
method: 'POST',
|
|
142
|
+
await webSocketManager.initWebSocket({
|
|
165
143
|
body: fakeSubscribeRequest,
|
|
166
|
-
|
|
144
|
+
resource: RTD_SUBSCRIBE_API,
|
|
167
145
|
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('should not send X-ORGANIZATION-ID header when services.isIntegrationEnvironment is not available', async () => {
|
|
171
|
-
const subscribeResponse = {
|
|
172
|
-
body: {
|
|
173
|
-
webSocketUrl: 'wss://fake-url',
|
|
174
|
-
},
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
// Mock services not available (defaults to production behavior)
|
|
178
|
-
(mockWebex as any).internal = undefined;
|
|
179
|
-
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
180
|
-
|
|
181
|
-
webSocketManager = new WebSocketManager({ webex: mockWebex });
|
|
182
|
-
|
|
183
|
-
setTimeout(() => {
|
|
184
|
-
MockWebSocket.inst.onopen();
|
|
185
|
-
MockWebSocket.inst.onmessage({ data: JSON.stringify({ type: "Welcome" }) });
|
|
186
|
-
}, 1);
|
|
187
|
-
|
|
188
|
-
await webSocketManager.initWebSocket({ body: fakeSubscribeRequest, resource: SUBSCRIBE_API });
|
|
189
146
|
|
|
190
147
|
expect(mockWebex.request).toHaveBeenCalledWith({
|
|
191
148
|
service: WCC_API_GATEWAY,
|
|
192
|
-
resource:
|
|
149
|
+
resource: RTD_SUBSCRIBE_API,
|
|
193
150
|
method: 'POST',
|
|
194
151
|
body: fakeSubscribeRequest,
|
|
195
|
-
headers: undefined,
|
|
196
152
|
});
|
|
197
153
|
});
|
|
198
154
|
|
|
199
|
-
it('should log error and throw when register API fails in initWebSocket', async () => {
|
|
200
|
-
const error = new Error('Register API failed');
|
|
201
|
-
|
|
202
|
-
(mockWebex.request as jest.Mock).mockRejectedValueOnce(error);
|
|
203
|
-
|
|
204
|
-
await expect(
|
|
205
|
-
webSocketManager.initWebSocket({ body: fakeSubscribeRequest, resource: SUBSCRIBE_API })
|
|
206
|
-
).rejects.toThrow(error);
|
|
207
|
-
|
|
208
|
-
expect(LoggerProxy.error).toHaveBeenCalledWith(
|
|
209
|
-
`Register API Failed, Request to RoutingNotifs websocket registration API failed ${error}`,
|
|
210
|
-
{ module: WEB_SOCKET_MANAGER_FILE, method: 'register' }
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
expect(LoggerProxy.error).toHaveBeenCalledWith(
|
|
214
|
-
`[WebSocketStatus] | Error in registering Websocket ${error}`,
|
|
215
|
-
{ module: WEB_SOCKET_MANAGER_FILE, method: 'initWebSocket' }
|
|
216
|
-
);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
155
|
it('should close WebSocket connection', async () => {
|
|
220
156
|
const subscribeResponse = {
|
|
221
157
|
body: {
|
|
@@ -225,12 +161,12 @@ describe('WebSocketManager', () => {
|
|
|
225
161
|
|
|
226
162
|
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
227
163
|
|
|
228
|
-
await webSocketManager.initWebSocket({
|
|
164
|
+
await webSocketManager.initWebSocket({body: fakeSubscribeRequest, resource: SUBSCRIBE_API});
|
|
229
165
|
|
|
230
166
|
webSocketManager.close(true, 'Test reason');
|
|
231
167
|
|
|
232
168
|
expect(MockWebSocket.inst.close).toHaveBeenCalled();
|
|
233
|
-
expect(mockWorker.postMessage).toHaveBeenCalledWith({
|
|
169
|
+
expect(mockWorker.postMessage).toHaveBeenCalledWith({type: 'terminate'});
|
|
234
170
|
});
|
|
235
171
|
|
|
236
172
|
it('should handle WebSocket keepalive messages', async () => {
|
|
@@ -242,19 +178,19 @@ describe('WebSocketManager', () => {
|
|
|
242
178
|
|
|
243
179
|
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
244
180
|
|
|
245
|
-
await webSocketManager.initWebSocket({
|
|
181
|
+
await webSocketManager.initWebSocket({body: fakeSubscribeRequest, resource: SUBSCRIBE_API});
|
|
246
182
|
|
|
247
183
|
setTimeout(() => {
|
|
248
184
|
MockWebSocket.inst.onopen();
|
|
249
|
-
MockWebSocket.inst.onmessage({
|
|
185
|
+
MockWebSocket.inst.onmessage({data: JSON.stringify({type: 'keepalive'})});
|
|
250
186
|
mockWorker.onmessage({
|
|
251
187
|
data: {
|
|
252
|
-
type: 'keepalive'
|
|
253
|
-
}
|
|
188
|
+
type: 'keepalive',
|
|
189
|
+
},
|
|
254
190
|
});
|
|
255
191
|
}, 1);
|
|
256
192
|
|
|
257
|
-
expect(MockWebSocket.inst.send).toHaveBeenCalledWith(JSON.stringify({
|
|
193
|
+
expect(MockWebSocket.inst.send).toHaveBeenCalledWith(JSON.stringify({keepalive: 'true'}));
|
|
258
194
|
});
|
|
259
195
|
|
|
260
196
|
it('should handle WebSocket close due to network issue', async () => {
|
|
@@ -266,7 +202,7 @@ describe('WebSocketManager', () => {
|
|
|
266
202
|
|
|
267
203
|
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
268
204
|
|
|
269
|
-
await webSocketManager.initWebSocket({
|
|
205
|
+
await webSocketManager.initWebSocket({body: fakeSubscribeRequest, resource: SUBSCRIBE_API});
|
|
270
206
|
|
|
271
207
|
// Mock navigator.onLine to simulate network issue
|
|
272
208
|
Object.defineProperty(global, 'navigator', {
|
|
@@ -289,10 +225,10 @@ describe('WebSocketManager', () => {
|
|
|
289
225
|
// Wait for the close event to be handled
|
|
290
226
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
291
227
|
|
|
292
|
-
expect(mockWorker.postMessage).toHaveBeenCalledWith({
|
|
228
|
+
expect(mockWorker.postMessage).toHaveBeenCalledWith({type: 'terminate'});
|
|
293
229
|
expect(LoggerProxy.error).toHaveBeenCalledWith(
|
|
294
230
|
'[WebSocketStatus] | event=webSocketClose | WebSocket connection closed REASON: network issue',
|
|
295
|
-
{
|
|
231
|
+
{module: WEB_SOCKET_MANAGER_FILE, method: 'webSocketOnCloseHandler'}
|
|
296
232
|
);
|
|
297
233
|
|
|
298
234
|
// Restore navigator.onLine to true
|
|
@@ -313,14 +249,14 @@ describe('WebSocketManager', () => {
|
|
|
313
249
|
|
|
314
250
|
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
315
251
|
|
|
316
|
-
await webSocketManager.initWebSocket({
|
|
252
|
+
await webSocketManager.initWebSocket({body: fakeSubscribeRequest, resource: SUBSCRIBE_API});
|
|
317
253
|
|
|
318
254
|
const errorEvent = new Event('error');
|
|
319
255
|
MockWebSocket.inst.onerror(errorEvent);
|
|
320
256
|
|
|
321
257
|
expect(LoggerProxy.error).toHaveBeenCalledWith(
|
|
322
258
|
'[WebSocketStatus] | event=socketConnectionFailed | WebSocket connection failed [object Event]',
|
|
323
|
-
{
|
|
259
|
+
{module: WEB_SOCKET_MANAGER_FILE, method: 'connect'}
|
|
324
260
|
);
|
|
325
261
|
});
|
|
326
262
|
|
|
@@ -333,17 +269,17 @@ describe('WebSocketManager', () => {
|
|
|
333
269
|
|
|
334
270
|
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
335
271
|
|
|
336
|
-
await webSocketManager.initWebSocket({
|
|
272
|
+
await webSocketManager.initWebSocket({body: fakeSubscribeRequest, resource: SUBSCRIBE_API});
|
|
337
273
|
|
|
338
274
|
const messageEvent = new MessageEvent('message', {
|
|
339
|
-
data: JSON.stringify({
|
|
275
|
+
data: JSON.stringify({type: 'AGENT_MULTI_LOGIN'}),
|
|
340
276
|
});
|
|
341
277
|
MockWebSocket.inst.onmessage(messageEvent);
|
|
342
278
|
|
|
343
279
|
expect(MockWebSocket.inst.close).toHaveBeenCalled();
|
|
344
280
|
expect(LoggerProxy.error).toHaveBeenCalledWith(
|
|
345
281
|
'[WebSocketStatus] | event=agentMultiLogin | WebSocket connection closed by agent multiLogin',
|
|
346
|
-
{
|
|
282
|
+
{module: WEB_SOCKET_MANAGER_FILE, method: 'connect'}
|
|
347
283
|
);
|
|
348
284
|
});
|
|
349
285
|
|
|
@@ -356,10 +292,10 @@ describe('WebSocketManager', () => {
|
|
|
356
292
|
|
|
357
293
|
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
358
294
|
|
|
359
|
-
await webSocketManager.initWebSocket({
|
|
295
|
+
await webSocketManager.initWebSocket({body: fakeSubscribeRequest, resource: SUBSCRIBE_API});
|
|
360
296
|
|
|
361
297
|
const messageEvent = new MessageEvent('message', {
|
|
362
|
-
data: JSON.stringify({
|
|
298
|
+
data: JSON.stringify({type: 'Welcome', data: {someData: 'data'}}),
|
|
363
299
|
});
|
|
364
300
|
MockWebSocket.inst.onmessage(messageEvent);
|
|
365
301
|
|
|
@@ -375,7 +311,7 @@ describe('WebSocketManager', () => {
|
|
|
375
311
|
|
|
376
312
|
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
377
313
|
|
|
378
|
-
await webSocketManager.initWebSocket({
|
|
314
|
+
await webSocketManager.initWebSocket({body: fakeSubscribeRequest, resource: SUBSCRIBE_API});
|
|
379
315
|
|
|
380
316
|
webSocketManager['forceCloseWebSocketOnTimeout'] = true;
|
|
381
317
|
|
|
@@ -394,10 +330,10 @@ describe('WebSocketManager', () => {
|
|
|
394
330
|
// Wait for the close event to be handled
|
|
395
331
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
396
332
|
|
|
397
|
-
expect(mockWorker.postMessage).toHaveBeenCalledWith({
|
|
333
|
+
expect(mockWorker.postMessage).toHaveBeenCalledWith({type: 'terminate'});
|
|
398
334
|
expect(LoggerProxy.error).toHaveBeenCalledWith(
|
|
399
335
|
'[WebSocketStatus] | event=webSocketClose | WebSocket connection closed REASON: WebSocket auto close timed out. Forcefully closed websocket.',
|
|
400
|
-
{
|
|
336
|
+
{module: WEB_SOCKET_MANAGER_FILE, method: 'webSocketOnCloseHandler'}
|
|
401
337
|
);
|
|
402
338
|
});
|
|
403
339
|
|
|
@@ -410,7 +346,7 @@ describe('WebSocketManager', () => {
|
|
|
410
346
|
|
|
411
347
|
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
412
348
|
|
|
413
|
-
await webSocketManager.initWebSocket({
|
|
349
|
+
await webSocketManager.initWebSocket({body: fakeSubscribeRequest, resource: SUBSCRIBE_API});
|
|
414
350
|
webSocketManager.shouldReconnect = false;
|
|
415
351
|
// Simulate the WebSocket close event
|
|
416
352
|
setTimeout(() => {
|
|
@@ -421,13 +357,13 @@ describe('WebSocketManager', () => {
|
|
|
421
357
|
target: MockWebSocket.inst,
|
|
422
358
|
});
|
|
423
359
|
}, 1);
|
|
424
|
-
|
|
360
|
+
|
|
425
361
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
426
362
|
|
|
427
|
-
expect(mockWorker.postMessage).toHaveBeenCalledWith({
|
|
363
|
+
expect(mockWorker.postMessage).toHaveBeenCalledWith({type: 'terminate'});
|
|
428
364
|
expect(LoggerProxy.error).not.toHaveBeenCalledWith(
|
|
429
365
|
'[WebSocketStatus] | event=webSocketClose | WebSocket connection closed REASON: no reconnect',
|
|
430
|
-
{
|
|
366
|
+
{module: WEB_SOCKET_MANAGER_FILE, method: 'webSocketOnCloseHandler'}
|
|
431
367
|
);
|
|
432
368
|
});
|
|
433
369
|
|
|
@@ -440,7 +376,7 @@ describe('WebSocketManager', () => {
|
|
|
440
376
|
|
|
441
377
|
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);
|
|
442
378
|
|
|
443
|
-
await webSocketManager.initWebSocket({
|
|
379
|
+
await webSocketManager.initWebSocket({body: fakeSubscribeRequest, resource: SUBSCRIBE_API});
|
|
444
380
|
|
|
445
381
|
// Simulate the WebSocket close event
|
|
446
382
|
setTimeout(() => {
|
|
@@ -455,10 +391,10 @@ describe('WebSocketManager', () => {
|
|
|
455
391
|
// Wait for the close event to be handled
|
|
456
392
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
457
393
|
|
|
458
|
-
expect(mockWorker.postMessage).toHaveBeenCalledWith({
|
|
394
|
+
expect(mockWorker.postMessage).toHaveBeenCalledWith({type: 'terminate'});
|
|
459
395
|
expect(LoggerProxy.error).not.toHaveBeenCalledWith(
|
|
460
396
|
'[WebSocketStatus] | event=webSocketClose | WebSocket connection closed REASON: clean close',
|
|
461
|
-
{
|
|
397
|
+
{module: WEB_SOCKET_MANAGER_FILE, method: 'webSocketOnCloseHandler'}
|
|
462
398
|
);
|
|
463
399
|
});
|
|
464
|
-
});
|
|
400
|
+
});
|
|
@@ -3,9 +3,7 @@ import {WebSocketManager} from '../../../../../../src/services/core/websocket/We
|
|
|
3
3
|
import {SubscribeRequest} from '../../../../../../src/types';
|
|
4
4
|
import LoggerProxy from '../../../../../../src/logger-proxy';
|
|
5
5
|
import {CONNECTIVITY_CHECK_INTERVAL} from '../../../../../../src/services/core/constants';
|
|
6
|
-
import {
|
|
7
|
-
import {SUBSCRIBE_API} from '../../../../../../src/services/constants';
|
|
8
|
-
|
|
6
|
+
import {CONNECTION_SERVICE_FILE} from '../../../../../../src/constants';
|
|
9
7
|
|
|
10
8
|
jest.mock('../../../../../../src/services/core/websocket/WebSocketManager');
|
|
11
9
|
jest.mock('../../../../../../src/logger-proxy', () => ({
|
|
@@ -111,7 +109,10 @@ describe('ConnectionService', () => {
|
|
|
111
109
|
'event=socketConnectionRetry | Trying to reconnect to websocket',
|
|
112
110
|
{module: CONNECTION_SERVICE_FILE, method: 'handleSocketClose'}
|
|
113
111
|
);
|
|
114
|
-
expect(mockWebSocketManager.initWebSocket).toHaveBeenCalledWith({
|
|
112
|
+
expect(mockWebSocketManager.initWebSocket).toHaveBeenCalledWith({
|
|
113
|
+
body: mockSubscribeRequest,
|
|
114
|
+
resource: 'v1/notification/subscribe',
|
|
115
|
+
});
|
|
115
116
|
});
|
|
116
117
|
|
|
117
118
|
describe('ConnectionService onPing', () => {
|
|
@@ -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
|
+
|