@webex/contact-center 3.12.0-task-refactor.10 → 3.12.0-task-refactor.11

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/src/cc.ts CHANGED
@@ -351,6 +351,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
351
351
  this.$webex.once(READY, () => {
352
352
  // @ts-ignore
353
353
  this.$config = this.config;
354
+ this.validatePluginConfig();
354
355
 
355
356
  /**
356
357
  * This is used for handling the async requests by sending webex.request and wait for corresponding websocket event.
@@ -406,6 +407,16 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
406
407
  this.trigger(TASK_EVENTS.TASK_HYDRATE, task);
407
408
  };
408
409
 
410
+ /**
411
+ * Handles multi-login hydrate events for SDK instances without Mobius registration
412
+ * @private
413
+ * @param {ITask} task The task object associated with the multi-login hydrate
414
+ */
415
+ private handleTaskMultiLoginHydrate = (task: ITask) => {
416
+ // @ts-ignore
417
+ this.trigger(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, task);
418
+ };
419
+
409
420
  /**
410
421
  * Handles task merged events when tasks are combined eg: EPDN merge/transfer
411
422
  * @private
@@ -424,6 +435,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
424
435
  private incomingTaskListener() {
425
436
  this.taskManager.on(TASK_EVENTS.TASK_INCOMING, this.handleIncomingTask);
426
437
  this.taskManager.on(TASK_EVENTS.TASK_HYDRATE, this.handleTaskHydrate);
438
+ this.taskManager.on(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, this.handleTaskMultiLoginHydrate);
427
439
  this.taskManager.on(TASK_EVENTS.TASK_MERGED, this.handleTaskMerged);
428
440
  }
429
441
 
@@ -553,6 +565,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
553
565
 
554
566
  this.taskManager.off(TASK_EVENTS.TASK_INCOMING, this.handleIncomingTask);
555
567
  this.taskManager.off(TASK_EVENTS.TASK_HYDRATE, this.handleTaskHydrate);
568
+ this.taskManager.off(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, this.handleTaskMultiLoginHydrate);
556
569
  this.taskManager.unregisterIncomingCallEvent();
557
570
 
558
571
  this.services.webSocketManager.off('message', this.handleWebsocketMessage);
@@ -769,7 +782,8 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
769
782
  }
770
783
  if (
771
784
  this.agentConfig.webRtcEnabled &&
772
- this.agentConfig.loginVoiceOptions.includes(LoginOption.BROWSER)
785
+ this.agentConfig.loginVoiceOptions.includes(LoginOption.BROWSER) &&
786
+ !this.isWebRTCRegistrationDisabled()
773
787
  ) {
774
788
  this.$webex.internal.mercury
775
789
  .connect()
@@ -785,6 +799,14 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
785
799
  method: METHODS.CONNECT_WEBSOCKET,
786
800
  });
787
801
  });
802
+ } else if (this.isWebRTCRegistrationDisabled()) {
803
+ LoggerProxy.info(
804
+ 'Skipping Mobius registration because disableWebRTCRegistration is enabled',
805
+ {
806
+ module: CC_FILE,
807
+ method: METHODS.CONNECT_WEBSOCKET,
808
+ }
809
+ );
788
810
  }
789
811
  if (this.$config && this.$config.allowAutomatedRelogin) {
790
812
  await this.silentRelogin();
@@ -868,8 +890,24 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
868
890
  },
869
891
  });
870
892
 
871
- if (this.agentConfig.webRtcEnabled && data.loginOption === LoginOption.BROWSER) {
893
+ if (
894
+ this.agentConfig.webRtcEnabled &&
895
+ data.loginOption === LoginOption.BROWSER &&
896
+ !this.isWebRTCRegistrationDisabled()
897
+ ) {
872
898
  await this.webCallingService.registerWebCallingLine();
899
+ } else if (
900
+ this.agentConfig.webRtcEnabled &&
901
+ data.loginOption === LoginOption.BROWSER &&
902
+ this.isWebRTCRegistrationDisabled()
903
+ ) {
904
+ LoggerProxy.info(
905
+ 'Skipping web calling line registration because disableWebRTCRegistration is enabled',
906
+ {
907
+ module: CC_FILE,
908
+ method: METHODS.STATION_LOGIN,
909
+ }
910
+ );
873
911
  }
874
912
 
875
913
  const resp = await loginResponse;
@@ -1235,6 +1273,30 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
1235
1273
  this.taskManager.handleRealtimeWebsocketEvent(event);
1236
1274
  };
1237
1275
 
1276
+ /**
1277
+ * Checks whether Mobius/WebRTC registration should be skipped by config
1278
+ * @returns {boolean} True when browser WebRTC registration is disabled
1279
+ * @private
1280
+ */
1281
+ private isWebRTCRegistrationDisabled(): boolean {
1282
+ return this.$config?.disableWebRTCRegistration === true;
1283
+ }
1284
+
1285
+ /**
1286
+ * Validates contact-center plugin configuration before service initialization
1287
+ * @private
1288
+ */
1289
+ private validatePluginConfig(): void {
1290
+ if (
1291
+ this.$config?.disableWebRTCRegistration === true &&
1292
+ this.$config?.allowMultiLogin === false
1293
+ ) {
1294
+ throw new Error(
1295
+ 'Invalid Contact Center configuration: disableWebRTCRegistration cannot be true when allowMultiLogin is false. Enable allowMultiLogin or allow WebRTC registration so an SDK instance can receive Mobius/WebRTC task events.'
1296
+ );
1297
+ }
1298
+ }
1299
+
1238
1300
  /**
1239
1301
  * Initializes event listeners for the Contact Center service
1240
1302
  * Sets up handlers for connection state changes and other core events
@@ -1380,6 +1442,16 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
1380
1442
  this.agentConfig.deviceType = deviceType;
1381
1443
  switch (deviceType) {
1382
1444
  case LoginOption.BROWSER:
1445
+ if (this.isWebRTCRegistrationDisabled()) {
1446
+ LoggerProxy.info(
1447
+ 'Skipping web calling line registration because disableWebRTCRegistration is enabled',
1448
+ {
1449
+ module: CC_FILE,
1450
+ method: METHODS.HANDLE_DEVICE_TYPE,
1451
+ }
1452
+ );
1453
+ break;
1454
+ }
1383
1455
  try {
1384
1456
  await this.webCallingService.registerWebCallingLine();
1385
1457
  } catch (error) {
package/src/config.ts CHANGED
@@ -25,6 +25,12 @@ export default {
25
25
  * @default true
26
26
  */
27
27
  allowAutomatedRelogin: true,
28
+ /**
29
+ * Whether to skip Mobius/WebRTC registration for browser login flows.
30
+ * @type {boolean}
31
+ * @default false
32
+ */
33
+ disableWebRTCRegistration: false,
28
34
  /**
29
35
  * The type of client making the connection.
30
36
  * @type {string}
@@ -278,7 +278,9 @@ export default abstract class Task extends EventEmitter implements ITask {
278
278
  const currentState = snapshot.value as TaskState;
279
279
  const context = snapshot.context as TaskContext;
280
280
 
281
- return computeUIControls(currentState, context, this.data);
281
+ const uiControls = computeUIControls(currentState, context, this.data);
282
+
283
+ return uiControls;
282
284
  }
283
285
 
284
286
  /**
@@ -418,9 +420,11 @@ export default abstract class Task extends EventEmitter implements ITask {
418
420
  emitTaskOfferContact: this.createEmitSelfAction(TASK_EVENTS.TASK_OFFER_CONTACT, {
419
421
  updateTaskData: true,
420
422
  }),
421
- emitTaskAssigned: this.createEmitSelfAction(TASK_EVENTS.TASK_ASSIGNED, {
422
- updateTaskData: true,
423
- }),
423
+ emitTaskAssigned: ({event}: TaskActionArgs) => {
424
+ this.updateTaskFromEvent(event);
425
+ this.emit(TASK_EVENTS.TASK_ASSIGNED, this);
426
+ this.emit(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, this);
427
+ },
424
428
  emitTaskEnd: this.createEmitSelfAction(TASK_EVENTS.TASK_END, {updateTaskData: true}),
425
429
  emitTaskOfferConsult: this.createEmitSelfAction(TASK_EVENTS.TASK_OFFER_CONSULT, {
426
430
  updateTaskData: true,
@@ -435,6 +439,7 @@ export default abstract class Task extends EventEmitter implements ITask {
435
439
  } else {
436
440
  this.emit(TASK_EVENTS.TASK_CONSULTING, this);
437
441
  }
442
+ this.emit(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, this);
438
443
  },
439
444
  emitTaskConsultAccepted: this.createEmitSelfAction(TASK_EVENTS.TASK_CONSULT_ACCEPTED),
440
445
  emitTaskConsultEnd: this.createEmitSelfAction(TASK_EVENTS.TASK_CONSULT_END, {
@@ -630,6 +630,10 @@ export default class TaskManager extends EventEmitter {
630
630
  this.emit(TASK_EVENTS.TASK_HYDRATE, t);
631
631
  });
632
632
 
633
+ task.on(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, (t: ITask) => {
634
+ this.emit(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, t);
635
+ });
636
+
633
637
  // Listen for internal cleanup signal emitted by the state machine
634
638
  task.on(TASK_EVENTS.TASK_CLEANUP, (t: ITask, options?: {removeFromCollection?: boolean}) => {
635
639
  this.handleTaskCleanup(t);
@@ -438,7 +438,7 @@ function computeVoiceInteractionUIControls(
438
438
  if (allowHeldMainLegControlsForNonInitiator) return VISIBLE_ENABLED;
439
439
  if (showMainLegConferenceControlsDuringConsult) return VISIBLE_DISABLED;
440
440
  if (isHydratedConferenceConsultPending && currentLeg === 'main') return VISIBLE_DISABLED;
441
- if (!config.isEndTaskEnabled) return DISABLED;
441
+ if (!config.isEndTaskEnabled && !isWebrtc) return DISABLED;
442
442
  if (hasParallelConsultLeg) {
443
443
  return isConnected && isEpDnConsult ? VISIBLE_ENABLED : VISIBLE_DISABLED;
444
444
  }
@@ -628,6 +628,17 @@ export enum TASK_EVENTS {
628
628
  * ```
629
629
  */
630
630
  TASK_POST_CALL_ACTIVITY = 'task:postCallActivity',
631
+
632
+ /**
633
+ * Triggered when a multi-login task update should hydrate SDK instances without Mobius registration.
634
+ * @example
635
+ * ```typescript
636
+ * task.on(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, (task: ITask) => {
637
+ * console.log('Multi-login hydrate:', task.data.interactionId);
638
+ * });
639
+ * ```
640
+ */
641
+ TASK_MULTI_LOGIN_HYDRATE = 'task:multiLoginHydrate',
631
642
  }
632
643
 
633
644
  /**
package/src/types.ts CHANGED
@@ -156,6 +156,8 @@ export interface CCPluginConfig {
156
156
  };
157
157
  /** Configuration for the calling client */
158
158
  callingClientConfig: CallingClientConfig;
159
+ /** Whether to skip Mobius/WebRTC registration for browser login flows */
160
+ disableWebRTCRegistration?: boolean;
159
161
  }
