@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
@@ -13,6 +13,7 @@ import MediaSharesUtils from '@webex/plugin-meetings/src/locus-info//mediaShares
13
13
  import LocusDeltaParser from '@webex/plugin-meetings/src/locus-info/parser';
14
14
  import Metrics from '@webex/plugin-meetings/src/metrics';
15
15
  import * as HashTreeParserModule from '@webex/plugin-meetings/src/hashTree/hashTreeParser';
16
+ import MeetingsUtil from '@webex/plugin-meetings/src/meetings/util';
16
17
 
17
18
  import {
18
19
  LOCUSINFO,
@@ -220,6 +221,47 @@ describe('plugin-meetings', () => {
220
221
  assert.isTrue(locusInfo.emitChange);
221
222
  });
222
223
 
224
+ it('calls onLocusSynced callback passed as second argument with full locus from join response', async () => {
225
+ const syncedLocus = {url: 'http://locus-url.com', participants: []};
226
+ const onLocusSynced = sinon.stub();
227
+
228
+ await locusInfo.initialSetup(
229
+ {
230
+ trigger: 'join-response',
231
+ locus: syncedLocus,
232
+ },
233
+ onLocusSynced
234
+ );
235
+
236
+ assert.calledOnceWithExactly(onLocusSynced, syncedLocus);
237
+ });
238
+
239
+ it('swallows onLocusSynced callback errors and logs warn', async () => {
240
+ const syncedLocus = {url: 'http://locus-url.com', participants: []};
241
+ const callbackError = new Error('onLocusSynced failed');
242
+ const onLocusSynced = sinon.stub().throws(callbackError);
243
+ const loggerWarnStub = LoggerProxy.logger.warn?.isSinonProxy
244
+ ? LoggerProxy.logger.warn
245
+ : sinon.stub(LoggerProxy.logger, 'warn');
246
+
247
+ loggerWarnStub.resetHistory();
248
+
249
+ await locusInfo.initialSetup(
250
+ {
251
+ trigger: 'join-response',
252
+ locus: syncedLocus,
253
+ },
254
+ onLocusSynced
255
+ );
256
+
257
+ assert.calledOnceWithExactly(onLocusSynced, syncedLocus);
258
+ assert.calledOnce(loggerWarnStub);
259
+ assert.match(
260
+ loggerWarnStub.firstCall.args[0],
261
+ /Locus-info:index#initialSetup --> onLocusSynced callback failed/
262
+ );
263
+ });
264
+
223
265
  it('should initialize the hash tree parser correctly when triggered from a get loci response containing visible datasets', async () => {
224
266
  const visibleDataSets = ['dataset1', 'dataset2'];
225
267
  const locus = createLocusWithVisibleDataSets(visibleDataSets);
@@ -413,7 +455,7 @@ describe('plugin-meetings', () => {
413
455
  };
414
456
 
415
457
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
416
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
458
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
417
459
  updatedObjects: [{htMeta: {elementId: {type: 'self'}}, data: newSelf}],
418
460
  });
419
461
 
@@ -440,7 +482,7 @@ describe('plugin-meetings', () => {
440
482
  locusInfo.info.isWebinar = true;
441
483
 
442
484
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
443
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
485
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
444
486
  updatedObjects: [{htMeta: {elementId: {type: 'self'}}, data: newSelf}],
445
487
  });
446
488
 
@@ -473,7 +515,7 @@ describe('plugin-meetings', () => {
473
515
  locusInfo.info.isWebinar = true;
474
516
 
475
517
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
476
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
518
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
477
519
  updatedObjects: [{htMeta: {elementId: {type: 'self'}}, data: newSelf}],
478
520
  });
479
521
 
@@ -501,7 +543,7 @@ describe('plugin-meetings', () => {
501
543
  };
502
544
 
503
545
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
504
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
546
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
505
547
  updatedObjects: [{htMeta: {elementId: {type: 'fullState'}}, data: newFullState}],
506
548
  });
507
549
 
@@ -519,7 +561,7 @@ describe('plugin-meetings', () => {
519
561
  };
520
562
 
521
563
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
522
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
564
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
523
565
  updatedObjects: [{htMeta: {elementId: {type: 'info'}}, data: newInfo}],
524
566
  });
525
567
 
@@ -537,7 +579,7 @@ describe('plugin-meetings', () => {
537
579
  };
538
580
 
539
581
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
540
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
582
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
541
583
  updatedObjects: [{htMeta: {elementId: {type: 'links'}}, data: newLinks}],
542
584
  });
543
585
 
@@ -557,7 +599,7 @@ describe('plugin-meetings', () => {
557
599
  };
558
600
 
559
601
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
560
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
602
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
561
603
  updatedObjects: [{htMeta: newLocusHtMeta, data: newLocus}],
562
604
  });
563
605
 
