UNPKG

scv-connector-base

Version:
840 lines (785 loc) 232 kB
/* * Copyright (c) 2021, salesforce.com, inc. * All rights reserved. * SPDX-License-Identifier: BSD-3-Clause * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import { initializeConnector, Constants, publishEvent, publishError, publishLog, AgentStatusInfo, AgentVendorStatusInfo, StateChangeResult, CustomError } from '../main/index'; import { ActiveCallsResult, InitResult, CallResult, HoldToggleResult, GenericResult, ContactsResult, PhoneContactsResult, MuteToggleResult, ParticipantResult, RecordingToggleResult, Contact, PhoneCall, CallInfo, VendorConnector, TelephonyConnector, SharedCapabilitiesResult, VoiceCapabilitiesResult, AgentConfigResult, Phone, HangupResult, SignedRecordingUrlResult, LogoutResult, AudioStats, StatsInfo, AudioStatsElement, SuperviseCallResult, SupervisorHangupResult, SupervisedCallInfo, ShowStorageAccessResult, AudioDevicesResult, ACWInfo, SetAgentConfigResult, SetAgentStateResult } from '../main/index'; import baseConstants from '../main/constants'; import { log } from '../main/logger'; jest.mock('../main/logger'); const constants = { ...baseConstants, SHARED_MESSAGE_TYPE: { ...baseConstants.SHARED_MESSAGE_TYPE, DONT_SETUP_CONNECTOR: 'DONT_SETUP_CONNECTOR', CALLS_IN_PROGRESS: 'CALLS_IN_PROGRESS', INVALID_CALL: 'INVALID_CALL' }, GENERIC_ERROR_KEY: 'GENERIC_ERROR', AGENT_ERROR_KEY: 'AGENT_ERROR', OPTIONAL_ERROR: 'OPTIONAL_ERROR', CONNECTOR_CONFIG: 'CONNECTOR_CONFIG', CONTAINER: 'CONTAINER' } global.console.error = jest.fn(); //do not print console.error from dispatchError const loginFrameHeight = 300; const invalidResult = {}; const dummyPhoneNumber = '123456789'; const dummyCallId = 'callId' const dummyConsultCallId = 'consultCallId'; const dummyContact = new Contact({ phoneNumber: dummyPhoneNumber }); const dummyCallInfo = new CallInfo({ isOnHold: false }); const dummyPhoneCall = new PhoneCall({ callId: dummyCallId, callType: constants.CALL_TYPE.INBOUND, callSubtype: constants.CALL_SUBTYPE.PSTN, state: 'state', callAttributes: {}, phoneNumber: '100'}); const dummyNonReplayablePhoneCall = new PhoneCall({ callId: dummyCallId, callType: constants.CALL_TYPE.INBOUND, callSubtype: constants.CALL_SUBTYPE.PSTN, state: 'state', callAttributes: {}, phoneNumber: '100', callInfo: new CallInfo({ isReplayable: false })}); const dummyBargeAbleCall = new PhoneCall({ callId: dummyCallId, callInfo: new CallInfo({ isBargeable: true })}); const dummyBargeAbleDeskPhoneCall = new PhoneCall({ callId: dummyCallId, callInfo: new CallInfo({ isBargeable: true, isSoftphoneCall : false })}); const dummyCallback = new PhoneCall({ callId: dummyCallId, callType: constants.CALL_TYPE.CALLBACK, callSubtype: constants.CALL_SUBTYPE.PSTN, state: 'state', callAttributes: {}, phoneNumber: '100'}); const dummyDialedCallback = new PhoneCall({ callId: dummyCallId, callType: constants.CALL_TYPE.DIALED_CALLBACK, callSubtype: constants.CALL_SUBTYPE.PSTN, state: 'state', callAttributes: {}, phoneNumber: '100'}); const dummyRingingPhoneCall = new PhoneCall({ callId: dummyCallId, callType: constants.CALL_TYPE.INBOUND, callSubtype: constants.CALL_SUBTYPE.PSTN, contact: dummyContact, state: constants.CALL_STATE.RINGING, callAttributes: { initialCallHasEnded: false }, phoneNumber: '100'}); const dummyConnectedPhoneCall = new PhoneCall({ callId: dummyCallId, callType: constants.CALL_TYPE.INBOUND, callSubtype: constants.CALL_SUBTYPE.PSTN, contact: dummyContact, state: constants.CALL_STATE.CONNECTED, callAttributes: { initialCallHasEnded: false }, phoneNumber: '100'}); const dummySupervisorRingingPhoneCall = new PhoneCall({ callId: dummyCallId, callType: constants.CALL_TYPE.INBOUND, callSubtype: constants.CALL_SUBTYPE.PSTN, contact: dummyContact, state: constants.CALL_STATE.RINGING, callAttributes: { initialCallHasEnded: false, participantType: constants.PARTICIPANT_TYPE.SUPERVISOR, hasSupervisorBargedIn: false }, phoneNumber: '100'}); const dummySupervisorConnectedPhoneCall = new PhoneCall({ callId: dummyCallId, callType: constants.CALL_TYPE.INBOUND, callSubtype: constants.CALL_SUBTYPE.PSTN, contact: dummyContact, state: constants.CALL_STATE.CONNECTED, callAttributes: { initialCallHasEnded: false, participantType: constants.PARTICIPANT_TYPE.SUPERVISOR, hasSupervisorBargedIn: false }, phoneNumber: '100'}); const dummySupervisorBargedInPhoneCall = new PhoneCall({ callId: dummyCallId, callType: constants.CALL_TYPE.INBOUND, callSubtype: constants.CALL_SUBTYPE.PSTN, contact: dummyContact, state: constants.CALL_STATE.CONNECTED, callAttributes: { initialCallHasEnded: false, participantType: constants.PARTICIPANT_TYPE.SUPERVISOR, hasSupervisorBargedIn: true }, phoneNumber: '100'}); const thirdPartyRemovedResult = new CallResult({ call: new PhoneCall({ callId: dummyCallId, callType: constants.CALL_TYPE.ADD_PARTICIPANT, callSubtype: constants.CALL_SUBTYPE.PSTN, reason: dummyReason, state: 'state', callAttributes: { participantType: constants.PARTICIPANT_TYPE.THIRD_PARTY }, phoneNumber: '100'}) }); const initialCallerRemovedResult = new CallResult({ call: new PhoneCall({ callId: dummyCallId, callType: constants.CALL_TYPE.ADD_PARTICIPANT, callSubtype: constants.CALL_SUBTYPE.PSTN, reason: dummyReason, state: 'state', callAttributes: { participantType: constants.PARTICIPANT_TYPE.INITIAL_CALLER }, phoneNumber: '100'}) }); const dummyTransferringCall = new PhoneCall({ callId: 'callId', callType: constants.CALL_TYPE.ADD_PARTICIPANT, callSubtype: constants.CALL_SUBTYPE.PSTN, contact: dummyContact, state: constants.CALL_STATE.TRANSFERRING, callAttributes: { initialCallHasEnded: false }, phoneNumber: '100'}); const dummyTransferredCall = new PhoneCall({ callId: 'dummyCallId', callType: constants.CALL_TYPE.ADD_PARTICIPANT, callSubtype: constants.CALL_SUBTYPE.PSTN, contact: dummyContact, state: constants.CALL_STATE.TRANSFERRED, callAttributes: { initialCallHasEnded: false }, phoneNumber: '100'}); const dummyConsultCall = new PhoneCall({ callId: dummyConsultCallId, callType: constants.CALL_TYPE.CONSULT, callSubtype: constants.CALL_SUBTYPE.PSTN, contact: dummyContact, state: constants.CALL_STATE.TRANSFERRED, callAttributes: { initialCallHasEnded: false }, phoneNumber: '101'}); const dummyActiveTransferringCallResult = new ActiveCallsResult({ activeCalls: [dummyTransferringCall] }); const dummyTransferringPhoneCall = new PhoneCall({ callId: dummyCallId, callType: constants.CALL_TYPE.INBOUND, callSubtype: constants.CALL_SUBTYPE.PSTN, contact: dummyContact, state: constants.CALL_STATE.TRANSFERRING, callAttributes: { initialCallHasEnded: false }, phoneNumber: '100'}); const dummyTransferredPhoneCall = new PhoneCall({ callId: dummyCallId, callType: constants.CALL_TYPE.INBOUND, callSubtype: constants.CALL_SUBTYPE.PSTN, contact: dummyContact, state: constants.CALL_STATE.TRANSFERRED, callAttributes: { initialCallHasEnded: false, isAutoMergeOn: true }, phoneNumber: '100'}); const dummyReason = 'dummyReason'; const dummyCloseCallOnError = true; const dummyIsOmniSoftphone = true; const dummyCallType = constants.CALL_TYPE.OUTBOUND; const dummyCallSubtype = constants.CALL_SUBTYPE.PSTN; const dummyAgentStatus = 'dummyAgentStatus'; const dummyLabelName = 'dummyLabelName'; const dummyNamespace = 'dummyNamespace'; const dummyMessage = 'dummyMessage'; const dummyCustomErrorPayload = { customError: { labelName: dummyLabelName, namespace: dummyNamespace, message: dummyMessage } }; const initResult_showLogin = new InitResult({ showLogin: true, loginFrameHeight }); const initResult_connectorReady = new InitResult({ showLogin: false, loginFrameHeight }); const initResult_isSilentLogin = new InitResult({ isSilentLogin: true }); const initResult_showStorageAccessTrue = new InitResult({ showStorageAccess: true }); const initResult_showStorageAccessFalse = new InitResult({ showStorageAccess: false }); const emptyActiveCallsResult = new ActiveCallsResult({ activeCalls: [] }); const activeCallsResult = new ActiveCallsResult({ activeCalls: [ dummyPhoneCall ] }); const activeCallsResult1 = new ActiveCallsResult({ activeCalls: [ dummyPhoneCall, dummyRingingPhoneCall, dummyConnectedPhoneCall, dummyTransferringPhoneCall, dummyTransferredPhoneCall, dummySupervisorRingingPhoneCall, dummySupervisorConnectedPhoneCall, dummySupervisorBargedInPhoneCall ] }); const activeCallsResult2 = new ActiveCallsResult({ activeCalls: [ dummyNonReplayablePhoneCall ] }); const dummyConsultCallResult = new CallResult({ call: new PhoneCall({ callId: dummyConsultCallId, callType: constants.CALL_TYPE.CONSULT, callSubtype: constants.CALL_SUBTYPE.PSTN, reason: dummyReason, state: 'state', callAttributes: { participantType: constants.PARTICIPANT_TYPE.INITIAL_CALLER }, phoneNumber: '101'}) }); const callResult = new CallResult({ call: dummyPhoneCall }); const callbackResult = new CallResult({ call: dummyCallback }); const dialedCallbackResult = new CallResult( { call: dummyDialedCallback}); const callHangUpResult = new HangupResult({ calls: [new PhoneCall({ reason: dummyReason, callId: dummyCallId, closeCallOnError: dummyCloseCallOnError, callType: dummyCallType, callSubtype: dummyCallSubtype, agentStatus: dummyAgentStatus, isOmniSoftphone: dummyIsOmniSoftphone })]}); const muteToggleResult = new MuteToggleResult({ isMuted: true }); const unmuteToggleResult = new MuteToggleResult({ isMuted: false }); const signedRecordingUrlResult = new SignedRecordingUrlResult({ success: true, url: 'recordingUrl', duration: 10, callId: 'callId' }); const audioDevicesResult = new AudioDevicesResult({ deviceIdsPromise: Promise.resolve() }); const calls = [dummyPhoneCall]; const isThirdPartyOnHold = false; const isCustomerOnHold = true; const holdToggleResult = new HoldToggleResult({ isThirdPartyOnHold, isCustomerOnHold, calls }); const success = true; const genericResult = new GenericResult({ success }); const setAgentConfigResult = new SetAgentConfigResult({ success, isSystemEvent: false }); const setAgentStateResult = new SetAgentStateResult({ success, isStatusSyncNeeded: true }); const logoutResult = new LogoutResult({ success, loginFrameHeight }); const customErrorResult = new CustomError({ labelName: dummyLabelName, namespace: dummyNamespace, message: dummyMessage }); const contacts = [ new Contact({}) ]; const contactTypes = [ Constants.CONTACT_TYPE.AGENT, Constants.CONTACT_TYPE.QUEUE ] const phoneContactsResult = new PhoneContactsResult({ contacts, contactTypes }); const contactsResult = new ContactsResult({ contacts, contactTypes }); const participantResult = new ParticipantResult({ initialCallHasEnded: true, callInfo: dummyCallInfo, phoneNumber: dummyPhoneNumber, callId: dummyCallId }); const isRecordingPaused = true; const contactId = 'contactId'; const initialContactId = 'initialContactId'; const instanceId = 'instanceId'; const region = 'region'; const recordingToggleResult = new RecordingToggleResult({ isRecordingPaused, contactId, initialContactId, instanceId, region }); const superviseCallResult = new SuperviseCallResult({call:dummyBargeAbleCall}); const superviseDeskphoneCallResult = new SuperviseCallResult({call:dummyBargeAbleDeskPhoneCall}); const supervisorHangupResult = new SupervisorHangupResult({calls:dummyBargeAbleDeskPhoneCall}); const supervisorHangupMultipleCallsResult = new SupervisorHangupResult({calls: [dummyBargeAbleDeskPhoneCall]}); const showStorageAccessResultSuccess = new ShowStorageAccessResult({success:true}); const showStorageAccessResultShowLogin = new ShowStorageAccessResult({success:true, showLogin: true, loginFrameHeight}); const showStorageAccessResultShowLoginFail = new ShowStorageAccessResult({success:false, showLogin: true, loginFrameHeight}); const hasMute = false; const hasRecord = false; const hasMerge = true; const hasSwap = true; const hasSignedRecordingUrl = true; const phones = ["DESK_PHONE", "SOFT_PHONE"]; const selectedPhone = new Phone({type: "DESK_PHONE", number: "555 888 3345"}); const softphone = new Phone({type: "SOFT_PHONE"}); const supportsMos = true; const agentConfigResult = new AgentConfigResult({ phones, selectedPhone }); const agentConfigResultWithSoftphone = new AgentConfigResult({ phones, selectedPhone : softphone}); const agentConfigPayload = { [constants.AGENT_CONFIG_TYPE.PHONES] : agentConfigResult.phones, [constants.AGENT_CONFIG_TYPE.SELECTED_PHONE] : agentConfigResult.selectedPhone }; const sharedCapabilitiesResult = new SharedCapabilitiesResult({}); const voiceCapabilitiesResult = new VoiceCapabilitiesResult({ hasMute, hasRecord, hasMerge, hasSwap, hasSignedRecordingUrl }); const capabilitiesPayload = { [constants.SHARED_CAPABILITIES_TYPE.DEBUG_ENABLED] : sharedCapabilitiesResult.debugEnabled, [constants.SHARED_CAPABILITIES_TYPE.CONTACT_SEARCH] : sharedCapabilitiesResult.hasContactSearch, [constants.SHARED_CAPABILITIES_TYPE.VENDOR_PROVIDED_AVAILABILITY] : sharedCapabilitiesResult.hasAgentAvailability, [constants.SHARED_CAPABILITIES_TYPE.VENDOR_PROVIDED_QUEUE_WAIT_TIME] : sharedCapabilitiesResult.hasQueueWaitTime, [constants.SHARED_CAPABILITIES_TYPE.TRANSFER_TO_OMNI_FLOW] : sharedCapabilitiesResult.hasTransferToOmniFlow, [constants.SHARED_CAPABILITIES_TYPE.PENDING_STATUS_CHANGE] : sharedCapabilitiesResult.hasPendingStatusChange, [constants.SHARED_CAPABILITIES_TYPE.SFDC_PENDING_STATE]: sharedCapabilitiesResult.hasSFDCPendingState, [constants.SHARED_CAPABILITIES_TYPE.AUTO_ACCEPT_ENABLED]: sharedCapabilitiesResult.hasAutoAcceptEnabled, [constants.VOICE_CAPABILITIES_TYPE.MUTE] : voiceCapabilitiesResult.hasMute, [constants.VOICE_CAPABILITIES_TYPE.RECORD] : voiceCapabilitiesResult.hasRecord, [constants.VOICE_CAPABILITIES_TYPE.MERGE] : voiceCapabilitiesResult.hasMerge, [constants.VOICE_CAPABILITIES_TYPE.SWAP] : voiceCapabilitiesResult.hasSwap, [constants.VOICE_CAPABILITIES_TYPE.BLIND_TRANSFER] : voiceCapabilitiesResult.hasBlindTransfer, [constants.VOICE_CAPABILITIES_TYPE.SIGNED_RECORDING_URL] : voiceCapabilitiesResult.hasSignedRecordingUrl, [constants.VOICE_CAPABILITIES_TYPE.SUPERVISOR_LISTEN_IN] : voiceCapabilitiesResult.hasSupervisorListenIn, [constants.VOICE_CAPABILITIES_TYPE.SUPERVISOR_BARGE_IN] : voiceCapabilitiesResult.hasSupervisorBargeIn, [constants.VOICE_CAPABILITIES_TYPE.MOS] : voiceCapabilitiesResult.supportsMos, [constants.VOICE_CAPABILITIES_TYPE.PHONEBOOK] : voiceCapabilitiesResult.hasPhoneBook, [constants.VOICE_CAPABILITIES_TYPE.HAS_GET_EXTERNAL_SPEAKER] : voiceCapabilitiesResult.hasGetExternalSpeakerDeviceSetting, [constants.VOICE_CAPABILITIES_TYPE.HAS_SET_EXTERNAL_SPEAKER] : voiceCapabilitiesResult.hasSetExternalSpeakerDeviceSetting, [constants.VOICE_CAPABILITIES_TYPE.HAS_GET_EXTERNAL_MICROPHONE] : voiceCapabilitiesResult.hasGetExternalMicrophoneDeviceSetting, [constants.VOICE_CAPABILITIES_TYPE.HAS_SET_EXTERNAL_MICROPHONE] : voiceCapabilitiesResult.hasSetExternalMicrophoneDeviceSetting, [constants.VOICE_CAPABILITIES_TYPE.CAN_CONSULT]: voiceCapabilitiesResult.canConsult, [constants.VOICE_CAPABILITIES_TYPE.DIAL_PAD]: voiceCapabilitiesResult.isDialPadDisabled, [constants.VOICE_CAPABILITIES_TYPE.HAS_HID_SUPPORT]: voiceCapabilitiesResult.isHidSupported, [constants.VOICE_CAPABILITIES_TYPE.PHONEBOOK_DISABLE]: voiceCapabilitiesResult.isPhoneBookDisabled }; const capabilitiesResultWithMos = new VoiceCapabilitiesResult({ hasMute, hasRecord, hasMerge, hasSwap, hasSignedRecordingUrl, supportsMos }); const capabilitiesPayloadWithMos = { ...capabilitiesPayload, [constants.VOICE_CAPABILITIES_TYPE.MOS] : capabilitiesResultWithMos.supportsMos }; const dummyActiveTransferredallResult = new ActiveCallsResult({ activeCalls: [dummyTransferredCall] }); const dummyConsultCallEndResult = new ActiveCallsResult({ activeCalls: [dummyTransferredCall, dummyConsultCall] }); const config = { config: { selectedPhone } }; const dummyStatusInfo = {statusId: 'dummyStatusId', statusApiName: 'dummyStatusApiName', statusName: 'dummyStatusName'}; const error = 'error'; const sanitizePayload = (payload) => { if (payload && typeof(payload) === 'object') { const isArray = Array.isArray(payload); const sanitizedPayload = isArray ? [] : {}; if (isArray) { payload.forEach(element => { sanitizedPayload.push(sanitizePayload(element)); }); } else { for (const property in payload) { // expect.Anything() doesn't serialize well so not sanitizing that if (property === 'error') { sanitizedPayload[property] = payload[property]; } else if (property !== 'phoneNumber' && property !== 'number' && property !== 'name' && property !== 'callAttributes' && property !== '/reqHvcc/reqTelephonyIntegrationCertificate') { sanitizedPayload[property] = sanitizePayload(payload[property]); } } } return sanitizedPayload; } return payload; } const dummyAudioStatsElement = new AudioStatsElement({ inputChannelStats: new StatsInfo({packetsCount: 10, packetsLost: 2, jitterBufferMillis: 10, roundTripTimeMillis: 30}), outputChannelStats: new StatsInfo({packetsCount: 20, packetsLost: 1, jitterBufferMillis: 20, roundTripTimeMillis: 40}) }); const dummyAudioStatsElementWithAudioOutput = new AudioStatsElement({ outputChannelStats: new StatsInfo({packetsCount: 20, packetsLost: 1, jitterBufferMillis: 20, roundTripTimeMillis: 40}) }); const dummyAudioStatsElementWithAudioInput = new AudioStatsElement({ inputChannelStats: new StatsInfo({packetsCount: 10, packetsLost: 2, jitterBufferMillis: 10, roundTripTimeMillis: 30}) }); class ErrorResult { constructor({ type, message }) { this.type = type; this.message = message; } } const supervisedCallInfo = new SupervisedCallInfo({ callId: "callId", voiceCallId: "voiceCallId", callType: constants.CALL_TYPE.INBOUND, from: "from", to: "to", supervisorName: "supervisorName", isBargedIn: true }); describe('SCVConnectorBase tests', () => { class DemoAdapter extends VendorConnector {} class DemoTelephonyAdapter extends TelephonyConnector {} const adapter = new DemoAdapter(); const telephonyAdapter = new DemoTelephonyAdapter(); // VendorConnector overrides DemoAdapter.prototype.init = jest.fn().mockResolvedValue(initResult_connectorReady); DemoAdapter.prototype.getTelephonyConnector = jest.fn().mockResolvedValue(telephonyAdapter); DemoAdapter.prototype.setAgentStatus = jest.fn().mockResolvedValue(setAgentStateResult); DemoAdapter.prototype.logout = jest.fn().mockResolvedValue(logoutResult); DemoAdapter.prototype.handleMessage = jest.fn(), DemoAdapter.prototype.downloadLogs = jest.fn(); DemoAdapter.prototype.logMessageToVendor = jest.fn(); DemoAdapter.prototype.onAgentWorkEvent = jest.fn(); DemoAdapter.prototype.getContacts = jest.fn().mockResolvedValue(contactsResult); DemoAdapter.prototype.getAudioDevices = jest.fn().mockResolvedValue(audioDevicesResult); DemoAdapter.prototype.getSharedCapabilities = jest.fn().mockResolvedValue(sharedCapabilitiesResult); // TelephonyConnector overrides DemoTelephonyAdapter.prototype.acceptCall = jest.fn().mockResolvedValue(callResult); DemoTelephonyAdapter.prototype.declineCall = jest.fn().mockResolvedValue(callResult); DemoTelephonyAdapter.prototype.endCall = jest.fn().mockResolvedValue(); DemoTelephonyAdapter.prototype.mute = jest.fn().mockResolvedValue(muteToggleResult); DemoTelephonyAdapter.prototype.unmute = jest.fn().mockResolvedValue(unmuteToggleResult); DemoTelephonyAdapter.prototype.hold = jest.fn().mockResolvedValue(holdToggleResult); DemoTelephonyAdapter.prototype.resume = jest.fn().mockResolvedValue(holdToggleResult); DemoTelephonyAdapter.prototype.dial = jest.fn().mockResolvedValue(callResult); DemoTelephonyAdapter.prototype.sendDigits = jest.fn().mockResolvedValue({}); DemoTelephonyAdapter.prototype.getPhoneContacts = jest.fn().mockResolvedValue(phoneContactsResult); DemoTelephonyAdapter.prototype.swap = jest.fn().mockResolvedValue(holdToggleResult); DemoTelephonyAdapter.prototype.conference = jest.fn().mockResolvedValue(holdToggleResult); DemoTelephonyAdapter.prototype.addParticipant = jest.fn().mockResolvedValue(participantResult); DemoTelephonyAdapter.prototype.getActiveCalls = jest.fn().mockResolvedValue(activeCallsResult); DemoTelephonyAdapter.prototype.pauseRecording = jest.fn().mockResolvedValue(recordingToggleResult); DemoTelephonyAdapter.prototype.resumeRecording = jest.fn().mockResolvedValue(recordingToggleResult); DemoTelephonyAdapter.prototype.getVoiceCapabilities = jest.fn().mockResolvedValue(voiceCapabilitiesResult); DemoTelephonyAdapter.prototype.getSignedRecordingUrl = jest.fn().mockResolvedValue(signedRecordingUrlResult); DemoTelephonyAdapter.prototype.wrapUpCall = jest.fn(); DemoTelephonyAdapter.prototype.getAgentConfig = jest.fn().mockResolvedValue(agentConfigResult); DemoTelephonyAdapter.prototype.setAgentConfig = jest.fn().mockResolvedValue(setAgentConfigResult); DemoTelephonyAdapter.prototype.setAgentConfigGenericResult = jest.fn().mockResolvedValue(genericResult); const eventMap = {}; const channelPort = { postMessage: jest.fn() }; const fireMessage = (type, args) => { channelPort.onmessage(args ? { data: { type, ...args } } : {data: {type}}); }; const message = { data: { type: constants.SHARED_MESSAGE_TYPE.SETUP_CONNECTOR, connectorConfig: constants.CONNECTOR_CONFIG }, ports: [channelPort], origin: 'https://validOrgDomain.lightning.force.com' }; const assertChannelPortPayload = ({ eventType, payload }) => { expect(channelPort.postMessage).toHaveBeenCalledWith({ type: constants.SHARED_MESSAGE_TYPE.TELEPHONY_EVENT_DISPATCHED, payload: { telephonyEventType: eventType, telephonyEventPayload: payload } }); } const assertChannelPortPayloadEventLog = ({ eventType, payload, isError }) => { expect(channelPort.postMessage).toHaveBeenCalledWith({ type: constants.SHARED_MESSAGE_TYPE.LOG, payload: { eventType, payload: sanitizePayload(payload), isError } }); } beforeAll(() => { window.addEventListener = (event, cb) => { eventMap[event] = cb; }; }); beforeEach(() => { initializeConnector(adapter); }); describe('SCVConnectorBase initialization tests', () => { it('Should NOT dispatch init to the vendor after wrong initialization', () => { const message = { data: { type: constants.SHARED_MESSAGE_TYPE.DONT_SETUP_CONNECTOR, connectorConfig: constants.CONNECTOR_CONFIG }, ports: [channelPort] }; eventMap['message'](message); expect(adapter.init).not.toHaveBeenCalled(); }); it('Should NOT dispatch init to the vendor for a message from non Salesforce domain', () => { const message = { data: { type: constants.SHARED_MESSAGE_TYPE.SETUP_CONNECTOR, connectorConfig: constants.CONNECTOR_CONFIG }, ports: [channelPort], origin: 'https://nonSfDomain.domain.com' }; eventMap['message'](message); expect(adapter.init).not.toHaveBeenCalled(); }); it('Should dispatch init to the vendor for a message from a Salesforce domain with port', () => { const message = { data: { type: constants.SHARED_MESSAGE_TYPE.SETUP_CONNECTOR, connectorConfig: constants.CONNECTOR_CONFIG }, ports: [channelPort], origin: 'https://validOrgDomain.lightning.force.com:8080' }; adapter.init = jest.fn().mockResolvedValue(initResult_connectorReady); eventMap['message'](message); expect(adapter.init).toHaveBeenCalledWith(constants.CONNECTOR_CONFIG); }); it('Should dispatch init to the vendor for a message from a Salesforce soma domain', () => { const message = { data: { type: constants.SHARED_MESSAGE_TYPE.SETUP_CONNECTOR, connectorConfig: constants.CONNECTOR_CONFIG }, ports: [channelPort], origin: 'https://ise240.lightning.mist78.soma.force.com' }; adapter.init = jest.fn().mockResolvedValue(initResult_connectorReady); eventMap['message'](message); expect(adapter.init).toHaveBeenCalledWith(constants.CONNECTOR_CONFIG); }); it('Should dispatch init to the vendor for a message from a Salesforce workspace domain', () => { const message = { data: { type: constants.SHARED_MESSAGE_TYPE.SETUP_CONNECTOR, connectorConfig: constants.CONNECTOR_CONFIG }, ports: [channelPort], origin: 'https://orgfarm-d506aff378.lightning.force-com.cj6x25uar7dm14thqzvy0.wc.crm.dev' }; adapter.init = jest.fn().mockResolvedValue(initResult_connectorReady); eventMap['message'](message); expect(adapter.init).toHaveBeenCalledWith(constants.CONNECTOR_CONFIG); }); it('Should dispatch init to the vendor for a message from a Salesforce military domain (crmforce.mil)', () => { const message = { data: { type: constants.SHARED_MESSAGE_TYPE.SETUP_CONNECTOR, connectorConfig: constants.CONNECTOR_CONFIG }, ports: [channelPort], origin: 'https://military-org.pc-rnd.crmforce.mil' }; adapter.init = jest.fn().mockResolvedValue(initResult_connectorReady); eventMap['message'](message); expect(adapter.init).toHaveBeenCalledWith(constants.CONNECTOR_CONFIG); }); it('Should dispatch init to the vendor for a message from a Salesforce military domain (salesforce.mil)', () => { const message = { data: { type: constants.SHARED_MESSAGE_TYPE.SETUP_CONNECTOR, connectorConfig: constants.CONNECTOR_CONFIG }, ports: [channelPort], origin: 'https://dod-enterprise.pc-rnd.salesforce.mil' }; adapter.init = jest.fn().mockResolvedValue(initResult_connectorReady); eventMap['message'](message); expect(adapter.init).toHaveBeenCalledWith(constants.CONNECTOR_CONFIG); }); it('Should NOT dispatch init to the vendor for a message from invalid military domain', () => { const message = { data: { type: constants.SHARED_MESSAGE_TYPE.SETUP_CONNECTOR, connectorConfig: constants.CONNECTOR_CONFIG }, ports: [channelPort], origin: 'https://invalid.army.mil' }; eventMap['message'](message); expect(adapter.init).not.toHaveBeenCalled(); }); it('Should dispatch init to the vendor for a message from a Lightning production military domain', () => { const message = { data: { type: constants.SHARED_MESSAGE_TYPE.SETUP_CONNECTOR, connectorConfig: constants.CONNECTOR_CONFIG }, ports: [channelPort], origin: 'https://usa9402scrt2voicegov.lightning.crmforce.mil' }; adapter.init = jest.fn().mockResolvedValue(initResult_connectorReady); eventMap['message'](message); expect(adapter.init).toHaveBeenCalledWith(constants.CONNECTOR_CONFIG); }); it('Should dispatch init to the vendor for a message from a sandbox Lightning military domain', () => { const message = { data: { type: constants.SHARED_MESSAGE_TYPE.SETUP_CONNECTOR, connectorConfig: constants.CONNECTOR_CONFIG }, ports: [channelPort], origin: 'https://scrtusa9402mil--scrt.sandbox.lightning.crmforce.mil' }; adapter.init = jest.fn().mockResolvedValue(initResult_connectorReady); eventMap['message'](message); expect(adapter.init).toHaveBeenCalledWith(constants.CONNECTOR_CONFIG); }); it('Should log the right fields when init is called', () => { const message = { data: { type: constants.SHARED_MESSAGE_TYPE.SETUP_CONNECTOR, connectorConfig: { "/reqGeneralInfo/reqAdapterUrl": "abc", "invalidKey": "unknown", "/reqHvcc/1": "1", "/reqHvcc/2": "2", "/reqHvcc/reqTelephonyIntegrationCertificate" : "abc" } }, ports: [channelPort], origin: 'https://validOrgDomain.lightning.force.com:8080' }; log.mockClear(); adapter.init = jest.fn().mockResolvedValue(initResult_connectorReady); eventMap['message'](message); expect(log).toBeCalledTimes(1); expect(log.mock.calls[0][0]).toEqual({ eventType: constants.SHARED_MESSAGE_TYPE.SETUP_CONNECTOR, payload: { "/reqGeneralInfo/reqAdapterUrl": "abc", "/reqHvcc/1": "1", "/reqHvcc/2": "2" } }); }); it('Should log the right fields when init is called with an empty payload', () => { const message = { data: { type: constants.SHARED_MESSAGE_TYPE.SETUP_CONNECTOR, connectorConfig: undefined }, ports: [channelPort], origin: 'https://validOrgDomain.lightning.force.com:8080' }; log.mockClear(); adapter.init = jest.fn().mockResolvedValue(initResult_connectorReady); eventMap['message'](message); expect(log).toBeCalledTimes(1); expect(log.mock.calls[0][0]).toEqual({ eventType: constants.SHARED_MESSAGE_TYPE.SETUP_CONNECTOR, payload: {} }); }); it('Should dispatch default error after invalid initialization result', async () => { adapter.init = jest.fn().mockResolvedValue(invalidResult); eventMap['message'](message); await expect(adapter.init()).resolves.toBe(invalidResult); assertChannelPortPayload({ eventType: constants.SHARED_EVENT_TYPE.ERROR, payload: { message: constants.SHARED_ERROR_TYPE.CAN_NOT_LOG_IN }}); assertChannelPortPayloadEventLog({ eventType: constants.SHARED_MESSAGE_TYPE.SETUP_CONNECTOR, payload: { errorType: constants.SHARED_ERROR_TYPE.CAN_NOT_LOG_IN, error: expect.anything() }, isError: true }); }); it('Should dispatch typed error after invalid param result', async () => { const errorResult = new ErrorResult({ type: Constants.VOICE_ERROR_TYPE.INVALID_PARAMS }); adapter.init = jest.fn().mockRejectedValue(errorResult); eventMap['message'](message); await expect(adapter.init()).rejects.toBe(errorResult); assertChannelPortPayload({ eventType: constants.SHARED_EVENT_TYPE.ERROR, payload: { message: constants.VOICE_ERROR_TYPE.INVALID_PARAMS }}); assertChannelPortPayloadEventLog({ eventType: constants.SHARED_MESSAGE_TYPE.SETUP_CONNECTOR, payload: { errorType: constants.VOICE_ERROR_TYPE.INVALID_PARAMS, error: expect.anything() }, isError: true }); }); it('Should dispatch custom error after failing to setup connector', async () => { adapter.init = jest.fn().mockRejectedValue(customErrorResult); eventMap['message'](message); await expect(adapter.init()).rejects.toBe(customErrorResult); assertChannelPortPayload({ eventType: constants.SHARED_EVENT_TYPE.ERROR, payload: dummyCustomErrorPayload }); assertChannelPortPayloadEventLog({ eventType: constants.SHARED_MESSAGE_TYPE.SETUP_CONNECTOR, payload: { errorType: constants.SHARED_ERROR_TYPE.CUSTOM_ERROR, error: expect.anything() }, isError: true }); }); it('Should dispatch SHOW_LOGIN after initialization', async () => { adapter.init = jest.fn().mockResolvedValue(initResult_showStorageAccessTrue); eventMap['message'](message); await expect(adapter.init()).resolves.toBe(initResult_showStorageAccessTrue); assertChannelPortPayload({ eventType: constants.SHARED_EVENT_TYPE.SHOW_STORAGE_ACCESS, payload: { success: true }}); assertChannelPortPayloadEventLog({ eventType: constants.SHARED_EVENT_TYPE.SHOW_STORAGE_ACCESS, payload: { success: true }, isError: false }); }); it('Should dispatch SHOW_LOGIN (isSilentLogin) after initialization', async () => { adapter.init = jest.fn().mockResolvedValue(initResult_isSilentLogin); eventMap['message'](message); await expect(adapter.init()).resolves.toBe(initResult_isSilentLogin); }); it('Should dispatch SHOW_LOGIN (initResult_showStorageAccess as True) after initialization', async () => { adapter.init = jest.fn().mockResolvedValue(initResult_showStorageAccessTrue); eventMap['message'](message); await expect(adapter.init()).resolves.toBe(initResult_showStorageAccessTrue); }); it('Should dispatch SHOW_LOGIN (initResult_showStorageAccess as False) after initialization', async () => { adapter.init = jest.fn().mockResolvedValue(initResult_showStorageAccessFalse); eventMap['message'](message); await expect(adapter.init()).resolves.toBe(initResult_showStorageAccessFalse); }); it('Should dispatch SHOW_STORAGE_ACCESS after initialization', async () => { adapter.init = jest.fn().mockResolvedValue(initResult_showLogin); eventMap['message'](message); await expect(adapter.init()).resolves.toBe(initResult_showLogin); assertChannelPortPayload({ eventType: constants.SHARED_EVENT_TYPE.SHOW_LOGIN, payload: { loginFrameHeight }}); assertChannelPortPayloadEventLog({ eventType: constants.SHARED_EVENT_TYPE.SHOW_LOGIN, payload: { loginFrameHeight }, isError: false }); }); it('Should dispatch CONNECTOR_READY after initialization', async () => { adapter.init = jest.fn().mockResolvedValue(initResult_connectorReady); eventMap['message'](message); expect(adapter.init).toHaveBeenCalledWith(constants.CONNECTOR_CONFIG); await expect(adapter.init()).resolves.toBe(initResult_connectorReady); await expect(adapter.getSharedCapabilities()).resolves.toBe(sharedCapabilitiesResult); await expect(adapter.getTelephonyConnector()).resolves.toBe(telephonyAdapter); await expect(telephonyAdapter.getAgentConfig()).resolves.toBe(agentConfigResult); await expect(telephonyAdapter.getVoiceCapabilities()).resolves.toBe(voiceCapabilitiesResult); await expect(telephonyAdapter.getActiveCalls()).resolves.toBe(activeCallsResult); expect(channelPort.postMessage).toHaveBeenCalledWith({ type: constants.SHARED_MESSAGE_TYPE.CONNECTOR_READY, payload: { agentConfig: agentConfigPayload, capabilities: capabilitiesPayload, callInProgress: dummyPhoneCall } }); assertChannelPortPayloadEventLog({ eventType: constants.SHARED_MESSAGE_TYPE.CONNECTOR_READY, payload: { agentConfig: agentConfigPayload, capabilities: capabilitiesPayload, callInProgress: dummyPhoneCall }, isError: false }); }); it('Should dispatch CONNECTOR_READY after initialization without any active calls', async () => { adapter.init = jest.fn().mockResolvedValue(initResult_connectorReady); eventMap['message'](message); expect(adapter.init).toHaveBeenCalledWith(constants.CONNECTOR_CONFIG); telephonyAdapter.getActiveCalls = jest.fn().mockResolvedValue(emptyActiveCallsResult); await expect(adapter.init()).resolves.toBe(initResult_connectorReady); await expect(adapter.getTelephonyConnector()).resolves.toBe(telephonyAdapter); await expect(adapter.getSharedCapabilities()).resolves.toBe(sharedCapabilitiesResult); await expect(telephonyAdapter.getAgentConfig()).resolves.toBe(agentConfigResult); await expect(telephonyAdapter.getVoiceCapabilities()).resolves.toBe(voiceCapabilitiesResult); await expect(telephonyAdapter.getActiveCalls()).resolves.toBe(emptyActiveCallsResult); //expect(channelPort.postMessage).toHaveBeenCalledTimes(3); expect(channelPort.postMessage).toHaveBeenNthCalledWith(1, { type: constants.SHARED_MESSAGE_TYPE.LOG, payload: { eventType: "SETUP_CONNECTOR", isError: false, payload: {} } }); expect(channelPort.postMessage).toHaveBeenNthCalledWith(2, { type: constants.SHARED_MESSAGE_TYPE.CONNECTOR_READY, payload: { agentConfig: agentConfigPayload, capabilities: capabilitiesPayload, callInProgress: null } }); assertChannelPortPayloadEventLog({ eventType: constants.SHARED_MESSAGE_TYPE.CONNECTOR_READY, payload: { agentConfig: agentConfigPayload, capabilities: capabilitiesPayload, callInProgress: null }, isError: false }); }); it('Should dispatch CONNECTOR_READY on a failed getAgentConfig invocation', async () => { adapter.init = jest.fn().mockResolvedValue(initResult_connectorReady); telephonyAdapter.getAgentConfig = jest.fn().mockResolvedValue(invalidResult); telephonyAdapter.getActiveCalls = jest.fn().mockResolvedValue(activeCallsResult); telephonyAdapter.getVoiceCapabilities = jest.fn().mockResolvedValue(voiceCapabilitiesResult); eventMap['message'](message); expect(adapter.init).toHaveBeenCalledWith(constants.CONNECTOR_CONFIG); await expect(adapter.init()).resolves.toBe(initResult_connectorReady); await expect(adapter.getSharedCapabilities()).resolves.toBe(sharedCapabilitiesResult); await expect(adapter.getTelephonyConnector()).resolves.toBe(telephonyAdapter); await expect(telephonyAdapter.getAgentConfig()).resolves.toBe(invalidResult); await expect(telephonyAdapter.getVoiceCapabilities()).resolves.toBe(voiceCapabilitiesResult); await expect(telephonyAdapter.getActiveCalls()).resolves.toBe(activeCallsResult); expect(channelPort.postMessage).toHaveBeenCalledWith({ type: constants.SHARED_MESSAGE_TYPE.CONNECTOR_READY, payload: {} }); assertChannelPortPayloadEventLog({ eventType: constants.SHARED_MESSAGE_TYPE.CONNECTOR_READY, payload: {}, isError: false }); }); it('Should NOT dispatch invalid call to the vendor', () => { expect(() => fireMessage(constants.SHARED_MESSAGE_TYPE.INVALID_CALL)).not.toThrowError(); expect(channelPort.postMessage).not.toHaveBeenCalledWith(); }); afterAll(() => { telephonyAdapter.getActiveCalls = jest.fn().mockResolvedValue(activeCallsResult); telephonyAdapter.getAgentConfig = jest.fn().mockResolvedValue(agentConfigResult); telephonyAdapter.getVoiceCapabilities = jest.fn().mockResolvedValue(voiceCapabilitiesResult); }); }); describe('Agent available', () => { it('Should replay active calls on agent available', async () => { telephonyAdapter.getActiveCalls = jest.fn().mockResolvedValue(activeCallsResult1); fireMessage(constants.VOICE_MESSAGE_TYPE.AGENT_AVAILABLE, { isAvailable: true }); await expect(adapter.getTelephonyConnector()).resolves.toBe(telephonyAdapter); await expect(telephonyAdapter.getActiveCalls()).resolves.toBe(activeCallsResult1); assertChannelPortPayload({ eventType: constants.VOICE_EVENT_TYPE.PARTICIPANT_CONNECTED, payload: { phoneNumber: dummyTransferredPhoneCall.contact.phoneNumber, contact:dummyTransferredPhoneCall.contact, callInfo: dummyTransferredPhoneCall.callInfo, callAttributes: dummyTransferredPhoneCall.callAttributes, initialCallHasEnded: dummyTransferredPhoneCall.callAttributes.initialCallHasEnded, callId: dummyTransferredPhoneCall.callId, connectionId: dummyTransferredPhoneCall.connectionId }}); assertChannelPortPayload({ eventType: constants.VOICE_EVENT_TYPE.PARTICIPANT_ADDED, payload: { phoneNumber: dummyTransferringPhoneCall.contact.phoneNumber, contact:dummyTransferredPhoneCall.contact, callInfo: dummyTransferringPhoneCall.callInfo, callAttributes: dummyTransferringPhoneCall.callAttributes, initialCallHasEnded: dummyTransferringPhoneCall.callAttributes.initialCallHasEnded, callId: dummyTransferringPhoneCall.callId, connectionId: dummyTransferredPhoneCall.connectionId } }); assertChannelPortPayload({ eventType: constants.VOICE_EVENT_TYPE.CALL_STARTED, payload: dummyRingingPhoneCall }); assertChannelPortPayload({ eventType: constants.VOICE_EVENT_TYPE.CALL_CONNECTED, payload: dummyConnectedPhoneCall }); assertChannelPortPayload({ eventType: constants.VOICE_EVENT_TYPE.SUPERVISOR_CALL_STARTED, payload: dummySupervisorRingingPhoneCall }); assertChannelPortPayload({ eventType: constants.VOICE_EVENT_TYPE.SUPERVISOR_CALL_CONNECTED, payload: dummySupervisorConnectedPhoneCall }); assertChannelPortPayload({ eventType: constants.VOICE_EVENT_TYPE.SUPERVISOR_BARGED_IN, payload: dummySupervisorBargedInPhoneCall }); }); it('Should replay active calls on agent available with barge in', async () => { await expect(adapter.getTelephonyConnector()).resolves.toBe(telephonyAdapter); telephonyAdapter.getActiveCalls = jest.fn().mockResolvedValue(activeCallsResult1); fireMessage(constants.VOICE_MESSAGE_TYPE.AGENT_AVAILABLE, { isAvailable: true }); await expect(adapter.getTelephonyConnector()).resolves.toBe(telephonyAdapter); await expect(telephonyAdapter.getActiveCalls()).resolves.toBe(activeCallsResult1); assertChannelPortPayload({ eventType: constants.VOICE_EVENT_TYPE.PARTICIPANT_CONNECTED, payload: { phoneNumber: dummyTransferredPhoneCall.contact.phoneNumber, contact: dummyTransferredPhoneCall.contact, callInfo: dummyTransferredPhoneCall.callInfo, callAttributes: dummyTransferredPhoneCall.callAttributes, initialCallHasEnded: dummyTransferredPhoneCall.callAttributes.initialCallHasEnded, callId: dummyTransferredPhoneCall.callId, connectionId: dummyTransferredPhoneCall.connectionId }}); assertChannelPortPayload({ eventType: constants.VOICE_EVENT_TYPE.PARTICIPANT_ADDED, payload: { phoneNumber: dummyTransferringPhoneCall.contact.phoneNumber, contact: dummyTransferredPhoneCall.contact, callInfo: dummyTransferringPhoneCall.callInfo, callAttributes: dummyTransferringPhoneCall.callAttributes, initialCallHasEnded: dummyTransferringPhoneCall.callAttributes.initialCallHasEnded, callId: dummyTransferringPhoneCall.callId, connectionId: dummyTransferredPhoneCall.connectionId } }); assertChannelPortPayload({ eventType: constants.VOICE_EVENT_TYPE.CALL_STARTED, payload: dummyRingingPhoneCall }); assertChannelPortPayload({ eventType: constants.VOICE_EVENT_TYPE.CALL_CONNECTED, payload: dummyConnectedPhoneCall }); assertChannelPortPayload({ eventType: constants.VOICE_EVENT_TYPE.SUPERVISOR_CALL_STARTED, payload: dummySupervisorRingingPhoneCall }); assertChannelPortPayload({ eventType: constants.VOICE_EVENT_TYPE.SUPERVISOR_CALL_CONNECTED, payload: dummySupervisorConnectedPhoneCall }); }); it ('Should NOT replay active calls on when is not replayable', async () => { await expect(adapter.getTelephonyConnector()).resolves.toBe(telephonyAdapter); telephonyAdapter.getActiveCalls = jest.fn().mockResolvedValue(activeCallsResult2); fireMessage(constants.VOICE_MESSAGE_TYPE.AGENT_AVAILABLE, { isAvailable: true }); await expect(telephonyAdapter.getActiveCalls()).resolves.toBe(activeCallsResult2); expect(channelPort.postMessage).toBeCalledTimes(1); }); it ('Should NOT replay active calls on agent un-available', async () => { await expect(adapter.getTelephonyConnector()).resolves.toBe(telephonyAdapter); telephonyAdapter.getActiveCalls = jest.fn().mockResolvedValue(activeCallsResult1); fireMessage(constants.VOICE_MESSAGE_TYPE.AGENT_AVAILABLE, { isAvailable: false }); expect(telephonyAdapter.getActiveCalls).not.toHaveBeenCalled(); }); }); describe('SCVConnectorBase event tests', () => { beforeEach(async () => { adapter.init = jest.fn().mockResolvedValue(initResult_connectorReady); telephonyAdapter.getActiveCalls = jest.fn().mockResolvedValue(activeCallsResult); eventMap['message'](message); expect(adapter.init).toHaveBeenCalledWith(constants.CONNECTOR_CONFIG); await expect(adapter.init()).resolves.toBe(initResult_connectorReady); await expect(adapter.getTelephonyConnector()).resolves.toBe(telephonyAdapter); await expect(telephonyAdapter.getAgentConfig()).resolves.toBe(agentConfigResult); await expect(telephonyAdapter.getVoiceCapabilities()).resolves.toBe(voiceCapabilitiesResult); await expect(telephonyAdapter.getActiveCalls()).resolves.toBe(activeCallsResult); await new Promise((r) => setTimeout(r, 10)); // wait a hundredth of a second for the event to have been fired expect(channelPort.postMessage).toHaveBeenCalledWith({ type: constants.SHARED_MESSAGE_TYPE.CONNECTOR_READY, payload: { agentConfig: agentConfigPayload, capabilities: capabilitiesPayload, callInProgress: dummyPhoneCall } }); assertChannelPortPayloadEventLog({ eventType: constants.SHARED_MESSAGE_TYPE.CONNECTOR_READY, payload: { agentConfig: agentConfigPayload, capabilities: capabilitiesPayload, callInProgress: dummyPhoneCall }, isError: false }); }); describe('acceptCall()', () => { it('Should dispatch CAN_NOT_ACCEPT_THE_CALL on a failed acceptCall() invocation', async () => { telephonyAdapter.acceptCall = jest.fn().mockResolvedValue(invalidResult); fireMessage(constants.VOICE_MESSAGE_TYPE.ACCEPT_CALL); await expect(adapter.getTelephonyConnector