@webex/plugin-meetings 3.12.0-next.9 → 3.12.0-next.91

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 (213) hide show
  1. package/AGENTS.md +9 -0
  2. package/dist/aiEnableRequest/index.js +15 -2
  3. package/dist/aiEnableRequest/index.js.map +1 -1
  4. package/dist/breakouts/breakout.js +8 -3
  5. package/dist/breakouts/breakout.js.map +1 -1
  6. package/dist/breakouts/index.js +26 -2
  7. package/dist/breakouts/index.js.map +1 -1
  8. package/dist/config.js +2 -2
  9. package/dist/config.js.map +1 -1
  10. package/dist/constants.js +35 -9
  11. package/dist/constants.js.map +1 -1
  12. package/dist/controls-options-manager/constants.js +11 -1
  13. package/dist/controls-options-manager/constants.js.map +1 -1
  14. package/dist/controls-options-manager/index.js +67 -29
  15. package/dist/controls-options-manager/index.js.map +1 -1
  16. package/dist/controls-options-manager/util.js +91 -0
  17. package/dist/controls-options-manager/util.js.map +1 -1
  18. package/dist/hashTree/constants.js +13 -1
  19. package/dist/hashTree/constants.js.map +1 -1
  20. package/dist/hashTree/hashTreeParser.js +880 -382
  21. package/dist/hashTree/hashTreeParser.js.map +1 -1
  22. package/dist/hashTree/utils.js +42 -0
  23. package/dist/hashTree/utils.js.map +1 -1
  24. package/dist/index.js +7 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/interceptors/dataChannelAuthToken.js +75 -15
  27. package/dist/interceptors/dataChannelAuthToken.js.map +1 -1
  28. package/dist/interceptors/locusRetry.js +23 -8
  29. package/dist/interceptors/locusRetry.js.map +1 -1
  30. package/dist/interpretation/index.js +10 -1
  31. package/dist/interpretation/index.js.map +1 -1
  32. package/dist/interpretation/interpretation.types.js +7 -0
  33. package/dist/interpretation/interpretation.types.js.map +1 -0
  34. package/dist/interpretation/siLanguage.js +1 -1
  35. package/dist/locus-info/controlsUtils.js +4 -1
  36. package/dist/locus-info/controlsUtils.js.map +1 -1
  37. package/dist/locus-info/index.js +303 -88
  38. package/dist/locus-info/index.js.map +1 -1
  39. package/dist/locus-info/infoUtils.js +36 -5
  40. package/dist/locus-info/infoUtils.js.map +1 -1
  41. package/dist/locus-info/types.js +19 -0
  42. package/dist/locus-info/types.js.map +1 -1
  43. package/dist/media/index.js +3 -1
  44. package/dist/media/index.js.map +1 -1
  45. package/dist/media/properties.js +1 -0
  46. package/dist/media/properties.js.map +1 -1
  47. package/dist/meeting/in-meeting-actions.js +5 -1
  48. package/dist/meeting/in-meeting-actions.js.map +1 -1
  49. package/dist/meeting/index.js +1233 -793
  50. package/dist/meeting/index.js.map +1 -1
  51. package/dist/meeting/locusMediaRequest.js +229 -82
  52. package/dist/meeting/locusMediaRequest.js.map +1 -1
  53. package/dist/meeting/muteState.js +10 -1
  54. package/dist/meeting/muteState.js.map +1 -1
  55. package/dist/meeting/request.js +5 -2
  56. package/dist/meeting/request.js.map +1 -1
  57. package/dist/meeting/util.js +35 -2
  58. package/dist/meeting/util.js.map +1 -1
  59. package/dist/meeting-info/meeting-info-v2.js +2 -2
  60. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  61. package/dist/meetings/index.js +231 -118
  62. package/dist/meetings/index.js.map +1 -1
  63. package/dist/meetings/meetings.types.js +6 -1
  64. package/dist/meetings/meetings.types.js.map +1 -1
  65. package/dist/meetings/request.js +39 -0
  66. package/dist/meetings/request.js.map +1 -1
  67. package/dist/meetings/util.js +79 -5
  68. package/dist/meetings/util.js.map +1 -1
  69. package/dist/member/index.js +10 -0
  70. package/dist/member/index.js.map +1 -1
  71. package/dist/member/types.js.map +1 -1
  72. package/dist/member/util.js +30 -0
  73. package/dist/member/util.js.map +1 -1
  74. package/dist/members/index.js +101 -43
  75. package/dist/members/index.js.map +1 -1
  76. package/dist/members/util.js +8 -1
  77. package/dist/members/util.js.map +1 -1
  78. package/dist/metrics/constants.js +5 -1
  79. package/dist/metrics/constants.js.map +1 -1
  80. package/dist/multistream/codec/constants.js +63 -0
  81. package/dist/multistream/codec/constants.js.map +1 -0
  82. package/dist/multistream/mediaRequestManager.js +62 -15
  83. package/dist/multistream/mediaRequestManager.js.map +1 -1
  84. package/dist/multistream/receiveSlot.js +9 -0
  85. package/dist/multistream/receiveSlot.js.map +1 -1
  86. package/dist/reachability/index.js +10 -13
  87. package/dist/reachability/index.js.map +1 -1
  88. package/dist/reachability/reachability.types.js +2 -0
  89. package/dist/reachability/reachability.types.js.map +1 -1
  90. package/dist/reachability/util.js +45 -0
  91. package/dist/reachability/util.js.map +1 -1
  92. package/dist/reactions/reactions.type.js.map +1 -1
  93. package/dist/recording-controller/index.js +1 -3
  94. package/dist/recording-controller/index.js.map +1 -1
  95. package/dist/types/config.d.ts +2 -2
  96. package/dist/types/constants.d.ts +12 -1
  97. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  98. package/dist/types/controls-options-manager/index.d.ts +16 -0
  99. package/dist/types/hashTree/constants.d.ts +2 -0
  100. package/dist/types/hashTree/hashTreeParser.d.ts +146 -17
  101. package/dist/types/hashTree/utils.d.ts +18 -0
  102. package/dist/types/index.d.ts +3 -0
  103. package/dist/types/interceptors/locusRetry.d.ts +4 -4
  104. package/dist/types/interpretation/interpretation.types.d.ts +10 -0
  105. package/dist/types/locus-info/index.d.ts +50 -6
  106. package/dist/types/locus-info/infoUtils.d.ts +8 -0
  107. package/dist/types/locus-info/types.d.ts +21 -1
  108. package/dist/types/media/properties.d.ts +1 -0
  109. package/dist/types/meeting/in-meeting-actions.d.ts +4 -0
  110. package/dist/types/meeting/index.d.ts +94 -7
  111. package/dist/types/meeting/locusMediaRequest.d.ts +12 -0
  112. package/dist/types/meeting/request.d.ts +1 -0
  113. package/dist/types/meeting/util.d.ts +9 -0
  114. package/dist/types/meetings/index.d.ts +30 -18
  115. package/dist/types/meetings/meetings.types.d.ts +15 -0
  116. package/dist/types/meetings/request.d.ts +14 -0
  117. package/dist/types/member/index.d.ts +1 -0
  118. package/dist/types/member/types.d.ts +1 -0
  119. package/dist/types/member/util.d.ts +9 -0
  120. package/dist/types/members/index.d.ts +15 -2
  121. package/dist/types/members/util.d.ts +1 -5
  122. package/dist/types/metrics/constants.d.ts +4 -0
  123. package/dist/types/multistream/codec/constants.d.ts +7 -0
  124. package/dist/types/multistream/mediaRequestManager.d.ts +22 -5
  125. package/dist/types/reachability/index.d.ts +1 -1
  126. package/dist/types/reachability/reachability.types.d.ts +7 -0
  127. package/dist/types/reachability/util.d.ts +17 -0
  128. package/dist/types/reactions/reactions.type.d.ts +3 -0
  129. package/dist/webinar/index.js +305 -159
  130. package/dist/webinar/index.js.map +1 -1
  131. package/package.json +22 -22
  132. package/src/aiEnableRequest/index.ts +16 -0
  133. package/src/breakouts/breakout.ts +3 -1
  134. package/src/breakouts/index.ts +31 -0
  135. package/src/config.ts +2 -2
  136. package/src/constants.ts +20 -3
  137. package/src/controls-options-manager/constants.ts +14 -1
  138. package/src/controls-options-manager/index.ts +87 -28
  139. package/src/controls-options-manager/util.ts +81 -1
  140. package/src/hashTree/constants.ts +16 -0
  141. package/src/hashTree/hashTreeParser.ts +580 -196
  142. package/src/hashTree/utils.ts +36 -0
  143. package/src/index.ts +6 -0
  144. package/src/interceptors/dataChannelAuthToken.ts +88 -12
  145. package/src/interceptors/locusRetry.ts +25 -4
  146. package/src/interpretation/index.ts +27 -9
  147. package/src/interpretation/interpretation.types.ts +11 -0
  148. package/src/locus-info/controlsUtils.ts +3 -1
  149. package/src/locus-info/index.ts +304 -100
  150. package/src/locus-info/infoUtils.ts +41 -6
  151. package/src/locus-info/types.ts +25 -1
  152. package/src/media/index.ts +3 -0
  153. package/src/media/properties.ts +1 -0
  154. package/src/meeting/in-meeting-actions.ts +8 -0
  155. package/src/meeting/index.ts +494 -67
  156. package/src/meeting/locusMediaRequest.ts +106 -6
  157. package/src/meeting/muteState.ts +10 -1
  158. package/src/meeting/request.ts +11 -0
  159. package/src/meeting/util.ts +47 -5
  160. package/src/meeting-info/meeting-info-v2.ts +4 -2
  161. package/src/meetings/index.ts +133 -78
  162. package/src/meetings/meetings.types.ts +19 -0
  163. package/src/meetings/request.ts +43 -0
  164. package/src/meetings/util.ts +97 -1
  165. package/src/member/index.ts +10 -0
  166. package/src/member/types.ts +1 -0
  167. package/src/member/util.ts +29 -0
  168. package/src/members/index.ts +70 -7
  169. package/src/members/util.ts +12 -4
  170. package/src/metrics/constants.ts +4 -0
  171. package/src/multistream/codec/constants.ts +58 -0
  172. package/src/multistream/mediaRequestManager.ts +119 -28
  173. package/src/multistream/receiveSlot.ts +18 -0
  174. package/src/reachability/index.ts +19 -15
  175. package/src/reachability/reachability.types.ts +11 -0
  176. package/src/reachability/util.ts +36 -0
  177. package/src/reactions/reactions.type.ts +3 -0
  178. package/src/recording-controller/index.ts +1 -2
  179. package/src/webinar/index.ts +214 -36
  180. package/test/unit/spec/aiEnableRequest/index.ts +86 -0
  181. package/test/unit/spec/breakouts/breakout.ts +9 -3
  182. package/test/unit/spec/breakouts/index.ts +49 -0
  183. package/test/unit/spec/controls-options-manager/index.js +244 -29
  184. package/test/unit/spec/controls-options-manager/util.js +165 -0
  185. package/test/unit/spec/hashTree/hashTreeParser.ts +1838 -180
  186. package/test/unit/spec/hashTree/utils.ts +125 -1
  187. package/test/unit/spec/interceptors/dataChannelAuthToken.ts +196 -0
  188. package/test/unit/spec/interceptors/locusRetry.ts +205 -4
  189. package/test/unit/spec/interpretation/index.ts +26 -4
  190. package/test/unit/spec/locus-info/controlsUtils.js +172 -57
  191. package/test/unit/spec/locus-info/index.js +499 -81
  192. package/test/unit/spec/locus-info/infoUtils.js +184 -1
  193. package/test/unit/spec/media/index.ts +31 -0
  194. package/test/unit/spec/meeting/in-meeting-actions.ts +4 -0
  195. package/test/unit/spec/meeting/index.js +1615 -42
  196. package/test/unit/spec/meeting/locusMediaRequest.ts +302 -4
  197. package/test/unit/spec/meeting/muteState.js +81 -0
  198. package/test/unit/spec/meeting/request.js +12 -0
  199. package/test/unit/spec/meeting/utils.js +157 -1
  200. package/test/unit/spec/meeting-info/meetinginfov2.js +19 -10
  201. package/test/unit/spec/meetings/index.js +363 -36
  202. package/test/unit/spec/meetings/request.js +141 -0
  203. package/test/unit/spec/meetings/utils.js +189 -0
  204. package/test/unit/spec/member/index.js +7 -0
  205. package/test/unit/spec/member/util.js +89 -0
  206. package/test/unit/spec/members/index.js +216 -0
  207. package/test/unit/spec/members/request.js +37 -1
  208. package/test/unit/spec/members/utils.js +63 -0
  209. package/test/unit/spec/multistream/mediaRequestManager.ts +501 -37
  210. package/test/unit/spec/reachability/index.ts +59 -46
  211. package/test/unit/spec/reachability/util.ts +31 -0
  212. package/test/unit/spec/recording-controller/index.js +9 -8
  213. package/test/unit/spec/webinar/index.ts +329 -28
