@webex/plugin-meetings 3.12.0-next.5 → 3.12.0-next.51

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 (136) 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 +6 -2
  5. package/dist/breakouts/breakout.js.map +1 -1
  6. package/dist/breakouts/index.js +1 -1
  7. package/dist/config.js +1 -0
  8. package/dist/config.js.map +1 -1
  9. package/dist/constants.js +6 -3
  10. package/dist/constants.js.map +1 -1
  11. package/dist/controls-options-manager/constants.js +11 -1
  12. package/dist/controls-options-manager/constants.js.map +1 -1
  13. package/dist/controls-options-manager/index.js +38 -24
  14. package/dist/controls-options-manager/index.js.map +1 -1
  15. package/dist/controls-options-manager/util.js +91 -0
  16. package/dist/controls-options-manager/util.js.map +1 -1
  17. package/dist/hashTree/constants.js +10 -1
  18. package/dist/hashTree/constants.js.map +1 -1
  19. package/dist/hashTree/hashTreeParser.js +646 -371
  20. package/dist/hashTree/hashTreeParser.js.map +1 -1
  21. package/dist/hashTree/utils.js +22 -0
  22. package/dist/hashTree/utils.js.map +1 -1
  23. package/dist/index.js +7 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/interceptors/locusRetry.js +23 -8
  26. package/dist/interceptors/locusRetry.js.map +1 -1
  27. package/dist/interpretation/index.js +10 -1
  28. package/dist/interpretation/index.js.map +1 -1
  29. package/dist/interpretation/siLanguage.js +1 -1
  30. package/dist/locus-info/controlsUtils.js +4 -1
  31. package/dist/locus-info/controlsUtils.js.map +1 -1
  32. package/dist/locus-info/index.js +289 -86
  33. package/dist/locus-info/index.js.map +1 -1
  34. package/dist/locus-info/types.js +19 -0
  35. package/dist/locus-info/types.js.map +1 -1
  36. package/dist/media/properties.js +1 -0
  37. package/dist/media/properties.js.map +1 -1
  38. package/dist/meeting/in-meeting-actions.js +3 -1
  39. package/dist/meeting/in-meeting-actions.js.map +1 -1
  40. package/dist/meeting/index.js +842 -521
  41. package/dist/meeting/index.js.map +1 -1
  42. package/dist/meeting/util.js +19 -2
  43. package/dist/meeting/util.js.map +1 -1
  44. package/dist/meetings/index.js +205 -77
  45. package/dist/meetings/index.js.map +1 -1
  46. package/dist/meetings/meetings.types.js +6 -1
  47. package/dist/meetings/meetings.types.js.map +1 -1
  48. package/dist/meetings/request.js +39 -0
  49. package/dist/meetings/request.js.map +1 -1
  50. package/dist/meetings/util.js +67 -5
  51. package/dist/meetings/util.js.map +1 -1
  52. package/dist/member/index.js +10 -0
  53. package/dist/member/index.js.map +1 -1
  54. package/dist/member/types.js.map +1 -1
  55. package/dist/member/util.js +3 -0
  56. package/dist/member/util.js.map +1 -1
  57. package/dist/metrics/constants.js +2 -1
  58. package/dist/metrics/constants.js.map +1 -1
  59. package/dist/recording-controller/index.js +1 -3
  60. package/dist/recording-controller/index.js.map +1 -1
  61. package/dist/types/config.d.ts +1 -0
  62. package/dist/types/constants.d.ts +2 -0
  63. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  64. package/dist/types/controls-options-manager/index.d.ts +10 -0
  65. package/dist/types/hashTree/constants.d.ts +1 -0
  66. package/dist/types/hashTree/hashTreeParser.d.ts +83 -16
  67. package/dist/types/hashTree/utils.d.ts +11 -0
  68. package/dist/types/index.d.ts +2 -0
  69. package/dist/types/interceptors/locusRetry.d.ts +4 -4
  70. package/dist/types/locus-info/index.d.ts +46 -6
  71. package/dist/types/locus-info/types.d.ts +21 -1
  72. package/dist/types/media/properties.d.ts +1 -0
  73. package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
  74. package/dist/types/meeting/index.d.ts +70 -1
  75. package/dist/types/meeting/util.d.ts +8 -0
  76. package/dist/types/meetings/index.d.ts +20 -2
  77. package/dist/types/meetings/meetings.types.d.ts +15 -0
  78. package/dist/types/meetings/request.d.ts +14 -0
  79. package/dist/types/member/index.d.ts +1 -0
  80. package/dist/types/member/types.d.ts +1 -0
  81. package/dist/types/member/util.d.ts +1 -0
  82. package/dist/types/metrics/constants.d.ts +1 -0
  83. package/dist/webinar/index.js +361 -235
  84. package/dist/webinar/index.js.map +1 -1
  85. package/package.json +22 -22
  86. package/src/aiEnableRequest/index.ts +16 -0
  87. package/src/breakouts/breakout.ts +2 -1
  88. package/src/config.ts +1 -0
  89. package/src/constants.ts +5 -1
  90. package/src/controls-options-manager/constants.ts +14 -1
  91. package/src/controls-options-manager/index.ts +47 -24
  92. package/src/controls-options-manager/util.ts +81 -1
  93. package/src/hashTree/constants.ts +9 -0
  94. package/src/hashTree/hashTreeParser.ts +362 -174
  95. package/src/hashTree/utils.ts +17 -0
  96. package/src/index.ts +5 -0
  97. package/src/interceptors/locusRetry.ts +25 -4
  98. package/src/interpretation/index.ts +25 -8
  99. package/src/locus-info/controlsUtils.ts +3 -1
  100. package/src/locus-info/index.ts +291 -93
  101. package/src/locus-info/types.ts +25 -1
  102. package/src/media/properties.ts +1 -0
  103. package/src/meeting/in-meeting-actions.ts +4 -0
  104. package/src/meeting/index.ts +315 -26
  105. package/src/meeting/util.ts +20 -2
  106. package/src/meetings/index.ts +109 -43
  107. package/src/meetings/meetings.types.ts +19 -0
  108. package/src/meetings/request.ts +43 -0
  109. package/src/meetings/util.ts +80 -1
  110. package/src/member/index.ts +10 -0
  111. package/src/member/types.ts +1 -0
  112. package/src/member/util.ts +3 -0
  113. package/src/metrics/constants.ts +1 -0
  114. package/src/recording-controller/index.ts +1 -2
  115. package/src/webinar/index.ts +162 -21
  116. package/test/unit/spec/aiEnableRequest/index.ts +86 -0
  117. package/test/unit/spec/breakouts/breakout.ts +7 -3
  118. package/test/unit/spec/controls-options-manager/index.js +140 -29
  119. package/test/unit/spec/controls-options-manager/util.js +165 -0
  120. package/test/unit/spec/hashTree/hashTreeParser.ts +1341 -140
  121. package/test/unit/spec/hashTree/utils.ts +88 -1
  122. package/test/unit/spec/interceptors/locusRetry.ts +205 -4
  123. package/test/unit/spec/interpretation/index.ts +26 -4
  124. package/test/unit/spec/locus-info/controlsUtils.js +172 -57
  125. package/test/unit/spec/locus-info/index.js +475 -81
  126. package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
  127. package/test/unit/spec/meeting/index.js +836 -41
  128. package/test/unit/spec/meeting/muteState.js +3 -0
  129. package/test/unit/spec/meeting/utils.js +33 -0
  130. package/test/unit/spec/meetings/index.js +309 -10
  131. package/test/unit/spec/meetings/request.js +141 -0
  132. package/test/unit/spec/meetings/utils.js +161 -0
  133. package/test/unit/spec/member/index.js +7 -0
  134. package/test/unit/spec/member/util.js +24 -0
  135. package/test/unit/spec/recording-controller/index.js +9 -8
  136. 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: {
@@ -1355,6 +1358,87 @@ describe('plugin-meetings', () => {
1355
1358
  );
1356
1359
  });
