@webex/plugin-meetings 3.0.0-beta.43 → 3.0.0-beta.45

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 (57) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +12 -3
  3. package/dist/breakouts/index.js.map +1 -1
  4. package/dist/constants.js +15 -3
  5. package/dist/constants.js.map +1 -1
  6. package/dist/locus-info/controlsUtils.js +6 -2
  7. package/dist/locus-info/controlsUtils.js.map +1 -1
  8. package/dist/locus-info/index.js +35 -1
  9. package/dist/locus-info/index.js.map +1 -1
  10. package/dist/locus-info/selfUtils.js +28 -0
  11. package/dist/locus-info/selfUtils.js.map +1 -1
  12. package/dist/meeting/in-meeting-actions.js +9 -1
  13. package/dist/meeting/in-meeting-actions.js.map +1 -1
  14. package/dist/meeting/index.js +43 -4
  15. package/dist/meeting/index.js.map +1 -1
  16. package/dist/meeting/muteState.js +45 -20
  17. package/dist/meeting/muteState.js.map +1 -1
  18. package/dist/meeting/util.js +15 -0
  19. package/dist/meeting/util.js.map +1 -1
  20. package/dist/members/index.js +15 -3
  21. package/dist/members/index.js.map +1 -1
  22. package/dist/members/util.js +33 -12
  23. package/dist/members/util.js.map +1 -1
  24. package/dist/multistream/remoteMediaManager.js +112 -55
  25. package/dist/multistream/remoteMediaManager.js.map +1 -1
  26. package/dist/types/constants.d.ts +11 -0
  27. package/dist/types/locus-info/index.d.ts +7 -0
  28. package/dist/types/meeting/in-meeting-actions.d.ts +8 -0
  29. package/dist/types/meeting/index.d.ts +12 -2
  30. package/dist/types/meeting/muteState.d.ts +16 -0
  31. package/dist/types/members/index.d.ts +7 -2
  32. package/dist/types/multistream/remoteMediaManager.d.ts +20 -2
  33. package/package.json +18 -18
  34. package/src/breakouts/index.ts +8 -1
  35. package/src/constants.ts +13 -0
  36. package/src/locus-info/controlsUtils.ts +8 -0
  37. package/src/locus-info/index.ts +42 -1
  38. package/src/locus-info/selfUtils.ts +34 -0
  39. package/src/meeting/in-meeting-actions.ts +16 -0
  40. package/src/meeting/index.ts +53 -4
  41. package/src/meeting/muteState.ts +49 -30
  42. package/src/meeting/util.ts +15 -0
  43. package/src/members/index.ts +12 -4
  44. package/src/members/util.ts +21 -8
  45. package/src/multistream/remoteMediaManager.ts +57 -26
  46. package/test/unit/spec/breakouts/index.ts +2 -2
  47. package/test/unit/spec/locus-info/controlsUtils.js +104 -46
  48. package/test/unit/spec/locus-info/index.js +131 -16
  49. package/test/unit/spec/locus-info/selfConstant.js +9 -5
  50. package/test/unit/spec/locus-info/selfUtils.js +39 -16
  51. package/test/unit/spec/meeting/in-meeting-actions.ts +9 -1
  52. package/test/unit/spec/meeting/index.js +208 -79
  53. package/test/unit/spec/meeting/muteState.js +72 -6
  54. package/test/unit/spec/meeting/utils.js +35 -0
  55. package/test/unit/spec/members/index.js +75 -0
  56. package/test/unit/spec/members/utils.js +112 -0
  57. package/test/unit/spec/multistream/remoteMediaManager.ts +127 -0
@@ -52,9 +52,9 @@ class MuteState {
52
52
  },
53
53
  server: {
54
54
  localMute: false,
55
- // initial values available only for audio (REMOTE_MUTE_VIDEO_MISSING_IMPLEMENTATION)
56
- remoteMute: type === AUDIO ? meeting.remoteMuted : false,
57
- unmuteAllowed: type === AUDIO ? meeting.unmuteAllowed : true,
55
+ // because remoteVideoMuted and unmuteVideoAllowed are updated seperately, they might be undefined
56
+ remoteMute: type === AUDIO ? meeting.remoteMuted : meeting.remoteVideoMuted ?? false,
57
+ unmuteAllowed: type === AUDIO ? meeting.unmuteAllowed : meeting.unmuteVideoAllowed ?? true,
58
58
  },
