@webex/plugin-meetings 3.9.0-next.14 → 3.9.0-next.16

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/package.json CHANGED
@@ -92,5 +92,5 @@
92
92
  "//": [
93
93
  "TODO: upgrade jwt-decode when moving to node 18"
94
94
  ],
95
- "version": "3.9.0-next.14"
95
+ "version": "3.9.0-next.16"
96
96
  }
@@ -31,6 +31,51 @@ import LocusDeltaParser from './parser';
31
31
  import Metrics from '../metrics';
32
32
  import BEHAVIORAL_METRICS from '../metrics/constants';
33
33
 
34
+ export type LocusDTO = {
35
+ controls?: any;
36
+ fullState?: {
37
+ active: boolean;
38
+ count: number;
39
+ lastActive: string;
40
+ locked: boolean;
41
+ sessionId: string;
42
+ seessionIds: string[];
43
+ startTime: number;
44
+ state: string;
45
+ type: string;
46
+ };
47
+ host?: {
48
+ id: string;
49
+ incomingCallProtocols: any[];
50
+ isExternal: boolean;
51
+ name: string;
52
+ orgId: string;
53
+ };
54
+ info?: any;
55
+ links?: any;
56
+ mediaShares?: any[];
57
+ meetings?: any[];
58
+ participants: any[];
59
+ replaces?: any[];
60
+ self?: any;
61
+ sequence?: {
62
+ dirtyParticipants: number;
63
+ entries: number[];
64
+ rangeEnd: number;
65
+ rangeStart: number;
66
+ sequenceHash: number;
67
+ sessionToken: string;
68
+ since: string;
69
+ totalParticipants: number;
70
+ };
71
+ syncUrl?: string;
72
+ url?: string;
73
+ };
74
+
75
+ export type LocusApiResponseBody = {
76
+ locus: LocusDTO; // this LocusDTO here might not be the full one (for example it won't have all the participants, but it should have self)
77
+ };
78
+
34
79
  /**
35
80
  * @description LocusInfo extends ChildEmitter to convert locusInfo info a private emitter to parent object
36
81
  * @export
@@ -294,7 +339,7 @@ export default class LocusInfo extends EventsScope {
294
339
  this.updateLocusCache(locus);
295
340
  // above section only updates the locusInfo object
296
341
  // The below section makes sure it updates the locusInfo as well as updates the meeting object
297
- this.updateParticipants(locus.participants);
342
+ this.updateParticipants(locus.participants, []);
298
343
  // For 1:1 space meeting the conversation Url does not exist in locus.conversation
299
344
  this.updateConversationUrl(locus.conversationUrl, locus.info);
300
345
  this.updateControls(locus.controls, locus.self);
@@ -323,6 +368,16 @@ export default class LocusInfo extends EventsScope {
323
368
  this.emitChange = true;
324
369
  }
325
370
 
371
+ /**
372
+ * Handles HTTP response from Locus API call.
373
+ * @param {Meeting} meeting meeting object
374
+ * @param {LocusApiResponseBody} responseBody body of the http response from Locus API call
375
+ * @returns {void}
376
+ */
377
+ handleLocusAPIResponse(meeting, responseBody: LocusApiResponseBody): void {
378
+ this.handleLocusDelta(responseBody.locus, meeting);
379
+ }
380
+
326
381
  /**
327
382
  * @param {Meeting} meeting
328
383
  * @param {Object} data
@@ -335,6 +390,8 @@ export default class LocusInfo extends EventsScope {
335
390
  const locus = this.getTheLocusToUpdate(data.locus);
336
391
  LoggerProxy.logger.info(`Locus-info:index#parse --> received locus data: ${eventType}`);
337
392
 
393
+ locus.jsSdkMeta = {removedParticipantIds: []};
394
+
338
395
  switch (eventType) {
339
396
  case LOCUSEVENT.PARTICIPANT_JOIN:
340
397
  case LOCUSEVENT.PARTICIPANT_LEFT:
@@ -400,7 +457,11 @@ export default class LocusInfo extends EventsScope {
400
457
  this.participants = locus.participants;
401
458
  const isReplaceMembers = ControlsUtils.isNeedReplaceMembers(this.controls, locus.controls);
402
459
  this.updateLocusInfo(locus);
403
- this.updateParticipants(locus.participants, isReplaceMembers);
460
+ this.updateParticipants(
461
+ locus.participants,
462
+ locus.jsSdkMeta?.removedParticipantIds,
463
+ isReplaceMembers
464
+ );
404
465
  this.isMeetingActive();
405
466
  this.handleOneOnOneEvent(eventType);
406
467
  this.updateEmbeddedApps(locus.embeddedApps);
@@ -462,7 +523,11 @@ export default class LocusInfo extends EventsScope {
462
523
  const isReplaceMembers = ControlsUtils.isNeedReplaceMembers(this.controls, locus.controls);
463
524
  this.mergeParticipants(this.participants, locus.participants);
464
525
  this.updateLocusInfo(locus);
465
- this.updateParticipants(locus.participants, isReplaceMembers);
526
+ this.updateParticipants(
527
+ locus.participants,
528
+ locus.jsSdkMeta?.removedParticipantIds,
529
+ isReplaceMembers
530
+ );
466
531
  this.isMeetingActive();
467
532
  }
468
533
 
@@ -753,11 +818,12 @@ export default class LocusInfo extends EventsScope {
753
818
  /**
754
819
  * update meeting's members
755
820
  * @param {Object} participants new participants object
821
+ * @param {Array} removedParticipantIds list of removed participants
756
822
  * @param {Boolean} isReplace is replace the whole members
757
823
  * @returns {Array} updatedParticipants
758
824
  * @memberof LocusInfo
759
825
  */
