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

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 +593 -358
  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 +277 -86
  33. package/dist/locus-info/index.js.map +1 -1
  34. package/dist/locus-info/types.js +16 -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 +199 -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 +61 -15
  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 +17 -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 +18 -1
  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 +306 -160
  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 +276 -93
  101. package/src/locus-info/types.ts +19 -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 +104 -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 +1294 -191
  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 +443 -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 +275 -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,113 @@ 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('syncAllHashTreeDatasets in syncMeetings', () => {
1718
+ it('should call syncAllHashTreeDatasets for multiple meetings, skipping those without locusInfo', async () => {
1719
+ const mockLocusInfo1 = {
1720
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1721
+ };
1722
+ const mockLocusInfo2 = {
1723
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1724
+ };
1725
+
1726
+ webex.meetings.request.getActiveMeetings = sinon.stub().resolves({loci: []});
1727
+ webex.meetings.meetingCollection.getAll = sinon.stub().returns({
1728
+ meeting1: {locusInfo: mockLocusInfo1},
1729
+ meeting2: {locusInfo: undefined},
1730
+ meeting3: {locusInfo: mockLocusInfo2},
1731
+ meeting4: {},
1732
+ });
1733
+
1734
+ await webex.meetings.syncMeetings({keepOnlyLocusMeetings: false});
1735
+
1736
+ assert.calledOnce(mockLocusInfo1.syncAllHashTreeDatasets);
1737
+ assert.calledOnce(mockLocusInfo2.syncAllHashTreeDatasets);
1738
+ });
1739
+
1740
+ it('should not call syncAllHashTreeDatasets when getActiveMeetings throws an error', async () => {
1741
+ const mockLocusInfo = {
1742
+ syncAllHashTreeDatasets: sinon.stub().resolves(),
1743
+ };
1744
+
1745
+ webex.meetings.request.getActiveMeetings = sinon.stub().rejects(new Error('network error'));
1746
+ webex.meetings.meetingCollection.getAll = sinon.stub().returns({
1747
+ meeting1: {locusInfo: mockLocusInfo},
1748
+ });
1749
+
1750
+ try {
1751
+ await webex.meetings.syncMeetings();
1752
+ assert.fail('should have thrown');
1753
+ } catch (err) {
1754
+ assert.equal(err.message, 'network error');
1755
+ }
1756
+
1757
+ assert.notCalled(mockLocusInfo.syncAllHashTreeDatasets);
1758
+ });
1759
+ });
1555
1760
  });
1556
1761
  describe('#fetchStaticMeetingLink', () => {
1557
1762
  const conversationUrl = 'conv.fakeconversationurl.com';
@@ -2015,7 +2220,7 @@ describe('plugin-meetings', () => {
2015
2220
  },
2016
2221
  },
2017
2222
  hashTreeMessage: undefined,
2018
- });
2223
+ }, sinon.match.func);
2019
2224
  });
2020
2225
  it('should setup the meeting from a hash tree event', async () => {
2021
2226
  const selfData = {};
@@ -2049,7 +2254,7 @@ describe('plugin-meetings', () => {
2049
2254
  info: infoData,
2050
2255
  },
2051
2256
  hashTreeMessage,
2052
- });
2257
+ }, sinon.match.func);
2053
2258
  });
2054
2259
 
2055
2260
  it('should ignore hash tree event when created locus has INACTIVE fullState', async () => {
@@ -2129,7 +2334,7 @@ describe('plugin-meetings', () => {
2129
2334
  },
2130
2335
  },
2131
2336
  hashTreeMessage: undefined,
2132
- });
2337
+ }, sinon.match.func);
2133
2338
  });
2134
2339
 
2135
2340
  it('sends client event correctly on finally', async () => {
@@ -2205,7 +2410,7 @@ describe('plugin-meetings', () => {
2205
2410
  },
2206
2411
  },
2207
2412
  hashTreeMessage: undefined,
2208
- });
2413
+ }, sinon.match.func);
2209
2414
  });
2210
2415
 