59
59
  syncToServerInProgress: false,
60
60
  };
@@ -241,35 +241,28 @@ class MuteState {
241
241
  * @returns {Promise}
242
242
  */
243
243
  private sendRemoteMuteRequestToServer(meeting?: any) {
244
- if (this.type === AUDIO) {
245
- const remoteMute = this.state.client.localMute;
244
+ const remoteMute = this.state.client.localMute;
246
245
 
247
- LoggerProxy.logger.info(
248
- `Meeting:muteState#sendRemoteMuteRequestToServer --> ${this.type}: sending remote mute:${remoteMute} to server`
249
- );
246
+ LoggerProxy.logger.info(
247
+ `Meeting:muteState#sendRemoteMuteRequestToServer --> ${this.type}: sending remote mute:${remoteMute} to server`
248
+ );
250
249
 
251
- return meeting.members
252
- .muteMember(meeting.members.selfId, remoteMute)
253
- .then(() => {
254
- LoggerProxy.logger.info(
255
- `Meeting:muteState#sendRemoteMuteRequestToServer --> ${this.type}: remote mute:${remoteMute} applied to server`
256
- );
257
-
258
- this.state.server.remoteMute = remoteMute;
259
- })
260
- .catch((remoteUpdateError) => {
261
- LoggerProxy.logger.warn(
262
- `Meeting:muteState#sendRemoteMuteRequestToServer --> ${this.type}: failed to apply remote mute ${remoteMute} to server: ${remoteUpdateError}`
263
- );
264
-
265
- return Promise.reject(remoteUpdateError);
266
- });
267
- }
250
+ return meeting.members
251
+ .muteMember(meeting.members.selfId, remoteMute, this.type === AUDIO)
252
+ .then(() => {
253
+ LoggerProxy.logger.info(
254
+ `Meeting:muteState#sendRemoteMuteRequestToServer --> ${this.type}: remote mute:${remoteMute} applied to server`
255
+ );
268
256
 
269
- // for now we don't need to support remote muting of video (REMOTE_MUTE_VIDEO_MISSING_IMPLEMENTATION)
270
- this.state.server.remoteMute = this.state.client.localMute;
257
+ this.state.server.remoteMute = remoteMute;
258
+ })
259
+ .catch((remoteUpdateError) => {
260
+ LoggerProxy.logger.warn(
261
+ `Meeting:muteState#sendRemoteMuteRequestToServer --> ${this.type}: failed to apply remote mute ${remoteMute} to server: ${remoteUpdateError}`
262
+ );
271
263
 
272
- return Promise.resolve();
264
+ return Promise.reject(remoteUpdateError);
265
+ });
273
266
  }
274
267
 
275
268
  /**
@@ -285,8 +278,12 @@ class MuteState {
285
278
  LoggerProxy.logger.info(
286
279
  `Meeting:muteState#handleServerRemoteMuteUpdate --> ${this.type}: updating server remoteMute to (${muted})`
287
280
  );
288
- this.state.server.remoteMute = muted;
289
- this.state.server.unmuteAllowed = unmuteAllowed;
281
+ if (muted !== undefined) {
282
+ this.state.server.remoteMute = muted;
283
+ }
284
+ if (unmuteAllowed !== undefined) {
285
+ this.state.server.unmuteAllowed = unmuteAllowed;
286
+ }
290
287
  }
291
288
 
292
289
  /**
@@ -330,6 +327,28 @@ class MuteState {
330
327
  );
331
328
  }
332
329
 
330
+ /**
331
+ * Returns true if the user is remotely muted
332
+ *
333
+ * @public
334
+ * @memberof MuteState
335
+ * @returns {Boolean}
336
+ */
337
+ public isRemotelyMuted() {
338
+ return this.state.server.remoteMute;
339
+ }
340
+
341
+ /**
342
+ * Returns true if unmute is allowed
343
+ *
344
+ * @public
345
+ * @memberof MuteState
346
+ * @returns {Boolean}
347
+ */
348
+ public isUnmuteAllowed() {
349
+ return this.state.server.unmuteAllowed;
350
+ }
351
+
333
352
  /**
334
353
  * Returns true if the user is locally muted
335
354
  *
@@ -375,6 +375,21 @@ MeetingUtil.bothLeaveAndEndMeetingAvailable = (displayHints) =>
375
375
  displayHints.includes(DISPLAY_HINTS.LEAVE_TRANSFER_HOST_END_MEETING) ||
376
376
  displayHints.includes(DISPLAY_HINTS.LEAVE_END_MEETING);
377
377
 
378
+ MeetingUtil.canManageBreakout = (displayHints) =>
379
+ displayHints.includes(DISPLAY_HINTS.BREAKOUT_MANAGEMENT);
380
+
381
+ MeetingUtil.isSuppressBreakoutSupport = (displayHints) =>
382
+ displayHints.includes(DISPLAY_HINTS.UCF_SUPPRESS_BREAKOUTS_SUPPORT);
383
+
384
+ MeetingUtil.canAdmitLobbyToBreakout = (displayHints) =>
385
+ !displayHints.includes(DISPLAY_HINTS.DISABLE_LOBBY_TO_BREAKOUT);
386
+
387
+ MeetingUtil.isBreakoutPreassignmentsEnabled = (displayHints) =>
388
+ !displayHints.includes(DISPLAY_HINTS.DISABLE_BREAKOUT_PREASSIGNMENTS);
389
+
390
+ MeetingUtil.canUserAskForHelp = (displayHints) =>
391
+ !displayHints.includes(DISPLAY_HINTS.DISABLE_ASK_FOR_HELP);
392
+
378
393
  MeetingUtil.lockMeeting = (actions, request, locusUrl) => {
379
394
  if (actions && actions.canLock) {
380
395
  return request.lockMeeting({locusUrl, lock: true});
@@ -733,15 +733,22 @@ export default class Members extends StatelessWebexPlugin {
733
733
  /**
734
734
  * Admits waiting members (invited guests to meeting)
735
735
  * @param {Array} memberIds
736
+ * @param {Object} sessionLocusUrls: {authorizingLocusUrl, mainLocusUrl}
736
737
  * @returns {Promise}
737
738
  * @public
738
739
  * @memberof Members
739
740
  */
740
- public admitMembers(memberIds: Array<any>) {
741
+ public admitMembers(
742
+ memberIds: Array<any>,
743
+ sessionLocusUrls?: {authorizingLocusUrl: string; mainLocusUrl: string}
744
+ ) {
741
745
  if (isEmpty(memberIds)) {
742
746
  return Promise.reject(new ParameterError('No member ids provided to admit.'));
743
747
  }
744
- const options = MembersUtil.generateAdmitMemberOptions(memberIds, this.locusUrl);
748
+ const options = {
749
+ sessionLocusUrls,
750
+ ...MembersUtil.generateAdmitMemberOptions(memberIds, this.locusUrl),
751
+ };
745
752
 
746
753
  return this.membersRequest.admitMember(options);
747
754
  }
@@ -773,11 +780,12 @@ export default class Members extends StatelessWebexPlugin {
773
780
  * Audio mutes another member in a meeting
774
781
  * @param {String} memberId
775
782
  * @param {boolean} [mute] default true
783
+ * @param {boolean} [isAudio] default true
776
784
  * @returns {Promise}
777
785
  * @public
778
786
  * @memberof Members
779
787
  */
780
- public muteMember(memberId: string, mute = true) {
788
+ public muteMember(memberId: string, mute = true, isAudio = true) {
781
789
  if (!this.locusUrl) {
782
790
  return Promise.reject(
783
791
  new ParameterError(
@@ -790,7 +798,7 @@ export default class Members extends StatelessWebexPlugin {
790
798
  new ParameterError('The member id must be defined to mute the member.')
791
799
  );
792
800
  }
793
- const options = MembersUtil.generateMuteMemberOptions(memberId, mute, this.locusUrl);
801
+ const options = MembersUtil.generateMuteMemberOptions(memberId, mute, this.locusUrl, isAudio);
794
802
 
795
803
  return this.membersRequest.muteMember(options);
796
804
  }
@@ -54,20 +54,31 @@ MembersUtil.getAddMemberBody = (options: any) => ({
54
54
  });
55
55
 
56
56
  /**
57
- * @param {Object} options with {memberIds}
57
+ * @param {Object} options with {memberIds, authorizingLocusUrl}
58
58
  * @returns {Object} admit with {memberIds}
59
59
  */
60
- MembersUtil.getAdmitMemberRequestBody = (options: any) => ({
61
- admit: {participantIds: options.memberIds},
62
- });
60
+ MembersUtil.getAdmitMemberRequestBody = (options: any) => {
61
+ const {memberIds, sessionLocusUrls} = options;
62
+ const body: any = {admit: {participantIds: memberIds}};
63
+ if (sessionLocusUrls) {
64
+ const {authorizingLocusUrl} = sessionLocusUrls;
65
+
66
+ return {authorizingLocusUrl, ...body};
67
+ }
68
+
69
+ return body;
70
+ };
63
71
 
64
72
  /**
65
- * @param {Object} format with {memberIds, locusUrl}
73
+ * @param {Object} format with {memberIds, locusUrl, sessionLocusUrls}
66
74
  * @returns {Object} the request parameters (method, uri, body) needed to make a admitMember request
75
+ * if a host/cohost is in a breakout session, the locus url should be the main session locus url
67
76
  */
68
77
  MembersUtil.getAdmitMemberRequestParams = (format: any) => {
69
78
  const body = MembersUtil.getAdmitMemberRequestBody(format);
70
- const uri = `${format.locusUrl}/${CONTROLS}`;
79
+ const {locusUrl, sessionLocusUrls} = format;
80
+ const baseUrl = sessionLocusUrls?.mainLocusUrl || locusUrl;
81
+ const uri = `${baseUrl}/${CONTROLS}`;
71
82
 
72
83
  return {
73
84
  method: HTTP_VERBS.PUT,
@@ -128,10 +139,11 @@ MembersUtil.generateRemoveMemberOptions = (removal, locusUrl) => ({
128
139
  locusUrl,
129
140
  });
130
141
 
131
- MembersUtil.generateMuteMemberOptions = (memberId, status, locusUrl) => ({
142
+ MembersUtil.generateMuteMemberOptions = (memberId, status, locusUrl, isAudio) => ({
132
143
  memberId,
133
144
  muted: status,
134
145
  locusUrl,
146
+ isAudio,
135
147
  });
136
148
 
137
149
  MembersUtil.generateRaiseHandMemberOptions = (memberId, status, locusUrl) => ({
@@ -146,8 +158,9 @@ MembersUtil.generateLowerAllHandsMemberOptions = (requestingParticipantId, locus
146
158
  });
147
159
 
148
160
  MembersUtil.getMuteMemberRequestParams = (options) => {
161
+ const property = options.isAudio === false ? 'video' : 'audio';
149
162
  const body = {
150
- audio: {
163
+ [property]: {
151
164
  muted: options.muted,
152
165
  },
153
166
  };
@@ -601,25 +601,37 @@ export class RemoteMediaManager extends EventsScope {
601
601
  }
602
602
 
603
603
  /**
604
- * Allocates receive slots to all video panes in the current selected layout
604
+ * Allocates receive slots to all active speaker video panes
605
+ * in the current selected layout.
606
+ *
607
+ * Allocation tries to keep the same order of the slots between the previous
608
+ * layout and the new one. Sorting helps making sure that highest priority slots
609
+ * go in the same order in the new layout.
605
610
  */
606
- private allocateSlotsToVideoPaneGroups() {
607
- this.receiveSlotAllocations = {activeSpeaker: {}, receiverSelected: {}};
608
-
609
- this.currentLayout?.activeSpeakerVideoPaneGroups?.forEach((group) => {
610
- this.receiveSlotAllocations.activeSpeaker[group.id] = {slots: []};
611
-
612
- for (let paneIndex = 0; paneIndex < group.numPanes; paneIndex += 1) {
613
- // allocate a slot from the "unused" list
614
- const freeSlot = this.slots.video.unused.pop();
615
-
616
- if (freeSlot) {
617
- this.slots.video.activeSpeaker.push(freeSlot);
618
- this.receiveSlotAllocations.activeSpeaker[group.id].slots.push(freeSlot);
611
+ private allocateSlotsToActiveSpeakerPaneGroups() {
612
+ this.currentLayout?.activeSpeakerVideoPaneGroups
613
+ // sorting in descending order based on group priority
614
+ ?.sort((a, b) => (a.priority < b.priority ? 1 : -1))
615
+ ?.forEach((group) => {
616
+ this.receiveSlotAllocations.activeSpeaker[group.id] = {slots: []};
617
+
618
+ for (let paneIndex = 0; paneIndex < group.numPanes; paneIndex += 1) {
619
+ // allocate a slot from the "unused" list, by grabbing in same order (shift) as previous layout
620
+ const freeSlot = this.slots.video.unused.shift();
621
+
622
+ if (freeSlot) {
623
+ this.slots.video.activeSpeaker.push(freeSlot);
624
+ this.receiveSlotAllocations.activeSpeaker[group.id].slots.push(freeSlot);
625
+ }
619
626
  }
620
- }
621
- });
627
+ });
628
+ }
622
629
 
630
+ /**
631
+ * Allocates receive slots to all receiver selected video panes
632
+ * in the current selected layout
633
+ */
634
+ private allocateSlotsToReceiverSelectedVideoPaneGroups() {
623
635
  this.currentLayout?.memberVideoPanes?.forEach((memberPane) => {
624
636
  // check if there is existing slot for this csi
625
637
  const existingSlot = this.slots.video.receiverSelected.find(
@@ -646,19 +658,15 @@ export class RemoteMediaManager extends EventsScope {
646
658
  }
647
659
 
648
660
  /**
649
- * Makes sure we have the right number of receive slots created for the current layout
650
- * and allocates them to the right video panes / pane groups
651
- *
652
- * @returns {Promise}
661
+ * Ensures that we have enough slots for the current layout.
653
662
  */
654
- private async updateVideoReceiveSlots() {
663
+ private async refillRequiredSlotsIfNeeded() {
655
664
  const requiredNumSlots = this.getRequiredNumVideoSlotsForLayout(this.currentLayout);
656
665
  const totalNumSlots =
657
666
  this.slots.video.unused.length +
658
667
  this.slots.video.activeSpeaker.length +
659
668
  this.slots.video.receiverSelected.length;
660
669
 
661
- // ensure we have enough total slots for current layout
662
670
  if (totalNumSlots < requiredNumSlots) {
663
671
  let numSlotsToCreate = requiredNumSlots - totalNumSlots;
664
672
 
@@ -671,16 +679,39 @@ export class RemoteMediaManager extends EventsScope {
671
679
  numSlotsToCreate -= 1;
672
680
  }
673
681
  }
682
+ }
683
+
684
+ /**
685
+ * Move all active speaker slots to "unused"
686
+ */
687
+ private trimActiveSpeakerSlots() {
688
+ this.slots.video.unused.push(...this.slots.video.activeSpeaker);
689
+ this.slots.video.activeSpeaker.length = 0;
690
+ }
691
+
692
+ /**
693
+ * Makes sure we have the right number of receive slots created for the current layout
694
+ * and allocates them to the right video panes / pane groups
695
+ *
696
+ * @returns {Promise}
697
+ */
698
+ private async updateVideoReceiveSlots() {
699
+ // move all active speaker slots to "unused"
700
+ this.trimActiveSpeakerSlots();
674
701
 
675
702
  // move all no longer needed receiver-selected slots to "unused"
676
703
  this.trimReceiverSelectedSlots();
677
704
 
678
- // move all active speaker slots to "unused"
679
- this.slots.video.unused.push(...this.slots.video.activeSpeaker);
680
- this.slots.video.activeSpeaker.length = 0;
705
+ // ensure we have enough total slots for current layout
706
+ await this.refillRequiredSlotsIfNeeded();
681
707
 
682
708
  // allocate the slots to the right panes / pane groups
683
- this.allocateSlotsToVideoPaneGroups();
709
+ // reset allocations
710
+ this.receiveSlotAllocations = {activeSpeaker: {}, receiverSelected: {}};
711
+ // allocate active speaker
712
+ this.allocateSlotsToActiveSpeakerPaneGroups();
713
+ // allocate receiver selected
714
+ this.allocateSlotsToReceiverSelectedVideoPaneGroups();
684
715
 
685
716
  LoggerProxy.logger.log(
686
717
  `RemoteMediaManager#updateVideoReceiveSlots --> receive slots updated: unused=${this.slots.video.unused.length}, activeSpeaker=${this.slots.video.activeSpeaker.length}, receiverSelected=${this.slots.video.receiverSelected.length}`
@@ -511,8 +511,8 @@ describe('plugin-meetings', () => {
511
511
 
512
512
  assert.equal(arg.uri, 'url');
513
513
  assert.equal(arg.method, 'PUT');
514
- assert.deepEqual(argObj1, {id:'groupId', action: 'START', allowBackToMain: false, allowToJoinLater: false});
515
- assert.deepEqual(argObj2, {id:'id', action: 'START', allowBackToMain: false, allowToJoinLater: false, someOtherParam: 'someOtherParam'});
514
+ assert.deepEqual(argObj1, {id:'groupId', action: 'START', allowBackToMain: false, allowToJoinLater: false, duration: BREAKOUTS.DEFAULT_DURATION});
515
+ assert.deepEqual(argObj2, {id:'id', action: 'START', allowBackToMain: false, allowToJoinLater: false, someOtherParam: 'someOtherParam', duration: BREAKOUTS.DEFAULT_DURATION});
516
516
  assert.deepEqual(result, {body: getBOResponse('OPEN')});
517
517
  });
518
518
 
@@ -39,64 +39,122 @@ describe('plugin-meetings', () => {
39
39
 
40
40
  assert.equal(parsedControls.entryExitTone, null);
41
41
  });
42
- });
43
- });
44
42
 
45
- describe('getControls', () => {
46
- it('returns hasEntryExitToneChanged = true when mode changed', () => {
47
- const newControls = {
48
- entryExitTone: {
49
- enabled: true,
50
- mode: 'bar',
51
- },
52
- };
53
- const {updates} = ControlsUtils.getControls(defaultControls, newControls);
54
-
55
- assert.equal(updates.hasEntryExitToneChanged, true);
56
- });
43
+ describe('videoEnabled', () => {
44
+ it('returns expected', () => {
45
+ const result = ControlsUtils.parse({video: {enabled: true}});
46
+ assert.deepEqual(result, {
47
+ video: {
48
+ enabled: true,
49
+ },
50
+ videoEnabled: true,
51
+ });
52
+ });
57
53
 
58
- it('returns hasEntryExitToneChanged = true when enabled changed', () => {
59
- const newControls = {
60
- entryExitTone: {
61
- enabled: false,
62
- mode: 'foo',
63
- },
64
- };
65
- const {updates} = ControlsUtils.getControls(defaultControls, newControls);
54
+ it('returns expected from undefined', () => {
55
+ const result = ControlsUtils.parse();
56
+ assert.deepEqual(result, {});
57
+ });
66
58
 
67
- assert.equal(updates.hasEntryExitToneChanged, true);
59
+ it('returns expected from undefined controls', () => {
60
+ const result = ControlsUtils.parse({});
61
+ assert.deepEqual(result, {});
62
+ });
63
+ });
68
64
  });
69
65
 
70
- it('returns hasEntryExitToneChanged = false when nothing changed', () => {
71
- const newControls = {
72
- entryExitTone: {
73
- enabled: true,
74
- mode: 'foo',
75
- },
76
- };
77
- const {updates} = ControlsUtils.getControls(defaultControls, newControls);
66
+ describe('getControls', () => {
67
+ it('returns hasEntryExitToneChanged = true when mode changed', () => {
68
+ const newControls = {
69
+ entryExitTone: {
70
+ enabled: true,
71
+ mode: 'bar',
72
+ },
73
+ };
74
+ const {updates} = ControlsUtils.getControls(defaultControls, newControls);
78
75
 
79
- assert.equal(updates.hasEntryExitToneChanged, false);
80
- });
76
+ assert.equal(updates.hasEntryExitToneChanged, true);
77
+ });
81
78
 
82
- it('returns hasBreakoutChanged = true when it has changed', () => {
83
- const newControls = {
84
- breakout: 'breakout'
85
- };
79
+ it('returns hasEntryExitToneChanged = true when enabled changed', () => {
80
+ const newControls = {
81
+ entryExitTone: {
82
+ enabled: false,
83
+ mode: 'foo',
84
+ },
85
+ };
86
+ const {updates} = ControlsUtils.getControls(defaultControls, newControls);
86
87
 
87
- const {updates} = ControlsUtils.getControls({breakout: 'old breakout'}, newControls);
88
+ assert.equal(updates.hasEntryExitToneChanged, true);
89
+ });
88
90
 
89
- assert.equal(updates.hasBreakoutChanged, true);
90
- });
91
+ it('returns hasEntryExitToneChanged = false when nothing changed', () => {
92
+ const newControls = {
93
+ entryExitTone: {
94
+ enabled: true,
95
+ mode: 'foo',
96
+ },
97
+ };
98
+ const {updates} = ControlsUtils.getControls(defaultControls, newControls);
99
+
100
+ assert.equal(updates.hasEntryExitToneChanged, false);
101
+ });
102
+
103
+ it('returns hasBreakoutChanged = true when it has changed', () => {
104
+ const newControls = {
105
+ breakout: 'breakout',
106
+ };
107
+
108
+ const {updates} = ControlsUtils.getControls({breakout: 'old breakout'}, newControls);
109
+
110
+ assert.equal(updates.hasBreakoutChanged, true);
111
+ });
91
112
 
92
- it('returns hasBreakoutChanged = false when it has not changed', () => {
93
- const newControls = {
94
- breakout: 'breakout'
95
- };
113
+ it('returns hasBreakoutChanged = false when it has not changed', () => {
114
+ const newControls = {
115
+ breakout: 'breakout',
116
+ };
96
117
 
97
- const {updates} = ControlsUtils.getControls({breakout: 'breakout'}, newControls);
118
+ const {updates} = ControlsUtils.getControls({breakout: 'breakout'}, newControls);
98
119
 
99
- assert.equal(updates.hasBreakoutChanged, false);
120
+ assert.equal(updates.hasBreakoutChanged, false);
121
+ });
122
+
123
+ describe('videoEnabled', () => {
124
+ const testVideoEnabled = (oldControls, newControls, updatedProperty) => {
125
+ const result = ControlsUtils.getControls(oldControls, newControls);
126
+
127
+ let expectedPrevious = oldControls;
128
+ if (Object.keys(oldControls).length) {
129
+ expectedPrevious = {
130
+ ...expectedPrevious,
131
+ ...{videoEnabled: oldControls.video.enabled},
132
+ };
133
+ }
134
+ const expectedCurrent = {...newControls, ...{videoEnabled: newControls.video.enabled}};
135
+
136
+ assert.deepEqual(result.previous, expectedPrevious);
137
+ assert.deepEqual(result.current, expectedCurrent);
138
+ if (updatedProperty !== undefined) {
139
+ assert.deepEqual(
140
+ result.updates.hasVideoEnabledChanged,
141
+ !isEqual(oldControls, newControls)
142
+ );
143
+ }
144
+ };
145
+
146
+ it('returns expected from undefined', () => {
147
+ testVideoEnabled({}, {video: {enabled: true}});
148
+ });
149
+
150
+ it('returns expected from defined', () => {
151
+ testVideoEnabled({video: {enabled: false}}, {video: {enabled: true}});
152
+ });
153
+
154
+ it('returns expected for unchanged', () => {
155
+ testVideoEnabled({video: {enabled: false}}, {video: {enabled: false}});
156
+ });
157
+ });
100
158
  });
101
159
  });
102
160
  });