@@ -590,7 +632,7 @@ describe('plugin-meetings', () => {
590
632
  };
591
633
 
592
634
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
593
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
635
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
594
636
  updatedObjects: [
595
637
  {
596
638
  htMeta: newLocusHtMeta,
@@ -637,7 +679,7 @@ describe('plugin-meetings', () => {
637
679
  };
638
680
 
639
681
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
640
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
682
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
641
683
  updatedObjects: [
642
684
  // first, a removal of LOCUS object
643
685
  {htMeta: {elementId: {type: 'locus'}}, data: null},
@@ -671,7 +713,7 @@ describe('plugin-meetings', () => {
671
713
  const newLocusHtMeta = {elementId: {type: 'locus', version: 99}};
672
714
 
673
715
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
674
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
716
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
675
717
  updatedObjects: [
676
718
  // first, an update
677
719
  {htMeta: newLocusHtMeta, data: newLocus},
@@ -700,7 +742,7 @@ describe('plugin-meetings', () => {
700
742
  };
701
743
 
702
744
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
703
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
745
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
704
746
  updatedObjects: [
705
747
  // first, an update
706
748
  {htMeta: {elementId: {type: 'locus'}}, data: newLocus1},
@@ -730,7 +772,7 @@ describe('plugin-meetings', () => {
730
772
  };
731
773
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
732
774
  // with 1 participant added, 1 updated, and 1 removed
733
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
775
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
734
776
  updatedObjects: [
735
777
  {htMeta: {elementId: {type: 'participant', id: 'fake-ht-participant-1'}}, data: null},
736
778
  {
@@ -774,7 +816,7 @@ describe('plugin-meetings', () => {
774
816
  };
775
817
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
776
818
  // with 1 participant added, 1 updated, and 1 removed
777
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
819
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
778
820
  updatedObjects: [
779
821
  {htMeta: {elementId: {type: 'mediashare', id: 'fake-ht-mediaShare-1'}}, data: null},
780
822
  {
@@ -807,7 +849,7 @@ describe('plugin-meetings', () => {
807
849
  };
808
850
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
809
851
  // with 1 embedded app added, 1 updated, and 1 removed
810
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
852
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
811
853
  updatedObjects: [
812
854
  {htMeta: {elementId: {type: 'embeddedapp', id: 'fake-ht-embeddedApp-1'}}, data: null},
813
855
  {
@@ -844,7 +886,7 @@ describe('plugin-meetings', () => {
844
886
  };
845
887
 
846
888
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
847
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
889
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
848
890
  updatedObjects: [
849
891
  {
850
892
  htMeta: {elementId: {type: 'mediashare', id: 'fake-ht-mediaShare-2'}},
@@ -883,7 +925,7 @@ describe('plugin-meetings', () => {
883
925
  };
884
926
 
885
927
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
886
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
928
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
887
929
  updatedObjects: [
888
930
  {
889
931
  htMeta: {elementId: {type: 'controlentry', id: 'control-1'}},
@@ -911,7 +953,7 @@ describe('plugin-meetings', () => {
911
953
 
912
954
  it('should process locus update correctly when CONTROL object is received with no data', () => {
913
955
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
914
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
956
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
915
957
  updatedObjects: [
916
958
  {
917
959
  htMeta: {elementId: {type: 'controlentry', id: 'some-control-id'}},
@@ -935,7 +977,7 @@ describe('plugin-meetings', () => {
935
977
  const destroyStub = sinon.stub(locusInfo.webex.meetings, 'destroy');
936
978
 
937
979
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
938
- locusInfoUpdateCallback(MEETING_ENDED);
980
+ locusInfoUpdateCallback({updateType: MEETING_ENDED});
939
981
 
940
982
  assert.calledOnceWithExactly(collectionGetStub, locusInfo.meetingId);
941
983
  assert.calledOnceWithExactly(
@@ -953,7 +995,7 @@ describe('plugin-meetings', () => {
953
995
  const destroyStub = sinon.stub(locusInfo.webex.meetings, 'destroy');
954
996
 
955
997
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
956
- locusInfoUpdateCallback(MEETING_ENDED);
998
+ locusInfoUpdateCallback({updateType: MEETING_ENDED});
957
999
 
958
1000
  assert.calledOnceWithExactly(collectionGetStub, locusInfo.meetingId);
959
1001
  assert.notCalled(destroyStub);
@@ -963,7 +1005,7 @@ describe('plugin-meetings', () => {
963
1005
  const createdHashTreeParser = locusInfo.hashTreeParsers.get('fake-locus-url');
964
1006
  createdHashTreeParser.initializedFromHashTree = false;
965
1007
 
966
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
1008
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
967
1009
  updatedObjects: [
968
1010
  {
969
1011
  htMeta: {elementId: {type: 'self'}},
@@ -978,7 +1020,7 @@ describe('plugin-meetings', () => {
978
1020
  });
979
1021
 
980
1022
  it('should set forceReplaceMembers to false on subsequent updates (initializedFromHashTree is true)', () => {
981
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
1023
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
982
1024
  updatedObjects: [
983
1025
  {
984
1026
  htMeta: {elementId: {type: 'self'}},
@@ -994,7 +1036,7 @@ describe('plugin-meetings', () => {
994
1036
  it('should copy participant data to self when participant matches self identity and state is LEFT with reason MOVED', () => {
995
1037
  locusInfo.self = {id: 'fake-self', identity: 'user-123'};
996
1038
 
997
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
1039
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
998
1040
  updatedObjects: [
999
1041
  {
1000
1042
  htMeta: {elementId: {type: 'participant', id: 99}},
@@ -1289,6 +1331,8 @@ describe('plugin-meetings', () => {
1289
1331
  state: RECORDING_STATE.IDLE,
1290
1332
  modifiedBy: 'George Kittle',
1291
1333
  lastModified: 'TODAY',
1334
+ modifiedByServiceAppName: undefined,
1335
+ modifiedByServiceAppId: undefined,
1292
1336
  }
1293
1337
  );
1294
1338
  });
@@ -1323,6 +1367,8 @@ describe('plugin-meetings', () => {
1323
1367
  state: RECORDING_STATE.RECORDING,
1324
1368
  modifiedBy: 'George Kittle',
1325
1369
  lastModified: 'TODAY',
1370
+ modifiedByServiceAppName: undefined,
1371
+ modifiedByServiceAppId: undefined,
1326
1372
  }
1327
1373
  );
1328
1374
  });
@@ -1358,6 +1404,8 @@ describe('plugin-meetings', () => {
1358
1404
  state: RECORDING_STATE.PAUSED,
1359
1405
  modifiedBy: 'George Kittle',
1360
1406
  lastModified: 'TODAY',
1407
+ modifiedByServiceAppName: undefined,
1408
+ modifiedByServiceAppId: undefined,
1361
1409
  }
1362
1410
  );
1363
1411
  });
@@ -1394,6 +1442,8 @@ describe('plugin-meetings', () => {
1394
1442
  state: RECORDING_STATE.RESUMED,
1395
1443
  modifiedBy: 'George Kittle',
1396
1444
  lastModified: 'TODAY',
1445
+ modifiedByServiceAppName: undefined,
1446
+ modifiedByServiceAppId: undefined,
1397
1447
  }
1398
1448
  );
1399
1449
  });
@@ -1429,6 +1479,44 @@ describe('plugin-meetings', () => {
1429
1479
  state: RECORDING_STATE.IDLE,
1430
1480
  modifiedBy: 'George Kittle',
1431
1481
  lastModified: 'TODAY',
1482
+ modifiedByServiceAppName: undefined,
1483
+ modifiedByServiceAppId: undefined,
1484
+ }
1485
+ );
1486
+ });
1487
+
1488
+ it('should include service app fields in the recording event when present', () => {
1489
+ locusInfo.controls = {
1490
+ record: {
1491
+ recording: false,
1492
+ paused: false,
1493
+ meta: {
1494
+ lastModified: 'TODAY',
1495
+ modifiedBy: 'George Kittle',
1496
+ },
1497
+ },
1498
+ shareControl: {},
1499
+ transcribe: {},
1500
+ };
1501
+ newControls.record.recording = true;
1502
+ newControls.record.meta.modifiedByServiceAppName = 'My Bot';
1503
+ newControls.record.meta.modifiedByServiceAppId = 'app-id-123';
1504
+ locusInfo.emitScoped = sinon.stub();
1505
+ locusInfo.updateControls(newControls);
1506
+
1507
+ assert.calledWith(
1508
+ locusInfo.emitScoped,
1509
+ {
1510
+ file: 'locus-info',
1511
+ function: 'updateControls',
1512
+ },
1513
+ LOCUSINFO.EVENTS.CONTROLS_RECORDING_UPDATED,
1514
+ {
1515
+ state: RECORDING_STATE.RECORDING,
1516
+ modifiedBy: 'George Kittle',
1517
+ lastModified: 'TODAY',
1518
+ modifiedByServiceAppName: 'My Bot',
1519
+ modifiedByServiceAppId: 'app-id-123',
1432
1520
  }
1433
1521
  );
1434
1522
  });
@@ -2024,7 +2112,7 @@ describe('plugin-meetings', () => {
2024
2112
  function: 'updateSelf',
2025
2113
  },
2026
2114
  LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED,
2027
- {muted: true, unmuteAllowed: true}
2115
+ {muted: true, unmuteAllowed: true, modifiedBy: null}
2028
2116
  );
2029
2117
 
2030
2118
  // but sometimes "previous self" is defined, but without controls.audio.muted, so we test this here:
@@ -2039,7 +2127,7 @@ describe('plugin-meetings', () => {
2039
2127
  function: 'updateSelf',
2040
2128
  },
2041
2129
  LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED,
2042
- {muted: true, unmuteAllowed: true}
2130
+ {muted: true, unmuteAllowed: true, modifiedBy: null}
2043
2131
  );
2044
2132
  });
2045
2133
 
@@ -2098,7 +2186,7 @@ describe('plugin-meetings', () => {
2098
2186
  function: 'updateSelf',
2099
2187
  },
2100
2188
  LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED,
2101
- {muted: true, unmuteAllowed: true}
2189
+ {muted: true, unmuteAllowed: true, modifiedBy: null}
2102
2190
  );
2103
2191
  });
2104
2192
 
@@ -2237,7 +2325,7 @@ describe('plugin-meetings', () => {
2237
2325
  function: 'updateSelf',
2238
2326
  },
2239
2327
  LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED,
2240
- {muted: true, unmuteAllowed: false}
2328
+ {muted: true, unmuteAllowed: false, modifiedBy: null}
2241
2329
  );
2242
2330
 
2243
2331
  // now change only disallowUnmute
@@ -2255,7 +2343,28 @@ describe('plugin-meetings', () => {
2255
2343
  function: 'updateSelf',
2256
2344
  },
2257
2345
  LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED,
2258
- {muted: true, unmuteAllowed: true}
2346
+ {muted: true, unmuteAllowed: true, modifiedBy: null}
2347
+ );
2348
+ });
2349
+
2350
+ it('should include modifiedBy in payload when muted by host', () => {
2351
+ locusInfo.webex.internal.device.url = self.deviceUrl;
2352
+ locusInfo.updateSelf(self);
2353
+ const newSelf = cloneDeep(self);
2354
+ newSelf.controls.audio.muted = true;
2355
+ newSelf.controls.audio.meta = {modifiedBy: 'host-uuid-123'};
2356
+
2357
+ locusInfo.emitScoped = sinon.stub();
2358
+ locusInfo.updateSelf(newSelf);
2359
+
2360
+ assert.calledWith(
2361
+ locusInfo.emitScoped,
2362
+ {
2363
+ file: 'locus-info',
2364
+ function: 'updateSelf',
2365
+ },
2366
+ LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED,
2367
+ {muted: true, unmuteAllowed: true, modifiedBy: 'host-uuid-123'}
2259
2368
  );
2260
2369
  });
2261
2370
 
@@ -3154,13 +3263,14 @@ describe('plugin-meetings', () => {
3154
3263
  const createMockParser = (state = 'active') => ({
3155
3264
  state,
3156
3265
  stop: sinon.stub(),
3157
- resume: sinon.stub(),
3266
+ resumeFromMessage: sinon.stub(),
3158
3267
  handleMessage: sinon.stub(),
3159
3268
  });
3160
3269
 
3161
3270
  const createSelfElementWithReplaces = (replacedLocusUrl, replacedAt) => ({
3162
3271
  htMeta: {elementId: {type: 'Self'}},
3163
3272
  data: {
3273
+ deviceUrl,
3164
3274
  devices: [{url: deviceUrl, replaces: [{locusUrl: replacedLocusUrl, replacedAt}]}],
3165
3275
  },
3166
3276
  });
@@ -3236,7 +3346,7 @@ describe('plugin-meetings', () => {
3236
3346
  stateElementsMessage: message,
3237
3347
  });
3238
3348
 
3239
- assert.calledOnce(parserA.resume);
3349
+ assert.calledOnce(parserA.resumeFromMessage);
3240
3350
  assert.calledOnce(parserB.stop);
3241
3351
  });
3242
3352
 
@@ -3259,7 +3369,7 @@ describe('plugin-meetings', () => {
3259
3369
  stateElementsMessage: message,
3260
3370
  });
3261
3371
 
3262
- assert.notCalled(parserA.resume);
3372
+ assert.notCalled(parserA.resumeFromMessage);
3263
3373
  assert.notCalled(parserB.stop);
3264
3374
  });
3265
3375
 
@@ -3278,7 +3388,7 @@ describe('plugin-meetings', () => {
3278
3388
  stateElementsMessage: message,
3279
3389
  });
3280
3390
 
3281
- assert.notCalled(parserA.resume);
3391
+ assert.notCalled(parserA.resumeFromMessage);
3282
3392
  assert.notCalled(parserA.handleMessage);
3283
3393
  });
3284
3394
 
@@ -3362,6 +3472,51 @@ describe('plugin-meetings', () => {
3362
3472
 
3363
3473
  assert.calledOnceWithExactly(parserA.handleMessage, message);
3364
3474
  });
3475
+
3476
+ it('should send mismatch metric when eventType is not HASH_TREE_DATA_UPDATED', () => {
3477
+ const locusUrlA = 'http://locus-url-A.com';
3478
+ const parserA = {state: 'active', handleMessage: sinon.stub()};
3479
+ locusInfo.hashTreeParsers.set(locusUrlA, {parser: parserA, initializedFromHashTree: true});
3480
+
3481
+ locusInfo.parse(mockMeeting, {
3482
+ eventType: LOCUSEVENT.SELF_CHANGED,
3483
+ stateElementsMessage: {locusUrl: locusUrlA, locusStateElements: [], dataSets: []},
3484
+ });
3485
+
3486
+ assert.calledOnceWithExactly(
3487
+ sendBehavioralMetricStub,
3488
+ 'js_sdk_locus_classic_vs_hash_tree_mismatch',
3489
+ {
3490
+ correlationId: mockMeeting.correlationId,
3491
+ message: `got ${LOCUSEVENT.SELF_CHANGED}, expected ${LOCUSEVENT.HASH_TREE_DATA_UPDATED}`,
3492
+ }
3493
+ );
3494
+ assert.notCalled(parserA.handleMessage);
3495
+ });
3496
+ });
3497
+
3498
+ describe('#sendClassicVsHashTreeMismatchMetric', () => {
3499
+ it('should send the metric when called for the first time', () => {
3500
+ locusInfo.sendClassicVsHashTreeMismatchMetric(mockMeeting, 'some mismatch');
3501
+
3502
+ assert.calledOnceWithExactly(
3503
+ sendBehavioralMetricStub,
3504
+ 'js_sdk_locus_classic_vs_hash_tree_mismatch',
3505
+ {
3506
+ correlationId: mockMeeting.correlationId,
3507
+ message: 'some mismatch',
3508
+ }
3509
+ );
3510
+ });
3511
+
3512
+ it('should send the metric up to 5 times and stop after that', () => {
3513
+ for (let i = 0; i < 7; i += 1) {
3514
+ locusInfo.sendClassicVsHashTreeMismatchMetric(mockMeeting, `mismatch ${i}`);
3515
+ }
3516
+
3517
+ assert.callCount(sendBehavioralMetricStub, 5);
3518
+ assert.equal(locusInfo.classicVsHashTreeMismatchMetricCounter, 5);
3519
+ });
3365
3520
  });
3366
3521
 
3367
3522
  describe('#handleLocusAPIResponse', () => {
@@ -3417,19 +3572,174 @@ describe('plugin-meetings', () => {
3417
3572
  assert.calledOnceWithExactly(locusInfo.handleLocusDelta, fakeLocus, mockMeeting);
3418
3573
  });
3419
3574
 
3420
- it('should send mismatch metric when hash tree parser exists but dataSets are missing in wrapped response', () => {
3575
+ it('should send mismatch metric in classic mode when wrapped response has dataSets', () => {
3421
3576
  const fakeLocus = {url: 'http://locus-url.com'};
3422
- const mockHashTreeParser = {handleLocusUpdate: sinon.stub()};
3423
- locusInfo.hashTreeParsers.set(fakeLocus.url, {
3424
- parser: mockHashTreeParser,
3425
- initializedFromHashTree: true,
3577
+ sinon.stub(locusInfo, 'handleLocusDelta');
3578
+
3579
+ locusInfo.handleLocusAPIResponse(mockMeeting, {
3580
+ locus: fakeLocus,
3581
+ dataSets: [{name: 'dataset1', url: 'test-url'}],
3426
3582
  });
3427
- sinon.stub(locusInfo, 'sendClassicVsHashTreeMismatchMetric');
3428
3583
 
3429
- locusInfo.handleLocusAPIResponse(mockMeeting, {locus: fakeLocus});
3584
+ assert.calledOnceWithExactly(
3585
+ sendBehavioralMetricStub,
3586
+ 'js_sdk_locus_classic_vs_hash_tree_mismatch',
3587
+ {
3588
+ correlationId: mockMeeting.correlationId,
3589
+ message: 'unexpected hash tree dataSets in API response',
3590
+ }
3591
+ );
3592
+ assert.calledOnce(locusInfo.handleLocusDelta);
3593
+ });
3430
3594
 
3431
- assert.calledOnce(locusInfo.sendClassicVsHashTreeMismatchMetric);
3432
- assert.calledOnce(mockHashTreeParser.handleLocusUpdate);
3595
+ describe('parser switch via API response', () => {
3596
+ const deviceUrl = 'http://device-url.com';
3597
+ const locusUrlA = 'http://locus-url-A.com';
3598
+ const locusUrlB = 'http://locus-url-B.com';
3599
+
3600
+ let HashTreeParserStub;
3601
+
3602
+ const createMockApiParser = (state = 'active') => ({
3603
+ state,
3604
+ stop: sinon.stub(),
3605
+ resumeFromApiResponse: sinon.stub(),
3606
+ handleLocusUpdate: sinon.stub(),
3607
+ initializeFromGetLociResponse: sinon.stub(),
3608
+ });
3609
+
3610
+ const createLocusWithReplaces = (url, replacedLocusUrl, replacedAt) => ({
3611
+ url,
3612
+ self: {
3613
+ devices: [{url: deviceUrl, replaces: [{locusUrl: replacedLocusUrl, replacedAt}]}],
3614
+ },
3615
+ });
3616
+
3617
+ const createLocusWithoutReplaces = (url) => ({
3618
+ url,
3619
+ self: {devices: [{url: deviceUrl}]},
3620
+ });
3621
+
3622
+ beforeEach(() => {
3623
+ locusInfo.webex.internal.device.url = deviceUrl;
3624
+ HashTreeParserStub = sinon
3625
+ .stub(HashTreeParserModule, 'default')
3626
+ .returns(createMockApiParser());
3627
+ });
3628
+
3629
+ it('should create a new parser and initialize it when no entry exists for the locusUrl', () => {
3630
+ // existing parser for a different url so hashTreeParsers.size > 0
3631
+ locusInfo.hashTreeParsers.set(locusUrlA, {parser: createMockApiParser(), initializedFromHashTree: true});
3632
+
3633
+ const locus = createLocusWithReplaces(locusUrlB, locusUrlA, '2026-01-01T00:00:00Z');
3634
+ sinon.stub(locusInfo, 'handleLocusDelta');
3635
+
3636
+ locusInfo.handleLocusAPIResponse(mockMeeting, {locus});
3637
+
3638
+ assert.isTrue(locusInfo.hashTreeParsers.has(locusUrlB));
3639
+ const newEntry = locusInfo.hashTreeParsers.get(locusUrlB);
3640
+ assert.isFalse(newEntry.initializedFromHashTree);
3641
+
3642
+ // the stub returns the mock, so initializeFromGetLociResponse should be called on it
3643
+ const createdParser = HashTreeParserStub.returnValues[0];
3644
+ assert.calledOnceWithExactly(createdParser.initializeFromGetLociResponse, locus);
3645
+ assert.notCalled(locusInfo.handleLocusDelta);
3646
+ });
3647
+
3648
+ it('should reactivate a stopped parser when replaces info is newer', () => {
3649
+ const parserA = createMockApiParser('stopped');
3650
+ const parserB = createMockApiParser('active');
3651
+ locusInfo.hashTreeParsers.set(locusUrlA, {parser: parserA, replacedAt: '2026-01-01T00:00:00Z', initializedFromHashTree: true});
3652
+ locusInfo.hashTreeParsers.set(locusUrlB, {parser: parserB, initializedFromHashTree: true});
3653
+
3654
+ const locus = createLocusWithReplaces(locusUrlA, locusUrlB, '2026-02-01T00:00:00Z');
3655
+
3656
+ locusInfo.handleLocusAPIResponse(mockMeeting, {locus});
3657
+
3658
+ assert.calledOnce(parserA.resumeFromApiResponse);
3659
+ assert.calledWithExactly(parserA.resumeFromApiResponse, locus);
3660
+ assert.calledOnce(parserB.stop);
3661
+ assert.equal(locusInfo.hashTreeParsers.get(locusUrlB).replacedAt, '2026-02-01T00:00:00Z');
3662
+ assert.isFalse(locusInfo.hashTreeParsers.get(locusUrlA).initializedFromHashTree);
3663
+ });
3664
+
3665
+ it('should not reactivate a stopped parser when replaces info is not newer', () => {
3666
+ const parserA = createMockApiParser('stopped');
3667
+ const parserB = createMockApiParser('active');
3668
+ locusInfo.hashTreeParsers.set(locusUrlA, {parser: parserA, replacedAt: '2026-03-01T00:00:00Z', initializedFromHashTree: true});
3669
+ locusInfo.hashTreeParsers.set(locusUrlB, {parser: parserB, initializedFromHashTree: true});
3670
+
3671
+ const locus = createLocusWithReplaces(locusUrlA, locusUrlB, '2026-01-01T00:00:00Z');
3672
+
3673
+ locusInfo.handleLocusAPIResponse(mockMeeting, {locus});
3674
+
3675
+ assert.notCalled(parserA.resumeFromApiResponse);
3676
+ assert.notCalled(parserB.stop);
3677
+ });
3678
+
3679
+ it('should not reactivate a stopped parser when no replaces info is available', () => {
3680
+ const parserA = createMockApiParser('stopped');
3681
+ locusInfo.hashTreeParsers.set(locusUrlA, {parser: parserA, initializedFromHashTree: true});
3682
+
3683
+ const locus = createLocusWithoutReplaces(locusUrlA);
3684
+
3685
+ locusInfo.handleLocusAPIResponse(mockMeeting, {locus});
3686
+
3687
+ assert.notCalled(parserA.resumeFromApiResponse);
3688
+ });
3689
+ });
3690
+ });
3691
+
3692
+ describe('#syncAllHashTreeDatasets', () => {
3693
+ it('should call syncAllDatasets on each parser that has an entry', async () => {
3694
+ const parser1 = {syncAllDatasets: sinon.stub().resolves()};
3695
+ const parser2 = {syncAllDatasets: sinon.stub().resolves()};
3696
+ locusInfo.hashTreeParsers.set('url1', {parser: parser1});
3697
+ locusInfo.hashTreeParsers.set('url2', {parser: parser2});
3698
+
3699
+ await locusInfo.syncAllHashTreeDatasets();
3700
+
3701
+ assert.calledOnce(parser1.syncAllDatasets);
3702
+ assert.calledOnce(parser2.syncAllDatasets);
3703
+ });
3704
+
3705
+ it('should skip parser entries without a parser object', async () => {
3706
+ const parser1 = {syncAllDatasets: sinon.stub().resolves()};
3707
+ locusInfo.hashTreeParsers.set('url1', {parser: parser1});
3708
+ locusInfo.hashTreeParsers.set('url2', {parser: undefined});
3709
+
3710
+ await locusInfo.syncAllHashTreeDatasets();
3711
+
3712
+ assert.calledOnce(parser1.syncAllDatasets);
3713
+ });
3714
+
3715
+ it('should await each parsers syncAllDatasets sequentially', async () => {
3716
+ const callOrder = [];
3717
+ const parser1 = {syncAllDatasets: sinon.stub().callsFake(() => {
3718
+ callOrder.push('start1');
3719
+ return new Promise((resolve) => {
3720
+ setTimeout(() => {
3721
+ callOrder.push('end1');
3722
+ resolve();
3723
+ }, 100);
3724
+ });
3725
+ })};
3726
+ const parser2 = {syncAllDatasets: sinon.stub().callsFake(() => {
3727
+ callOrder.push('start2');
3728
+ return Promise.resolve();
3729
+ })};
3730
+ locusInfo.hashTreeParsers.set('url1', {parser: parser1});
3731
+ locusInfo.hashTreeParsers.set('url2', {parser: parser2});
3732
+
3733
+ const clock = sinon.useFakeTimers();
3734
+ const promise = locusInfo.syncAllHashTreeDatasets();
3735
+ // parser1 started but parser2 not yet
3736
+ assert.deepEqual(callOrder, ['start1']);
3737
+
3738
+ await clock.tickAsync(100);
3739
+ await promise;
3740
+ // parser1 finished, then parser2 started and finished
3741
+ assert.deepEqual(callOrder, ['start1', 'end1', 'start2']);
3742
+ clock.restore();
3433
3743
  });
3434
3744
  });
3435
3745
 
@@ -3521,49 +3831,23 @@ describe('plugin-meetings', () => {
3521
3831
  assert.deepEqual(callOrder, ['updateLocusUrl', 'updateMeetingInfo']);
3522
3832
  });
3523
3833
 
3524
- it('#updateLocusInfo ignores breakout LEFT message', () => {
3525
- const newLocus = {
3526
- self: {
3527
- reason: 'MOVED',
3528
- state: 'LEFT',
3529
- },
3530
- };
3834
+ it('#updateLocusInfo ignores locus when isSelfMovedOrBreakoutEnded returns true', () => {
3835
+ const newLocus = {self: {state: 'JOINED'}};
3836
+
3837
+ sinon.stub(MeetingsUtil, 'isSelfMovedOrBreakoutEnded').returns(true);
3531
3838
 
3532
3839
  locusInfo.updateControls = sinon.stub();
3533
- locusInfo.updateConversationUrl = sinon.stub();
3534
- locusInfo.updateCreated = sinon.stub();
3535
3840
  locusInfo.updateFullState = sinon.stub();
3536
- locusInfo.updateHostInfo = sinon.stub();
3537
- locusInfo.updateMeetingInfo = sinon.stub();
3538
- locusInfo.updateMediaShares = sinon.stub();
3539
- locusInfo.updateReplaces = sinon.stub();
3540
3841
  locusInfo.updateSelf = sinon.stub();
3541
- locusInfo.updateLocusUrl = sinon.stub();
3542
- locusInfo.updateAclUrl = sinon.stub();
3543
- locusInfo.updateBasequence = sinon.stub();
3544
- locusInfo.updateSequence = sinon.stub();
3545
- locusInfo.updateEmbeddedApps = sinon.stub();
3546
- locusInfo.updateLinks = sinon.stub();
3547
- locusInfo.compareAndUpdate = sinon.stub();
3548
3842
 
3549
3843
  locusInfo.updateLocusInfo(newLocus);
3550
3844
 
3845
+ assert.calledOnceWithExactly(MeetingsUtil.isSelfMovedOrBreakoutEnded, newLocus);
3551
3846
  assert.notCalled(locusInfo.updateControls);
3552
- assert.notCalled(locusInfo.updateConversationUrl);
3553
- assert.notCalled(locusInfo.updateCreated);
3554
3847
  assert.notCalled(locusInfo.updateFullState);
3555
- assert.notCalled(locusInfo.updateHostInfo);
3556
- assert.notCalled(locusInfo.updateMeetingInfo);
3557
- assert.notCalled(locusInfo.updateMediaShares);
3558
- assert.notCalled(locusInfo.updateReplaces);
3559
3848
  assert.notCalled(locusInfo.updateSelf);
3560
- assert.notCalled(locusInfo.updateLocusUrl);
3561
- assert.notCalled(locusInfo.updateAclUrl);
3562
- assert.notCalled(locusInfo.updateBasequence);
3563
- assert.notCalled(locusInfo.updateSequence);
3564
- assert.notCalled(locusInfo.updateEmbeddedApps);
3565
- assert.notCalled(locusInfo.updateLinks);
3566
- assert.notCalled(locusInfo.compareAndUpdate);
3849
+
3850
+ MeetingsUtil.isSelfMovedOrBreakoutEnded.restore();
3567
3851
  });
3568
3852
 
3569
3853
  it('#updateLocusInfo puts the Locus DTO top level properties at the right place in LocusInfo class', () => {
@@ -4413,6 +4697,31 @@ describe('plugin-meetings', () => {
4413
4697
  });
4414
4698
  });
4415
4699
 
4700
+ describe('#cleanUp', () => {
4701
+ it('calls cleanUp on all hash tree parsers and clears maps', () => {
4702
+ const parser1 = {cleanUp: sinon.stub()};
4703
+ const parser2 = {cleanUp: sinon.stub()};
4704
+
4705
+ locusInfo.hashTreeParsers.set('url1', {parser: parser1, initializedFromHashTree: true});
4706
+ locusInfo.hashTreeParsers.set('url2', {parser: parser2, initializedFromHashTree: true});
4707
+ locusInfo.hashTreeObjectId2ParticipantId.set(1, 'participant1');
4708
+
4709
+ locusInfo.cleanUp();
4710
+
4711
+ assert.calledOnce(parser1.cleanUp);
4712
+ assert.calledOnce(parser2.cleanUp);
4713
+ assert.equal(locusInfo.hashTreeParsers.size, 0);
4714
+ assert.equal(locusInfo.hashTreeObjectId2ParticipantId.size, 0);
4715
+ });
4716
+
4717
+ it('works when there are no hash tree parsers', () => {
4718
+ locusInfo.cleanUp();
4719
+
4720
+ assert.equal(locusInfo.hashTreeParsers.size, 0);
4721
+ assert.equal(locusInfo.hashTreeObjectId2ParticipantId.size, 0);
4722
+ });
4723
+ });
4724
+
4416
4725
  describe('#handleOneonOneEvent', () => {
4417
4726
  beforeEach(() => {
4418
4727
  locusInfo.emitScoped = sinon.stub();
@@ -4455,6 +4764,9 @@ describe('plugin-meetings', () => {
4455
4764
  });
4456
4765
 
4457
4766
  describe('#isMeetingActive', () => {
4767
+ beforeEach(() => {
4768
+ webex.internal.newMetrics.submitClientEvent.resetHistory();
4769
+ });
4458
4770
  forEach([_CALL_, _SIP_BRIDGE_, _SPACE_SHARE_], (type) => {
4459
4771
  describe(`type = ${type}`, () => {
4460
4772
  it('sends client event correctly for state = inactive', () => {
@@ -4521,7 +4833,7 @@ describe('plugin-meetings', () => {
4521
4833
  });
4522
4834
  });
4523
4835
 
4524
- it('sends client event correctly for state = MEETING_INACTIVE_TERMINATING', () => {
4836
+ it('sends client event correctly for state = MEETING_INACTIVE', () => {
4525
4837
  locusInfo.getLocusPartner = sinon.stub().returns({state: MEETING_STATE.STATES.LEFT});
4526
4838
  locusInfo.parsedLocus = {
4527
4839
  fullState: {
@@ -4543,7 +4855,7 @@ describe('plugin-meetings', () => {
4543
4855
  });
4544
4856
  });
4545
4857
 
4546
- it('sends client event correctly for state = FULLSTATE_REMOVED', () => {
4858
+ it('does not send client event when state = INACTIVE and endMeetingReason = BREAKOUT_ENDED', () => {
4547
4859
  locusInfo.getLocusPartner = sinon.stub().returns({state: MEETING_STATE.STATES.LEFT});
4548
4860
  locusInfo.parsedLocus = {
4549
4861
  fullState: {
@@ -4552,17 +4864,41 @@ describe('plugin-meetings', () => {
4552
4864
  };
4553
4865
 
4554
4866
  locusInfo.fullState = {
4555
- removed: true,
4867
+ state: LOCUS.STATE.INACTIVE,
4868
+ endMeetingReason: 'BREAKOUT_ENDED',
4556
4869
  };
4557
4870
 
4558
4871
  locusInfo.isMeetingActive();
4559
4872
 
4560
- assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
4561
- name: 'client.call.remote-ended',
4562
- options: {
4563
- meetingId: locusInfo.meetingId,
4873
+ assert.notCalled(webex.internal.newMetrics.submitClientEvent);
4874
+ });
4875
+
4876
+ it('sends client event correctly for state self removed', () => {
4877
+ locusInfo.emitScoped = sinon.stub();
4878
+ locusInfo.parsedLocus = {
4879
+ fullState: {
4880
+ type: _MEETING_,
4564
4881
  },
4565
- });
4882
+ self: {
4883
+ removed: true,
4884
+ }
4885
+ };
4886
+
4887
+ locusInfo.isMeetingActive();
4888
+
4889
+ assert.notCalled(webex.internal.newMetrics.submitClientEvent);
4890
+ assert.calledOnceWithExactly(
4891
+ locusInfo.emitScoped,
4892
+ {
4893
+ file: 'locus-info',
4894
+ function: 'isMeetingActive',
4895
+ },
4896
+ EVENTS.DESTROY_MEETING,
4897
+ {
4898
+ reason: MEETING_REMOVED_REASON.SELF_REMOVED,
4899
+ shouldLeave: false,
4900
+ }
4901
+ );
4566
4902
  });
4567
4903
  });
4568
4904
 
@@ -4954,6 +5290,31 @@ describe('plugin-meetings', () => {
4954
5290
  );
4955
5291
  assert.notCalled(getTheLocusToUpdateStub);
4956
5292
  });
5293
+
5294
+ it('should call handleLocusAPIResponse for SDK_LOCUS_FROM_SYNC_MEETINGS when hash tree parsers exist', () => {
5295
+ const fakeLocusUrl = 'http://locus-url.com';
5296
+ const fakeLocus = {url: fakeLocusUrl, fullState: {state: 'ACTIVE'}};
5297
+ const mockHashTreeParser = {
5298
+ handleMessage: sinon.stub(),
5299
+ handleLocusUpdate: sinon.stub(),
5300
+ };
5301
+ locusInfo.hashTreeParsers.set(fakeLocusUrl, {
5302
+ parser: mockHashTreeParser,
5303
+ initializedFromHashTree: true,
5304
+ });
5305
+
5306
+ sinon.stub(locusInfo, 'handleLocusDelta');
5307
+
5308
+ locusInfo.parse(mockMeeting, {
5309
+ eventType: LOCUSEVENT.SDK_LOCUS_FROM_SYNC_MEETINGS,
5310
+ locus: fakeLocus,
5311
+ });
5312
+
5313
+ // should route through handleLocusAPIResponse which passes unwrapped LocusDTO to parser
5314
+ assert.calledOnce(mockHashTreeParser.handleLocusUpdate);
5315
+ assert.notCalled(mockHashTreeParser.handleMessage);
5316
+ assert.notCalled(locusInfo.handleLocusDelta);
5317
+ });
4957
5318
  });
4958
5319
  });
4959
5320
 
@@ -5146,6 +5507,7 @@ describe('plugin-meetings', () => {
5146
5507
  return {
5147
5508
  htMeta: {elementId: {type: 'Self'}},
5148
5509
  data: {
5510
+ deviceUrl,
5149
5511
  devices,
5150
5512
  },
5151
5513
  };