@@ -14,12 +14,14 @@ import StaticConfig from '@webex/plugin-meetings/src/common/config';
14
14
  import TriggerProxy from '@webex/plugin-meetings/src/common/events/trigger-proxy';
15
15
  import LoggerProxy from '@webex/plugin-meetings/src/common/logs/logger-proxy';
16
16
  import LoggerConfig from '@webex/plugin-meetings/src/common/logs/logger-config';
17
+ import ParameterError from '@webex/plugin-meetings/src/common/errors/parameter';
17
18
  import Meeting, {CallStateForMetrics} from '@webex/plugin-meetings/src/meeting';
18
19
  import {Services} from '@webex/webex-core';
19
20
  import MeetingUtil from '@webex/plugin-meetings/src/meeting/util';
20
21
  import Meetings from '@webex/plugin-meetings/src/meetings';
21
22
  import MeetingCollection from '@webex/plugin-meetings/src/meetings/collection';
22
23
  import MeetingsUtil from '@webex/plugin-meetings/src/meetings/util';
24
+ import {SitePreferenceSelectOption} from '@webex/plugin-meetings/src/meetings/meetings.types';
23
25
  import PersonalMeetingRoom from '@webex/plugin-meetings/src/personal-meeting-room';
24
26
  import Reachability from '@webex/plugin-meetings/src/reachability';
