@webex/plugin-meetings 3.6.0-next.9 → 3.7.0

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.
Files changed (88) hide show
  1. package/README.md +2 -1
  2. package/dist/breakouts/breakout.js +1 -1
  3. package/dist/breakouts/index.js +1 -1
  4. package/dist/config.js +2 -1
  5. package/dist/config.js.map +1 -1
  6. package/dist/constants.js +24 -2
  7. package/dist/constants.js.map +1 -1
  8. package/dist/controls-options-manager/enums.js +1 -0
  9. package/dist/controls-options-manager/enums.js.map +1 -1
  10. package/dist/controls-options-manager/index.js +10 -3
  11. package/dist/controls-options-manager/index.js.map +1 -1
  12. package/dist/controls-options-manager/types.js.map +1 -1
  13. package/dist/controls-options-manager/util.js +12 -0
  14. package/dist/controls-options-manager/util.js.map +1 -1
  15. package/dist/interpretation/index.js +1 -1
  16. package/dist/interpretation/siLanguage.js +1 -1
  17. package/dist/locus-info/controlsUtils.js +28 -4
  18. package/dist/locus-info/controlsUtils.js.map +1 -1
  19. package/dist/locus-info/fullState.js +2 -1
  20. package/dist/locus-info/fullState.js.map +1 -1
  21. package/dist/locus-info/index.js +61 -3
  22. package/dist/locus-info/index.js.map +1 -1
  23. package/dist/meeting/in-meeting-actions.js +19 -1
  24. package/dist/meeting/in-meeting-actions.js.map +1 -1
  25. package/dist/meeting/index.js +536 -409
  26. package/dist/meeting/index.js.map +1 -1
  27. package/dist/meetings/index.js +2 -0
  28. package/dist/meetings/index.js.map +1 -1
  29. package/dist/members/index.js +3 -2
  30. package/dist/members/index.js.map +1 -1
  31. package/dist/members/util.js +9 -5
  32. package/dist/members/util.js.map +1 -1
  33. package/dist/networkQualityMonitor/index.js +227 -0
  34. package/dist/networkQualityMonitor/index.js.map +1 -0
  35. package/dist/reachability/index.js +3 -3
  36. package/dist/reachability/index.js.map +1 -1
  37. package/dist/reachability/request.js +2 -1
  38. package/dist/reachability/request.js.map +1 -1
  39. package/dist/rtcMetrics/constants.js +11 -0
  40. package/dist/rtcMetrics/constants.js.map +1 -0
  41. package/dist/rtcMetrics/index.js +197 -0
  42. package/dist/rtcMetrics/index.js.map +1 -0
  43. package/dist/types/config.d.ts +1 -0
  44. package/dist/types/constants.d.ts +19 -0
  45. package/dist/types/controls-options-manager/enums.d.ts +2 -1
  46. package/dist/types/controls-options-manager/index.d.ts +2 -1
  47. package/dist/types/controls-options-manager/types.d.ts +2 -0
  48. package/dist/types/locus-info/index.d.ts +9 -0
  49. package/dist/types/meeting/in-meeting-actions.d.ts +18 -0
  50. package/dist/types/meeting/index.d.ts +12 -1
  51. package/dist/types/members/index.d.ts +2 -1
  52. package/dist/types/members/util.d.ts +3 -1
  53. package/dist/types/networkQualityMonitor/index.d.ts +70 -0
  54. package/dist/types/rtcMetrics/constants.d.ts +4 -0
  55. package/dist/types/rtcMetrics/index.d.ts +71 -0
  56. package/dist/webinar/index.js +32 -19
  57. package/dist/webinar/index.js.map +1 -1
  58. package/package.json +22 -22
  59. package/src/config.ts +1 -0
  60. package/src/constants.ts +25 -0
  61. package/src/controls-options-manager/enums.ts +1 -0
  62. package/src/controls-options-manager/index.ts +19 -2
  63. package/src/controls-options-manager/types.ts +2 -0
  64. package/src/controls-options-manager/util.ts +12 -0
  65. package/src/locus-info/controlsUtils.ts +46 -2
  66. package/src/locus-info/fullState.ts +1 -0
  67. package/src/locus-info/index.ts +60 -0
  68. package/src/meeting/in-meeting-actions.ts +37 -0
  69. package/src/meeting/index.ts +114 -9
  70. package/src/meetings/index.ts +46 -39
  71. package/src/members/index.ts +4 -2
  72. package/src/members/util.ts +3 -1
  73. package/src/reachability/index.ts +3 -3
  74. package/src/reachability/request.ts +1 -0
  75. package/src/webinar/index.ts +31 -17
  76. package/test/unit/spec/controls-options-manager/index.js +56 -32
  77. package/test/unit/spec/controls-options-manager/util.js +44 -0
  78. package/test/unit/spec/locus-info/controlsUtils.js +80 -4
  79. package/test/unit/spec/locus-info/index.js +59 -2
  80. package/test/unit/spec/meeting/in-meeting-actions.ts +18 -0
  81. package/test/unit/spec/meeting/index.js +222 -82
  82. package/test/unit/spec/meetings/index.js +16 -1
  83. package/test/unit/spec/members/index.js +25 -2
  84. package/test/unit/spec/members/request.js +37 -3
  85. package/test/unit/spec/members/utils.js +15 -1
  86. package/test/unit/spec/reachability/index.ts +1 -1
  87. package/test/unit/spec/reachability/request.js +13 -8
  88. package/test/unit/spec/webinar/index.ts +82 -16