160
162
 
161
163
  /**
@@ -195,6 +195,35 @@ describe('webex.cc', () => {
195
195
  webex.emit('ready');
196
196
  });
197
197
 
198
+ it('should throw when WebRTC registration is disabled without multi-login', () => {
199
+ const invalidWebex = MockWebex({
200
+ children: {
201
+ mercury: Mercury,
202
+ },
203
+ logger: {
204
+ log: jest.fn(),
205
+ error: jest.fn(),
206
+ info: jest.fn(),
207
+ },
208
+ credentials: {
209
+ getOrgId: jest.fn(() => 'mockOrgId'),
210
+ },
211
+ config: {
212
+ ...config,
213
+ cc: {
214
+ ...config.cc,
215
+ allowMultiLogin: false,
216
+ disableWebRTCRegistration: true,
217
+ },
218
+ },
219
+ once: jest.fn((event, callback) => callback()),
220
+ }) as unknown as WebexSDK;
221
+
222
+ expect(() => new ContactCenter({parent: invalidWebex})).toThrow(
223
+ 'Invalid Contact Center configuration: disableWebRTCRegistration cannot be true when allowMultiLogin is false. Enable allowMultiLogin or allow WebRTC registration so an SDK instance can receive Mobius/WebRTC task events.'
224
+ );
225
+ });
226
+
198
227
  describe('cc.getDeviceId', () => {
199
228
  it('should return dialNumber when loginOption is EXTENSION', () => {
200
229
  const loginOption = LoginOption.EXTENSION;
@@ -328,6 +357,10 @@ describe('webex.cc', () => {
328
357
  TASK_EVENTS.TASK_HYDRATE,
329
358
  expect.any(Function)
330
359
  );
360
+ expect(mockTaskManager.on).toHaveBeenCalledWith(
361
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
362
+ expect.any(Function)
363
+ );
331
364
  expect(mockWebSocketManager.on).toHaveBeenCalledWith('message', expect.any(Function));
332
365
 
333
366
  expect(configSpy).toHaveBeenCalled();
@@ -475,6 +508,10 @@ describe('webex.cc', () => {
475
508
  TASK_EVENTS.TASK_HYDRATE,
476
509
  expect.any(Function)
477
510
  );
511
+ expect(mockTaskManager.on).toHaveBeenCalledWith(
512
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
513
+ expect.any(Function)
514
+ );
478
515
  expect(mockWebSocketManager.on).toHaveBeenCalledWith('message', expect.any(Function));
479
516
 
480
517
  expect(configSpy).toHaveBeenCalled();
@@ -486,6 +523,36 @@ describe('webex.cc', () => {
486
523
  expect(result).toEqual(mockAgentProfile);
487
524
  });
488
525
 
526
+ it('should skip mercury connection when disableWebRTCRegistration is enabled', async () => {
527
+ webex.cc.$config = {
528
+ ...webex.cc.$config,
529
+ allowAutomatedRelogin: false,
530
+ disableWebRTCRegistration: true,
531
+ };
532
+ mockAgentProfile.webRtcEnabled = true;
533
+ const mercurySpy = jest.spyOn(webex.internal.mercury, 'connect');
534
+ const connectWebsocketSpy = jest.spyOn(webex.cc, 'connectWebsocket');
535
+ const setupEventListenersSpy = jest.spyOn(webex.cc, 'setupEventListeners');
536
+ jest.spyOn(webex.cc.services.config, 'getAgentConfig').mockResolvedValue(mockAgentProfile);
537
+ mockWebSocketManager.initWebSocket.mockResolvedValue({
538
+ agentId: 'agent123',
539
+ });
540
+
541
+ const result = await webex.cc.register();
542
+
543
+ expect(connectWebsocketSpy).toHaveBeenCalled();
544
+ expect(setupEventListenersSpy).toHaveBeenCalled();
545
+ expect(mercurySpy).not.toHaveBeenCalled();
546
+ expect(LoggerProxy.info).toHaveBeenCalledWith(
547
+ 'Skipping Mobius registration because disableWebRTCRegistration is enabled',
548
+ {
549
+ module: CC_FILE,
550
+ method: 'connectWebsocket',
551
+ }
552
+ );
553
+ expect(result).toEqual(mockAgentProfile);
554
+ });
555
+
489
556
  it('should not attempt for mercury connection when webrtc is disabled', async () => {
490
557
  mockAgentProfile.webRtcEnabled = false;
491
558
  const mercurySpy = jest.spyOn(webex.internal.mercury, 'connect');
@@ -658,6 +725,59 @@ describe('webex.cc', () => {
658
725
  );
659
726
  });
660
727
 
728
+ it('should skip web calling line registration when disableWebRTCRegistration is enabled', async () => {
729
+ const options = {
730
+ teamId: 'teamId',
731
+ loginOption: LoginOption.BROWSER,
732
+ };
733
+
734
+ webex.cc.$config = {
735
+ ...webex.cc.$config,
736
+ disableWebRTCRegistration: true,
737
+ };
738
+ webex.cc.agentConfig = {
739
+ agentId: 'agentId',
740
+ webRtcEnabled: true,
741
+ loginVoiceOptions: ['BROWSER', 'EXTENSION', 'AGENT_DN'],
742
+ };
743
+
744
+ const registerWebCallingLineSpy = jest.spyOn(
745
+ webex.cc.webCallingService,
746
+ 'registerWebCallingLine'
747
+ );
748
+
749
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
750
+ data: {
751
+ loginOption: LoginOption.BROWSER,
752
+ agentId: 'agentId',
753
+ teamId: 'teamId',
754
+ siteId: 'siteId',
755
+ roles: [AGENT],
756
+ channelsMap: {
757
+ chat: [],
758
+ email: [],
759
+ social: [],
760
+ telephony: [],
761
+ },
762
+ },
763
+ trackingId: 'notifs_52628',
764
+ orgId: 'orgId',
765
+ type: 'StationLoginSuccess',
766
+ eventType: 'STATION_LOGIN',
767
+ } as unknown as StationLoginSuccess);
768
+
769
+ await webex.cc.stationLogin(options);
770
+
771
+ expect(registerWebCallingLineSpy).not.toHaveBeenCalled();
772
+ expect(LoggerProxy.info).toHaveBeenCalledWith(
773
+ 'Skipping web calling line registration because disableWebRTCRegistration is enabled',
774
+ {
775
+ module: CC_FILE,
776
+ method: 'stationLogin',
777
+ }
778
+ );
779
+ });
780
+
661
781
  it('should not attempt mobius registration for LoginOption.BROWSER if webrtc is disabled', async () => {
662
782
  const options = {
663
783
  teamId: 'teamId',
@@ -1568,6 +1688,10 @@ describe('webex.cc', () => {
1568
1688
  TASK_EVENTS.TASK_HYDRATE,
1569
1689
  expect.any(Function)
1570
1690
  );
1691
+ expect(mockTaskManager.off).toHaveBeenCalledWith(
1692
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
1693
+ expect.any(Function)
1694
+ );
1571
1695
  expect(mockWebSocketManager.off).toHaveBeenCalledWith('message', expect.any(Function));
1572
1696
  expect(webex.cc.services.connectionService.off).toHaveBeenCalledWith(
1573
1697
  'connectionLost',
@@ -1617,6 +1741,13 @@ describe('webex.cc', () => {
1617
1741
  const [, hydrateCallback] = hydrateCalls[0];
1618
1742
  expect(hydrateCallback).toBe(webex.cc['handleTaskHydrate']);
1619
1743
 
1744
+ const multiLoginHydrateCalls = mockTaskManager.off.mock.calls.filter(
1745
+ ([evt]) => evt === TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE
1746
+ );
1747
+ expect(multiLoginHydrateCalls).toHaveLength(1);
1748
+ const [, multiLoginHydrateCallback] = multiLoginHydrateCalls[0];
1749
+ expect(multiLoginHydrateCallback).toBe(webex.cc['handleTaskMultiLoginHydrate']);
1750
+
1620
1751
  const messageCalls = mockWebSocketManager.off.mock.calls.filter(([evt]) => evt === 'message');
1621
1752
  expect(messageCalls).toHaveLength(1);
1622
1753
  const [, messageCallback] = messageCalls[0];
@@ -1642,6 +1773,10 @@ describe('webex.cc', () => {
1642
1773
  TASK_EVENTS.TASK_HYDRATE,
1643
1774
  expect.any(Function)
1644
1775
  );
1776
+ expect(mockTaskManager.off).toHaveBeenCalledWith(
1777
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
1778
+ expect.any(Function)
1779
+ );
1645
1780
  expect(mockWebSocketManager.off).toHaveBeenCalledWith('message', expect.any(Function));
1646
1781
  expect(webex.cc.services.connectionService.off).toHaveBeenCalledWith(
1647
1782
  'connectionLost',
@@ -1687,6 +1822,10 @@ describe('webex.cc', () => {
1687
1822
  TASK_EVENTS.TASK_HYDRATE,
1688
1823
  expect.any(Function)
1689
1824
  );
1825
+ expect(mockTaskManager.off).toHaveBeenCalledWith(
1826
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
1827
+ expect.any(Function)
1828
+ );
1690
1829
 
1691
1830
  expect(LoggerProxy.error).toHaveBeenCalledWith(`Error during deregister: ${mockError}`, {
1692
1831
  module: CC_FILE,
@@ -92,6 +92,10 @@ describe('TaskManager', () => {
92
92
  }
93
93
  }
94
94
 
95
+ if ([TaskEvent.ASSIGN, TaskEvent.CONSULTING_ACTIVE].includes(event.type as TaskEvent)) {
96
+ task.emit(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, task);
97
+ }
98
+
95
99
  // Auto-answer is now handled at the Task layer (triggered by state machine actions)
96
100
  if (
97
101
  [TaskEvent.TASK_OFFERED, TaskEvent.OFFER_CONSULT].includes(event.type as TaskEvent) &&
@@ -494,6 +498,7 @@ describe('TaskManager', () => {
494
498
  taskManager.getTask(payload.data.interactionId),
495
499
  'emit'
496
500
  );
501
+ const taskManagerEmitSpy = jest.spyOn(taskManager, 'emit');
497
502
 
498
503
  webSocketManagerMock.emit('message', JSON.stringify(assignedPayload));
499
504
 
@@ -501,6 +506,10 @@ describe('TaskManager', () => {
501
506
  TASK_EVENTS.TASK_ASSIGNED,
502
507
  taskManager.getTask(taskId)
503
508
  );
509
+ expect(taskManagerEmitSpy).toHaveBeenCalledWith(
510
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
511
+ taskManager.getTask(taskId)
512
+ );
504
513
  });
505
514
 
506
515
  it('should handle WebSocket message for AGENT_CONTACT_RESERVED and emit task:incoming for extension case', () => {
@@ -1837,6 +1846,7 @@ describe('TaskManager', () => {
1837
1846
  taskManager.getTask(taskId).data.isConsulted = false;
1838
1847
  const task = taskManager.getTask(taskId);
1839
1848
  const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1849
+ const taskManagerEmitSpy = jest.spyOn(taskManager, 'emit');
1840
1850
  const consultingPayload = {
1841
1851
  data: {
1842
1852
  ...initalPayload.data,
@@ -1846,6 +1856,10 @@ describe('TaskManager', () => {
1846
1856
  };
1847
1857
  webSocketManagerMock.emit('message', JSON.stringify(consultingPayload));
1848
1858
  expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONSULTING_ACTIVE);
1859
+ expect(taskManagerEmitSpy).toHaveBeenCalledWith(
1860
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
1861
+ taskManager.getTask(taskId)
1862
+ );
1849
1863
  sendStateMachineEventSpy.mockRestore();
1850
1864
  });
1851
1865
 
@@ -1784,6 +1784,53 @@ describe('uiControlsComputer outdial accept/decline controls', () => {
1784
1784
  });
1785
1785
  });
1786
1786
 
1787
+ describe('uiControlsComputer WebRTC call-control end button', () => {
1788
+ function createConnectedContext(voiceVariant: 'webrtc' | 'pstn'): TaskContext {
1789
+ const taskData = createTaskData({
1790
+ interaction: {
1791
+ state: 'connected',
1792
+ } as any,
1793
+ });
1794
+
1795
+ return {
1796
+ taskData,
1797
+ consultInitiator: false,
1798
+ exitingConference: false,
1799
+ consultFromConference: false,
1800
+ transferConferenceRequested: false,
1801
+ consultDestinationType: null,
1802
+ consultDestinationAgentId: null,
1803
+ consultDestinationAgentJoined: false,
1804
+ consultCallHeld: false,
1805
+ recordingControlsAvailable: true,
1806
+ recordingInProgress: true,
1807
+ uiControlConfig: {
1808
+ isEndTaskEnabled: false,
1809
+ isEndConsultEnabled: true,
1810
+ channelType: TASK_CHANNEL_TYPE.VOICE,
1811
+ isRecordingEnabled: true,
1812
+ agentId: taskData.agentId,
1813
+ voiceVariant,
1814
+ },
1815
+ uiControls: getDefaultUIControls(),
1816
+ };
1817
+ }
1818
+
1819
+ it('shows main end for connected WebRTC call-control tasks even when end task is disabled', () => {
1820
+ const context = createConnectedContext('webrtc');
1821
+ const uiControls = computeUIControls(TaskState.CONNECTED, context, context.taskData);
1822
+
1823
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
1824
+ });
1825
+
1826
+ it('keeps main end hidden for non-WebRTC voice tasks when end task is disabled', () => {
1827
+ const context = createConnectedContext('pstn');
1828
+ const uiControls = computeUIControls(TaskState.CONNECTED, context, context.taskData);
1829
+
1830
+ expect(uiControls.main.end).toEqual({isVisible: false, isEnabled: false});
1831
+ });
1832
+ });
1833
+
1787
1834
  describe('uiControlsComputer conference controls', () => {
1788
1835
  function createConferenceTaskData(participantCount: number) {
1789
1836
  const participants: Record<string, any> = {