1357
1360
  });
1361
+ describe('#fetchSitePreferencesMeViaSite', () => {
1362
+ const sitePreferencesResponse = {
1363
+ scheduling: {
1364
+ supportScheduleWebinar: true,
1365
+ webinarWebLink: 'https://go.webex.com/webappng/sites/go/webinar/scheduler',
1366
+ },
1367
+ };
1368
+
1369
+ beforeEach(() => {
1370
+ webex.meetings.request.fetchSitePreferencesMeViaSite = sinon
1371
+ .stub()
1372
+ .resolves(sitePreferencesResponse);
1373
+ });
1374
+
1375
+ it('should have #fetchSitePreferencesMeViaSite', () => {
1376
+ assert.exists(webex.meetings.fetchSitePreferencesMeViaSite);
1377
+ });
1378
+
1379
+ it('fetches scheduling preferences for the preferred Webex site by default', async () => {
1380
+ webex.meetings.preferredWebexSite = 'go.webex.com';
1381
+
1382
+ const result = await webex.meetings.fetchSitePreferencesMeViaSite();
1383
+
1384
+ assert.deepEqual(result, sitePreferencesResponse);
1385
+ assert.calledOnceWithExactly(
1386
+ webex.meetings.request.fetchSitePreferencesMeViaSite,
1387
+ {
1388
+ siteUrl: 'go.webex.com',
1389
+ }
1390
+ );
1391
+ });
1392
+
1393
+ it('uses the provided Webex site instead of the preferred Webex site', async () => {
1394
+ webex.meetings.preferredWebexSite = 'preferred.webex.com';
1395
+
1396
+ await webex.meetings.fetchSitePreferencesMeViaSite({siteUrl: 'go.webex.com'});
1397
+
1398
+ assert.calledOnceWithExactly(
1399
+ webex.meetings.request.fetchSitePreferencesMeViaSite,
1400
+ {
1401
+ siteUrl: 'go.webex.com',
1402
+ }
1403
+ );
1404
+ });
1405
+
1406
+ it('forwards custom site name and preference sections to the request helper', async () => {
1407
+ webex.meetings.preferredWebexSite = 'go.webex.com';
1408
+
1409
+ await webex.meetings.fetchSitePreferencesMeViaSite({
1410
+ siteName: 'custom-site',
1411
+ selectOptions: [SitePreferenceSelectOption.SCHEDULING],
1412
+ });
1413
+
1414
+ assert.calledOnceWithExactly(
1415
+ webex.meetings.request.fetchSitePreferencesMeViaSite,
1416
+ {
1417
+ siteUrl: 'go.webex.com',
1418
+ siteName: 'custom-site',
1419
+ selectOptions: [SitePreferenceSelectOption.SCHEDULING],
1420
+ }
1421
+ );
1422
+ });
1423
+
1424
+ it('throws when no Webex site is available', () => {
1425
+ webex.meetings.preferredWebexSite = '';
1426
+ webex.meetings.request.fetchSitePreferencesMeViaSite.throws(
1427
+ new ParameterError(
1428
+ 'No siteUrl available. Call register() before fetching site preferences or provide options.siteUrl.'
1429
+ )
1430
+ );
1431
+
1432
+ assert.throws(
1433
+ () => webex.meetings.fetchSitePreferencesMeViaSite(),
1434
+ ParameterError,
1435
+ 'No siteUrl available. Call register() before fetching site preferences or provide options.siteUrl.'
1436
+ );
1437
+ assert.calledOnceWithExactly(webex.meetings.request.fetchSitePreferencesMeViaSite, {
1438
+ siteUrl: '',
1439
+ });
1440
+ });
1441
+ });
1358
1442
  describe('Static shortcut proxy methods', () => {
1359
1443
  describe('MeetingCollection getByKey proxies', () => {
1360
1444
  beforeEach(() => {
@@ -1391,7 +1475,7 @@ describe('plugin-meetings', () => {
1391
1475
  it('should have #syncMeetings', () => {
1392
1476
  assert.exists(webex.meetings.syncMeetings);
1393
1477
  });
1394
- it('should do nothing and return a resolved promise if unverified guest', async () => {
1478
+ it('should skip getActiveMeetings but still call syncAllHashTreeDatasets if unverified guest', async () => {
1395
1479
  webex.meetings.request.getActiveMeetings = sinon.stub().returns(
1396
1480
  Promise.resolve({
1397
1481
  loci: [
@@ -1404,13 +1488,23 @@ describe('plugin-meetings', () => {
1404
1488
  webex.credentials.isUnverifiedGuest = true;
1405
1489
  LoggerProxy.logger.info = sinon.stub();
1406
1490
 
1491
+ const mockLocusInfo = {
1492
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1493
+ };
1494
+ webex.meetings.meetingCollection.getAll = sinon.stub().returns({
1495
+ meeting1: {locusInfo: mockLocusInfo},
1496
+ meeting2: {locusInfo: undefined},
1497
+ meeting3: {},
1498
+ });
1499
+
1407
1500
  await webex.meetings.syncMeetings();
1408
1501
 
1409
1502
  assert.notCalled(webex.meetings.request.getActiveMeetings);
1410
1503
  assert.calledWith(
1411
1504
  LoggerProxy.logger.info,
1412
- 'Meetings:index#syncMeetings --> skipping meeting sync as unverified guest'
1505
+ 'Meetings:index#syncMeetings --> user is unverified guest, skipping calling Locus for meeting sync'
1413
1506
  );
1507
+ assert.calledOnce(mockLocusInfo.syncAllHashTreeDatasets);
1414
1508
  });
1415
1509
  describe('succesful requests', () => {
1416
1510
  beforeEach(() => {
@@ -1429,6 +1523,9 @@ describe('plugin-meetings', () => {
1429
1523
  webex.meetings.meetingCollection.getByKey = sinon.stub().returns({
1430
1524
  locusInfo,
1431
1525
  });
1526
+ webex.meetings.meetingCollection.getAll = sinon.stub().returns({
1527
+ meeting1: {locusInfo, locusUrl: url1},
1528
+ });
1432
1529
  });
1433
1530
  it('tests the sync meeting calls for existing meeting', async () => {
1434
1531
  await webex.meetings.syncMeetings();
@@ -1436,6 +1533,7 @@ describe('plugin-meetings', () => {
1436
1533
  assert.calledOnce(webex.meetings.meetingCollection.getByKey);
1437
1534
  assert.calledOnce(locusInfo.parse);
1438
1535
  assert.calledWith(webex.meetings.meetingCollection.getByKey, 'locusUrl', url1);
1536
+ assert.calledOnce(locusInfo.syncAllHashTreeDatasets);
1439
1537
  });
1440
1538
  });
1441
1539
  describe('when meeting is not returned', () => {
@@ -1474,7 +1572,7 @@ describe('plugin-meetings', () => {
1474
1572
  url: url1,
1475
1573
  },
1476
1574
  hashTreeMessage: undefined,
1477
- });
1575
+ }, sinon.match.func);
1478
1576
  });
1479
1577
  });
1480
1578
  describe('when destroying meeting is needed', () => {
@@ -1520,7 +1618,7 @@ describe('plugin-meetings', () => {
1520
1618
  it('destroy any meeting that has no active locus url if keepOnlyLocusMeetings is not defined', async () => {
1521
1619
  await webex.meetings.syncMeetings();
1522
1620
  assert.calledOnce(webex.meetings.request.getActiveMeetings);
1523
- assert.calledOnce(webex.meetings.meetingCollection.getAll);
1621
+ assert.calledTwice(webex.meetings.meetingCollection.getAll);
1524
1622
  assert.calledWith(destroySpy, meetingCollectionMeetings.noLongerValidLocusMeeting);
1525
1623
  assert.calledWith(destroySpy, meetingCollectionMeetings.otherNonLocusMeeting1);
1526
1624
  assert.calledWith(destroySpy, meetingCollectionMeetings.otherNonLocusMeeting2);
@@ -1532,7 +1630,7 @@ describe('plugin-meetings', () => {
1532
1630
  it('destroy any meeting that has no active locus url if keepOnlyLocusMeetings === true', async () => {
1533
1631
  await webex.meetings.syncMeetings({keepOnlyLocusMeetings: true});
1534
1632
  assert.calledOnce(webex.meetings.request.getActiveMeetings);
1535
- assert.calledOnce(webex.meetings.meetingCollection.getAll);
1633
+ assert.calledTwice(webex.meetings.meetingCollection.getAll);
1536
1634
  assert.calledWith(destroySpy, meetingCollectionMeetings.noLongerValidLocusMeeting);
1537
1635
  assert.calledWith(destroySpy, meetingCollectionMeetings.otherNonLocusMeeting1);
1538
1636
  assert.calledWith(destroySpy, meetingCollectionMeetings.otherNonLocusMeeting2);
@@ -1544,7 +1642,7 @@ describe('plugin-meetings', () => {
1544
1642
  it('destroy any LOCUS meetings that have no active locus url if keepOnlyLocusMeetings === false', async () => {
1545
1643
  await webex.meetings.syncMeetings({keepOnlyLocusMeetings: false});
1546
1644
  assert.calledOnce(webex.meetings.request.getActiveMeetings);
1547
- assert.calledOnce(webex.meetings.meetingCollection.getAll);
1645
+ assert.calledTwice(webex.meetings.meetingCollection.getAll);
1548
1646
  assert.calledWith(destroySpy, meetingCollectionMeetings.noLongerValidLocusMeeting);
1549
1647
  assert.callCount(destroySpy, 1);
1550
1648
 
@@ -1552,6 +1650,147 @@ describe('plugin-meetings', () => {
1552
1650
  });
1553
1651
  });
1554
1652
  });
1653
+
1654
+ describe('when globalMeetingId preserves breakout meetings', () => {
1655
+ let destroySpy;
1656
+ let cleanUpSpy;
1657
+
1658
+ beforeEach(() => {
1659
+ destroySpy = sinon.spy(webex.meetings, 'destroy');
1660
+ cleanUpSpy = sinon.stub(MeetingUtil, 'cleanUp').returns(Promise.resolve());
1661
+ });
1662
+
1663
+ afterEach(() => {
1664
+ cleanUpSpy.restore();
1665
+ });
1666
+
1667
+ it('should not destroy a meeting whose globalMeetingId matches an active locus', async () => {
1668
+ const meetingCollectionMeetings = {
1669
+ breakoutMeeting: {
1670
+ locusUrl: 'breakout-url',
1671
+ locusInfo: {
1672
+ info: {globalMeetingId: 'gmid-123'},
1673
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1674
+ },
1675
+ sendCallAnalyzerMetrics: sinon.stub(),
1676
+ },
1677
+ };
1678
+
1679
+ webex.meetings.meetingCollection.getAll = sinon
1680
+ .stub()
1681
+ .returns(meetingCollectionMeetings);
1682
+ webex.meetings.request.getActiveMeetings = sinon.stub().resolves({
1683
+ loci: [{url: 'main-url', info: {globalMeetingId: 'gmid-123'}}],
1684
+ });
1685
+
1686
+ await webex.meetings.syncMeetings();
1687
+
1688
+ assert.notCalled(destroySpy);
1689
+ });
1690
+
1691
+ it('should destroy a meeting whose globalMeetingId does NOT match any active locus', async () => {
1692
+ const meetingCollectionMeetings = {
1693
+ breakoutMeeting: {
1694
+ locusUrl: 'breakout-url',
1695
+ locusInfo: {
1696
+ info: {globalMeetingId: 'gmid-other'},
1697
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1698
+ },
1699
+ sendCallAnalyzerMetrics: sinon.stub(),
1700
+ },
1701
+ };
1702
+
1703
+ webex.meetings.meetingCollection.getAll = sinon
1704
+ .stub()
1705
+ .returns(meetingCollectionMeetings);
1706
+ webex.meetings.request.getActiveMeetings = sinon.stub().resolves({
1707
+ loci: [{url: 'main-url', info: {globalMeetingId: 'gmid-123'}}],
1708
+ });
1709
+
1710
+ await webex.meetings.syncMeetings();
1711
+
1712
+ assert.calledOnce(destroySpy);
1713
+ assert.calledWith(destroySpy, meetingCollectionMeetings.breakoutMeeting);
1714
+ });
1715
+ });
1716
+
1717
+ describe('skipHashTreeSync parameter', () => {
1718
+ it('should skip syncAllHashTreeDatasets when skipHashTreeSync is true', async () => {
1719
+ const mockLocusInfo = {
1720
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1721
+ };
1722
+
1723
+ webex.meetings.request.getActiveMeetings = sinon.stub().resolves({loci: []});
1724
+ webex.meetings.meetingCollection.getAll = sinon.stub().returns({
1725
+ meeting1: {locusInfo: mockLocusInfo},
1726
+ });
1727
+
1728
+ await webex.meetings.syncMeetings({keepOnlyLocusMeetings: false, skipHashTreeSync: true});
1729
+
1730
+ assert.calledOnce(webex.meetings.request.getActiveMeetings);
1731
+ assert.notCalled(mockLocusInfo.syncAllHashTreeDatasets);
1732
+ });
1733
+
1734
+ it('should call syncAllHashTreeDatasets when skipHashTreeSync is false (default)', async () => {
1735
+ const mockLocusInfo = {
1736
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1737
+ };
1738
+
1739
+ webex.meetings.request.getActiveMeetings = sinon.stub().resolves({loci: []});
1740
+ webex.meetings.meetingCollection.getAll = sinon.stub().returns({
1741
+ meeting1: {locusInfo: mockLocusInfo},
1742
+ });
1743
+
1744
+ await webex.meetings.syncMeetings({keepOnlyLocusMeetings: false, skipHashTreeSync: false});
1745
+
1746
+ assert.calledOnce(webex.meetings.request.getActiveMeetings);
1747
+ assert.calledOnce(mockLocusInfo.syncAllHashTreeDatasets);
1748
+ });
1749
+ });
1750
+
1751
+ describe('syncAllHashTreeDatasets in syncMeetings', () => {
1752
+ it('should call syncAllHashTreeDatasets for multiple meetings, skipping those without locusInfo', async () => {
1753
+ const mockLocusInfo1 = {
1754
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1755
+ };
1756
+ const mockLocusInfo2 = {
1757
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1758
+ };
1759
+
1760
+ webex.meetings.request.getActiveMeetings = sinon.stub().resolves({loci: []});
1761
+ webex.meetings.meetingCollection.getAll = sinon.stub().returns({
1762
+ meeting1: {locusInfo: mockLocusInfo1},
1763
+ meeting2: {locusInfo: undefined},
1764
+ meeting3: {locusInfo: mockLocusInfo2},
1765
+ meeting4: {},
1766
+ });
1767
+
1768
+ await webex.meetings.syncMeetings({keepOnlyLocusMeetings: false});
1769
+
1770
+ assert.calledOnce(mockLocusInfo1.syncAllHashTreeDatasets);
1771
+ assert.calledOnce(mockLocusInfo2.syncAllHashTreeDatasets);
1772
+ });
1773
+
1774
+ it('should not call syncAllHashTreeDatasets when getActiveMeetings throws an error', async () => {
1775
+ const mockLocusInfo = {
1776
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1777
+ };
1778
+
1779
+ webex.meetings.request.getActiveMeetings = sinon.stub().rejects(new Error('network error'));
1780
+ webex.meetings.meetingCollection.getAll = sinon.stub().returns({
1781
+ meeting1: {locusInfo: mockLocusInfo},
1782
+ });
1783
+
1784
+ try {
1785
+ await webex.meetings.syncMeetings();
1786
+ assert.fail('should have thrown');
1787
+ } catch (err) {
1788
+ assert.equal(err.message, 'network error');
1789
+ }
1790
+
1791
+ assert.notCalled(mockLocusInfo.syncAllHashTreeDatasets);
1792
+ });
1793
+ });
1555
1794
  });
1556
1795
  describe('#fetchStaticMeetingLink', () => {
1557
1796
  const conversationUrl = 'conv.fakeconversationurl.com';
@@ -2015,7 +2254,7 @@ describe('plugin-meetings', () => {
2015
2254
  },
2016
2255
  },
2017
2256
  hashTreeMessage: undefined,
2018
- });
2257
+ }, sinon.match.func);
2019
2258
  });
2020
2259
  it('should setup the meeting from a hash tree event', async () => {
2021
2260
  const selfData = {};
@@ -2049,7 +2288,7 @@ describe('plugin-meetings', () => {
2049
2288
  info: infoData,
2050
2289
  },
2051
2290
  hashTreeMessage,
2052
- });
2291
+ }, sinon.match.func);
2053
2292
  });
2054
2293
 
2055
2294
  it('should ignore hash tree event when created locus has INACTIVE fullState', async () => {
@@ -2129,7 +2368,7 @@ describe('plugin-meetings', () => {
2129
2368
  },
2130
2369
  },
2131
2370
  hashTreeMessage: undefined,
2132
- });
2371
+ }, sinon.match.func);
2133
2372
  });
2134
2373
 
2135
2374
  it('sends client event correctly on finally', async () => {
@@ -2205,7 +2444,7 @@ describe('plugin-meetings', () => {
2205
2444
  },
2206
2445
  },
2207
2446
  hashTreeMessage: undefined,
2208
- });
2447
+ }, sinon.match.func);
2209
2448
  });
2210
2449
 
2211
2450
  const generateFakeLocusData = (isUnifiedSpaceMeeting) => ({
@@ -2833,6 +3072,39 @@ describe('plugin-meetings', () => {
2833
3072
  checkCreateMeetingWithNoMeetingInfo(true, true);
2834
3073
  });
2835
3074
 
3075
+ it('does not emit meeting:added when meeting is destroyed due to missing meeting info', async () => {
3076
+ // Make destroy actually remove the meeting from the collection
3077
+ // so that getMeetingByType returns null in the finally block
3078
+ webex.meetings.destroy = sinon.stub().callsFake((meeting) => {
3079
+ webex.meetings.meetingCollection.delete(meeting.id);
3080
+ });
3081
+
3082
+ try {
3083
+ await webex.meetings.createMeeting(
3084
+ 'test destination',
3085
+ 'test type',
3086
+ undefined,
3087
+ undefined,
3088
+ undefined,
3089
+ true
3090
+ );
3091
+ assert.fail('should have thrown NoMeetingInfoError');
3092
+ } catch (err) {
3093
+ assert.instanceOf(err, NoMeetingInfoError);
3094
+ }
3095
+
3096
+ assert.calledOnce(webex.meetings.destroy);
3097
+
3098
+ // meeting:added should NOT have been triggered since the meeting was destroyed
3099
+ assert.neverCalledWith(
3100
+ TriggerProxy.trigger,
3101
+ sinon.match.any,
3102
+ sinon.match({function: 'createMeeting'}),
3103
+ 'meeting:added',
3104
+ sinon.match.any
3105
+ );
3106
+ });
3107
+
2836
3108
  it('creates the meeting avoiding meeting info fetch by passing type as DESTINATION_TYPE.ONE_ON_ONE_CALL', async () => {
2837
3109
  const meeting = await webex.meetings.createMeeting(
2838
3110
  'test destination',
@@ -3426,6 +3698,21 @@ describe('plugin-meetings', () => {
3426
3698
  'Meetings:index#isNeedHandleMainLocus --> self device left&moved in main locus with self joined status, not need to handle'
3427
3699
  );
3428
3700
  });
3701
+
3702
+ it('check breakout ended with self removed, return false', () => {
3703
+ webex.meetings.meetingCollection.getActiveBreakoutLocus = sinon.stub().returns(null);
3704
+ newLocus.self.state = 'LEFT';
3705
+ newLocus.self.reason = 'OTHER';
3706
+ newLocus.self.removed = true;
3707
+ newLocus.fullState = {state: 'INACTIVE', endMeetingReason: 'BREAKOUT_ENDED'};
3708
+ LoggerProxy.logger.log = sinon.stub();
3709
+ const result = webex.meetings.isNeedHandleMainLocus(meeting, newLocus);
3710
+ assert.equal(result, false);
3711
+ assert.calledWith(
3712
+ LoggerProxy.logger.log,
3713
+ 'Meetings:index#isNeedHandleMainLocus --> self moved main locus with self removed status or with device resource moved, not need to handle'
3714
+ );
3715
+ });
3429
3716
  });
3430
3717
 
3431
3718
  describe('#isNeedHandleLocusDTO', () => {
@@ -3486,6 +3773,18 @@ describe('plugin-meetings', () => {
3486
3773
  const result = webex.meetings.isNeedHandleLocusDTO(meeting, newLocus);
3487
3774
  assert.equal(result, false);
3488
3775
  });
3776
+ it('breakout session with breakout ended, return false', () => {
3777
+ newLocus.controls.breakout = {
3778
+ sessionType: 'BREAKOUT',
3779
+ };
3780
+ newLocus.self.state = 'LEFT';
3781
+ newLocus.self.reason = 'OTHER';
3782
+ newLocus.self.devices = [];
3783
+ newLocus.fullState = {state: 'INACTIVE', endMeetingReason: 'BREAKOUT_ENDED'};
3784
+ LoggerProxy.logger.log = sinon.stub();
3785
+ const result = webex.meetings.isNeedHandleLocusDTO(meeting, newLocus);
3786
+ assert.equal(result, false);
3787
+ });
3489
3788
  it('moved to lobby, return true', () => {
3490
3789
  newLocus.controls.breakout = {
3491
3790
  sessionType: 'MAIN',