@@ -90,8 +90,8 @@ import WebExMeetingsErrors from '../../../../src/common/errors/webex-meetings-er
90
90
  import ParameterError from '../../../../src/common/errors/parameter';
91
91
  import PasswordError from '../../../../src/common/errors/password-error';
92
92
  import CaptchaError from '../../../../src/common/errors/captcha-error';
93
- import PermissionError from '../../../../src/common/errors/permission';
94
- import WebinarRegistrationError from '../../../../src/common/errors/webinar-registration-error';
93
+ import PermissionError from '../../../../src/common/errors/permission';
94
+ import WebinarRegistrationError from '../../../../src/common/errors/webinar-registration-error';
95
95
  import IntentToJoinError from '../../../../src/common/errors/intent-to-join';
96
96
  import testUtils from '../../../utils/testUtils';
97
97
  import {
@@ -377,7 +377,10 @@ describe('plugin-meetings', () => {
377
377
  }
378
378
  );
379
379
  assert.equal(newMeeting.correlationId, newMeeting.id);
380
- assert.deepEqual(newMeeting.callStateForMetrics, {correlationId: newMeeting.id, sessionCorrelationId: ''});
380
+ assert.deepEqual(newMeeting.callStateForMetrics, {
381
+ correlationId: newMeeting.id,
382
+ sessionCorrelationId: '',
383
+ });
381
384
  });
382
385
 
383
386
  it('correlationId can be provided in callStateForMetrics', () => {
@@ -3442,47 +3445,60 @@ describe('plugin-meetings', () => {
3442
3445
  });
3443
3446
  });
3444
3447
 
3445
- it('should pass bundlePolicy to createMediaConnection', async () => {
3448
+ describe('bundlePolicy', () => {
3446
3449
  const FAKE_TURN_URL = 'turns:webex.com:3478';
3447
3450
  const FAKE_TURN_USER = 'some-turn-username';
3448
3451
  const FAKE_TURN_PASSWORD = 'some-password';
3449
3452
 
3450
- meeting.meetingState = 'ACTIVE';
3451
- Media.createMediaConnection.resetHistory();
3452
-
3453
- meeting.roap.doTurnDiscovery = sinon.stub().resolves({
3454
- turnServerInfo: {
3455
- url: FAKE_TURN_URL,
3456
- username: FAKE_TURN_USER,
3457
- password: FAKE_TURN_PASSWORD,
3458
- },
3459
- turnDiscoverySkippedReason: undefined,
3460
- });
3461
- const media = meeting.addMedia({
3462
- mediaSettings: {},
3463
- bundlePolicy: 'bundlePolicy-value',
3464
- });
3453
+ beforeEach(() => {
3454
+ meeting.meetingState = 'ACTIVE';
3455
+ Media.createMediaConnection.resetHistory();
3465
3456
 
3466
- assert.exists(media);
3467
- await media;
3468
- assert.calledOnce(meeting.roap.doTurnDiscovery);
3469
- assert.calledWith(meeting.roap.doTurnDiscovery, meeting, false);
3470
- assert.calledOnce(Media.createMediaConnection);
3471
- assert.calledWith(
3472
- Media.createMediaConnection,
3473
- false,
3474
- meeting.getMediaConnectionDebugId(),
3475
- meeting.id,
3476
- sinon.match({
3457
+ meeting.roap.doTurnDiscovery = sinon.stub().resolves({
3477
3458
  turnServerInfo: {
3478
3459
  url: FAKE_TURN_URL,
3479
3460
  username: FAKE_TURN_USER,
3480
3461
  password: FAKE_TURN_PASSWORD,
3481
3462
  },
3482
- bundlePolicy: 'bundlePolicy-value',
3483
- })
3484
- );
3485
- assert.calledOnce(fakeMediaConnection.initiateOffer);
3463
+ turnDiscoverySkippedReason: undefined,
3464
+ });
3465
+ });
3466
+
3467
+ const runCheck = async (bundlePolicy, expectedValue) => {
3468
+ const media = meeting.addMedia({
3469
+ mediaSettings: {},
3470
+ bundlePolicy,
3471
+ });
3472
+
3473
+ assert.exists(media);
3474
+ await media;
3475
+ assert.calledOnce(meeting.roap.doTurnDiscovery);
3476
+ assert.calledWith(meeting.roap.doTurnDiscovery, meeting, false);
3477
+ assert.calledOnce(Media.createMediaConnection);
3478
+ assert.calledWith(
3479
+ Media.createMediaConnection,
3480
+ false,
3481
+ meeting.getMediaConnectionDebugId(),
3482
+ meeting.id,
3483
+ sinon.match({
3484
+ turnServerInfo: {
3485
+ url: FAKE_TURN_URL,
3486
+ username: FAKE_TURN_USER,
3487
+ password: FAKE_TURN_PASSWORD,
3488
+ },
3489
+ bundlePolicy: expectedValue,
3490
+ })
3491
+ );
3492
+ assert.calledOnce(fakeMediaConnection.initiateOffer);
3493
+ };
3494
+
3495
+ it('should pass bundlePolicy to createMediaConnection', async () => {
3496
+ await runCheck('max-compat', 'max-compat');
3497
+ });
3498
+
3499
+ it('should pass max-bundle to createMediaConnection if bundlePolicy is not provided', async () => {
3500
+ await runCheck(undefined, 'max-bundle');
3501
+ });
3486
3502
  });
3487
3503
 
3488
3504
  it('succeeds even if getDevices() throws', async () => {
@@ -3795,12 +3811,12 @@ describe('plugin-meetings', () => {
3795
3811
  id: 'fake locus from mocked join request',
3796
3812
  locusUrl: 'fake locus url',
3797
3813
  mediaId: 'fake media id',
3798
- })
3814
+ });
3799
3815
  sinon.stub(meeting.meetingRequest, 'joinMeeting').resolves({
3800
3816
  headers: {
3801
3817
  trackingid: 'fake tracking id',
3802
- }
3803
- })
3818
+ },
3819
+ });
3804
3820
  await meeting.join({enableMultistream: isMultistream});
