@webex/plugin-meetings 3.12.0-next.6 → 3.12.0-next.60

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 (158) 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 -0
  9. package/dist/config.js.map +1 -1
  10. package/dist/constants.js +6 -3
  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 +38 -24
  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 +10 -1
  19. package/dist/hashTree/constants.js.map +1 -1
  20. package/dist/hashTree/hashTreeParser.js +716 -370
  21. package/dist/hashTree/hashTreeParser.js.map +1 -1
  22. package/dist/hashTree/utils.js +22 -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/locusRetry.js +23 -8
  27. package/dist/interceptors/locusRetry.js.map +1 -1
  28. package/dist/interpretation/index.js +10 -1
  29. package/dist/interpretation/index.js.map +1 -1
  30. package/dist/interpretation/siLanguage.js +1 -1
  31. package/dist/locus-info/controlsUtils.js +4 -1
  32. package/dist/locus-info/controlsUtils.js.map +1 -1
  33. package/dist/locus-info/index.js +289 -87
  34. package/dist/locus-info/index.js.map +1 -1
  35. package/dist/locus-info/types.js +19 -0
  36. package/dist/locus-info/types.js.map +1 -1
  37. package/dist/media/index.js +3 -1
  38. package/dist/media/index.js.map +1 -1
  39. package/dist/media/properties.js +1 -0
  40. package/dist/media/properties.js.map +1 -1
  41. package/dist/meeting/in-meeting-actions.js +3 -1
  42. package/dist/meeting/in-meeting-actions.js.map +1 -1
  43. package/dist/meeting/index.js +907 -535
  44. package/dist/meeting/index.js.map +1 -1
  45. package/dist/meeting/util.js +19 -2
  46. package/dist/meeting/util.js.map +1 -1
  47. package/dist/meetings/index.js +231 -78
  48. package/dist/meetings/index.js.map +1 -1
  49. package/dist/meetings/meetings.types.js +6 -1
  50. package/dist/meetings/meetings.types.js.map +1 -1
  51. package/dist/meetings/request.js +39 -0
  52. package/dist/meetings/request.js.map +1 -1
  53. package/dist/meetings/util.js +79 -5
  54. package/dist/meetings/util.js.map +1 -1
  55. package/dist/member/index.js +10 -0
  56. package/dist/member/index.js.map +1 -1
  57. package/dist/member/types.js.map +1 -1
  58. package/dist/member/util.js +3 -0
  59. package/dist/member/util.js.map +1 -1
  60. package/dist/metrics/constants.js +4 -1
  61. package/dist/metrics/constants.js.map +1 -1
  62. package/dist/multistream/codec/constants.js +63 -0
  63. package/dist/multistream/codec/constants.js.map +1 -0
  64. package/dist/multistream/mediaRequestManager.js +62 -15
  65. package/dist/multistream/mediaRequestManager.js.map +1 -1
  66. package/dist/multistream/receiveSlot.js +9 -0
  67. package/dist/multistream/receiveSlot.js.map +1 -1
  68. package/dist/reactions/reactions.type.js.map +1 -1
  69. package/dist/recording-controller/index.js +1 -3
  70. package/dist/recording-controller/index.js.map +1 -1
  71. package/dist/types/config.d.ts +2 -0
  72. package/dist/types/constants.d.ts +2 -0
  73. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  74. package/dist/types/controls-options-manager/index.d.ts +10 -0
  75. package/dist/types/hashTree/constants.d.ts +1 -0
  76. package/dist/types/hashTree/hashTreeParser.d.ts +92 -16
  77. package/dist/types/hashTree/utils.d.ts +11 -0
  78. package/dist/types/index.d.ts +2 -0
  79. package/dist/types/interceptors/locusRetry.d.ts +4 -4
  80. package/dist/types/locus-info/index.d.ts +46 -6
  81. package/dist/types/locus-info/types.d.ts +21 -1
  82. package/dist/types/media/properties.d.ts +1 -0
  83. package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
  84. package/dist/types/meeting/index.d.ts +87 -3
  85. package/dist/types/meeting/util.d.ts +8 -0
  86. package/dist/types/meetings/index.d.ts +30 -2
  87. package/dist/types/meetings/meetings.types.d.ts +15 -0
  88. package/dist/types/meetings/request.d.ts +14 -0
  89. package/dist/types/member/index.d.ts +1 -0
  90. package/dist/types/member/types.d.ts +1 -0
  91. package/dist/types/member/util.d.ts +1 -0
  92. package/dist/types/metrics/constants.d.ts +3 -0
  93. package/dist/types/multistream/codec/constants.d.ts +7 -0
  94. package/dist/types/multistream/mediaRequestManager.d.ts +22 -5
  95. package/dist/types/reactions/reactions.type.d.ts +3 -0
  96. package/dist/webinar/index.js +361 -235
  97. package/dist/webinar/index.js.map +1 -1
  98. package/package.json +22 -22
  99. package/src/aiEnableRequest/index.ts +16 -0
  100. package/src/breakouts/breakout.ts +3 -1
  101. package/src/breakouts/index.ts +31 -0
  102. package/src/config.ts +2 -0
  103. package/src/constants.ts +5 -1
  104. package/src/controls-options-manager/constants.ts +14 -1
  105. package/src/controls-options-manager/index.ts +47 -24
  106. package/src/controls-options-manager/util.ts +81 -1
  107. package/src/hashTree/constants.ts +9 -0
  108. package/src/hashTree/hashTreeParser.ts +429 -183
  109. package/src/hashTree/utils.ts +17 -0
  110. package/src/index.ts +5 -0
  111. package/src/interceptors/locusRetry.ts +25 -4
  112. package/src/interpretation/index.ts +25 -8
  113. package/src/locus-info/controlsUtils.ts +3 -1
  114. package/src/locus-info/index.ts +291 -97
  115. package/src/locus-info/types.ts +25 -1
  116. package/src/media/index.ts +3 -0
  117. package/src/media/properties.ts +1 -0
  118. package/src/meeting/in-meeting-actions.ts +4 -0
  119. package/src/meeting/index.ts +388 -33
  120. package/src/meeting/util.ts +20 -2
  121. package/src/meetings/index.ts +134 -44
  122. package/src/meetings/meetings.types.ts +19 -0
  123. package/src/meetings/request.ts +43 -0
  124. package/src/meetings/util.ts +97 -1
  125. package/src/member/index.ts +10 -0
  126. package/src/member/types.ts +1 -0
  127. package/src/member/util.ts +3 -0
  128. package/src/metrics/constants.ts +3 -0
  129. package/src/multistream/codec/constants.ts +58 -0
  130. package/src/multistream/mediaRequestManager.ts +119 -28
  131. package/src/multistream/receiveSlot.ts +18 -0
  132. package/src/reactions/reactions.type.ts +3 -0
  133. package/src/recording-controller/index.ts +1 -2
  134. package/src/webinar/index.ts +162 -21
  135. package/test/unit/spec/aiEnableRequest/index.ts +86 -0
  136. package/test/unit/spec/breakouts/breakout.ts +9 -3
  137. package/test/unit/spec/breakouts/index.ts +49 -0
  138. package/test/unit/spec/controls-options-manager/index.js +140 -29
  139. package/test/unit/spec/controls-options-manager/util.js +165 -0
  140. package/test/unit/spec/hashTree/hashTreeParser.ts +1508 -149
  141. package/test/unit/spec/hashTree/utils.ts +88 -1
  142. package/test/unit/spec/interceptors/locusRetry.ts +205 -4
  143. package/test/unit/spec/interpretation/index.ts +26 -4
  144. package/test/unit/spec/locus-info/controlsUtils.js +172 -57
  145. package/test/unit/spec/locus-info/index.js +475 -81
  146. package/test/unit/spec/media/index.ts +31 -0
  147. package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
  148. package/test/unit/spec/meeting/index.js +1131 -49
  149. package/test/unit/spec/meeting/muteState.js +3 -0
  150. package/test/unit/spec/meeting/utils.js +33 -0
  151. package/test/unit/spec/meetings/index.js +360 -10
  152. package/test/unit/spec/meetings/request.js +141 -0
  153. package/test/unit/spec/meetings/utils.js +189 -0
  154. package/test/unit/spec/member/index.js +7 -0
  155. package/test/unit/spec/member/util.js +24 -0
  156. package/test/unit/spec/multistream/mediaRequestManager.ts +501 -37
  157. package/test/unit/spec/recording-controller/index.js +9 -8
  158. package/test/unit/spec/webinar/index.ts +141 -16
