@webex/plugin-meetings 1.160.0 → 2.1.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.
@@ -646,6 +646,18 @@ export default class Meeting extends StatelessWebexPlugin {
646
646
  * @memberof Meeting
647
647
  */
648
648
  this.mediaConnections = null;
649
+
650
+ /**
651
+ * Fetching meeting info can be done randomly 2-5 mins before meeting start
652
+ * In case it is done before the timer expires, this timeout id is reset to cancel the timer.
653
+ * @instance
654
+ * @type {Number}
655
+ * @readonly
656
+ * @private
657
+ * @memberof Meeting
658
+ */
659
+ this.fetchMeetingInfoTimeoutId = null;
660
+
649
661
  /**
650
662
  * Update the MediaConnections property with new information
651
663
  * @param {array} mediaConnections
@@ -908,15 +920,20 @@ export default class Meeting extends StatelessWebexPlugin {
908
920
  /**
909
921
  * Fetches meeting information.
910
922
  * @param {Object} options
911
- * @param {String} options.destination
912
- * @param {String} options.type
913
- * @private
923
+ * @param {String} [options.password] optional
924
+ * @param {String} [options.captchaCode] optional
925
+ * @public
914
926
  * @memberof Meeting
915
927
  * @returns {Promise}
916
928
  */
917
929
  async fetchMeetingInfo({
918
930
  password = null, captchaCode = null
919
931
  }) {
932
+ // when fetch meeting info is called directly by the client, we want to clear out the random timer for sdk to do it
933
+ if (this.fetchMeetingInfoTimeoutId) {
934
+ clearTimeout(this.fetchMeetingInfoTimeoutId);
935
+ this.fetchMeetingInfoTimeoutId = undefined;
936
+ }
920
937
  if (captchaCode && !this.requiredCaptcha) {
921
938
  return Promise.reject(new Error('fetchMeetingInfo() called with captchaCode when captcha was not required'));
922
939
  }
@@ -929,7 +946,7 @@ export default class Meeting extends StatelessWebexPlugin {
929
946
 
930
947
  const info = await this.attrs.meetingInfoProvider.fetchMeetingInfo(this.destination, this.destinationType, password, captchaInfo);
931
948
 
932
- this.parseMeetingInfo(info);
949
+ this.parseMeetingInfo(info, this.destination);
933
950
  this.meetingInfo = info ? info.body : null;
934
951
  this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.NONE;
935
952
  this.requiredCaptcha = null;
@@ -940,6 +957,15 @@ export default class Meeting extends StatelessWebexPlugin {
940
957
  this.passwordStatus = PASSWORD_STATUS.NOT_REQUIRED;
941
958
  }
942
959
 
960
+ Trigger.trigger(
961
+ this,
962
+ {
963
+ file: 'meetings',
964
+ function: 'fetchMeetingInfo'
965
+ },
966
+ EVENT_TRIGGERS.MEETING_INFO_AVAILABLE
967
+ );
968
+
943
969
  return Promise.resolve();
944
970
  }
945
971
  catch (err) {
@@ -1485,6 +1511,12 @@ export default class Meeting extends StatelessWebexPlugin {
1485
1511
  * meeting:recording:stopped
1486
1512
  * meeting:recording:paused
1487
1513
  * meeting:recording:resumed
1514
+ *
1515
+ * Set up the locus info meeeting container listener
1516
+ * update meetingContainerUrl value for the meeting
1517
+ * notifies consumer with:
1518
+ * meeting:meetingContainer:update
1519
+ *
1488
1520
  * @returns {undefined}
1489
1521
  * @private
1490
1522
  * @memberof Meeting
@@ -1529,6 +1561,19 @@ export default class Meeting extends StatelessWebexPlugin {
1529
1561
  this.recording
1530
1562
  );
1531
1563
  });
1564
+
1565
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_MEETING_CONTAINER_UPDATED,
1566
+ ({meetingContainerUrl}) => {
1567
+ Trigger.trigger(
1568
+ this,
1569
+ {
1570
+ file: 'meeting/index',
1571
+ function: 'setupLocusControlsListener'
1572
+ },
1573
+ EVENT_TRIGGERS.MEETING_MEETING_CONTAINER_UPDATE,
1574
+ {meetingContainerUrl}
1575
+ );
1576
+ });
1532
1577
  }