3805
3821
  });
3806
3822
 
@@ -3993,7 +4009,10 @@ describe('plugin-meetings', () => {
3993
4009
  assert.notCalled(
3994
4010
  meeting.sendSlotManager.getSlot(MediaType.AudioMain).publishStream
3995
4011
  );
3996
- assert.throws(meeting.publishStreams(localStreams), `Attempted to publish microphone stream with ended readyState, correlationId=${meeting.correlationId}`);
4012
+ assert.throws(
4013
+ meeting.publishStreams(localStreams),
4014
+ `Attempted to publish microphone stream with ended readyState, correlationId=${meeting.correlationId}`
4015
+ );
3997
4016
  } else {
3998
4017
  assert.calledOnceWithExactly(
3999
4018
  meeting.sendSlotManager.getSlot(MediaType.AudioMain).publishStream,
@@ -4006,7 +4025,10 @@ describe('plugin-meetings', () => {
4006
4025
  assert.notCalled(
4007
4026
  meeting.sendSlotManager.getSlot(MediaType.VideoMain).publishStream
4008
4027
  );
4009
- assert.throws(meeting.publishStreams(localStreams), `Attempted to publish camera stream with ended readyState, correlationId=${meeting.correlationId}`);
4028
+ assert.throws(
4029
+ meeting.publishStreams(localStreams),
4030
+ `Attempted to publish camera stream with ended readyState, correlationId=${meeting.correlationId}`
4031
+ );
4010
4032
  } else {
4011
4033
  assert.calledOnceWithExactly(
4012
4034
  meeting.sendSlotManager.getSlot(MediaType.VideoMain).publishStream,
@@ -4019,7 +4041,10 @@ describe('plugin-meetings', () => {
4019
4041
  assert.notCalled(
4020
4042
  meeting.sendSlotManager.getSlot(MediaType.AudioSlides).publishStream
4021
4043
  );
4022
- assert.throws(meeting.publishStreams(localStreams), `Attempted to publish screenShare audio stream with ended readyState, correlationId=${meeting.correlationId}`);
4044
+ assert.throws(
4045
+ meeting.publishStreams(localStreams),
4046
+ `Attempted to publish screenShare audio stream with ended readyState, correlationId=${meeting.correlationId}`
4047
+ );
4023
4048
  } else {
4024
4049
  assert.calledOnceWithExactly(
4025
4050
  meeting.sendSlotManager.getSlot(MediaType.AudioSlides).publishStream,
@@ -4032,7 +4057,10 @@ describe('plugin-meetings', () => {
4032
4057
  assert.notCalled(
4033
4058
  meeting.sendSlotManager.getSlot(MediaType.VideoSlides).publishStream
4034
4059
  );
4035
- assert.throws(meeting.publishStreams(localStreams), `Attempted to publish screenShare video stream with ended readyState, correlationId=${meeting.correlationId}`);
4060
+ assert.throws(
4061
+ meeting.publishStreams(localStreams),
4062
+ `Attempted to publish screenShare video stream with ended readyState, correlationId=${meeting.correlationId}`
4063
+ );
4036
4064
  } else {
4037
4065
  assert.calledOnceWithExactly(
4038
4066
  meeting.sendSlotManager.getSlot(MediaType.VideoSlides).publishStream,
@@ -4327,14 +4355,14 @@ describe('plugin-meetings', () => {
4327
4355
  const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
4328
4356
  await meeting.addMedia({audioEnabled: false});
4329
4357
  //calling handleDeviceLogging with audioEnaled as true adn videoEnabled as false
4330
- assert.calledWith(handleDeviceLoggingSpy,false,true);
4358
+ assert.calledWith(handleDeviceLoggingSpy, false, true);
4331
4359
  });
4332
4360
 
4333
4361
  it('addMedia() works correctly when video is disabled with no streams to publish', async () => {
4334
4362
  const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
4335
4363
  await meeting.addMedia({videoEnabled: false});
4336
4364
  //calling handleDeviceLogging audioEnabled as true videoEnabled as false
4337
- assert.calledWith(handleDeviceLoggingSpy,true,false);
4365
+ assert.calledWith(handleDeviceLoggingSpy, true, false);
4338
4366
  });
4339
4367
 
4340
4368
  it('addMedia() works correctly when video is disabled with no streams to publish', async () => {
@@ -4403,12 +4431,11 @@ describe('plugin-meetings', () => {
4403
4431
  assert.calledTwice(locusMediaRequestStub);
4404
4432
  });
4405
4433
 
4406
-
4407
4434
  it('addMedia() works correctly when both shareAudio and shareVideo is disabled with no streams publish', async () => {
4408
4435
  const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
4409
4436
  await meeting.addMedia({shareAudioEnabled: false, shareVideoEnabled: false});
4410
4437
  //calling handleDeviceLogging with audioEnabled true and videoEnabled as true
4411
- assert.calledWith(handleDeviceLoggingSpy,true,true);
4438
+ assert.calledWith(handleDeviceLoggingSpy, true, true);
4412
4439
  });
4413
4440
 
4414
4441
  describe('publishStreams()/unpublishStreams() calls', () => {
@@ -6263,14 +6290,22 @@ describe('plugin-meetings', () => {
6263
6290
  meeting.attrs.meetingInfoProvider = {
6264
6291
  fetchMeetingInfo: sinon
6265
6292
  .stub()
6266
- .throws(new MeetingInfoV2WebinarRegistrationError(403021, FAKE_MEETING_INFO, 'a message')),
6293
+ .throws(
6294
+ new MeetingInfoV2WebinarRegistrationError(403021, FAKE_MEETING_INFO, 'a message')
6295
+ ),
6267
6296
  };
6268
6297
 
6269
- await assert.isRejected(meeting.fetchMeetingInfo({sendCAevents: true}), WebinarRegistrationError);
6298
+ await assert.isRejected(
6299
+ meeting.fetchMeetingInfo({sendCAevents: true}),
6300
+ WebinarRegistrationError
6301
+ );
6270
6302
 
6271
6303
  assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
6272
6304
  assert.equal(meeting.meetingInfoFailureCode, 403021);
6273
- assert.equal(meeting.meetingInfoFailureReason, MEETING_INFO_FAILURE_REASON.WEBINAR_REGISTRATION);
6305
+ assert.equal(
6306
+ meeting.meetingInfoFailureReason,
6307
+ MEETING_INFO_FAILURE_REASON.WEBINAR_REGISTRATION
6308
+ );
6274
6309
  });
6275
6310
  });
6276
6311
 
@@ -6985,7 +7020,10 @@ describe('plugin-meetings', () => {
6985
7020
  assert.deepEqual(meeting.callStateForMetrics, {correlationId, sessionCorrelationId: ''});
6986
7021
  meeting.setCorrelationId(uuid1);
6987
7022
  assert.equal(meeting.correlationId, uuid1);
6988
- assert.deepEqual(meeting.callStateForMetrics, {correlationId: uuid1, sessionCorrelationId: ''});
7023
+ assert.deepEqual(meeting.callStateForMetrics, {
7024
+ correlationId: uuid1,
7025
+ sessionCorrelationId: '',
7026
+ });
6989
7027
  });
6990
7028
  });
6991
7029
 
@@ -7657,11 +7695,11 @@ describe('plugin-meetings', () => {
7657
7695
  id: 'stream',
7658
7696
  getTracks: () => [{id: 'track', addEventListener: sinon.stub()}],
7659
7697
  };
7660
- const simulateConnectionStateChange = (newState) => {
7698
+ const simulateConnectionStateChange = async (newState) => {
7661
7699
  meeting.mediaProperties.webrtcMediaConnection.getConnectionState = sinon
7662
7700
  .stub()
7663
7701
  .returns(newState);
7664
- eventListeners[MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED]();
7702
+ await eventListeners[MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED]();
7665
7703
  };
7666
7704
 
7667
7705
  beforeEach(() => {
@@ -7731,11 +7769,17 @@ describe('plugin-meetings', () => {
7731
7769
  });
7732
7770
 
7733
7771
  it('should collect ice candidates', () => {
7734
- eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({candidate: 'candidate'});
7772
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({candidate: {candidate: 'candidate'}});
7735
7773
 
7736
7774
  assert.equal(meeting.iceCandidatesCount, 1);
7737
7775
  });
7738
7776
 
7777
+ it('should not collect empty ice candidates', () => {
7778
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({candidate: {candidate: ''}});
7779
+
7780
+ assert.equal(meeting.iceCandidatesCount, 0);
7781
+ });
7782
+
7739
7783
  it('should not collect null ice candidates', () => {
7740
7784
  eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({candidate: null});
7741
7785
 
@@ -7917,7 +7961,7 @@ describe('plugin-meetings', () => {
7917
7961
  meeting.reconnectionManager = new ReconnectionManager(meeting);
7918
7962
  meeting.reconnectionManager.iceReconnected = sinon.stub().returns(undefined);
7919
7963
  meeting.setNetworkStatus = sinon.stub().returns(undefined);
7920
- meeting.statsAnalyzer = {startAnalyzer: sinon.stub()};
7964
+ meeting.statsAnalyzer = {startAnalyzer: sinon.stub(), stopAnalyzer: sinon.stub()};
7921
7965
  meeting.mediaProperties.webrtcMediaConnection = {
7922
7966
  // mock the on() method and store all the listeners
7923
7967
  on: sinon.stub().callsFake((event, listener) => {
@@ -7992,10 +8036,10 @@ describe('plugin-meetings', () => {
7992
8036
  });
7993
8037
 
7994
8038
  describe('CONNECTION_STATE_CHANGED event when state = "Failed"', () => {
7995
- const mockFailedEvent = () => {
8039
+ const mockFailedEvent = async () => {
7996
8040
  meeting.setupMediaConnectionListeners();
7997
8041
 
7998
- simulateConnectionStateChange(ConnectionState.Failed);
8042
+ await simulateConnectionStateChange(ConnectionState.Failed);
7999
8043
  };
8000
8044
 
8001
8045
  const checkBehavioralMetricSent = (hasMediaConnectionConnectedAtLeastOnce = false) => {
@@ -8025,6 +8069,22 @@ describe('plugin-meetings', () => {
8025
8069
  assert.notCalled(webex.internal.newMetrics.submitClientEvent);
8026
8070
  checkBehavioralMetricSent(true);
8027
8071
  });
8072
+
8073
+ it('stop stats analyzer during reconnection ', async () => {
8074
+ meeting.hasMediaConnectionConnectedAtLeastOnce = true;
8075
+ meeting.statsAnalyzer.stopAnalyzer = sinon.stub().resolves();
8076
+ meeting.reconnectionManager = {
8077
+ reconnect: sinon.stub().resolves(),
8078
+ resetReconnectionTimer: () => {}
8079
+ };
8080
+ meeting.currentMediaStatus = {
8081
+ video: true
8082
+ };
8083
+
8084
+ await mockFailedEvent();
8085
+
8086
+ assert.calledOnce(meeting.statsAnalyzer.stopAnalyzer);
8087
+ });
8028
8088
  });
8029
8089
 
8030
8090
  describe('should send correct metrics for ROAP_FAILURE event', () => {
@@ -8885,6 +8945,78 @@ describe('plugin-meetings', () => {
8885
8945
  );
8886
8946
  });
8887
8947
 
8948
+ it('listens to MEETING_CONTROLS_WEBCAST_UPDATED', async () => {
8949
+ const state = {example: 'value'};
8950
+
8951
+ await meeting.locusInfo.emitScoped(
8952
+ {function: 'test', file: 'test'},
8953
+ LOCUSINFO.EVENTS.CONTROLS_WEBCAST_CHANGED,
8954
+ {state}
8955
+ );
8956
+
8957
+ assert.calledWith(
8958
+ TriggerProxy.trigger,
8959
+ meeting,
8960
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
8961
+ EVENT_TRIGGERS.MEETING_CONTROLS_WEBCAST_UPDATED,
8962
+ {state}
8963
+ );
8964
+ });
8965
+
8966
+ it('listens to MEETING_CONTROLS_MEETING_FULL_UPDATED', async () => {
8967
+ const state = {example: 'value'};
8968
+
8969
+ await meeting.locusInfo.emitScoped(
8970
+ {function: 'test', file: 'test'},
8971
+ LOCUSINFO.EVENTS.CONTROLS_MEETING_FULL_CHANGED,
8972
+ {state}
8973
+ );
8974
+
8975
+ assert.calledWith(
8976
+ TriggerProxy.trigger,
8977
+ meeting,
8978
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
8979
+ EVENT_TRIGGERS.MEETING_CONTROLS_MEETING_FULL_UPDATED,
8980
+ {state}
8981
+ );
8982
+ });
8983
+
8984
+ it('listens to MEETING_CONTROLS_PRACTICE_SESSION_STATUS_UPDATED', async () => {
8985
+ const state = {example: 'value'};
8986
+
8987
+ await meeting.locusInfo.emitScoped(
8988
+ {function: 'test', file: 'test'},
8989
+ LOCUSINFO.EVENTS.CONTROLS_PRACTICE_SESSION_STATUS_UPDATED,
8990
+ {state}
8991
+ );
8992
+
8993
+ assert.calledWith(
8994
+ TriggerProxy.trigger,
8995
+ meeting,
8996
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
8997
+ EVENT_TRIGGERS.MEETING_CONTROLS_PRACTICE_SESSION_STATUS_UPDATED,
8998
+ {state}
8999
+ );
9000
+ });
9001
+
9002
+ it('listens to MEETING_CONTROLS_STAGE_VIEW_UPDATED', async () => {
9003
+ const state = {example: 'value'};
9004
+
9005
+ await meeting.locusInfo.emitScoped(
9006
+ {function: 'test', file: 'test'},
9007
+ LOCUSINFO.EVENTS.CONTROLS_STAGE_VIEW_UPDATED,
9008
+ {state}
9009
+ );
9010
+
9011
+ assert.calledWith(
9012
+ TriggerProxy.trigger,
9013
+ meeting,
9014
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
9015
+ EVENT_TRIGGERS.MEETING_CONTROLS_STAGE_VIEW_UPDATED,
9016
+ {state}
9017
+ );
9018
+ });
9019
+
8888
9020
  it('listens to MEETING_CONTROLS_VIDEO_UPDATED', async () => {
8889
9021
  const state = {example: 'value'};
8890
9022
 
@@ -8998,12 +9130,6 @@ describe('plugin-meetings', () => {
8998
9130
  approval: {
8999
9131
  url: 'url',
9000
9132
  },
9001
- webcast: {
9002
- url: 'url',
9003
- },
9004
- webinarAttendeesSearching: {
9005
- url: 'url',
9006
- },
9007
9133
  },
9008
9134
  };
9009
9135
 
@@ -9017,10 +9143,6 @@ describe('plugin-meetings', () => {
9017
9143
  meeting.simultaneousInterpretation = {
9018
9144
  approvalUrlUpdate: sinon.stub().returns(undefined),
9019
9145
  };
9020
- meeting.webinar = {
9021
- webcastUrlUpdate: sinon.stub().returns(undefined),
9022
- webinarAttendeesSearchingUrlUpdate: sinon.stub().returns(undefined),
9023
- };
9024
9146
 
9025
9147
  meeting.locusInfo.emit(
9026
9148
  {function: 'test', file: 'test'},
@@ -9040,19 +9162,37 @@ describe('plugin-meetings', () => {
9040
9162
  meeting.simultaneousInterpretation.approvalUrlUpdate,
9041
9163
  newLocusServices.services.approval.url
9042
9164
  );
9043
- assert.calledWith(
9044
- meeting.webinar.webcastUrlUpdate,
9045
- newLocusServices.services.webcast.url
9046
- );
9047
- assert.calledWith(
9048
- meeting.webinar.webinarAttendeesSearchingUrlUpdate,
9049
- newLocusServices.services.webinarAttendeesSearching.url
9050
- );
9051
9165
  assert.calledOnce(meeting.recordingController.setSessionId);
9052
9166
  done();
9053
9167
  });
9054
9168
  });
9055
9169
 
9170
+ describe('#setUpLocusResourcesListener', () => {
9171
+ it('listens to the locus resources update event', (done) => {
9172
+ const newLocusResources = {
9173
+ resources: {
9174
+ webcastInstance: {
9175
+ url: 'url',
9176
+ },
9177
+ },
9178
+ };
9179
+
9180
+ meeting.webinar = {
9181
+ updateWebcastUrl: sinon.stub().returns(undefined),
9182
+ };
9183
+
9184
+ meeting.locusInfo.emit(
9185
+ {function: 'test', file: 'test'},
9186
+ 'LINKS_RESOURCES',
9187
+ newLocusResources
9188
+ );
9189
+
9190
+ assert.calledWith(meeting.webinar.updateWebcastUrl, newLocusResources);
9191
+
9192
+ done();
9193
+ });
9194
+ });
9195
+
9056
9196
  describe('#setUpLocusInfoMediaInactiveListener', () => {
9057
9197
  it('listens to disconnect due to un activity ', (done) => {
9058
9198
  TriggerProxy.trigger.reset();
@@ -12205,14 +12345,10 @@ describe('plugin-meetings', () => {
12205
12345
  const testEmit = async (unmuteAllowed) => {
12206
12346
  meeting.audio = {
12207
12347
  handleServerLocalUnmuteRequired: sinon.stub(),
12208
- }
12209
- await meeting.locusInfo.emitScoped(
12210
- {},
12211
- LOCUSINFO.EVENTS.LOCAL_UNMUTE_REQUIRED,
12212
- {
12213
- unmuteAllowed,
12214
- }
12215
- );
12348
+ };
12349
+ await meeting.locusInfo.emitScoped({}, LOCUSINFO.EVENTS.LOCAL_UNMUTE_REQUIRED, {
12350
+ unmuteAllowed,
12351
+ });
12216
12352
 
12217
12353
  assert.calledWith(
12218
12354
  TriggerProxy.trigger,
@@ -12228,7 +12364,11 @@ describe('plugin-meetings', () => {
12228
12364
  },
12229
12365
  }
12230
12366
  );
12231
- assert.calledOnceWithExactly(meeting.audio.handleServerLocalUnmuteRequired, meeting, unmuteAllowed)
12367
+ assert.calledOnceWithExactly(
12368
+ meeting.audio.handleServerLocalUnmuteRequired,
12369
+ meeting,
12370
+ unmuteAllowed
12371
+ );
12232
12372
  };
12233
12373
 
12234
12374
  [true, false].forEach((unmuteAllowed) => {
@@ -2077,6 +2077,21 @@ describe('plugin-meetings', () => {
2077
2077
  ]);
2078
2078
  });
2079
2079
 
2080
+ it('should handle failure to get user information if scopes are insufficient', async () => {
2081
+ loggerProxySpy = sinon.spy(LoggerProxy.logger, 'error');
2082
+ Object.assign(webex.people, {
2083
+ _getMe: sinon.stub().returns(Promise.reject()),
2084
+ });
2085
+
2086
+ await webex.meetings.fetchUserPreferredWebexSite();
2087
+
2088
+ assert.equal(webex.meetings.preferredWebexSite, '');
2089
+ assert.calledOnceWithExactly(
2090
+ loggerProxySpy,
2091
+ 'Failed to retrieve user information. No preferredWebexSite will be set'
2092
+ );
2093
+ });
2094
+
2080
2095
  const setup = ({me = { type: 'validuser'}, user} = {}) => {
2081
2096
  loggerProxySpy = sinon.spy(LoggerProxy.logger, 'error');
2082
2097
  assert.deepEqual(webex.internal.services._getCatalog().getAllowedDomains(), []);
@@ -2093,7 +2108,7 @@ describe('plugin-meetings', () => {
2093
2108
 
2094
2109
  Object.assign(webex.people, {
2095
2110
  _getMe: sinon.stub().returns(Promise.resolve(me)),
2096
- });
2111
+ });
2097
2112
  };
2098
2113
 
2099
2114
  it('should not call request.getMeetingPreferences if user is a guest', async () => {
@@ -660,17 +660,20 @@ describe('plugin-meetings', () => {
660
660
  resultPromise,
661
661
  spies,
662
662
  expectedRequestingMemberId,
663
- expectedLocusUrl
663
+ expectedLocusUrl,
664
+ expectedRoles,
664
665
  ) => {
665
666
  await assert.isFulfilled(resultPromise);
666
667
  assert.calledOnceWithExactly(
667
668
  spies.generateLowerAllHandsMemberOptions,
668
669
  expectedRequestingMemberId,
669
- expectedLocusUrl
670
+ expectedLocusUrl,
671
+ expectedRoles,
670
672
  );
671
673
  assert.calledOnceWithExactly(spies.lowerAllHandsMember, {
672
674
  requestingParticipantId: expectedRequestingMemberId,
673
675
  locusUrl: expectedLocusUrl,
676
+ ...(expectedRoles !== undefined && { roles: expectedRoles })
674
677
  });
675
678
  assert.strictEqual(resultPromise, spies.lowerAllHandsMember.getCall(0).returnValue);
676
679
  };
@@ -707,6 +710,26 @@ describe('plugin-meetings', () => {
707
710
 
708
711
  await checkValid(resultPromise, spies, requestingMemberId, url1);
709
712
  });
713
+
714
+ it('should make the correct request when called with valid requestingMemberId and roles', async () => {
715
+ const requestingMemberId = 'test-member-id';
716
+ const roles = ['panelist', 'attendee'];
717
+ const { members, spies } = setup('test-locus-url');
718
+
719
+ const resultPromise = members.lowerAllHands(requestingMemberId, roles);
720
+
721
+ await checkValid(resultPromise, spies, requestingMemberId, 'test-locus-url', roles);
722
+ });
723
+
724
+ it('should handle an empty roles array correctly', async () => {
725
+ const requestingMemberId = 'test-member-id';
726
+ const roles = [];
727
+ const { members, spies } = setup('test-locus-url');
728
+
729
+ const resultPromise = members.lowerAllHands(requestingMemberId, roles);
730
+
731
+ await checkValid(resultPromise, spies, requestingMemberId, 'test-locus-url', roles);
732
+ });
710
733
  });
711
734
 
712
735
  describe('#editDisplayName', () => {
@@ -225,7 +225,7 @@ describe('plugin-meetings', () => {
225
225
  });
226
226
 
227
227
  describe('#assignRolesMember', () => {
228
- it('sends a PATCH to the locus endpoint', async () => {
228
+ it('sends a assignRolesMember PATCH to the locus endpoint', async () => {
229
229
  const locusUrl = url1;
230
230
  const memberId = 'test1';
231
231
  const roles = [
@@ -255,7 +255,7 @@ describe('plugin-meetings', () => {
255
255
  });
256
256
 
257
257
  describe('#raiseHand', () => {
258
- it('sends a PATCH to the locus endpoint', async () => {
258
+ it('sends a raiseOrLowerHandMember PATCH to the locus endpoint', async () => {
259
259
  const locusUrl = url1;
260
260
  const memberId = 'test1';
261
261
 
@@ -319,7 +319,7 @@ describe('plugin-meetings', () => {
319
319
  assert.strictEqual(result, requestResponse);
320
320
  });
321
321
 
322
- it('sends a PATCH to the locus endpoint', async () => {
322
+ it('sends a lowerAllHandsMember PATCH to the locus endpoint', async () => {
323
323
  const locusUrl = url1;
324
324
  const memberId = 'test1';
325
325
 
@@ -348,6 +348,40 @@ describe('plugin-meetings', () => {
348
348
  },
349
349
  });
350
350
  });
351
+
352
+ it('sends a lowerAllHandsMember PATCH to the locus endpoint with roles', async () => {
353
+ const locusUrl = url1;
354
+ const memberId = 'test1';
355
+ const roles = ['attendee'];
356
+
357
+ const options = {
358
+ requestingParticipantId: memberId,
359
+ locusUrl,
360
+ roles,
361
+ };
362
+
363
+ const getRequestParamsSpy = sandbox.spy(membersUtil, 'getLowerAllHandsMemberRequestParams');
364
+
365
+ await membersRequest.lowerAllHandsMember(options);
366
+
367
+ assert.calledOnceWithExactly(getRequestParamsSpy, {
368
+ requestingParticipantId: memberId,
369
+ locusUrl: url1,
370
+ roles: ['attendee'],
371
+ });
372
+
373
+ checkRequest({
374
+ method: 'PATCH',
375
+ uri: `${locusUrl}/controls`,
376
+ body: {
377
+ hand: {
378
+ raised: false,
379
+ roles: ['attendee'],
380
+ },
381
+ requestingParticipantId: memberId,
382
+ },
383
+ });
384
+ });
351
385
  });
352
386
 
353
387
  describe('#editDisplayName', () => {
@@ -101,7 +101,7 @@ describe('plugin-meetings', () => {
101
101
  });
102
102
  });
103
103
  describe('#generateLowerAllHandsMemberOptions', () => {
104
- it('returns the correct options', () => {
104
+ it('returns the correct options without roles', () => {
105
105
  const requestingParticipantId = 'test';
106
106
  const locusUrl = 'urlTest1';
107
107
 
@@ -113,6 +113,20 @@ describe('plugin-meetings', () => {
113
113
  }
114
114
  );
115
115
  });
116
+ it('returns the correct options with roles', () => {
117
+ const requestingParticipantId = 'test';
118
+ const locusUrl = 'urlTest1';
119
+ const roles = ['panelist'];
120
+
121
+ assert.deepEqual(
122
+ MembersUtil.generateLowerAllHandsMemberOptions(requestingParticipantId, locusUrl, roles),
123
+ {
124
+ requestingParticipantId,
125
+ locusUrl,
126
+ roles,
127
+ }
128
+ );
129
+ });
116
130
  });
117
131
  describe('#generateEditDisplayNameMemberOptions', () => {
118
132
  it('returns the correct options', () => {