@@ -11,6 +11,7 @@ describe('plugin-meetings', () => {
11
11
  let audio;
12
12
  let video;
13
13
  let originalRemoteUpdateAudioVideo;
14
+ let originalUpdateLocusFromApiResponse;
14
15
 
15
16
  const fakeLocusResponse = {body: {locus: {info: 'this is a fake locus'}}};
16
17
 
@@ -45,6 +46,7 @@ describe('plugin-meetings', () => {
45
46
  };
46
47
 
47
48
  originalRemoteUpdateAudioVideo = MeetingUtil.remoteUpdateAudioVideo;
49
+ originalUpdateLocusFromApiResponse = MeetingUtil.updateLocusFromApiResponse;
48
50
 
49
51
  MeetingUtil.remoteUpdateAudioVideo = sinon.stub().resolves(fakeLocusResponse);
50
52
  MeetingUtil.updateLocusFromApiResponse = sinon.stub();
@@ -57,6 +59,7 @@ describe('plugin-meetings', () => {
57
59
 
58
60
  afterEach(() => {
59
61
  MeetingUtil.remoteUpdateAudioVideo = originalRemoteUpdateAudioVideo;
62
+ MeetingUtil.updateLocusFromApiResponse = originalUpdateLocusFromApiResponse;
60
63
  });
61
64
 
62
65
  describe('mute state library', () => {
@@ -60,6 +60,7 @@ describe('plugin-meetings', () => {
60
60
  meeting.annotaion = {cleanUp: sinon.stub()};
61
61
  meeting.getWebexObject = sinon.stub().returns(webex);
62
62
  meeting.simultaneousInterpretation = {cleanUp: sinon.stub()};
63
+ meeting.locusInfo = {cleanUp: sinon.stub()};
63
64
  meeting.trigger = sinon.stub();
64
65
  meeting.webex = webex;
65
66
  meeting.webex.internal.newMetrics.callDiagnosticMetrics =
@@ -89,6 +90,7 @@ describe('plugin-meetings', () => {
89
90
  assert.calledOnceWithExactly(meeting.cleanupLLMConneciton, {throwOnError: false});
90
91
  assert.calledOnce(meeting.breakouts.cleanUp);
91
92
  assert.calledOnce(meeting.simultaneousInterpretation.cleanUp);
93
+ assert.calledOnce(meeting.locusInfo.cleanUp);
92
94
  assert.calledOnce(webex.internal.device.meetingEnded);
93
95
  assert.calledOnceWithExactly(
94
96
  meeting.webex.internal.newMetrics.callDiagnosticMetrics.clearEventLimitsForCorrelationId,
@@ -110,6 +112,7 @@ describe('plugin-meetings', () => {
110
112
  assert.notCalled(meeting.cleanupLLMConneciton);
111
113
  assert.calledOnce(meeting.breakouts.cleanUp);
112
114
  assert.calledOnce(meeting.simultaneousInterpretation.cleanUp);
115
+ assert.calledOnce(meeting.locusInfo.cleanUp);
113
116
  assert.calledOnce(webex.internal.device.meetingEnded);
114
117
  assert.calledOnceWithExactly(
115
118
  meeting.webex.internal.newMetrics.callDiagnosticMetrics.clearEventLimitsForCorrelationId,
@@ -130,6 +133,7 @@ describe('plugin-meetings', () => {
130
133
  assert.notCalled(meeting.cleanupLLMConneciton);
131
134
  assert.calledOnce(meeting.breakouts.cleanUp);
132
135
  assert.calledOnce(meeting.simultaneousInterpretation.cleanUp);
136
+ assert.calledOnce(meeting.locusInfo.cleanUp);
133
137
  assert.calledOnce(webex.internal.device.meetingEnded);
134
138
  assert.calledOnceWithExactly(
135
139
  meeting.webex.internal.newMetrics.callDiagnosticMetrics.clearEventLimitsForCorrelationId,
@@ -272,6 +276,31 @@ describe('plugin-meetings', () => {
272
276
  assert.notCalled(meeting.locusInfo.handleLocusAPIResponse);
273
277
  });
274
278
 
279
+ it('should call handleLocusAPIResponse when response body is an unwrapped LocusDTO', () => {
280
+ const meeting = {
281
+ locusInfo: {
282
+ handleLocusAPIResponse: sinon.stub(),
283
+ },
284
+ };
285
+
286
+ const originalResponse = {
287
+ body: {
288
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/some-id',
289
+ participants: [],
290
+ self: {},
291
+ },
292
+ };
293
+
294
+ const response = MeetingUtil.updateLocusFromApiResponse(meeting, originalResponse);
295
+
296
+ assert.deepEqual(response, originalResponse);
297
+ assert.calledOnceWithExactly(
298
+ meeting.locusInfo.handleLocusAPIResponse,
299
+ meeting,
300
+ originalResponse.body
301
+ );
302
+ });
303
+
275
304
  it('should work with an undefined meeting', () => {
276
305
  const originalResponse = {
277
306
  body: {
@@ -1146,6 +1175,10 @@ describe('plugin-meetings', () => {
1146
1175
  {functionName: 'canSelectSpokenLanguages', displayHint: 'DISPLAY_NON_ENGLISH_ASR'},
1147
1176
  {functionName: 'waitingForOthersToJoin', displayHint: 'WAITING_FOR_OTHERS'},
1148
1177
  {functionName: 'showAutoEndMeetingWarning', displayHint: 'SHOW_AUTO_END_MEETING_WARNING'},
1178
+ {
1179
+ functionName: 'isAnonymizeDisplayNamesEnabled',
1180
+ displayHint: 'ANONYMOUS_DISPLAY_NAMES_ENABLED',
1181
+ },
1149
1182
  ].forEach(({functionName, displayHint}) => {
1150
1183
  describe(functionName, () => {
1151
1184
  it('works as expected', () => {
@@ -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: {
@@ -423,6 +426,33 @@ describe('plugin-meetings', () => {
423
426
  });
424
427
  });
425
428
 
429
+ describe('#_toggleEnableAv1SlidesSupport', () => {
430
+ it('should have _toggleEnableAv1SlidesSupport', () => {
431
+ assert.equal(typeof webex.meetings._toggleEnableAv1SlidesSupport, 'function');
432
+ });
433
+
434
+ describe('success', () => {
435
+ it('should update meetings config to enable AV1 slides support', () => {
436
+ webex.meetings._toggleEnableAv1SlidesSupport(true);
437
+ assert.equal(webex.meetings.config.enableAv1SlidesSupport, true);
438
+
439
+ webex.meetings._toggleEnableAv1SlidesSupport(false);
440
+ assert.equal(webex.meetings.config.enableAv1SlidesSupport, false);
441
+ });
442
+
443
+ it('should not update config when called with a non-boolean value', () => {
444
+ webex.meetings._toggleEnableAv1SlidesSupport(true);
445
+ assert.equal(webex.meetings.config.enableAv1SlidesSupport, true);
446
+
447
+ webex.meetings._toggleEnableAv1SlidesSupport('invalid');
448
+ assert.equal(webex.meetings.config.enableAv1SlidesSupport, true);
449
+
450
+ webex.meetings._toggleEnableAv1SlidesSupport(undefined);
451
+ assert.equal(webex.meetings.config.enableAv1SlidesSupport, true);
452
+ });
453
+ });
454
+ });
455
+
426
456
  describe('#_toggleStopIceGatheringAfterFirstRelayCandidate', () => {
427
457
  it('should have _toggleStopIceGatheringAfterFirstRelayCandidate', () => {
428
458
  assert.equal(
@@ -1355,6 +1385,87 @@ describe('plugin-meetings', () => {
1355
1385
  );
1356
1386
  });
1357
1387
  });
1388
+ describe('#fetchSitePreferencesMeViaSite', () => {
1389
+ const sitePreferencesResponse = {
1390
+ scheduling: {
1391
+ supportScheduleWebinar: true,
1392
+ webinarWebLink: 'https://go.webex.com/webappng/sites/go/webinar/scheduler',
1393
+ },
1394
+ };
1395
+
1396
+ beforeEach(() => {
1397
+ webex.meetings.request.fetchSitePreferencesMeViaSite = sinon
1398
+ .stub()
1399
+ .resolves(sitePreferencesResponse);
1400
+ });
1401
+
1402
+ it('should have #fetchSitePreferencesMeViaSite', () => {
1403
+ assert.exists(webex.meetings.fetchSitePreferencesMeViaSite);
1404
+ });
1405
+
1406
+ it('fetches scheduling preferences for the preferred Webex site by default', async () => {
1407
+ webex.meetings.preferredWebexSite = 'go.webex.com';
1408
+
1409
+ const result = await webex.meetings.fetchSitePreferencesMeViaSite();
1410
+
1411
+ assert.deepEqual(result, sitePreferencesResponse);
1412
+ assert.calledOnceWithExactly(
1413
+ webex.meetings.request.fetchSitePreferencesMeViaSite,
1414
+ {
1415
+ siteUrl: 'go.webex.com',
1416
+ }
1417
+ );
1418
+ });
1419
+
1420
+ it('uses the provided Webex site instead of the preferred Webex site', async () => {
1421
+ webex.meetings.preferredWebexSite = 'preferred.webex.com';
1422
+
1423
+ await webex.meetings.fetchSitePreferencesMeViaSite({siteUrl: 'go.webex.com'});
1424
+
1425
+ assert.calledOnceWithExactly(
1426
+ webex.meetings.request.fetchSitePreferencesMeViaSite,
1427
+ {
1428
+ siteUrl: 'go.webex.com',
1429
+ }
1430
+ );
1431
+ });
1432
+
1433
+ it('forwards custom site name and preference sections to the request helper', async () => {
1434
+ webex.meetings.preferredWebexSite = 'go.webex.com';
1435
+
1436
+ await webex.meetings.fetchSitePreferencesMeViaSite({
1437
+ siteName: 'custom-site',
1438
+ selectOptions: [SitePreferenceSelectOption.SCHEDULING],
1439
+ });
1440
+
1441
+ assert.calledOnceWithExactly(
1442
+ webex.meetings.request.fetchSitePreferencesMeViaSite,
1443
+ {
1444
+ siteUrl: 'go.webex.com',
1445
+ siteName: 'custom-site',
1446
+ selectOptions: [SitePreferenceSelectOption.SCHEDULING],
1447
+ }
1448
+ );
1449
+ });
1450
+
1451
+ it('throws when no Webex site is available', () => {
1452
+ webex.meetings.preferredWebexSite = '';
1453
+ webex.meetings.request.fetchSitePreferencesMeViaSite.throws(
1454
+ new ParameterError(
1455
+ 'No siteUrl available. Call register() before fetching site preferences or provide options.siteUrl.'
1456
+ )
1457
+ );
1458
+
1459
+ assert.throws(
1460
+ () => webex.meetings.fetchSitePreferencesMeViaSite(),
1461
+ ParameterError,
1462
+ 'No siteUrl available. Call register() before fetching site preferences or provide options.siteUrl.'
1463
+ );
1464
+ assert.calledOnceWithExactly(webex.meetings.request.fetchSitePreferencesMeViaSite, {
1465
+ siteUrl: '',
1466
+ });
1467
+ });
1468
+ });
1358
1469
  describe('Static shortcut proxy methods', () => {
1359
1470
  describe('MeetingCollection getByKey proxies', () => {
1360
1471
  beforeEach(() => {
@@ -1391,7 +1502,7 @@ describe('plugin-meetings', () => {
1391
1502
  it('should have #syncMeetings', () => {
1392
1503
  assert.exists(webex.meetings.syncMeetings);
1393
1504
  });
1394
- it('should do nothing and return a resolved promise if unverified guest', async () => {
1505
+ it('should skip getActiveMeetings but still call syncAllHashTreeDatasets if unverified guest', async () => {
1395
1506
  webex.meetings.request.getActiveMeetings = sinon.stub().returns(
1396
1507
  Promise.resolve({
1397
1508
  loci: [
@@ -1404,13 +1515,23 @@ describe('plugin-meetings', () => {
1404
1515
  webex.credentials.isUnverifiedGuest = true;
1405
1516
  LoggerProxy.logger.info = sinon.stub();
1406
1517
 
1518
+ const mockLocusInfo = {
1519
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1520
+ };
1521
+ webex.meetings.meetingCollection.getAll = sinon.stub().returns({
1522
+ meeting1: {locusInfo: mockLocusInfo},
1523
+ meeting2: {locusInfo: undefined},
1524
+ meeting3: {},
1525
+ });
1526
+
1407
1527
  await webex.meetings.syncMeetings();
1408
1528
 
1409
1529
  assert.notCalled(webex.meetings.request.getActiveMeetings);
1410
1530
  assert.calledWith(
1411
1531
  LoggerProxy.logger.info,
1412
- 'Meetings:index#syncMeetings --> skipping meeting sync as unverified guest'
1532
+ 'Meetings:index#syncMeetings --> user is unverified guest, skipping calling Locus for meeting sync'
1413
1533
  );
1534
+ assert.calledOnce(mockLocusInfo.syncAllHashTreeDatasets);
1414
1535
  });
1415
1536
  describe('succesful requests', () => {
1416
1537
  beforeEach(() => {
@@ -1429,6 +1550,9 @@ describe('plugin-meetings', () => {
1429
1550
  webex.meetings.meetingCollection.getByKey = sinon.stub().returns({
1430
1551
  locusInfo,
1431
1552
  });
1553
+ webex.meetings.meetingCollection.getAll = sinon.stub().returns({
1554
+ meeting1: {locusInfo, locusUrl: url1},
1555
+ });
1432
1556
  });
1433
1557
  it('tests the sync meeting calls for existing meeting', async () => {
1434
1558
  await webex.meetings.syncMeetings();
@@ -1436,6 +1560,7 @@ describe('plugin-meetings', () => {
1436
1560
  assert.calledOnce(webex.meetings.meetingCollection.getByKey);
1437
1561
  assert.calledOnce(locusInfo.parse);
1438
1562
  assert.calledWith(webex.meetings.meetingCollection.getByKey, 'locusUrl', url1);
1563
+ assert.calledOnce(locusInfo.syncAllHashTreeDatasets);
1439
1564
  });
1440
1565
  });
1441
1566
  describe('when meeting is not returned', () => {
@@ -1474,7 +1599,7 @@ describe('plugin-meetings', () => {
1474
1599
  url: url1,
1475
1600
  },
1476
1601
  hashTreeMessage: undefined,
1477
- });
1602
+ }, sinon.match.func);
1478
1603
  });
1479
1604
  });
1480
1605
  describe('when destroying meeting is needed', () => {
@@ -1520,7 +1645,7 @@ describe('plugin-meetings', () => {
1520
1645
  it('destroy any meeting that has no active locus url if keepOnlyLocusMeetings is not defined', async () => {
1521
1646
  await webex.meetings.syncMeetings();
1522
1647
  assert.calledOnce(webex.meetings.request.getActiveMeetings);
1523
- assert.calledOnce(webex.meetings.meetingCollection.getAll);
1648
+ assert.calledTwice(webex.meetings.meetingCollection.getAll);
1524
1649
  assert.calledWith(destroySpy, meetingCollectionMeetings.noLongerValidLocusMeeting);
1525
1650
  assert.calledWith(destroySpy, meetingCollectionMeetings.otherNonLocusMeeting1);
1526
1651
  assert.calledWith(destroySpy, meetingCollectionMeetings.otherNonLocusMeeting2);
@@ -1532,7 +1657,7 @@ describe('plugin-meetings', () => {
1532
1657
  it('destroy any meeting that has no active locus url if keepOnlyLocusMeetings === true', async () => {
1533
1658
  await webex.meetings.syncMeetings({keepOnlyLocusMeetings: true});
1534
1659
  assert.calledOnce(webex.meetings.request.getActiveMeetings);
1535
- assert.calledOnce(webex.meetings.meetingCollection.getAll);
1660
+ assert.calledTwice(webex.meetings.meetingCollection.getAll);
1536
1661
  assert.calledWith(destroySpy, meetingCollectionMeetings.noLongerValidLocusMeeting);
1537
1662
  assert.calledWith(destroySpy, meetingCollectionMeetings.otherNonLocusMeeting1);
1538
1663
  assert.calledWith(destroySpy, meetingCollectionMeetings.otherNonLocusMeeting2);
@@ -1544,7 +1669,7 @@ describe('plugin-meetings', () => {
1544
1669
  it('destroy any LOCUS meetings that have no active locus url if keepOnlyLocusMeetings === false', async () => {
1545
1670
  await webex.meetings.syncMeetings({keepOnlyLocusMeetings: false});
1546
1671
  assert.calledOnce(webex.meetings.request.getActiveMeetings);
1547
- assert.calledOnce(webex.meetings.meetingCollection.getAll);
1672
+ assert.calledTwice(webex.meetings.meetingCollection.getAll);
1548
1673
  assert.calledWith(destroySpy, meetingCollectionMeetings.noLongerValidLocusMeeting);
1549
1674
  assert.callCount(destroySpy, 1);
1550
1675
 
@@ -1552,6 +1677,147 @@ describe('plugin-meetings', () => {
1552
1677
  });
1553
1678
  });
1554
1679
  });
1680
+
1681
+ describe('when globalMeetingId preserves breakout meetings', () => {
1682
+ let destroySpy;
1683
+ let cleanUpSpy;
1684
+
1685
+ beforeEach(() => {
1686
+ destroySpy = sinon.spy(webex.meetings, 'destroy');
1687
+ cleanUpSpy = sinon.stub(MeetingUtil, 'cleanUp').returns(Promise.resolve());
1688
+ });
1689
+
1690
+ afterEach(() => {
1691
+ cleanUpSpy.restore();
1692
+ });
1693
+
1694
+ it('should not destroy a meeting whose globalMeetingId matches an active locus', async () => {
1695
+ const meetingCollectionMeetings = {
1696
+ breakoutMeeting: {
1697
+ locusUrl: 'breakout-url',
1698
+ locusInfo: {
1699
+ info: {globalMeetingId: 'gmid-123'},
1700
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1701
+ },
1702
+ sendCallAnalyzerMetrics: sinon.stub(),
1703
+ },
1704
+ };
1705
+
1706
+ webex.meetings.meetingCollection.getAll = sinon
1707
+ .stub()
1708
+ .returns(meetingCollectionMeetings);
1709
+ webex.meetings.request.getActiveMeetings = sinon.stub().resolves({
1710
+ loci: [{url: 'main-url', info: {globalMeetingId: 'gmid-123'}}],
1711
+ });
1712
+
1713
+ await webex.meetings.syncMeetings();
1714
+
1715
+ assert.notCalled(destroySpy);
1716
+ });
1717
+
1718
+ it('should destroy a meeting whose globalMeetingId does NOT match any active locus', async () => {
1719
+ const meetingCollectionMeetings = {
1720
+ breakoutMeeting: {
1721
+ locusUrl: 'breakout-url',
1722
+ locusInfo: {
1723
+ info: {globalMeetingId: 'gmid-other'},
1724
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1725
+ },
1726
+ sendCallAnalyzerMetrics: sinon.stub(),
1727
+ },
1728
+ };
1729
+
1730
+ webex.meetings.meetingCollection.getAll = sinon
1731
+ .stub()
1732
+ .returns(meetingCollectionMeetings);
1733
+ webex.meetings.request.getActiveMeetings = sinon.stub().resolves({
1734
+ loci: [{url: 'main-url', info: {globalMeetingId: 'gmid-123'}}],
1735
+ });
1736
+
1737
+ await webex.meetings.syncMeetings();
1738
+
1739
+ assert.calledOnce(destroySpy);
1740
+ assert.calledWith(destroySpy, meetingCollectionMeetings.breakoutMeeting);
1741
+ });
1742
+ });
1743
+
1744
+ describe('skipHashTreeSync parameter', () => {
1745
+ it('should skip syncAllHashTreeDatasets when skipHashTreeSync is true', async () => {
1746
+ const mockLocusInfo = {
1747
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1748
+ };
1749
+
1750
+ webex.meetings.request.getActiveMeetings = sinon.stub().resolves({loci: []});
1751
+ webex.meetings.meetingCollection.getAll = sinon.stub().returns({
1752
+ meeting1: {locusInfo: mockLocusInfo},
1753
+ });
1754
+
1755
+ await webex.meetings.syncMeetings({keepOnlyLocusMeetings: false, skipHashTreeSync: true});
1756
+
1757
+ assert.calledOnce(webex.meetings.request.getActiveMeetings);
1758
+ assert.notCalled(mockLocusInfo.syncAllHashTreeDatasets);
1759
+ });
1760
+
1761
+ it('should call syncAllHashTreeDatasets when skipHashTreeSync is false (default)', async () => {
1762
+ const mockLocusInfo = {
1763
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1764
+ };
1765
+
1766
+ webex.meetings.request.getActiveMeetings = sinon.stub().resolves({loci: []});
1767
+ webex.meetings.meetingCollection.getAll = sinon.stub().returns({
1768
+ meeting1: {locusInfo: mockLocusInfo},
1769
+ });
1770
+
1771
+ await webex.meetings.syncMeetings({keepOnlyLocusMeetings: false, skipHashTreeSync: false});
1772
+
1773
+ assert.calledOnce(webex.meetings.request.getActiveMeetings);
1774
+ assert.calledOnce(mockLocusInfo.syncAllHashTreeDatasets);
1775
+ });
1776
+ });
1777
+
1778
+ describe('syncAllHashTreeDatasets in syncMeetings', () => {
1779
+ it('should call syncAllHashTreeDatasets for multiple meetings, skipping those without locusInfo', async () => {
1780
+ const mockLocusInfo1 = {
1781
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1782
+ };
1783
+ const mockLocusInfo2 = {
1784
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1785
+ };
1786
+
1787
+ webex.meetings.request.getActiveMeetings = sinon.stub().resolves({loci: []});
1788
+ webex.meetings.meetingCollection.getAll = sinon.stub().returns({
1789
+ meeting1: {locusInfo: mockLocusInfo1},
1790
+ meeting2: {locusInfo: undefined},
1791
+ meeting3: {locusInfo: mockLocusInfo2},
1792
+ meeting4: {},
1793
+ });
1794
+
1795
+ await webex.meetings.syncMeetings({keepOnlyLocusMeetings: false});
1796
+
1797
+ assert.calledOnce(mockLocusInfo1.syncAllHashTreeDatasets);
1798
+ assert.calledOnce(mockLocusInfo2.syncAllHashTreeDatasets);
1799
+ });
1800
+
1801
+ it('should not call syncAllHashTreeDatasets when getActiveMeetings throws an error', async () => {
1802
+ const mockLocusInfo = {
1803
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1804
+ };
1805
+
1806
+ webex.meetings.request.getActiveMeetings = sinon.stub().rejects(new Error('network error'));
1807
+ webex.meetings.meetingCollection.getAll = sinon.stub().returns({
1808
+ meeting1: {locusInfo: mockLocusInfo},
1809
+ });
1810
+
1811
+ try {
1812
+ await webex.meetings.syncMeetings();
1813
+ assert.fail('should have thrown');
1814
+ } catch (err) {
1815
+ assert.equal(err.message, 'network error');
1816
+ }
1817
+
1818
+ assert.notCalled(mockLocusInfo.syncAllHashTreeDatasets);
1819
+ });
1820
+ });
1555
1821
  });
1556
1822
  describe('#fetchStaticMeetingLink', () => {
1557
1823
  const conversationUrl = 'conv.fakeconversationurl.com';
@@ -2015,7 +2281,7 @@ describe('plugin-meetings', () => {
2015
2281
  },
2016
2282
  },
2017
2283
  hashTreeMessage: undefined,
2018
- });
2284
+ }, sinon.match.func);
2019
2285
  });
2020
2286
  it('should setup the meeting from a hash tree event', async () => {
2021
2287
  const selfData = {};
@@ -2049,7 +2315,7 @@ describe('plugin-meetings', () => {
2049
2315
  info: infoData,
2050
2316
  },
2051
2317
  hashTreeMessage,
2052
- });
2318
+ }, sinon.match.func);
2053
2319
  });
2054
2320
 
2055
2321
  it('should ignore hash tree event when created locus has INACTIVE fullState', async () => {
@@ -2129,7 +2395,7 @@ describe('plugin-meetings', () => {
2129
2395
  },
2130
2396
  },
2131
2397
  hashTreeMessage: undefined,
2132
- });
2398
+ }, sinon.match.func);
2133
2399
  });
2134
2400
 
2135
2401
  it('sends client event correctly on finally', async () => {
@@ -2205,7 +2471,7 @@ describe('plugin-meetings', () => {
2205
2471
  },
2206
2472
  },
2207
2473
  hashTreeMessage: undefined,
2208
- });
2474
+ }, sinon.match.func);
2209
2475
  });
2210
2476
 
2211
2477
  const generateFakeLocusData = (isUnifiedSpaceMeeting) => ({
@@ -2833,6 +3099,39 @@ describe('plugin-meetings', () => {
2833
3099
  checkCreateMeetingWithNoMeetingInfo(true, true);
2834
3100
  });
2835
3101
 
3102
+ it('does not emit meeting:added when meeting is destroyed due to missing meeting info', async () => {
3103
+ // Make destroy actually remove the meeting from the collection
3104
+ // so that getMeetingByType returns null in the finally block
3105
+ webex.meetings.destroy = sinon.stub().callsFake((meeting) => {
3106
+ webex.meetings.meetingCollection.delete(meeting.id);
3107
+ });
3108
+
3109
+ try {
3110
+ await webex.meetings.createMeeting(
3111
+ 'test destination',
3112
+ 'test type',
3113
+ undefined,
3114
+ undefined,
3115
+ undefined,
3116
+ true
3117
+ );
3118
+ assert.fail('should have thrown NoMeetingInfoError');
3119
+ } catch (err) {
3120
+ assert.instanceOf(err, NoMeetingInfoError);
3121
+ }
3122
+
3123
+ assert.calledOnce(webex.meetings.destroy);
3124
+
3125
+ // meeting:added should NOT have been triggered since the meeting was destroyed
3126
+ assert.neverCalledWith(
3127
+ TriggerProxy.trigger,
3128
+ sinon.match.any,
3129
+ sinon.match({function: 'createMeeting'}),
3130
+ 'meeting:added',
3131
+ sinon.match.any
3132
+ );
3133
+ });
3134
+
2836
3135
  it('creates the meeting avoiding meeting info fetch by passing type as DESTINATION_TYPE.ONE_ON_ONE_CALL', async () => {
2837
3136
  const meeting = await webex.meetings.createMeeting(
2838
3137
  'test destination',
@@ -2847,6 +3146,30 @@ describe('plugin-meetings', () => {
2847
3146
 
2848
3147
  assert.notCalled(webex.meetings.meetingInfo.fetchMeetingInfo);
2849
3148
  });
3149
+
3150
+ [
3151
+ {fullStateType: 'CALL'},
3152
+ {fullStateType: 'SIP_BRIDGE'},
3153
+ {fullStateType: 'SPACE_SHARE'},
3154
+ ].forEach(({fullStateType}) => {
3155
+ it(`skips meeting info fetch when LOCUS_ID destination is a 1:1 call (fullState.type ${fullStateType})`, async () => {
3156
+ const locusDestination = {
3157
+ fullState: {type: fullStateType},
3158
+ };
3159
+
3160
+ const meeting = await webex.meetings.createMeeting(
3161
+ locusDestination,
3162
+ DESTINATION_TYPE.LOCUS_ID
3163
+ );
3164
+
3165
+ assert.instanceOf(
3166
+ meeting,
3167
+ Meeting,
3168
+ 'createMeeting should eventually resolve to a Meeting Object'
3169
+ );
3170
+ assert.notCalled(webex.meetings.meetingInfo.fetchMeetingInfo);
3171
+ });
3172
+ });
2850
3173
  });
2851
3174
 
2852
3175
  describe('rejected MeetingInfo.#fetchMeetingInfo - does not log for known Error types', () => {
@@ -3426,6 +3749,21 @@ describe('plugin-meetings', () => {
3426
3749
  'Meetings:index#isNeedHandleMainLocus --> self device left&moved in main locus with self joined status, not need to handle'
3427
3750
  );
3428
3751
  });
3752
+
3753
+ it('check breakout ended with self removed, return false', () => {
3754
+ webex.meetings.meetingCollection.getActiveBreakoutLocus = sinon.stub().returns(null);
3755
+ newLocus.self.state = 'LEFT';
3756
+ newLocus.self.reason = 'OTHER';
3757
+ newLocus.self.removed = true;
3758
+ newLocus.fullState = {state: 'INACTIVE', endMeetingReason: 'BREAKOUT_ENDED'};
3759
+ LoggerProxy.logger.log = sinon.stub();
3760
+ const result = webex.meetings.isNeedHandleMainLocus(meeting, newLocus);
3761
+ assert.equal(result, false);
3762
+ assert.calledWith(
3763
+ LoggerProxy.logger.log,
3764
+ 'Meetings:index#isNeedHandleMainLocus --> self moved main locus with self removed status or with device resource moved, not need to handle'
3765
+ );
3766
+ });
3429
3767
  });
3430
3768
 
3431
3769
  describe('#isNeedHandleLocusDTO', () => {
@@ -3486,6 +3824,18 @@ describe('plugin-meetings', () => {
3486
3824
  const result = webex.meetings.isNeedHandleLocusDTO(meeting, newLocus);
3487
3825
  assert.equal(result, false);
3488
3826
  });
3827
+ it('breakout session with breakout ended, return false', () => {
3828
+ newLocus.controls.breakout = {
3829
+ sessionType: 'BREAKOUT',
3830
+ };
3831
+ newLocus.self.state = 'LEFT';
3832
+ newLocus.self.reason = 'OTHER';
3833
+ newLocus.self.devices = [];
3834
+ newLocus.fullState = {state: 'INACTIVE', endMeetingReason: 'BREAKOUT_ENDED'};
3835
+ LoggerProxy.logger.log = sinon.stub();
3836
+ const result = webex.meetings.isNeedHandleLocusDTO(meeting, newLocus);
3837
+ assert.equal(result, false);
3838
+ });
3489
3839
  it('moved to lobby, return true', () => {
3490
3840
  newLocus.controls.breakout = {
3491
3841
  sessionType: 'MAIN',