1533
1578
 
1534
1579
  /**
@@ -2355,24 +2400,32 @@ export default class Meeting extends StatelessWebexPlugin {
2355
2400
  * @param {String} meetingInfo.body.locusUrl
2356
2401
  * @param {String} meetingInfo.body.sipUri
2357
2402
  * @param {Object} meetingInfo.body.owner
2403
+ * @param {Object | String} destination locus object with meeting data or destination string (sip url, meeting link, etc)
2358
2404
  * @returns {undefined}
2359
2405
  * @private
2360
2406
  * @memberof Meeting
2361
2407
  */
2362
- parseMeetingInfo(meetingInfo) {
2408
+ parseMeetingInfo(meetingInfo, destination = null) {
2363
2409
  const webexMeetingInfo = meetingInfo?.body;
2410
+ // We try to use as much info from Locus meeting object, stored in destination
2411
+
2412
+ let locusMeetingObject;
2413
+
2414
+ if (destination) {
2415
+ locusMeetingObject = typeof destination === 'object' ? destination : undefined;
2416
+ }
2364
2417
 
2365
2418
  // MeetingInfo will be undefined for 1:1 calls
2366
- if (webexMeetingInfo && !(meetingInfo.errors && meetingInfo.errors.length > 0)) {
2367
- this.conversationUrl = webexMeetingInfo.conversationUrl || this.conversationUrl;
2368
- this.locusUrl = webexMeetingInfo.locusUrl || this.locusUrl;
2369
- this.setSipUri(this.config.experimental.enableUnifiedMeetings ? webexMeetingInfo.sipUrl : webexMeetingInfo.sipMeetingUri || this.sipUri);
2419
+ if (locusMeetingObject || (webexMeetingInfo && !(meetingInfo?.errors && meetingInfo?.errors.length > 0))) {
2420
+ this.conversationUrl = locusMeetingObject?.conversationUrl || webexMeetingInfo?.conversationUrl || this.conversationUrl;
2421
+ this.locusUrl = locusMeetingObject?.url || webexMeetingInfo?.locusUrl || this.locusUrl;
2422
+ this.setSipUri(this.config.experimental.enableUnifiedMeetings ? locusMeetingObject?.info.sipUri || webexMeetingInfo?.sipUrl : locusMeetingObject?.info.sipUri || webexMeetingInfo?.sipMeetingUri || this.sipUri);
2370
2423
  if (this.config.experimental.enableUnifiedMeetings) {
2371
- this.meetingNumber = webexMeetingInfo.meetingNumber;
2372
- this.meetingJoinUrl = webexMeetingInfo.meetingJoinUrl;
2424
+ this.meetingNumber = locusMeetingObject?.info.webExMeetingId || webexMeetingInfo?.meetingNumber;
2425
+ this.meetingJoinUrl = webexMeetingInfo?.meetingJoinUrl;
2373
2426
  }
2374
- this.owner = webexMeetingInfo.owner || webexMeetingInfo.hostId || this.owner;
2375
- this.permissionToken = webexMeetingInfo.permissionToken;
2427
+ this.owner = locusMeetingObject?.info.owner || webexMeetingInfo?.owner || webexMeetingInfo?.hostId || this.owner;
2428
+ this.permissionToken = webexMeetingInfo?.permissionToken;
2376
2429
  }
2377
2430
  }
2378
2431
 
@@ -4737,19 +4790,7 @@ export default class Meeting extends StatelessWebexPlugin {
4737
4790
  return MeetingUtil.leaveMeeting(this, options)
4738
4791
  .then((leave) => {
4739
4792
  this.meetingFiniteStateMachine.leave();
4740
- this.audio = null;
4741
- this.video = null;
4742
- this.isSharing = false;
4743
- if (this.shareStatus === SHARE_STATUS.LOCAL_SHARE_ACTIVE) {
4744
- this.shareStatus = SHARE_STATUS.NO_SHARE;
4745
- }
4746
- this.queuedMediaUpdates = [];
4747
-
4748
- if (this.transcription) {
4749
- this.transcription.closeSocket();
4750
- this.triggerStopReceivingTranscriptionEvent();
4751
- this.transcription = undefined;
4752
- }
4793
+ this.clearMeetingData();
4753
4794
 
4754
4795
  // upload logs on leave irrespective of meeting delete
4755
4796
  Trigger.trigger(
@@ -5635,4 +5676,90 @@ export default class Meeting extends StatelessWebexPlugin {
5635
5676
 
5636
5677
  return (start && end) ? end - start : undefined;
5637
5678
  }
5679
+
5680
+
5681
+ /**
5682
+ * End the current meeting for all
5683
+ * @returns {Promise}
5684
+ * @public
5685
+ * @memberof Meeting
5686
+ */
5687
+ endMeetingForAll() {
5688
+ Metrics.postEvent({event: eventType.LEAVE, meeting: this, data: {trigger: trigger.USER_INTERACTION, canProceed: false}});
5689
+
5690
+ LoggerProxy.logger.log('Meeting:index#endMeetingForAll --> End meeting for All');
5691
+ Metrics.sendBehavioralMetric(
5692
+ BEHAVIORAL_METRICS.MEETING_END_ALL_INITIATED,
5693
+ {
5694
+ correlation_id: this.correlationId,
5695
+ locus_id: this.locusId
5696
+ }
5697
+ );
5698
+
5699
+ return MeetingUtil.endMeetingForAll(this)
5700
+ .then((end) => {
5701
+ this.meetingFiniteStateMachine.end();
5702
+
5703
+ this.clearMeetingData();
5704
+ // upload logs on leave irrespective of meeting delete
5705
+ Trigger.trigger(
5706
+ this,
5707
+ {
5708
+ file: 'meeting/index',
5709
+ function: 'endMeetingForAll'
5710
+ },
5711
+ EVENTS.REQUEST_UPLOAD_LOGS,
5712
+ this
5713
+ );
5714
+
5715
+ return end;
5716
+ }).catch((error) => {
5717
+ this.meetingFiniteStateMachine.fail(error);
5718
+ LoggerProxy.logger.error('Meeting:index#endMeetingForAll --> Failed to end meeting ', error);
5719
+ // upload logs on leave irrespective of meeting delete
5720
+ Trigger.trigger(
5721
+ this,
5722
+ {
5723
+ file: 'meeting/index',
5724
+ function: 'endMeetingForAll'
5725
+ },
5726
+ EVENTS.REQUEST_UPLOAD_LOGS,
5727
+ this
5728
+ );
5729
+ Metrics.sendBehavioralMetric(
5730
+ BEHAVIORAL_METRICS.MEETING_END_ALL_FAILURE,
5731
+ {
5732
+ correlation_id: this.correlationId,
5733
+ locus_id: this.locusUrl.split('/').pop(),
5734
+ reason: error.message,
5735
+ stack: error.stack,
5736
+ code: error.code
5737
+ }
5738
+ );
5739
+
5740
+ return Promise.reject(error);
5741
+ });
5742
+ }
5743
+
5744
+ /**
5745
+ * clear the meeting data
5746
+ * @returns {undefined}
5747
+ * @public
5748
+ * @memberof Meeting
5749
+ */
5750
+ clearMeetingData = () => {
5751
+ this.audio = null;
5752
+ this.video = null;
5753
+ this.isSharing = false;
5754
+ if (this.shareStatus === SHARE_STATUS.LOCAL_SHARE_ACTIVE) {
5755
+ this.shareStatus = SHARE_STATUS.NO_SHARE;
5756
+ }
5757
+ this.queuedMediaUpdates = [];
5758
+
5759
+ if (this.transcription) {
5760
+ this.transcription.closeSocket();
5761
+ this.triggerStopReceivingTranscriptionEvent();
5762
+ this.transcription = undefined;
5763
+ }
5764
+ };
5638
5765
  }
@@ -10,6 +10,7 @@ import {
10
10
  CALL,
11
11
  CONTROLS,
12
12
  DECLINE,
13
+ END,
13
14
  FLOOR_ACTION,
14
15
  HTTP_VERBS,
15
16
  LEAVE,
@@ -647,4 +648,21 @@ export default class MeetingRequest extends StatelessWebexPlugin {
647
648
  }
648
649
  });
649
650
  }
651
+
652
+ /**
653
+ * Make a network request to end meeting for all
654
+ * @param {Object} options
655
+ * @param {Url} options.locusUrl
656
+ * @returns {Promise}
657
+ */
658
+ endMeetingForAll({
659
+ locusUrl,
660
+ }) {
661
+ const uri = `${locusUrl}/${END}`;
662
+
663
+ return this.request({
664
+ method: HTTP_VERBS.POST,
665
+ uri
666
+ });
667
+ }
650
668
  }
@@ -71,7 +71,18 @@ const MeetingStateMachine = {
71
71
  ],
72
72
  to: MEETING_STATE_MACHINE.STATES.ENDED
73
73
  },
74
- // when declining an incoming meeting it must be from the ringing state, and it moves to DECLINED state
74
+ {
75
+ name: MEETING_STATE_MACHINE.TRANSITIONS.END,
76
+ from: [
77
+ MEETING_STATE_MACHINE.STATES.IDLE,
78
+ MEETING_STATE_MACHINE.STATES.RINGING,
79
+ MEETING_STATE_MACHINE.STATES.JOINED,
80
+ MEETING_STATE_MACHINE.STATES.ANSWERED,
81
+ MEETING_STATE_MACHINE.STATES.DECLINED,
82
+ MEETING_STATE_MACHINE.STATES.ERROR
83
+ ],
84
+ to: MEETING_STATE_MACHINE.STATES.ENDED
85
+ },
75
86
  {
76
87
  name: MEETING_STATE_MACHINE.TRANSITIONS.DECLINE,
77
88
  from: [MEETING_STATE_MACHINE.STATES.RINGING, MEETING_STATE_MACHINE.STATES.ERROR],
@@ -15,7 +15,7 @@ import {
15
15
  STATS,
16
16
  EVENT_TRIGGERS,
17
17
  FULL_STATE,
18
- PASSWORD_STATUS
18
+ PASSWORD_STATUS,
19
19
  } from '../constants';
20
20
  import Trigger from '../common/events/trigger-proxy';
21
21
  import IntentToJoinError from '../common/errors/intent-to-join';
@@ -744,4 +744,34 @@ MeetingUtil.handleDeviceLogging = (devices = []) => {
744
744
  });
745
745
  };