2211
2416
  const generateFakeLocusData = (isUnifiedSpaceMeeting) => ({
@@ -2833,6 +3038,39 @@ describe('plugin-meetings', () => {
2833
3038
  checkCreateMeetingWithNoMeetingInfo(true, true);
2834
3039
  });
2835
3040
 
3041
+ it('does not emit meeting:added when meeting is destroyed due to missing meeting info', async () => {
3042
+ // Make destroy actually remove the meeting from the collection
3043
+ // so that getMeetingByType returns null in the finally block
3044
+ webex.meetings.destroy = sinon.stub().callsFake((meeting) => {
3045
+ webex.meetings.meetingCollection.delete(meeting.id);
3046
+ });
3047
+
3048
+ try {
3049
+ await webex.meetings.createMeeting(
3050
+ 'test destination',
3051
+ 'test type',
3052
+ undefined,
3053
+ undefined,
3054
+ undefined,
3055
+ true
3056
+ );
3057
+ assert.fail('should have thrown NoMeetingInfoError');
3058
+ } catch (err) {
3059
+ assert.instanceOf(err, NoMeetingInfoError);
3060
+ }
3061
+
3062
+ assert.calledOnce(webex.meetings.destroy);
3063
+
3064
+ // meeting:added should NOT have been triggered since the meeting was destroyed
3065
+ assert.neverCalledWith(
3066
+ TriggerProxy.trigger,
3067
+ sinon.match.any,
3068
+ sinon.match({function: 'createMeeting'}),
3069
+ 'meeting:added',
3070
+ sinon.match.any
3071
+ );
3072
+ });
3073
+
2836
3074
  it('creates the meeting avoiding meeting info fetch by passing type as DESTINATION_TYPE.ONE_ON_ONE_CALL', async () => {
2837
3075
  const meeting = await webex.meetings.createMeeting(
2838
3076
  'test destination',
@@ -3426,6 +3664,21 @@ describe('plugin-meetings', () => {
3426
3664
  'Meetings:index#isNeedHandleMainLocus --> self device left&moved in main locus with self joined status, not need to handle'
3427
3665
  );
3428
3666
  });
3667
+
3668
+ it('check breakout ended with self removed, return false', () => {
3669
+ webex.meetings.meetingCollection.getActiveBreakoutLocus = sinon.stub().returns(null);
3670
+ newLocus.self.state = 'LEFT';
3671
+ newLocus.self.reason = 'OTHER';
3672
+ newLocus.self.removed = true;
3673
+ newLocus.fullState = {state: 'INACTIVE', endMeetingReason: 'BREAKOUT_ENDED'};
3674
+ LoggerProxy.logger.log = sinon.stub();
3675
+ const result = webex.meetings.isNeedHandleMainLocus(meeting, newLocus);
3676
+ assert.equal(result, false);
3677
+ assert.calledWith(
3678
+ LoggerProxy.logger.log,
3679
+ 'Meetings:index#isNeedHandleMainLocus --> self moved main locus with self removed status or with device resource moved, not need to handle'
3680
+ );
3681
+ });
3429
3682
  });
3430
3683
 
3431
3684
  describe('#isNeedHandleLocusDTO', () => {
@@ -3486,6 +3739,18 @@ describe('plugin-meetings', () => {
3486
3739
  const result = webex.meetings.isNeedHandleLocusDTO(meeting, newLocus);
3487
3740
  assert.equal(result, false);
3488
3741
  });
3742
+ it('breakout session with breakout ended, return false', () => {
3743
+ newLocus.controls.breakout = {
3744
+ sessionType: 'BREAKOUT',
3745
+ };
3746
+ newLocus.self.state = 'LEFT';
3747
+ newLocus.self.reason = 'OTHER';
3748
+ newLocus.self.devices = [];
3749
+ newLocus.fullState = {state: 'INACTIVE', endMeetingReason: 'BREAKOUT_ENDED'};
3750
+ LoggerProxy.logger.log = sinon.stub();
3751
+ const result = webex.meetings.isNeedHandleLocusDTO(meeting, newLocus);
3752
+ assert.equal(result, false);
3753
+ });
3489
3754
  it('moved to lobby, return true', () => {
3490
3755
  newLocus.controls.breakout = {
3491
3756
  sessionType: 'MAIN',
@@ -0,0 +1,141 @@
1
+ import 'jsdom-global/register';
2
+ import sinon from 'sinon';
3
+ import {assert} from '@webex/test-helper-chai';
4
+ import MockWebex from '@webex/test-helper-mock-webex';
5
+ import Meetings from '@webex/plugin-meetings';
6
+ import ParameterError from '@webex/plugin-meetings/src/common/errors/parameter';
7
+ import MeetingRequest from '@webex/plugin-meetings/src/meetings/request';
8
+ import {SitePreferenceSelectOption} from '@webex/plugin-meetings/src/meetings/meetings.types';
9
+
10
+ const multipartSitePrefixList = ['.my.', '.mydmz.', '.mybts.', '.mydev.', '.myats2.', '.myats.'];
11
+
12
+ describe('plugin-meetings/meetings/request', () => {
13
+ let meetingRequest;
14
+ let request;
15
+
16
+ beforeEach(() => {
17
+ const webex = new MockWebex({
18
+ children: {
19
+ meetings: Meetings,
20
+ },
21
+ });
22
+
23
+ request = sinon.stub().resolves({
24
+ body: {
25
+ scheduling: {
26
+ supportScheduleWebinar: true,
27
+ webinarWebLink: 'https://go.webex.com/webappng/sites/go/webinar/scheduler',
28
+ },
29
+ },
30
+ });
31
+
32
+ meetingRequest = new MeetingRequest(
33
+ {},
34
+ {
35
+ parent: webex,
36
+ }
37
+ );
38
+ meetingRequest.request = request;
39
+ meetingRequest.config.meetings.multipartSitePrefixList = multipartSitePrefixList;
40
+ });
41
+
42
+ afterEach(() => {
43
+ sinon.restore();
44
+ });
45
+
46
+ describe('#fetchSitePreferencesMeViaSite', () => {
47
+ const assertRequest = (expectedOptions) => {
48
+ assert.calledOnceWithExactly(request, expectedOptions);
49
+ };
50
+
51
+ it('throws a parameter error when no Webex site is available', () => {
52
+ assert.throws(
53
+ () => meetingRequest.fetchSitePreferencesMeViaSite(),
54
+ ParameterError,
55
+ 'No siteUrl available. Call register() before fetching site preferences or provide options.siteUrl.'
56
+ );
57
+ assert.notCalled(request);
58
+ });
59
+
60
+ it('fetches scheduling preferences by default', async () => {
61
+ const result = await meetingRequest.fetchSitePreferencesMeViaSite({siteUrl: 'go.webex.com'});
62
+
63
+ assert.deepEqual(result, {
64
+ scheduling: {
65
+ supportScheduleWebinar: true,
66
+ webinarWebLink: 'https://go.webex.com/webappng/sites/go/webinar/scheduler',
67
+ },
68
+ });
69
+ assertRequest({
70
+ method: 'GET',
71
+ uri: 'https://go.webex.com/wbxappapi/v1/users/me/preference?select=scheduling&siteurl=go',
72
+ });
73
+ });
74
+
75
+ it('derives the site name for my.webex.com sites', async () => {
76
+ await meetingRequest.fetchSitePreferencesMeViaSite({siteUrl: 'go.my.webex.com'});
77
+
78
+ assertRequest({
79
+ method: 'GET',
80
+ uri: 'https://go.my.webex.com/wbxappapi/v1/users/me/preference?select=scheduling&siteurl=go.my',
81
+ });
82
+ });
83
+
84
+ it('uses the configured multipart site prefix list to derive the site name', async () => {
85
+ meetingRequest.config.meetings.multipartSitePrefixList = ['.custom.'];
86
+
87
+ await meetingRequest.fetchSitePreferencesMeViaSite({siteUrl: 'go.my.webex.com'});
88
+
89
+ assertRequest({
90
+ method: 'GET',
91
+ uri: 'https://go.my.webex.com/wbxappapi/v1/users/me/preference?select=scheduling&siteurl=go',
92
+ });
93
+ });
94
+
95
+ it('falls back to the first label when no multipart site prefix list is configured', async () => {
96
+ delete meetingRequest.config.meetings.multipartSitePrefixList;
97
+
98
+ await meetingRequest.fetchSitePreferencesMeViaSite({siteUrl: 'go.my.webex.com'});
99
+
100
+ assertRequest({
101
+ method: 'GET',
102
+ uri: 'https://go.my.webex.com/wbxappapi/v1/users/me/preference?select=scheduling&siteurl=go',
103
+ });
104
+ });
105
+
106
+ it('supports custom site name overrides', async () => {
107
+ await meetingRequest.fetchSitePreferencesMeViaSite({
108
+ siteUrl: 'go.my.webex.com',
109
+ siteName: 'custom-site',
110
+ });
111
+
112
+ assertRequest({
113
+ method: 'GET',
114
+ uri: 'https://go.my.webex.com/wbxappapi/v1/users/me/preference?select=scheduling&siteurl=custom-site',
115
+ });
116
+ });
117
+
118
+ it('supports enum-backed preference sections', async () => {
119
+ await meetingRequest.fetchSitePreferencesMeViaSite({
120
+ siteUrl: 'go.webex.com',
121
+ selectOptions: [SitePreferenceSelectOption.SCHEDULING],
122
+ });
123
+
124
+ assertRequest({
125
+ method: 'GET',
126
+ uri: 'https://go.webex.com/wbxappapi/v1/users/me/preference?select=scheduling&siteurl=go',
127
+ });
128
+ });
129
+
130
+ it('does not suppress request errors', async () => {
131
+ const error = new Error('site preferences failed');
132
+
133
+ request.rejects(error);
134
+
135
+ await assert.isRejected(
136
+ meetingRequest.fetchSitePreferencesMeViaSite({siteUrl: 'go.webex.com'}),
137
+ 'site preferences failed'
138
+ );
139
+ });
140
+ });
141
+ });