760
- updateParticipants(participants: object, isReplace?: boolean) {
826
+ updateParticipants(participants: object, removedParticipantIds?: string[], isReplace?: boolean) {
761
827
  this.emitScoped(
762
828
  {
763
829
  file: 'locus-info',
@@ -766,6 +832,7 @@ export default class LocusInfo extends EventsScope {
766
832
  EVENTS.LOCUS_INFO_UPDATE_PARTICIPANTS,
767
833
  {
768
834
  participants,
835
+ removedParticipantIds,
769
836
  recordingId: this.parsedLocus.controls && this.parsedLocus.controls.record?.modifiedBy,
770
837
  selfIdentity: this.parsedLocus.self && this.parsedLocus.self.selfIdentity,
771
838
  selfId: this.parsedLocus.self && this.parsedLocus.self.selfId,
@@ -291,18 +291,14 @@ export class MuteState {
291
291
  );
292
292
 
293
293
  return MeetingUtil.remoteUpdateAudioVideo(meeting, audioMuted, videoMuted)
294
- .then((locus) => {
294
+ .then((response) => {
295
295
  LoggerProxy.logger.info(
296
296
  `Meeting:muteState#sendLocalMuteRequestToServer --> ${this.type}: local mute (audio=${audioMuted}, video=${videoMuted}) applied to server`
297
297
  );
298
298
 
299
299
  this.state.server.localMute = this.type === AUDIO ? audioMuted : videoMuted;
300
300
 
301
- if (locus) {
302
- meeting.locusInfo.handleLocusDelta(locus, meeting);
303
- }
304
-
305
- return locus;
301
+ return MeetingUtil.updateLocusFromApiResponse(meeting, response);
306
302
  })
307
303
  .catch((remoteUpdateError) => {
308
304
  LoggerProxy.logger.warn(
@@ -59,18 +59,16 @@ const MeetingUtil = {
59
59
  );
60
60
  }
61
61
 
62
- return meeting.locusMediaRequest
63
- .send({
64
- type: 'LocalMute',
65
- selfUrl: meeting.selfUrl,
66
- mediaId: meeting.mediaId,
67
- sequence: meeting.locusInfo.sequence,
68
- muteOptions: {
69
- audioMuted,
70
- videoMuted,
71
- },
72
- })
73
- .then((response) => response?.body?.locus);
62
+ return meeting.locusMediaRequest.send({
63
+ type: 'LocalMute',
64
+ selfUrl: meeting.selfUrl,
65
+ mediaId: meeting.mediaId,
66
+ sequence: meeting.locusInfo.sequence,
67
+ muteOptions: {
68
+ audioMuted,
69
+ videoMuted,
70
+ },
71
+ });
74
72
  },
75
73
 
76
74
  hasOwner: (info) => info && info.owner,
@@ -695,22 +693,20 @@ const MeetingUtil = {
695
693
  },
696
694
 
697
695
  /**
698
- * Updates the locus info for the meeting with the delta locus
699
- * returned from requests that include the sequence information
696
+ * Updates the locus info for the meeting with the locus
697
+ * information returned from API requests made to Locus
700
698
  * Returns the original response object
701
699
  * @param {Object} meeting The meeting object
702
700
  * @param {Object} response The response of the http request
703
701
  * @returns {Object}
704
702
  */
705
- updateLocusWithDelta: (meeting, response) => {
703
+ updateLocusFromApiResponse: (meeting, response) => {
706
704
  if (!meeting) {
707
705
  return response;
708
706
  }
709
707
 
710
- const locus = response?.body?.locus;
711
-
712
- if (locus) {
713
- meeting.locusInfo.handleLocusDelta(locus, meeting);
708
+ if (response?.body?.locus) {
709
+ meeting.locusInfo.handleLocusAPIResponse(meeting, response.body);
714
710
  }
715
711
 
716
712
  return response;
@@ -757,7 +753,7 @@ const MeetingUtil = {
757
753
 
758
754
  return meeting
759
755
  .request(options)
760
- .then((response) => MeetingUtil.updateLocusWithDelta(meeting, response));
756
+ .then((response) => MeetingUtil.updateLocusFromApiResponse(meeting, response));
761
757
  };
762
758
 
763
759
  return locusDeltaRequest;
@@ -109,4 +109,5 @@ export interface Participant {
109
109
  status: ParticipantMediaStatus;
110
110
  type: string;
111
111
  url: ParticipantUrl;
112
+ isRemoved: boolean; // JS-SDK internal field to indicate in updates when the participant is removed
112
113
  }
@@ -39,6 +39,17 @@ export default class MembersCollection {
39
39
  return this.members;
40
40
  }
41
41
 
42
+ /**
43
+ * Removes a member from the collection
44
+ * @param {String} id
45
+ * @returns {void}
46
+ */
47
+ remove(id: string) {
48
+ if (this.members[id]) {
49
+ delete this.members[id];
50
+ }
51
+ }
52
+
42
53
  /**
43
54
  * @returns {void}
44
55
  * reset members
@@ -74,7 +74,11 @@ import {Invitee} from '../meeting/type';
74
74
  * @memberof Members
75
75
  */
76
76
 
77
- type UpdatedMembers = {added: Array<Member>; updated: Array<Member>};
77
+ type UpdatedMembers = {
78
+ added: Array<Member>;
79
+ updated: Array<Member>;
80
+ removedIds?: Array<string>; // removed member ids
81
+ };
78
82
  /**
79
83
  * @class Members
80
84
  */
@@ -384,12 +388,18 @@ export default class Members extends StatelessWebexPlugin {
384
388
  * when new participant updates come in, both delta and full participants, update them in members collection
385
389
  * delta object in the event will have {updated, added} and full will be the full membersCollection
386
390
  * @param {Object} payload
387
- * @param {Object} payload.participants
391
+ * @param {Object} payload.participants new/updated participants
392
+ * @param {Boolean} payload.isReplace whether to replace the whole members collection
393
+ * @param {Object} payload.removedParticipantIds ids of the removed participants
388
394
  * @returns {undefined}
389
395
  * @private
390
396
  * @memberof Members
391
397
  */
392
- locusParticipantsUpdate(payload: {participants: object; isReplace?: boolean}) {
398
+ locusParticipantsUpdate(payload: {
399
+ participants: object;
400
+ isReplace?: boolean;
401
+ removedParticipantIds?: Array<string>;
402
+ }) {
393
403
  if (payload) {
394
404
  if (payload.isReplace) {
395
405
  this.clearMembers();
@@ -547,10 +557,22 @@ export default class Members extends StatelessWebexPlugin {
547
557
  private handleMembersUpdate(membersUpdate: UpdatedMembers) {
548
558
  this.constructMembers(membersUpdate.updated, true);
549
559
  this.constructMembers(membersUpdate.added, false);
560
+ this.removeMembers(membersUpdate.removedIds);
550
561
 
551
562
  return this.membersCollection.getAll();
552
563
  }
553
564
 
565
+ /**
566
+ * removes members from the collection
567
+ * @param {Array<string>} removedMembers removed members ids
568
+ * @returns {void}
569
+ */
570
+ private removeMembers(removedMembers: Array<string>) {
571
+ removedMembers.forEach((memberId) => {
572
+ this.membersCollection.remove(memberId);
573
+ });
574
+ }
575
+
554
576
  /**
555
577
  * set members to the member collection from each updated/added lists as passed in
556
578
  * @param {Array} list
@@ -600,6 +622,10 @@ export default class Members extends StatelessWebexPlugin {
600
622
  }
601
623
  const memberUpdate = this.update(payload.participants);
602
624
 
625
+ // this code depends on memberIds being the same as participantIds
626
+ // if MemberUtil.extractId() ever changes, this will need to be updated
627
+ memberUpdate.removedIds = payload.removedParticipantIds || [];
628
+
603
629
  return memberUpdate;
604
630
  }
605
631
 
@@ -772,7 +772,7 @@ describe('plugin-meetings', () => {
772
772
  },
773
773
  };
774
774
  locusInfo.emitScoped = sinon.stub();
775
- locusInfo.updateParticipants({});
775
+ locusInfo.updateParticipants({}, []);
776
776
 
777
777
  // if this assertion fails, double-check the attributes used in
778
778
  // the updateParticipants function in locus-info/index.js
@@ -790,6 +790,7 @@ describe('plugin-meetings', () => {
790
790
  selfId: '2',
791
791
  hostId: '3',
792
792
  isReplace: undefined,
793
+ removedParticipantIds: [],
793
794
  }
794
795
  );
795
796
  // note: in a real use case, recordingId, selfId, and hostId would all be the same
@@ -814,7 +815,7 @@ describe('plugin-meetings', () => {
814
815
  };
815
816
 
816
817
  locusInfo.emitScoped = sinon.stub();
817
- locusInfo.updateParticipants({}, true);
818
+ locusInfo.updateParticipants({}, [], true);
818
819
 
819
820
  assert.calledWith(
820
821
  locusInfo.emitScoped,
@@ -830,6 +831,7 @@ describe('plugin-meetings', () => {
830
831
  selfId: '2',
831
832
  hostId: '3',
832
833
  isReplace: true,
834
+ removedParticipantIds: [],
833
835
  }
834
836
  );
835
837
  });
@@ -847,7 +849,7 @@ describe('plugin-meetings', () => {
847
849
  ];
848
850
 
849
851
  locusInfo.emitScoped = sinon.stub();
850
- locusInfo.updateParticipants(failureParticipant);
852
+ locusInfo.updateParticipants(failureParticipant, []);
851
853
  assert.calledWith(
852
854
  locusInfo.emitScoped,
853
855
  {
@@ -2038,6 +2040,18 @@ describe('plugin-meetings', () => {
2038
2040
  });
2039
2041
  });
2040
2042
 
2043
+ describe('#handleLocusAPIResponse', () => {
2044
+ it('calls handleLocusDelta', () => {
2045
+ const fakeLocus = {eventType: LOCUSEVENT.DIFFERENCE};
2046
+
2047
+ sinon.stub(locusInfo, 'handleLocusDelta');
2048
+
2049
+ locusInfo.handleLocusAPIResponse(mockMeeting, {locus: fakeLocus});
2050
+
2051
+ assert.calledWith(locusInfo.handleLocusDelta, fakeLocus, mockMeeting);
2052
+ });
2053
+ });
2054
+
2041
2055
  describe('#LocusDeltaEvents', () => {
2042
2056
  const fakeMeeting = 'fakeMeeting';
2043
2057
  let sandbox = null;
@@ -2050,7 +2064,7 @@ describe('plugin-meetings', () => {
2050
2064
 
2051
2065
  fakeLocus = {
2052
2066
  meeting: true,
2053
- participants: true,
2067
+ participants: [],
2054
2068
  url: 'newLocusUrl',
2055
2069
  syncUrl: 'newSyncUrl',
2056
2070
  };
@@ -2525,7 +2539,7 @@ describe('plugin-meetings', () => {
2525
2539
  });
2526
2540
 
2527
2541
  it('onDeltaLocus handle delta data', () => {
2528
- fakeLocus.participants = {};
2542
+ fakeLocus.participants = [];
2529
2543
  const fakeBreakout = {
2530
2544
  sessionId: 'sessionId',
2531
2545
  groupId: 'groupId',
@@ -2542,11 +2556,11 @@ describe('plugin-meetings', () => {
2542
2556
  };
2543
2557
  locusInfo.updateParticipants = sinon.stub();
2544
2558
  locusInfo.onDeltaLocus(fakeLocus);
2545
- assert.calledWith(locusInfo.updateParticipants, {}, false);
2559
+ assert.calledWith(locusInfo.updateParticipants, [], undefined, false);
2546
2560
 
2547
2561
  fakeLocus.controls.breakout.sessionId = 'sessionId2';
2548
2562
  locusInfo.onDeltaLocus(fakeLocus);
2549
- assert.calledWith(locusInfo.updateParticipants, {}, true);
2563
+ assert.calledWith(locusInfo.updateParticipants, [], undefined, true);
2550
2564
  });
2551
2565
 
2552
2566
  it('onDeltaLocus merges delta participants with existing participants', () => {
@@ -2563,7 +2577,7 @@ describe('plugin-meetings', () => {
2563
2577
  existingParticipants,
2564
2578
  FAKE_DELTA_PARTICIPANTS
2565
2579
  );
2566
- assert.calledWith(locusInfo.updateParticipants, FAKE_DELTA_PARTICIPANTS, false);
2580
+ assert.calledWith(locusInfo.updateParticipants, FAKE_DELTA_PARTICIPANTS, undefined, false);
2567
2581
  });
2568
2582
 
2569
2583
  [true, false].forEach((isDelta) =>
@@ -3074,6 +3088,7 @@ describe('plugin-meetings', () => {
3074
3088
  id: 'test person id',
3075
3089
  },
3076
3090
  },
3091
+ participants: [],
3077
3092
  });
3078
3093
 
3079
3094
  updateLocusInfoStub.resetHistory();
@@ -12,7 +12,7 @@ describe('plugin-meetings', () => {
12
12
  let video;
13
13
  let originalRemoteUpdateAudioVideo;
14
14
 
15
- const fakeLocus = {info: 'this is a fake locus'};
15
+ const fakeLocusResponse = {body: {locus: {info: 'this is a fake locus'}}};
16
16
 
17
17
  const createFakeLocalStream = (id, userMuted, systemMuted) => {
18
18
  return {
@@ -38,9 +38,6 @@ describe('plugin-meetings', () => {
38
38
  unmuteAllowed: true,
39
39
  remoteVideoMuted: false,
40
40
  unmuteVideoAllowed: true,
41
- locusInfo: {
42
- handleLocusDelta: sinon.stub(),
43
- },
44
41
  members: {
45
42
  selfId: 'fake self id',
46
43
  muteMember: sinon.stub().resolves(),
@@ -49,7 +46,8 @@ describe('plugin-meetings', () => {
49
46
 
50
47
  originalRemoteUpdateAudioVideo = MeetingUtil.remoteUpdateAudioVideo;
51
48
 
52
- MeetingUtil.remoteUpdateAudioVideo = sinon.stub().resolves(fakeLocus);
49
+ MeetingUtil.remoteUpdateAudioVideo = sinon.stub().resolves(fakeLocusResponse);
50
+ MeetingUtil.updateLocusFromApiResponse = sinon.stub();
53
51
 
54
52
  audio = createMuteState(AUDIO, meeting, true);
55
53
  video = createMuteState(VIDEO, meeting, true);
@@ -141,6 +139,7 @@ describe('plugin-meetings', () => {
141
139
  // and local unmute was sent to server
142
140
  assert.calledOnce(MeetingUtil.remoteUpdateAudioVideo);
143
141
  assert.calledWith(MeetingUtil.remoteUpdateAudioVideo, meeting, false, undefined);
142
+ assert.calledWith(MeetingUtil.updateLocusFromApiResponse, meeting, fakeLocusResponse);
144
143
 
145
144
  assert.isFalse(audio.isMuted());
146
145
  });
@@ -173,6 +172,7 @@ describe('plugin-meetings', () => {
173
172
 
174
173
  // system was muted so local unmute was not sent to server
175
174
  assert.notCalled(MeetingUtil.remoteUpdateAudioVideo);
175
+ assert.notCalled(MeetingUtil.updateLocusFromApiResponse);
176
176
 
177
177
  assert.isTrue(audio.isMuted());
178
178
  });
@@ -207,6 +207,7 @@ describe('plugin-meetings', () => {
207
207
  // and local unmute was sent to server
208
208
  assert.calledOnce(MeetingUtil.remoteUpdateAudioVideo);
209
209
  assert.calledWith(MeetingUtil.remoteUpdateAudioVideo, meeting, undefined, false);
210
+ assert.calledWith(MeetingUtil.updateLocusFromApiResponse, meeting, fakeLocusResponse);
210
211
 
211
212
  assert.isFalse(video.isMuted());
212
213
  });
@@ -219,7 +220,9 @@ describe('plugin-meetings', () => {
219
220
 
220
221
  assert.isTrue(video.isMuted());
221
222
 
223
+ await testUtils.flushPromises();
222
224
  MeetingUtil.remoteUpdateAudioVideo.resetHistory();
225
+ MeetingUtil.updateLocusFromApiResponse.resetHistory();
223
226
 
224
227
  // now simulate server requiring us to locally unmute
225
228
  // assuming setServerMuted succeeds at updating userMuted
@@ -239,6 +242,7 @@ describe('plugin-meetings', () => {
239
242
 
240
243
  // system was muted so local unmute was not sent to server
241
244
  assert.notCalled(MeetingUtil.remoteUpdateAudioVideo);
245
+ assert.notCalled(MeetingUtil.updateLocusFromApiResponse);
242
246
 
243
247
  assert.isTrue(video.isMuted());
244
248
  });
@@ -443,6 +447,7 @@ describe('plugin-meetings', () => {
443
447
 
444
448
  assert.calledOnce(MeetingUtil.remoteUpdateAudioVideo);
445
449
  assert.calledWith(MeetingUtil.remoteUpdateAudioVideo, meeting, true, undefined);
450
+ assert.calledWith(MeetingUtil.updateLocusFromApiResponse, meeting, fakeLocusResponse);
446
451
 
447
452
  // now allow the first request to complete
448
453
  serverResponseResolve();
@@ -559,6 +564,7 @@ describe('plugin-meetings', () => {
559
564
  await testUtils.flushPromises();
560
565
 
561
566
  MeetingUtil.remoteUpdateAudioVideo.resetHistory();
567
+ MeetingUtil.updateLocusFromApiResponse.resetHistory();
562
568
  };
563
569
 
564
570
  const setupSpies = (mediaType) => {
@@ -605,13 +611,15 @@ describe('plugin-meetings', () => {
605
611
  {mediaType: VIDEO, title: 'video'},
606
612
  ];
607
613
 
614
+ const fakeLocusResponse = {body: {locus: {info: 'fake locus'}}};
615
+
608
616
  tests.forEach(({mediaType, title}) =>
609
617
  describe(title, () => {
610
618
  let originalRemoteUpdateAudioVideo;
611
619
 
612
620
  beforeEach(() => {
613
621
  originalRemoteUpdateAudioVideo = MeetingUtil.remoteUpdateAudioVideo;
614
- MeetingUtil.remoteUpdateAudioVideo = sinon.stub().resolves({info: 'fake locus'});
622
+ MeetingUtil.remoteUpdateAudioVideo = sinon.stub().resolves(fakeLocusResponse);
615
623
  });
616
624
 
617
625
  afterEach(() => {
@@ -660,6 +668,7 @@ describe('plugin-meetings', () => {
660
668
  assert.calledWith(setUnmuteAllowedSpy, muteState.state.server.unmuteAllowed);
661
669
  assert.notCalled(setServerMutedSpy);
662
670
  assert.notCalled(MeetingUtil.remoteUpdateAudioVideo);
671
+ assert.notCalled(MeetingUtil.updateLocusFromApiResponse);
663
672
  assert.isTrue(muteState.state.client.localMute);
664
673
  });
665
674
 
@@ -672,6 +681,7 @@ describe('plugin-meetings', () => {
672
681
  assert.calledWith(setUnmuteAllowedSpy, muteState.state.server.unmuteAllowed);
673
682
  assert.notCalled(setServerMutedSpy);
674
683
  assert.notCalled(MeetingUtil.remoteUpdateAudioVideo);
684
+ assert.notCalled(MeetingUtil.updateLocusFromApiResponse);
675
685
  assert.isTrue(muteState.state.client.localMute);
676
686
  });
677
687
 
@@ -681,9 +691,12 @@ describe('plugin-meetings', () => {
681
691
 
682
692
  muteState.init(meeting);
683
693
 
694
+ await testUtils.flushPromises();
695
+
684
696
  assert.calledWith(setUnmuteAllowedSpy, muteState.state.server.unmuteAllowed);
685
697
  assert.notCalled(setServerMutedSpy);
686
698
  assert.calledOnce(MeetingUtil.remoteUpdateAudioVideo);
699
+ assert.calledOnceWithExactly(MeetingUtil.updateLocusFromApiResponse, meeting, fakeLocusResponse);
687
700
  assert.isFalse(muteState.state.client.localMute);
688
701
  });
689
702
 
@@ -707,6 +720,7 @@ describe('plugin-meetings', () => {
707
720
  simulateUserMute(mediaType, true);
708
721
  muteState.handleLocalStreamMuteStateChange(meeting);
709
722
  assert.notCalled(MeetingUtil.remoteUpdateAudioVideo);
723
+ assert.notCalled(MeetingUtil.updateLocusFromApiResponse);
710
724
 
711
725
  assert.isFalse(muteState.state.client.localMute);
712
726
  });
@@ -716,8 +730,11 @@ describe('plugin-meetings', () => {
716
730
 
717
731
  simulateUserMute(mediaType, false);
718
732
  muteState.handleLocalStreamMuteStateChange(meeting);
733
+ await testUtils.flushPromises();
734
+
719
735
  assert.equal(muteState.state.client.localMute, false);
720
736
  assert.called(MeetingUtil.remoteUpdateAudioVideo);
737
+ assert.calledOnceWithExactly(MeetingUtil.updateLocusFromApiResponse, meeting, fakeLocusResponse);
721
738
  });
722
739
 
723
740
  it('tests localMute - user mute from false to true', async () => {
@@ -725,8 +742,11 @@ describe('plugin-meetings', () => {
725
742
 
726
743
  simulateUserMute(mediaType, true);
727
744
  muteState.handleLocalStreamMuteStateChange(meeting);
745
+ await testUtils.flushPromises();
746
+
728
747
  assert.equal(muteState.state.client.localMute, true);
729
748
  assert.called(MeetingUtil.remoteUpdateAudioVideo);
749
+ assert.calledOnceWithExactly(MeetingUtil.updateLocusFromApiResponse, meeting, fakeLocusResponse);
730
750
  });
731
751
 
732
752
  it('tests localMute - system mute from true to false', async () => {
@@ -734,8 +754,11 @@ describe('plugin-meetings', () => {
734
754
 
735
755
  simulateSystemMute(mediaType, false);
736
756
  muteState.handleLocalStreamMuteStateChange(meeting);
757
+ await testUtils.flushPromises();
758
+
737
759
  assert.equal(muteState.state.client.localMute, false);
738
760
  assert.called(MeetingUtil.remoteUpdateAudioVideo);
761
+ assert.calledOnceWithExactly(MeetingUtil.updateLocusFromApiResponse, meeting, fakeLocusResponse);
739
762
  });
740
763
 
741
764
  it('tests localMute - system mute from false to true', async () => {
@@ -743,8 +766,11 @@ describe('plugin-meetings', () => {
743
766
 
744
767
  simulateSystemMute(mediaType, true);
745
768
  muteState.handleLocalStreamMuteStateChange(meeting);
769
+ await testUtils.flushPromises();
770
+
746
771
  assert.equal(muteState.state.client.localMute, true);
747
772
  assert.called(MeetingUtil.remoteUpdateAudioVideo);
773
+ assert.calledOnceWithExactly(MeetingUtil.updateLocusFromApiResponse, meeting, fakeLocusResponse);
748
774
  });
749
775
  });
750
776