@webex/plugin-meetings 3.7.0-next.20 → 3.7.0-next.21

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.
@@ -1283,12 +1283,13 @@ export default class LocusInfo extends EventsScope {
1283
1283
  /**
1284
1284
  * handles when the locus.mediaShares is updated
1285
1285
  * @param {Object} mediaShares the locus.mediaShares property
1286
+ * @param {boolean} forceUpdate force to update the mediaShares
1286
1287
  * @returns {undefined}
1287
1288
  * @memberof LocusInfo
1288
1289
  * emits internal event locus_info_update_media_shares
1289
1290
  */
1290
- updateMediaShares(mediaShares: object) {
1291
- if (mediaShares && !isEqual(this.mediaShares, mediaShares)) {
1291
+ updateMediaShares(mediaShares: object, forceUpdate = false) {
1292
+ if (mediaShares && (!isEqual(this.mediaShares, mediaShares) || forceUpdate)) {
1292
1293
  const parsedMediaShares = MediaSharesUtils.getMediaShares(this.mediaShares, mediaShares);
1293
1294
 
1294
1295
  this.updateMeeting(parsedMediaShares.current);
@@ -1303,6 +1304,7 @@ export default class LocusInfo extends EventsScope {
1303
1304
  {
1304
1305
  current: parsedMediaShares.current,
1305
1306
  previous: parsedMediaShares.previous,
1307
+ forceUpdate,
1306
1308
  }
1307
1309
  );
1308
1310
  }
@@ -849,7 +849,7 @@ export default class Meeting extends StatelessWebexPlugin {
849
849
  * @memberof Meeting
850
850
  */
851
851
  // @ts-ignore
852
- this.webinar = new Webinar({}, {parent: this.webex});
852
+ this.webinar = new Webinar({meetingId: this.id}, {parent: this.webex});
853
853
  /**
854
854
  * helper class for managing receive slots (for multistream media connections)
855
855
  */
@@ -2740,6 +2740,7 @@ export default class Meeting extends StatelessWebexPlugin {
2740
2740
  this.triggerAnnotationInfoEvent(contentShare, previousContentShare);
2741
2741
 
2742
2742
  if (
2743
+ !payload.forceUpdate &&
2743
2744
  contentShare.beneficiaryId === previousContentShare?.beneficiaryId &&
2744
2745
  contentShare.disposition === previousContentShare?.disposition &&
2745
2746
  contentShare.deviceUrlSharing === previousContentShare.deviceUrlSharing &&
@@ -2786,7 +2787,11 @@ export default class Meeting extends StatelessWebexPlugin {
2786
2787
  // It does not matter who requested to share the whiteboard, everyone gets the same view
2787
2788
  else if (whiteboardShare.disposition === FLOOR_ACTION.GRANTED) {
2788
2789
  // WHITEBOARD - sharing whiteboard
2789
- newShareStatus = SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
2790
+ // Webinar attendee should receive whiteboard as remote share
2791
+ newShareStatus =
2792
+ this.locusInfo?.info?.isWebinar && this.webinar?.selfIsAttendee
2793
+ ? SHARE_STATUS.REMOTE_SHARE_ACTIVE
2794
+ : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
2790
2795
  }
2791
2796
  // or if content share is either released or null and whiteboard share is either released or null, no one is sharing
2792
2797
  else if (
@@ -2801,6 +2806,7 @@ export default class Meeting extends StatelessWebexPlugin {
2801
2806
  LoggerProxy.logger.info(
2802
2807
  `Meeting:index#setUpLocusInfoMediaInactiveListener --> this.shareStatus=${this.shareStatus} newShareStatus=${newShareStatus}`
2803
2808
  );
2809
+
2804
2810
  if (newShareStatus !== this.shareStatus) {
2805
2811
  const oldShareStatus = this.shareStatus;
2806
2812
 
@@ -3058,7 +3064,20 @@ export default class Meeting extends StatelessWebexPlugin {
3058
3064
  */
3059
3065
  private setUpLocusResourcesListener() {
3060
3066
  this.locusInfo.on(LOCUSINFO.EVENTS.LINKS_RESOURCES, (payload) => {
3061
- this.webinar.updateWebcastUrl(payload);
3067
+ if (payload) {
3068
+ this.webinar.updateWebcastUrl(payload);
3069
+ Trigger.trigger(
3070
+ this,
3071
+ {
3072
+ file: 'meeting/index',
3073
+ function: 'setUpLocusInfoMeetingInfoListener',
3074
+ },
3075
+ EVENT_TRIGGERS.MEETING_RESOURCE_LINKS_UPDATE,
3076
+ {
3077
+ payload,
3078
+ }
3079
+ );
3080
+ }
3062
3081
  });
3063
3082
  }
3064
3083
 
@@ -3377,6 +3396,7 @@ export default class Meeting extends StatelessWebexPlugin {
3377
3396
  payload.newRoles?.includes(SELF_ROLES.MODERATOR)
3378
3397
  );
3379
3398
  this.webinar.updateRoleChanged(payload);
3399
+
3380
3400
  Trigger.trigger(
3381
3401
  this,
3382
3402
  {
@@ -5580,17 +5600,23 @@ export default class Meeting extends StatelessWebexPlugin {
5580
5600
  */
5581
5601
  async updateLLMConnection() {
5582
5602
  // @ts-ignore - Fix type
5583
- const {url, info: {datachannelUrl} = {}} = this.locusInfo;
5603
+ const {url, info: {datachannelUrl, practiceSessionDatachannelUrl} = {}} = this.locusInfo;
5584
5604
 
5585
5605
  const isJoined = this.isJoined();
5586
5606
 
5607
+ // webinar panelist should use new data channel in practice session
5608
+ const dataChannelUrl =
5609
+ this.webinar.isJoinPracticeSessionDataChannel() && practiceSessionDatachannelUrl
5610
+ ? practiceSessionDatachannelUrl
5611
+ : datachannelUrl;
5612
+
5587
5613
  // @ts-ignore - Fix type
5588
5614
  if (this.webex.internal.llm.isConnected()) {
5589
5615
  if (
5590
5616
  // @ts-ignore - Fix type
5591
5617
  url === this.webex.internal.llm.getLocusUrl() &&
5592
5618
  // @ts-ignore - Fix type
5593
- datachannelUrl === this.webex.internal.llm.getDatachannelUrl() &&
5619
+ dataChannelUrl === this.webex.internal.llm.getDatachannelUrl() &&
5594
5620
  isJoined
5595
5621
  ) {
5596
5622
  return undefined;
@@ -5607,7 +5633,7 @@ export default class Meeting extends StatelessWebexPlugin {
5607
5633
 
5608
5634
  // @ts-ignore - Fix type
5609
5635
  return this.webex.internal.llm
5610
- .registerAndConnect(url, datachannelUrl)
5636
+ .registerAndConnect(url, dataChannelUrl)
5611
5637
  .then((registerAndConnectResult) => {
5612
5638
  // @ts-ignore - Fix type
5613
5639
  this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
@@ -441,6 +441,9 @@ const MeetingUtil = {
441
441
  displayHints.includes(DISPLAY_HINTS.LEAVE_END_MEETING),
442
442
 
443
443
  canManageBreakout: (displayHints) => displayHints.includes(DISPLAY_HINTS.BREAKOUT_MANAGEMENT),
444
+
445
+ canStartBreakout: (displayHints) => !displayHints.includes(DISPLAY_HINTS.DISABLE_BREAKOUT_START),
446
+
444
447
  canBroadcastMessageToBreakout: (displayHints, policies = {}) =>
445
448
  displayHints.includes(DISPLAY_HINTS.BROADCAST_MESSAGE_TO_BREAKOUT) &&
446
449
  !!policies[SELF_POLICY.SUPPORT_BROADCAST_MESSAGE],
@@ -4,7 +4,7 @@
4
4
  import {WebexPlugin, config} from '@webex/webex-core';
5
5
  import uuid from 'uuid';
6
6
  import {get} from 'lodash';
7
- import {HEADERS, HTTP_VERBS, MEETINGS, SELF_ROLES} from '../constants';
7
+ import {_ID_, HEADERS, HTTP_VERBS, MEETINGS, SELF_ROLES, SHARE_STATUS} from '../constants';
8
8
 
9
9
  import WebinarCollection from './collection';
10
10
  import LoggerProxy from '../common/logs/logger-proxy';
@@ -25,6 +25,7 @@ const Webinar = WebexPlugin.extend({
25
25
  selfIsPanelist: 'boolean', // self is panelist
26
26
  selfIsAttendee: 'boolean', // self is attendee
27
27
  practiceSessionEnabled: 'boolean', // practice session enabled
28
+ meetingId: 'string',
28
29
  },
29
30
 
30
31
  /**
@@ -68,14 +69,47 @@ const Webinar = WebexPlugin.extend({
68
69
  const isPromoted =
69
70
  oldRoles.includes(SELF_ROLES.ATTENDEE) && newRoles.includes(SELF_ROLES.PANELIST);
70
71
  const isDemoted =
71
- oldRoles.includes(SELF_ROLES.PANELIST) && newRoles.includes(SELF_ROLES.ATTENDEE);
72
+ (oldRoles.includes(SELF_ROLES.PANELIST) && newRoles.includes(SELF_ROLES.ATTENDEE)) ||
73
+ (!oldRoles.includes(SELF_ROLES.ATTENDEE) && newRoles.includes(SELF_ROLES.ATTENDEE)); // for attendee just join meeting case
72
74
  this.set('selfIsPanelist', newRoles.includes(SELF_ROLES.PANELIST));
73
75
  this.set('selfIsAttendee', newRoles.includes(SELF_ROLES.ATTENDEE));
74
76
  this.updateCanManageWebcast(newRoles.includes(SELF_ROLES.MODERATOR));
77
+ this.updateStatusByRole({isPromoted, isDemoted});
75
78
 
76
79
  return {isPromoted, isDemoted};
77
80
  },
78
81
 
82
+ /**
83
+ * should join practice session data channel or not
84
+ * @param {Object} {isPromoted: boolean, isDemoted: boolean}} Role transition states
85
+ * @returns {void}
86
+ */
87
+ updateStatusByRole({isPromoted, isDemoted}) {
88
+ const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
89
+
90
+ if (
91
+ (isDemoted && meeting?.shareStatus === SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE) ||
92
+ isPromoted
93
+ ) {
94
+ // attendees in webinar should subscribe streaming for whiteboard sharing
95
+ // while panelist still need subscribe native mode so trigger force update here
96
+ meeting?.locusInfo?.updateMediaShares(meeting?.locusInfo?.mediaShares, true);
97
+ }
98
+
99
+ if (this.practiceSessionEnabled) {
100
+ // may need change data channel in practice session
101
+ meeting?.updateLLMConnection();
102
+ }
103
+ },
104
+
105
+ /**
106
+ * should join practice session data channel or not
107
+ * @returns {boolean}
108
+ */
109
+ isJoinPracticeSessionDataChannel() {
110
+ return this.selfIsPanelist && this.practiceSessionEnabled;
111
+ },
112
+
79
113
  /**
80
114
  * start or stop practice session for webinar
81
115
  * @param {boolean} enabled
@@ -9,6 +9,7 @@ import LocusInfo from '@webex/plugin-meetings/src/locus-info';
9
9
  import SelfUtils from '@webex/plugin-meetings/src/locus-info/selfUtils';
10
10
  import InfoUtils from '@webex/plugin-meetings/src/locus-info/infoUtils';
11
11
  import EmbeddedAppsUtils from '@webex/plugin-meetings/src/locus-info/embeddedAppsUtils';
12
+ import MediaSharesUtils from '@webex/plugin-meetings/src/locus-info//mediaSharesUtils';
12
13
  import LocusDeltaParser from '@webex/plugin-meetings/src/locus-info/parser';
13
14
  import Metrics from '@webex/plugin-meetings/src/metrics';
14
15
 
@@ -1637,6 +1638,134 @@ describe('plugin-meetings', () => {
1637
1638
  });
1638
1639
  });
1639
1640
 
1641
+ describe('#updateMediaShares', () => {
1642
+ let getMediaSharesSpy;
1643
+
1644
+ beforeEach(() => {
1645
+ // Spy on MediaSharesUtils.getMediaShares
1646
+ getMediaSharesSpy = sinon.stub(MediaSharesUtils, 'getMediaShares');
1647
+
1648
+ // Stub the emitScoped method to monitor its calls
1649
+ sinon.stub(locusInfo, 'emitScoped');
1650
+ });
1651
+
1652
+ afterEach(() => {
1653
+ getMediaSharesSpy.restore();
1654
+ locusInfo.emitScoped.restore();
1655
+ });
1656
+
1657
+ it('should update media shares and emit LOCUS_INFO_UPDATE_MEDIA_SHARES when mediaShares change', () => {
1658
+ const initialMediaShares = { audio: true, video: false };
1659
+ const newMediaShares = { audio: false, video: true };
1660
+
1661
+ locusInfo.mediaShares = initialMediaShares;
1662
+ locusInfo.parsedLocus = { mediaShares: null };
1663
+
1664
+ const parsedMediaShares = {
1665
+ current: newMediaShares,
1666
+ previous: initialMediaShares,
1667
+ };
1668
+
1669
+ // Stub MediaSharesUtils.getMediaShares to return the expected parsedMediaShares
1670
+ getMediaSharesSpy.returns(parsedMediaShares);
1671
+
1672
+ // Call the function
1673
+ locusInfo.updateMediaShares(newMediaShares);
1674
+
1675
+ // Assert that MediaSharesUtils.getMediaShares was called with correct arguments
1676
+ assert.calledWith(getMediaSharesSpy, initialMediaShares, newMediaShares);
1677
+
1678
+ // Assert that updateMeeting was called with the parsed current media shares
1679
+ assert.deepEqual(locusInfo.parsedLocus.mediaShares, newMediaShares);
1680
+ assert.deepEqual(locusInfo.mediaShares, newMediaShares);
1681
+
1682
+ // Assert that emitScoped was called with the correct event
1683
+ assert.calledWith(
1684
+ locusInfo.emitScoped,
1685
+ {
1686
+ file: 'locus-info',
1687
+ function: 'updateMediaShares',
1688
+ },
1689
+ EVENTS.LOCUS_INFO_UPDATE_MEDIA_SHARES,
1690
+ {
1691
+ current: newMediaShares,
1692
+ previous: initialMediaShares,
1693
+ forceUpdate: false,
1694
+ }
1695
+ );
1696
+ });
1697
+
1698
+ it('should force update media shares and emit LOCUS_INFO_UPDATE_MEDIA_SHARES even if shares are the same', () => {
1699
+ const initialMediaShares = { audio: true, video: false };
1700
+ locusInfo.mediaShares = initialMediaShares;
1701
+ locusInfo.parsedLocus = { mediaShares: null };
1702
+
1703
+ const parsedMediaShares = {
1704
+ current: initialMediaShares,
1705
+ previous: initialMediaShares,
1706
+ };
1707
+
1708
+ getMediaSharesSpy.returns(parsedMediaShares);
1709
+
1710
+ // Call the function with forceUpdate = true
1711
+ locusInfo.updateMediaShares(initialMediaShares, true);
1712
+
1713
+ // Assert that MediaSharesUtils.getMediaShares was called
1714
+ assert.calledWith(getMediaSharesSpy, initialMediaShares, initialMediaShares);
1715
+
1716
+ // Assert that emitScoped was called with the correct event
1717
+ assert.calledWith(
1718
+ locusInfo.emitScoped,
1719
+ {
1720
+ file: 'locus-info',
1721
+ function: 'updateMediaShares',
1722
+ },
1723
+ EVENTS.LOCUS_INFO_UPDATE_MEDIA_SHARES,
1724
+ {
1725
+ current: initialMediaShares,
1726
+ previous: initialMediaShares,
1727
+ forceUpdate: true,
1728
+ }
1729
+ );
1730
+ });
1731
+
1732
+ it('should not emit LOCUS_INFO_UPDATE_MEDIA_SHARES if mediaShares do not change and forceUpdate is false', () => {
1733
+ const initialMediaShares = { audio: true, video: false };
1734
+ locusInfo.mediaShares = initialMediaShares;
1735
+
1736
+ // Call the function with the same mediaShares and forceUpdate = false
1737
+ locusInfo.updateMediaShares(initialMediaShares);
1738
+
1739
+ // Assert that MediaSharesUtils.getMediaShares was not called
1740
+ assert.notCalled(getMediaSharesSpy);
1741
+
1742
+ // Assert that emitScoped was not called
1743
+ assert.notCalled(locusInfo.emitScoped);
1744
+ });
1745
+
1746
+ it('should update internal state correctly when mediaShares are updated', () => {
1747
+ const initialMediaShares = { audio: true, video: false };
1748
+ const newMediaShares = { audio: false, video: true };
1749
+
1750
+ locusInfo.mediaShares = initialMediaShares;
1751
+ locusInfo.parsedLocus = { mediaShares: null };
1752
+
1753
+ const parsedMediaShares = {
1754
+ current: newMediaShares,
1755
+ previous: initialMediaShares,
1756
+ };
1757
+
1758
+ getMediaSharesSpy.returns(parsedMediaShares);
1759
+
1760
+ // Call the function
1761
+ locusInfo.updateMediaShares(newMediaShares);
1762
+
1763
+ // Assert that the internal state was updated correctly
1764
+ assert.deepEqual(locusInfo.parsedLocus.mediaShares, newMediaShares);
1765
+ assert.deepEqual(locusInfo.mediaShares, newMediaShares);
1766
+ });
1767
+ });
1768
+
1640
1769
  describe('#updateEmbeddedApps()', () => {
1641
1770
  const newEmbeddedApps = [
1642
1771
  {
@@ -10686,6 +10686,7 @@ describe('plugin-meetings', () => {
10686
10686
  meeting.webex.internal.llm.on = sinon.stub();
10687
10687
  meeting.webex.internal.llm.off = sinon.stub();
10688
10688
  meeting.processRelayEvent = sinon.stub();
10689
+ meeting.webinar.isJoinPracticeSessionDataChannel = sinon.stub().returns(false);
10689
10690
  });
10690
10691
 
10691
10692
  it('does not connect if the call is not joined yet', async () => {
@@ -10817,6 +10818,19 @@ describe('plugin-meetings', () => {
10817
10818
  meeting.processRelayEvent
10818
10819
  );
10819
10820
  });
10821
+
10822
+
10823
+ it('connect ps data channel if ps started in webinar', async () => {
10824
+ meeting.joinedWith = {state: 'JOINED'};
10825
+ meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url', practiceSessionDatachannelUrl: 'a ps datachannel url'}};
10826
+ meeting.webinar.isJoinPracticeSessionDataChannel = sinon.stub().returns(true);
10827
+ await meeting.updateLLMConnection();
10828
+
10829
+ assert.notCalled(webex.internal.llm.disconnectLLM);
10830
+ assert.calledWith(webex.internal.llm.registerAndConnect, 'a url', 'a ps datachannel url');
10831
+
10832
+ });
10833
+
10820
10834
  });
10821
10835
 
10822
10836
  describe('#setLocus', () => {
@@ -11008,6 +11022,7 @@ describe('plugin-meetings', () => {
11008
11022
  beforeEach(() => {
11009
11023
  meeting.selfId = '9528d952-e4de-46cf-8157-fd4823b98377';
11010
11024
  meeting.deviceUrl = 'my-web-url';
11025
+ meeting.locusInfo.info = {isWebinar: false};
11011
11026
  });
11012
11027
 
11013
11028
  const USER_IDS = {
@@ -11233,13 +11248,24 @@ describe('plugin-meetings', () => {
11233
11248
 
11234
11249
  activeSharingId.whiteboard = beneficiaryId;
11235
11250
 
11236
- eventTrigger.share.push({
11251
+ eventTrigger.share.push(meeting.webinar.selfIsAttendee ? {
11252
+ eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
11253
+ functionName: 'remoteShare',
11254
+ eventPayload: {
11255
+ memberId: null,
11256
+ url,
11257
+ shareInstanceId,
11258
+ annotationInfo: undefined,
11259
+ resourceType: undefined,
11260
+ },
11261
+ } : {
11237
11262
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
11238
11263
  functionName: 'startWhiteboardShare',
11239
11264
  eventPayload: {resourceUrl, memberId: beneficiaryId},
11240
11265
  });
11241
11266
 
11242
- shareStatus = SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11267
+ shareStatus = meeting.webinar.selfIsAttendee ? SHARE_STATUS.REMOTE_SHARE_ACTIVE : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11268
+
11243
11269
  }
11244
11270
 
11245
11271
  if (eventTrigger.member) {
@@ -11271,13 +11297,24 @@ describe('plugin-meetings', () => {
11271
11297
  newPayload.current.content.disposition = FLOOR_ACTION.ACCEPTED;
11272
11298
  newPayload.current.content.beneficiaryId = otherBeneficiaryId;
11273
11299
 
11274
- eventTrigger.share.push({
11300
+ eventTrigger.share.push(meeting.webinar.selfIsAttendee ? {
11301
+ eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
11302
+ functionName: 'remoteShare',
11303
+ eventPayload: {
11304
+ memberId: null,
11305
+ url,
11306
+ shareInstanceId,
11307
+ annotationInfo: undefined,
11308
+ resourceType: undefined,
11309
+ },
11310
+ } : {
11275
11311
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
11276
11312
  functionName: 'startWhiteboardShare',
11277
11313
  eventPayload: {resourceUrl, memberId: beneficiaryId},
11278
11314
  });
11279
11315
 
11280
- shareStatus = SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11316
+ shareStatus = meeting.webinar.selfIsAttendee ? SHARE_STATUS.REMOTE_SHARE_ACTIVE : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11317
+
11281
11318
  } else {
11282
11319
  eventTrigger.share.push({
11283
11320
  eventName: EVENT_TRIGGERS.MEETING_STOPPED_SHARING_WHITEBOARD,
@@ -11404,6 +11441,38 @@ describe('plugin-meetings', () => {
11404
11441
  assert.exists(meeting.setUpLocusMediaSharesListener);
11405
11442
  });
11406
11443
 
11444
+ describe('Whiteboard Share - Webinar Attendee', () => {
11445
+ it('Scenario #1: Whiteboard sharing as a webinar attendee', () => {
11446
+ // Set the webinar attendee flag
11447
+ meeting.webinar = { selfIsAttendee: true };
11448
+ meeting.locusInfo.info.isWebinar = true;
11449
+
11450
+ // Step 1: Start sharing whiteboard A
11451
+ const data1 = generateData(
11452
+ blankPayload, // Initial payload
11453
+ true, // isGranting: Granting share
11454
+ false, // isContent: Whiteboard (not content)
11455
+ USER_IDS.REMOTE_A, // Beneficiary ID: Remote user A
11456
+ RESOURCE_URLS.WHITEBOARD_A // Resource URL: Whiteboard A
11457
+ );
11458
+
11459
+ // Step 2: Stop sharing whiteboard A
11460
+ const data2 = generateData(
11461
+ data1.payload, // Updated payload from Step 1
11462
+ false, // isGranting: Stopping share
11463
+ false, // isContent: Whiteboard
11464
+ USER_IDS.REMOTE_A // Beneficiary ID: Remote user A
11465
+ );
11466
+
11467
+ // Validate the payload changes and status updates
11468
+ payloadTestHelper([data1]);
11469
+
11470
+ // Specific assertions for webinar attendee status
11471
+ assert.equal(meeting.shareStatus, SHARE_STATUS.REMOTE_SHARE_ACTIVE);
11472
+ });
11473
+ });
11474
+
11475
+
11407
11476
  describe('Whiteboard A --> Whiteboard B', () => {
11408
11477
  it('Scenario #1: you share both whiteboards', () => {
11409
11478
  const data1 = generateData(
@@ -26,7 +26,7 @@ describe('plugin-meetings', () => {
26
26
  webex.meetings.reachability = {
27
27
  getReachabilityReportToAttachToRoap: sinon.stub().resolves({}),
28
28
  getClientMediaPreferences: sinon.stub().resolves({}),
29
- };
29
+ };
30
30
 
31
31
  const logger = {
32
32
  info: sandbox.stub(),
@@ -409,17 +409,17 @@ describe('plugin-meetings', () => {
409
409
  const FAKE_CLIENT_MEDIA_PREFERENCES = {
410
410
  id: 'fake client media preferences',
411
411
  };
412
-
412
+
413
413
  webex.meetings.reachability.getReachabilityReportToAttachToRoap.resolves(FAKE_REACHABILITY_REPORT);
414
414
  webex.meetings.reachability.getClientMediaPreferences.resolves(FAKE_CLIENT_MEDIA_PREFERENCES);
415
-
415
+
416
416
  sinon
417
417
  .stub(webex.internal.device.ipNetworkDetector, 'supportsIpV4')
418
418
  .get(() => true);
419
419
  sinon
420
420
  .stub(webex.internal.device.ipNetworkDetector, 'supportsIpV6')
421
421
  .get(() => true);
422
-
422
+
423
423
  await MeetingUtil.joinMeeting(meeting, {
424
424
  reachability: 'reachability',
425
425
  roapMessage: 'roapMessage',
@@ -760,6 +760,13 @@ describe('plugin-meetings', () => {
760
760
  });
761
761
  });
762
762
 
763
+ describe('canStartBreakout', () => {
764
+ it('works as expected', () => {
765
+ assert.deepEqual(MeetingUtil.canStartBreakout(['DISABLE_BREAKOUT_START']), false);
766
+ assert.deepEqual(MeetingUtil.canStartBreakout([]), true);
767
+ });
768
+ });
769
+
763
770
  describe('canBroadcastMessageToBreakout', () => {
764
771
  it('works as expected', () => {
765
772
  assert.deepEqual(