25
27
  import Metrics from '@webex/plugin-meetings/src/metrics';
@@ -91,6 +93,7 @@ describe('plugin-meetings', () => {
91
93
  locusInfo = {
92
94
  parse: sinon.stub().returns(true),
93
95
  updateMainSessionLocusCache: sinon.stub(),
96
+ syncAllHashTreeDatasets: sinon.stub(),
94
97
  };
95
98
  webex = new MockWebex({
96
99
  children: {
@@ -355,32 +358,6 @@ describe('plugin-meetings', () => {
355
358
  });
356
359
  });
357
360
 
358
- describe('#_toggleTcpReachability', () => {
359
- it('should have _toggleTcpReachability', () => {
360
- assert.equal(typeof webex.meetings._toggleTcpReachability, 'function');
361
- });
362
-
363
- describe('success', () => {
364
- it('should update meetings to do TCP reachability', () => {
365
- webex.meetings._toggleTcpReachability(true);
366
- assert.equal(webex.meetings.config.experimental.enableTcpReachability, true);
367
- });
368
- });
369
- });
370
-
371
- describe('#_toggleTlsReachability', () => {
372
- it('should have _toggleTlsReachability', () => {
373
- assert.equal(typeof webex.meetings._toggleTlsReachability, 'function');
374
- });
375
-
376
- describe('success', () => {
377
- it('should update meetings to do TLS reachability', () => {
378
- webex.meetings._toggleTlsReachability(true);
379
- assert.equal(webex.meetings.config.experimental.enableTlsReachability, true);
380
- });
381
- });
382
- });
383
-
384
361
  describe('#_toggleIpv6BackendNativeSupport', () => {
385
362
  it('should have _toggleIpv6BackendNativeSupport', () => {
386
363
  assert.equal(typeof webex.meetings._toggleIpv6BackendNativeSupport, 'function');
@@ -423,6 +400,33 @@ describe('plugin-meetings', () => {
423
400
  });
424
401
  });
425
402
 
403
+ describe('#_toggleEnableAv1SlidesSupport', () => {
404
+ it('should have _toggleEnableAv1SlidesSupport', () => {
405
+ assert.equal(typeof webex.meetings._toggleEnableAv1SlidesSupport, 'function');
406
+ });
407
+
408
+ describe('success', () => {
409
+ it('should update meetings config to enable AV1 slides support', () => {
410
+ webex.meetings._toggleEnableAv1SlidesSupport(true);
411
+ assert.equal(webex.meetings.config.enableAv1SlidesSupport, true);
412
+
413
+ webex.meetings._toggleEnableAv1SlidesSupport(false);
414
+ assert.equal(webex.meetings.config.enableAv1SlidesSupport, false);
415
+ });
416
+
417
+ it('should not update config when called with a non-boolean value', () => {
418
+ webex.meetings._toggleEnableAv1SlidesSupport(true);
419
+ assert.equal(webex.meetings.config.enableAv1SlidesSupport, true);
420
+
421
+ webex.meetings._toggleEnableAv1SlidesSupport('invalid');
422
+ assert.equal(webex.meetings.config.enableAv1SlidesSupport, true);
423
+
424
+ webex.meetings._toggleEnableAv1SlidesSupport(undefined);
425
+ assert.equal(webex.meetings.config.enableAv1SlidesSupport, true);
426
+ });
427
+ });
428
+ });
429
+
426
430
  describe('#_toggleStopIceGatheringAfterFirstRelayCandidate', () => {
427
431
  it('should have _toggleStopIceGatheringAfterFirstRelayCandidate', () => {
428
432
  assert.equal(
@@ -1355,6 +1359,87 @@ describe('plugin-meetings', () => {
1355
1359
  );
1356
1360
  });
1357
1361
  });
1362
+ describe('#fetchSitePreferencesMeViaSite', () => {
1363
+ const sitePreferencesResponse = {
1364
+ scheduling: {
1365
+ supportScheduleWebinar: true,
1366
+ webinarWebLink: 'https://go.webex.com/webappng/sites/go/webinar/scheduler',
1367
+ },
1368
+ };
1369
+
1370
+ beforeEach(() => {
1371
+ webex.meetings.request.fetchSitePreferencesMeViaSite = sinon
1372
+ .stub()
1373
+ .resolves(sitePreferencesResponse);
1374
+ });
1375
+
1376
+ it('should have #fetchSitePreferencesMeViaSite', () => {
1377
+ assert.exists(webex.meetings.fetchSitePreferencesMeViaSite);
1378
+ });
1379
+
1380
+ it('fetches scheduling preferences for the preferred Webex site by default', async () => {
1381
+ webex.meetings.preferredWebexSite = 'go.webex.com';
1382
+
1383
+ const result = await webex.meetings.fetchSitePreferencesMeViaSite();
1384
+
1385
+ assert.deepEqual(result, sitePreferencesResponse);
1386
+ assert.calledOnceWithExactly(
1387
+ webex.meetings.request.fetchSitePreferencesMeViaSite,
1388
+ {
1389
+ siteUrl: 'go.webex.com',
1390
+ }
1391
+ );
1392
+ });
1393
+
1394
+ it('uses the provided Webex site instead of the preferred Webex site', async () => {
1395
+ webex.meetings.preferredWebexSite = 'preferred.webex.com';
1396
+
1397
+ await webex.meetings.fetchSitePreferencesMeViaSite({siteUrl: 'go.webex.com'});
1398
+
1399
+ assert.calledOnceWithExactly(
1400
+ webex.meetings.request.fetchSitePreferencesMeViaSite,
1401
+ {
1402
+ siteUrl: 'go.webex.com',
1403
+ }
1404
+ );
1405
+ });
1406
+
1407
+ it('forwards custom site name and preference sections to the request helper', async () => {
1408
+ webex.meetings.preferredWebexSite = 'go.webex.com';
1409
+
1410
+ await webex.meetings.fetchSitePreferencesMeViaSite({
1411
+ siteName: 'custom-site',
1412
+ selectOptions: [SitePreferenceSelectOption.SCHEDULING],
1413
+ });
1414
+
1415
+ assert.calledOnceWithExactly(
1416
+ webex.meetings.request.fetchSitePreferencesMeViaSite,
1417
+ {
1418
+ siteUrl: 'go.webex.com',
1419
+ siteName: 'custom-site',
1420
+ selectOptions: [SitePreferenceSelectOption.SCHEDULING],
1421
+ }
1422
+ );
1423
+ });
1424
+
1425
+ it('throws when no Webex site is available', () => {
1426
+ webex.meetings.preferredWebexSite = '';
1427
+ webex.meetings.request.fetchSitePreferencesMeViaSite.throws(
1428
+ new ParameterError(
1429
+ 'No siteUrl available. Call register() before fetching site preferences or provide options.siteUrl.'
1430
+ )
1431
+ );
1432
+
1433
+ assert.throws(
1434
+ () => webex.meetings.fetchSitePreferencesMeViaSite(),
1435
+ ParameterError,
1436
+ 'No siteUrl available. Call register() before fetching site preferences or provide options.siteUrl.'
1437
+ );
1438
+ assert.calledOnceWithExactly(webex.meetings.request.fetchSitePreferencesMeViaSite, {
1439
+ siteUrl: '',
1440
+ });
1441
+ });
1442
+ });
1358
1443
  describe('Static shortcut proxy methods', () => {
1359
1444
  describe('MeetingCollection getByKey proxies', () => {
1360
1445
  beforeEach(() => {
@@ -1391,7 +1476,7 @@ describe('plugin-meetings', () => {
1391
1476
  it('should have #syncMeetings', () => {
1392
1477
  assert.exists(webex.meetings.syncMeetings);
1393
1478
  });
1394
- it('should do nothing and return a resolved promise if unverified guest', async () => {
1479
+ it('should skip getActiveMeetings but still call syncAllHashTreeDatasets if unverified guest', async () => {
1395
1480
  webex.meetings.request.getActiveMeetings = sinon.stub().returns(
1396
1481
  Promise.resolve({
1397
1482
  loci: [
@@ -1404,13 +1489,23 @@ describe('plugin-meetings', () => {
1404
1489
  webex.credentials.isUnverifiedGuest = true;
1405
1490
  LoggerProxy.logger.info = sinon.stub();
1406
1491
 
1492
+ const mockLocusInfo = {
1493
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1494
+ };
1495
+ webex.meetings.meetingCollection.getAll = sinon.stub().returns({
1496
+ meeting1: {locusInfo: mockLocusInfo},
1497
+ meeting2: {locusInfo: undefined},
1498
+ meeting3: {},
1499
+ });
1500
+
1407
1501
  await webex.meetings.syncMeetings();
1408
1502
 
1409
1503
  assert.notCalled(webex.meetings.request.getActiveMeetings);
1410
1504
  assert.calledWith(
1411
1505
  LoggerProxy.logger.info,
1412
- 'Meetings:index#syncMeetings --> skipping meeting sync as unverified guest'
1506
+ 'Meetings:index#syncMeetings --> user is unverified guest, skipping calling Locus for meeting sync'
1413
1507
  );
1508
+ assert.calledOnce(mockLocusInfo.syncAllHashTreeDatasets);
1414
1509
  });
1415
1510
  describe('succesful requests', () => {
1416
1511
  beforeEach(() => {
@@ -1429,6 +1524,9 @@ describe('plugin-meetings', () => {
1429
1524
  webex.meetings.meetingCollection.getByKey = sinon.stub().returns({
1430
1525
  locusInfo,
1431
1526
  });
1527
+ webex.meetings.meetingCollection.getAll = sinon.stub().returns({
1528
+ meeting1: {locusInfo, locusUrl: url1},
1529
+ });
1432
1530
  });
1433
1531
  it('tests the sync meeting calls for existing meeting', async () => {
1434
1532
  await webex.meetings.syncMeetings();
@@ -1436,6 +1534,7 @@ describe('plugin-meetings', () => {
1436
1534
  assert.calledOnce(webex.meetings.meetingCollection.getByKey);
1437
1535
  assert.calledOnce(locusInfo.parse);
1438
1536
  assert.calledWith(webex.meetings.meetingCollection.getByKey, 'locusUrl', url1);
1537
+ assert.calledOnce(locusInfo.syncAllHashTreeDatasets);
1439
1538
  });
1440
1539
  });
1441
1540
  describe('when meeting is not returned', () => {
@@ -1474,7 +1573,7 @@ describe('plugin-meetings', () => {
1474
1573
  url: url1,
1475
1574
  },
1476
1575
  hashTreeMessage: undefined,
1477
- });
1576
+ }, sinon.match.func);
1478
1577
  });
1479
1578
  });
1480
1579
  describe('when destroying meeting is needed', () => {
@@ -1520,7 +1619,7 @@ describe('plugin-meetings', () => {
1520
1619
  it('destroy any meeting that has no active locus url if keepOnlyLocusMeetings is not defined', async () => {
1521
1620
  await webex.meetings.syncMeetings();
1522
1621
  assert.calledOnce(webex.meetings.request.getActiveMeetings);
1523
- assert.calledOnce(webex.meetings.meetingCollection.getAll);
1622
+ assert.calledTwice(webex.meetings.meetingCollection.getAll);
1524
1623
  assert.calledWith(destroySpy, meetingCollectionMeetings.noLongerValidLocusMeeting);
1525
1624
  assert.calledWith(destroySpy, meetingCollectionMeetings.otherNonLocusMeeting1);
1526
1625
  assert.calledWith(destroySpy, meetingCollectionMeetings.otherNonLocusMeeting2);
@@ -1532,7 +1631,7 @@ describe('plugin-meetings', () => {
1532
1631
  it('destroy any meeting that has no active locus url if keepOnlyLocusMeetings === true', async () => {
1533
1632
  await webex.meetings.syncMeetings({keepOnlyLocusMeetings: true});
1534
1633
  assert.calledOnce(webex.meetings.request.getActiveMeetings);
1535
- assert.calledOnce(webex.meetings.meetingCollection.getAll);
1634
+ assert.calledTwice(webex.meetings.meetingCollection.getAll);
1536
1635
  assert.calledWith(destroySpy, meetingCollectionMeetings.noLongerValidLocusMeeting);
1537
1636
  assert.calledWith(destroySpy, meetingCollectionMeetings.otherNonLocusMeeting1);
1538
1637
  assert.calledWith(destroySpy, meetingCollectionMeetings.otherNonLocusMeeting2);
@@ -1544,7 +1643,7 @@ describe('plugin-meetings', () => {
1544
1643
  it('destroy any LOCUS meetings that have no active locus url if keepOnlyLocusMeetings === false', async () => {
1545
1644
  await webex.meetings.syncMeetings({keepOnlyLocusMeetings: false});
1546
1645
  assert.calledOnce(webex.meetings.request.getActiveMeetings);
1547
- assert.calledOnce(webex.meetings.meetingCollection.getAll);
1646
+ assert.calledTwice(webex.meetings.meetingCollection.getAll);
1548
1647
  assert.calledWith(destroySpy, meetingCollectionMeetings.noLongerValidLocusMeeting);
1549
1648
  assert.callCount(destroySpy, 1);
1550
1649
 
@@ -1552,6 +1651,147 @@ describe('plugin-meetings', () => {
1552
1651
  });
1553
1652
  });
1554
1653
  });
1654
+
1655
+ describe('when globalMeetingId preserves breakout meetings', () => {
1656
+ let destroySpy;
1657
+ let cleanUpSpy;
1658
+
1659
+ beforeEach(() => {
1660
+ destroySpy = sinon.spy(webex.meetings, 'destroy');
1661
+ cleanUpSpy = sinon.stub(MeetingUtil, 'cleanUp').returns(Promise.resolve());
1662
+ });
1663
+
1664
+ afterEach(() => {
1665
+ cleanUpSpy.restore();
1666
+ });
1667
+
1668
+ it('should not destroy a meeting whose globalMeetingId matches an active locus', async () => {
1669
+ const meetingCollectionMeetings = {
1670
+ breakoutMeeting: {
1671
+ locusUrl: 'breakout-url',
1672
+ locusInfo: {
1673
+ info: {globalMeetingId: 'gmid-123'},
1674
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1675
+ },
1676
+ sendCallAnalyzerMetrics: sinon.stub(),
1677
+ },
1678
+ };
1679
+
1680
+ webex.meetings.meetingCollection.getAll = sinon
1681
+ .stub()
1682
+ .returns(meetingCollectionMeetings);
1683
+ webex.meetings.request.getActiveMeetings = sinon.stub().resolves({
1684
+ loci: [{url: 'main-url', info: {globalMeetingId: 'gmid-123'}}],
1685
+ });
1686
+
1687
+ await webex.meetings.syncMeetings();
1688
+
1689
+ assert.notCalled(destroySpy);
1690
+ });
1691
+
1692
+ it('should destroy a meeting whose globalMeetingId does NOT match any active locus', async () => {
1693
+ const meetingCollectionMeetings = {
1694
+ breakoutMeeting: {
1695
+ locusUrl: 'breakout-url',
1696
+ locusInfo: {
1697
+ info: {globalMeetingId: 'gmid-other'},
1698
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1699
+ },
1700
+ sendCallAnalyzerMetrics: sinon.stub(),
1701
+ },
1702
+ };
1703
+
1704
+ webex.meetings.meetingCollection.getAll = sinon
1705
+ .stub()
1706
+ .returns(meetingCollectionMeetings);
1707
+ webex.meetings.request.getActiveMeetings = sinon.stub().resolves({
1708
+ loci: [{url: 'main-url', info: {globalMeetingId: 'gmid-123'}}],
1709
+ });
1710
+
1711
+ await webex.meetings.syncMeetings();
1712
+
1713
+ assert.calledOnce(destroySpy);
1714
+ assert.calledWith(destroySpy, meetingCollectionMeetings.breakoutMeeting);
1715
+ });
1716
+ });
1717
+
1718
+ describe('skipHashTreeSync parameter', () => {
1719
+ it('should skip syncAllHashTreeDatasets when skipHashTreeSync is true', async () => {
1720
+ const mockLocusInfo = {
1721
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1722
+ };
1723
+
1724
+ webex.meetings.request.getActiveMeetings = sinon.stub().resolves({loci: []});
1725
+ webex.meetings.meetingCollection.getAll = sinon.stub().returns({
1726
+ meeting1: {locusInfo: mockLocusInfo},
1727
+ });
1728
+
1729
+ await webex.meetings.syncMeetings({keepOnlyLocusMeetings: false, skipHashTreeSync: true});
1730
+
1731
+ assert.calledOnce(webex.meetings.request.getActiveMeetings);
1732
+ assert.notCalled(mockLocusInfo.syncAllHashTreeDatasets);
1733
+ });
1734
+
1735
+ it('should call syncAllHashTreeDatasets when skipHashTreeSync is false (default)', async () => {
1736
+ const mockLocusInfo = {
1737
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1738
+ };
1739
+
1740
+ webex.meetings.request.getActiveMeetings = sinon.stub().resolves({loci: []});
1741
+ webex.meetings.meetingCollection.getAll = sinon.stub().returns({
1742
+ meeting1: {locusInfo: mockLocusInfo},
1743
+ });
1744
+
1745
+ await webex.meetings.syncMeetings({keepOnlyLocusMeetings: false, skipHashTreeSync: false});
1746
+
1747
+ assert.calledOnce(webex.meetings.request.getActiveMeetings);
1748
+ assert.calledOnce(mockLocusInfo.syncAllHashTreeDatasets);
1749
+ });
1750
+ });
1751
+
1752
+ describe('syncAllHashTreeDatasets in syncMeetings', () => {
1753
+ it('should call syncAllHashTreeDatasets for multiple meetings, skipping those without locusInfo', async () => {
1754
+ const mockLocusInfo1 = {
1755
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1756
+ };
1757
+ const mockLocusInfo2 = {
1758
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1759
+ };
1760
+
1761
+ webex.meetings.request.getActiveMeetings = sinon.stub().resolves({loci: []});
1762
+ webex.meetings.meetingCollection.getAll = sinon.stub().returns({
1763
+ meeting1: {locusInfo: mockLocusInfo1},
1764
+ meeting2: {locusInfo: undefined},
1765
+ meeting3: {locusInfo: mockLocusInfo2},
1766
+ meeting4: {},
1767
+ });
1768
+
1769
+ await webex.meetings.syncMeetings({keepOnlyLocusMeetings: false});
1770
+
1771
+ assert.calledOnce(mockLocusInfo1.syncAllHashTreeDatasets);
1772
+ assert.calledOnce(mockLocusInfo2.syncAllHashTreeDatasets);
1773
+ });
1774
+
1775
+ it('should not call syncAllHashTreeDatasets when getActiveMeetings throws an error', async () => {
1776
+ const mockLocusInfo = {
1777
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1778
+ };
1779
+
1780
+ webex.meetings.request.getActiveMeetings = sinon.stub().rejects(new Error('network error'));
1781
+ webex.meetings.meetingCollection.getAll = sinon.stub().returns({
1782
+ meeting1: {locusInfo: mockLocusInfo},
1783
+ });
1784
+
1785
+ try {
1786
+ await webex.meetings.syncMeetings();
1787
+ assert.fail('should have thrown');
1788
+ } catch (err) {
1789
+ assert.equal(err.message, 'network error');
1790
+ }
1791
+
1792
+ assert.notCalled(mockLocusInfo.syncAllHashTreeDatasets);
1793
+ });
1794
+ });
1555
1795
  });
1556
1796
  describe('#fetchStaticMeetingLink', () => {
1557
1797
  const conversationUrl = 'conv.fakeconversationurl.com';
@@ -2015,7 +2255,7 @@ describe('plugin-meetings', () => {
2015
2255
  },
2016
2256
  },
2017
2257
  hashTreeMessage: undefined,
2018
- });
2258
+ }, sinon.match.func);
2019
2259
  });
2020
2260
  it('should setup the meeting from a hash tree event', async () => {
2021
2261
  const selfData = {};
@@ -2049,7 +2289,7 @@ describe('plugin-meetings', () => {
2049
2289
  info: infoData,
2050
2290
  },
2051
2291
  hashTreeMessage,
2052
- });
2292
+ }, sinon.match.func);
2053
2293
  });
2054
2294
 
2055
2295
  it('should ignore hash tree event when created locus has INACTIVE fullState', async () => {
@@ -2129,7 +2369,7 @@ describe('plugin-meetings', () => {
2129
2369
  },
2130
2370
  },
2131
2371
  hashTreeMessage: undefined,
2132
- });
2372
+ }, sinon.match.func);
2133
2373
  });
2134
2374
 
2135
2375
  it('sends client event correctly on finally', async () => {
@@ -2205,7 +2445,7 @@ describe('plugin-meetings', () => {
2205
2445
  },
2206
2446
  },
2207
2447
  hashTreeMessage: undefined,
2208
- });
2448
+ }, sinon.match.func);
2209
2449
  });
2210
2450
 
2211
2451
  const generateFakeLocusData = (isUnifiedSpaceMeeting) => ({
@@ -2833,6 +3073,39 @@ describe('plugin-meetings', () => {
2833
3073
  checkCreateMeetingWithNoMeetingInfo(true, true);
2834
3074
  });
2835
3075
 
3076
+ it('does not emit meeting:added when meeting is destroyed due to missing meeting info', async () => {
3077
+ // Make destroy actually remove the meeting from the collection
3078
+ // so that getMeetingByType returns null in the finally block
3079
+ webex.meetings.destroy = sinon.stub().callsFake((meeting) => {
3080
+ webex.meetings.meetingCollection.delete(meeting.id);
3081
+ });
3082
+
3083
+ try {
3084
+ await webex.meetings.createMeeting(
3085
+ 'test destination',
3086
+ 'test type',
3087
+ undefined,
3088
+ undefined,
3089
+ undefined,
3090
+ true
3091
+ );
3092
+ assert.fail('should have thrown NoMeetingInfoError');
3093
+ } catch (err) {
3094
+ assert.instanceOf(err, NoMeetingInfoError);
3095
+ }
3096
+
3097
+ assert.calledOnce(webex.meetings.destroy);
3098
+
3099
+ // meeting:added should NOT have been triggered since the meeting was destroyed
3100
+ assert.neverCalledWith(
3101
+ TriggerProxy.trigger,
3102
+ sinon.match.any,
3103
+ sinon.match({function: 'createMeeting'}),
3104
+ 'meeting:added',
3105
+ sinon.match.any
3106
+ );
3107
+ });
3108
+
2836
3109
  it('creates the meeting avoiding meeting info fetch by passing type as DESTINATION_TYPE.ONE_ON_ONE_CALL', async () => {
2837
3110
  const meeting = await webex.meetings.createMeeting(
2838
3111
  'test destination',
@@ -2847,6 +3120,30 @@ describe('plugin-meetings', () => {
2847
3120
 
2848
3121
  assert.notCalled(webex.meetings.meetingInfo.fetchMeetingInfo);
2849
3122
  });
3123
+
3124
+ [
3125
+ {fullStateType: 'CALL'},
3126
+ {fullStateType: 'SIP_BRIDGE'},
3127
+ {fullStateType: 'SPACE_SHARE'},
3128
+ ].forEach(({fullStateType}) => {
3129
+ it(`skips meeting info fetch when LOCUS_ID destination is a 1:1 call (fullState.type ${fullStateType})`, async () => {
3130
+ const locusDestination = {
3131
+ fullState: {type: fullStateType},
3132
+ };
3133
+
3134
+ const meeting = await webex.meetings.createMeeting(
3135
+ locusDestination,
3136
+ DESTINATION_TYPE.LOCUS_ID
3137
+ );
3138
+
3139
+ assert.instanceOf(
3140
+ meeting,
3141
+ Meeting,
3142
+ 'createMeeting should eventually resolve to a Meeting Object'
3143
+ );
3144
+ assert.notCalled(webex.meetings.meetingInfo.fetchMeetingInfo);
3145
+ });
3146
+ });
2850
3147
  });
2851
3148
 
2852
3149
  describe('rejected MeetingInfo.#fetchMeetingInfo - does not log for known Error types', () => {
@@ -3270,10 +3567,13 @@ describe('plugin-meetings', () => {
3270
3567
  };
3271
3568
 
3272
3569
  it('triggers correct event when SELF_CANNOT_VIEW_PARTICIPANT_LIST_CHANGE emitted', async () => {
3570
+ sinon.stub(meeting, 'updateMeetingActions');
3273
3571
  checkSelfTrigger(
3274
3572
  LOCUSINFO.EVENTS.SELF_CANNOT_VIEW_PARTICIPANT_LIST_CHANGE,
3275
3573
  EVENT_TRIGGERS.MEETING_SELF_CANNOT_VIEW_PARTICIPANT_LIST
3276
3574
  );
3575
+ assert.calledOnce(meeting.updateMeetingActions);
3576
+ meeting.updateMeetingActions.restore();
3277
3577
  });
3278
3578
 
3279
3579
  it('triggers correct event when SELF_IS_SHARING_BLOCKED_CHANGE emitted', async () => {
@@ -3426,6 +3726,21 @@ describe('plugin-meetings', () => {
3426
3726
  'Meetings:index#isNeedHandleMainLocus --> self device left&moved in main locus with self joined status, not need to handle'
3427
3727
  );
3428
3728
  });
3729
+
3730
+ it('check breakout ended with self removed, return false', () => {
3731
+ webex.meetings.meetingCollection.getActiveBreakoutLocus = sinon.stub().returns(null);
3732
+ newLocus.self.state = 'LEFT';
3733
+ newLocus.self.reason = 'OTHER';
3734
+ newLocus.self.removed = true;
3735
+ newLocus.fullState = {state: 'INACTIVE', endMeetingReason: 'BREAKOUT_ENDED'};
3736
+ LoggerProxy.logger.log = sinon.stub();
3737
+ const result = webex.meetings.isNeedHandleMainLocus(meeting, newLocus);
3738
+ assert.equal(result, false);
3739
+ assert.calledWith(
3740
+ LoggerProxy.logger.log,
3741
+ 'Meetings:index#isNeedHandleMainLocus --> self moved main locus with self removed status or with device resource moved, not need to handle'
3742
+ );
3743
+ });
3429
3744
  });
3430
3745
 
3431
3746
  describe('#isNeedHandleLocusDTO', () => {
@@ -3486,6 +3801,18 @@ describe('plugin-meetings', () => {
3486
3801
  const result = webex.meetings.isNeedHandleLocusDTO(meeting, newLocus);
3487
3802
  assert.equal(result, false);
3488
3803
  });
3804
+ it('breakout session with breakout ended, return false', () => {
3805
+ newLocus.controls.breakout = {
3806
+ sessionType: 'BREAKOUT',
3807
+ };
3808
+ newLocus.self.state = 'LEFT';
3809
+ newLocus.self.reason = 'OTHER';
3810
+ newLocus.self.devices = [];
3811
+ newLocus.fullState = {state: 'INACTIVE', endMeetingReason: 'BREAKOUT_ENDED'};
3812
+ LoggerProxy.logger.log = sinon.stub();
3813
+ const result = webex.meetings.isNeedHandleLocusDTO(meeting, newLocus);
3814
+ assert.equal(result, false);
3815
+ });
3489
3816
  it('moved to lobby, return true', () => {
3490
3817
  newLocus.controls.breakout = {
3491
3818
  sessionType: 'MAIN',