746
746
 
747
+ MeetingUtil.endMeetingForAll = (meeting) => {
748
+ if (meeting.meetingState === FULL_STATE.INACTIVE) {
749
+ return Promise.reject(new MeetingNotActiveError());
750
+ }
751
+
752
+ const endOptions = {
753
+ locusUrl: meeting.locusUrl,
754
+ };
755
+
756
+ return meeting.meetingRequest
757
+ .endMeetingForAll(endOptions)
758
+ .then((response) => {
759
+ if (response && response.body && response.body.locus) {
760
+ meeting.locusInfo.onFullLocus(response.body.locus);
761
+ }
762
+
763
+ return Promise.resolve();
764
+ })
765
+ .then(() => MeetingUtil.cleanUp(meeting))
766
+ .catch((err) => {
767
+ LoggerProxy.logger.error(
768
+ `Meeting:util#endMeetingForAll An error occured while trying to end meeting for all with an id of ${
769
+ meeting.id
770
+ }, error: ${err}`
771
+ );
772
+
773
+ return Promise.reject(err);
774
+ });
775
+ };
776
+
747
777
  export default MeetingUtil;
@@ -21,6 +21,7 @@ import {
21
21
  READY,
22
22
  LOCUSEVENT,
23
23
  LOCUS_URL,
24
+ MAX_RANDOM_DELAY_FOR_MEETING_INFO,
24
25
  ROAP,
25
26
  ONLINE,
26
27
  OFFLINE,
@@ -193,12 +194,13 @@ export default class Meetings extends WebexPlugin {
193
194
  * @param {Object} data a locus event
194
195
  * @param {String} data.locusUrl
195
196
  * @param {Object} data.locus
197
+ * @param {Boolean} useRandomDelayForInfo whether a random delay should be added to fetching meeting info
196
198
  * @param {String} data.eventType
197
199
  * @returns {undefined}
198
200
  * @private
199
201
  * @memberof Meetings
200
202
  */
201
- handleLocusEvent(data) {
203
+ handleLocusEvent(data, useRandomDelayForInfo = false) {
202
204
  let meeting = null;
203
205
 
204
206
  // getting meeting by correlationId. This will happen for the new event
@@ -255,7 +257,7 @@ export default class Meetings extends WebexPlugin {
255
257
  return;
256
258
  }
257
259
 
258
- this.create(data.locus, _LOCUS_ID_).then((newMeeting) => {
260
+ this.create(data.locus, _LOCUS_ID_, useRandomDelayForInfo).then((newMeeting) => {
259
261
  meeting = newMeeting;
260
262
 
261
263
  // It's a new meeting so initialize the locus data
@@ -307,7 +309,7 @@ export default class Meetings extends WebexPlugin {
307
309
  const {eventType} = data;
308
310
 
309
311
  if (eventType && eventType !== LOCUSEVENT.MESSAGE_ROAP) {
310
- this.handleLocusEvent(data);
312
+ this.handleLocusEvent(data, true);
311
313
  }
312
314
  }
313
315
 
@@ -684,11 +686,12 @@ export default class Meetings extends WebexPlugin {
684
686
  * Create a meeting.
685
687
  * @param {string} destination - sipURL, spaceId, phonenumber, or locus object}
686
688
  * @param {string} [type] - the optional specified type, such as locusId
689
+ * @param {Boolean} useRandomDelayForInfo - whether a random delay should be added to fetching meeting info
687
690
  * @returns {Promise<Meeting>} A new Meeting.
688
691
  * @public
689
692
  * @memberof Meetings
690
693
  */
691
- create(destination, type = null) {
694
+ create(destination, type = null, useRandomDelayForInfo = false) {
692
695
  // TODO: type should be from a dictionary
693
696
 
694
697
  // Validate meeting information based on the provided destination and
@@ -726,11 +729,10 @@ export default class Meetings extends WebexPlugin {
726
729
  meeting = this.meetingCollection.getByKey(SIP_URI, targetDest);
727
730
  }
728
731
 
729
-
730
732
  // Validate if a meeting was found.
731
733
  if (!meeting) {
732
734
  // Create a meeting based on the normalized destination and type.
733
- return this.createMeeting(targetDest, type)
735
+ return this.createMeeting(targetDest, type, useRandomDelayForInfo)
734
736
  .then((createdMeeting) => {
735
737
  // If the meeting was successfully created.
736
738
  if (createdMeeting && createdMeeting.on) {
@@ -779,11 +781,12 @@ export default class Meetings extends WebexPlugin {
779
781
  /**
780
782
  * @param {String} destination see create()
781
783
  * @param {String} type see create()
784
+ * @param {Boolean} useRandomDelayForInfo whether a random delay should be added to fetching meeting info
782
785
  * @returns {Promise} a new meeting instance complete with meeting info and destination
783
786
  * @private
784
787
  * @memberof Meetings
785
788
  */
786
- async createMeeting(destination, type = null) {
789
+ async createMeeting(destination, type = null, useRandomDelayForInfo = false) {
787
790
  const meeting = new Meeting(
788
791
  {
789
792
  userId: this.webex.internal.device.userId,
@@ -803,7 +806,31 @@ export default class Meetings extends WebexPlugin {
803
806
  this.meetingCollection.set(meeting);
804
807
 
805
808
  try {
806
- await meeting.fetchMeetingInfo({});
809
+ // if no participant has joined the scheduled meeting (meaning meeting is not active) and we get a locusEvent,
810
+ // it means the meeting will start in 5-6 min. In that case, we want to fetchMeetingInfo
811
+ // between 5 and 2 min (random between 3 minutes) before the meeting starts
812
+ // to avoid a spike in traffic to the wbxappi service
813
+ let waitingTime = 0;
814
+
815
+ if (destination.meeting) {
816
+ const {startTime} = destination.meeting;
817
+ const startTimeDate = new Date(startTime);
818
+ const startTimeDatestamp = startTimeDate.getTime();
819
+ const timeToStart = startTimeDatestamp - Date.now();
820
+ const maxWaitingTime = Math.max(Math.min(timeToStart, MAX_RANDOM_DELAY_FOR_MEETING_INFO), 0);
821
+
822
+ waitingTime = Math.round(Math.random() * maxWaitingTime);
823
+ }
824
+ const isMeetingActive = !!destination.fullState?.active;
825
+ const {enableUnifiedMeetings} = this.config.experimental;
826
+
827
+ if (enableUnifiedMeetings && !isMeetingActive && useRandomDelayForInfo && waitingTime > 0) {
828
+ meeting.fetchMeetingInfoTimeoutId = setTimeout(() => meeting.fetchMeetingInfo({}), waitingTime);
829
+ meeting.parseMeetingInfo(undefined, destination);
830
+ }
831
+ else {
832
+ await meeting.fetchMeetingInfo({});
833
+ }
807
834
  }
808
835
  catch (err) {
809
836
  if (!(err instanceof CaptchaError) && !(err instanceof PasswordError)) {
@@ -13,6 +13,8 @@ const BEHAVIORAL_METRICS = {
13
13
  CONNECTION_SUCCESS: 'js_sdk_connection_success',
14
14
  CONNECTION_FAILURE: 'js_sdk_connection_failures',
15
15
  MEETING_LEAVE_FAILURE: 'js_sdk_meeting_leave_failure',
16
+ MEETING_END_ALL_FAILURE: 'js_sdk_meeting_end_for_all_failure',
17
+ MEETING_END_ALL_INITIATED: 'js_sdk_meeting_end_for_all_initiated',
16
18
  GET_USER_MEDIA_FAILURE: 'js_sdk_get_user_media_failures',
17
19
  GET_DISPLAY_MEDIA_FAILURE: 'js_sdk_get_display_media_failures',
18
20
  JOIN_WITH_MEDIA_FAILURE: 'js_sdk_join_with_media_failures',
@@ -3,6 +3,7 @@ import sinon from 'sinon';
3
3
  import {cloneDeep} from 'lodash';
4
4
  import {assert} from '@webex/test-helper-chai';
5
5
  import MockWebex from '@webex/test-helper-mock-webex';
6
+
6
7
  import Meetings from '@webex/plugin-meetings';
7
8
  import LocusInfo from '@webex/plugin-meetings/src/locus-info';
8
9
  import SelfUtils from '@webex/plugin-meetings/src/locus-info/selfUtils';
@@ -68,7 +69,10 @@ describe('plugin-meetings', () => {
68
69
  }
69
70
  },
70
71
  shareControl: {},
71
- transcribe: {}
72
+ transcribe: {},
73
+ meetingContainer: {
74
+ meetingContainerUrl: 'http://new-url.com'
75
+ }
72
76
  };
73
77
  });
74
78
 
@@ -248,6 +252,52 @@ describe('plugin-meetings', () => {
248
252
  lastModified: 'TODAY'
249
253
  });
250
254
  });
255
+
256
+ it('should update the meetingContainerURL from null', () => {
257
+ locusInfo.controls = {
258
+ meetingContainer: {meetingContainerUrl: null},
259
+ };
260
+
261
+ locusInfo.emitScoped = sinon.stub();
262
+ locusInfo.updateControls(newControls);
263
+
264
+ assert.calledWith(locusInfo.emitScoped, {
265
+ file: 'locus-info',
266
+ function: 'updateControls'
267
+ },
268
+ LOCUSINFO.EVENTS.CONTROLS_MEETING_CONTAINER_UPDATED,
269
+ {meetingContainerUrl: 'http://new-url.com'});
270
+ });
271
+
272
+ it('should update the meetingContainerURL from not null', () => {
273
+ locusInfo.controls = {
274
+ meetingContainer: {meetingContainerUrl: 'http://old-url.com'},
275
+ };
276
+
277
+ locusInfo.emitScoped = sinon.stub();
278
+ locusInfo.updateControls(newControls);
279
+
280
+ assert.calledWith(locusInfo.emitScoped, {
281
+ file: 'locus-info',
282
+ function: 'updateControls'
283
+ },
284
+ LOCUSINFO.EVENTS.CONTROLS_MEETING_CONTAINER_UPDATED,
285
+ {meetingContainerUrl: 'http://new-url.com'});
286
+ });
287
+
288
+ it('should update the meetingContainerURL from missing', () => {
289
+ locusInfo.controls = {};
290
+
291
+ locusInfo.emitScoped = sinon.stub();
292
+ locusInfo.updateControls(newControls);
293
+
294
+ assert.calledWith(locusInfo.emitScoped, {
295
+ file: 'locus-info',
296
+ function: 'updateControls'
297
+ },
298
+ LOCUSINFO.EVENTS.CONTROLS_MEETING_CONTAINER_UPDATED,
299
+ {meetingContainerUrl: 'http://new-url.com'});
300
+ });
251
301
  });
252
302
 
253
303
  describe('#updateParticipants()', () => {