@webex/plugin-meetings 3.9.0-next.2 → 3.9.0-next.20

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 (85) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.js +2 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/interpretation/index.js +1 -1
  6. package/dist/interpretation/siLanguage.js +1 -1
  7. package/dist/locus-info/index.js +38 -10
  8. package/dist/locus-info/index.js.map +1 -1
  9. package/dist/locus-info/parser.js +4 -1
  10. package/dist/locus-info/parser.js.map +1 -1
  11. package/dist/media/properties.js +53 -5
  12. package/dist/media/properties.js.map +1 -1
  13. package/dist/meeting/in-meeting-actions.js +2 -0
  14. package/dist/meeting/in-meeting-actions.js.map +1 -1
  15. package/dist/meeting/index.js +189 -122
  16. package/dist/meeting/index.js.map +1 -1
  17. package/dist/meeting/muteState.js +2 -5
  18. package/dist/meeting/muteState.js.map +1 -1
  19. package/dist/meeting/request.js +25 -0
  20. package/dist/meeting/request.js.map +1 -1
  21. package/dist/meeting/util.js +30 -11
  22. package/dist/meeting/util.js.map +1 -1
  23. package/dist/meeting-info/meeting-info-v2.js +29 -21
  24. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  25. package/dist/meetings/index.js +31 -25
  26. package/dist/meetings/index.js.map +1 -1
  27. package/dist/member/types.js.map +1 -1
  28. package/dist/members/collection.js +13 -0
  29. package/dist/members/collection.js.map +1 -1
  30. package/dist/members/index.js +42 -20
  31. package/dist/members/index.js.map +1 -1
  32. package/dist/members/util.js +7 -2
  33. package/dist/members/util.js.map +1 -1
  34. package/dist/metrics/constants.js +2 -1
  35. package/dist/metrics/constants.js.map +1 -1
  36. package/dist/reachability/index.js +3 -3
  37. package/dist/reachability/index.js.map +1 -1
  38. package/dist/types/constants.d.ts +2 -0
  39. package/dist/types/locus-info/index.d.ts +54 -1
  40. package/dist/types/media/properties.d.ts +21 -0
  41. package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
  42. package/dist/types/meeting/index.d.ts +11 -1
  43. package/dist/types/meeting/request.d.ts +9 -0
  44. package/dist/types/meeting/util.d.ts +10 -3
  45. package/dist/types/meeting-info/meeting-info-v2.d.ts +6 -3
  46. package/dist/types/meetings/index.d.ts +3 -1
  47. package/dist/types/member/types.d.ts +1 -0
  48. package/dist/types/members/collection.d.ts +6 -0
  49. package/dist/types/members/index.d.ts +12 -2
  50. package/dist/types/members/util.d.ts +6 -3
  51. package/dist/types/metrics/constants.d.ts +1 -0
  52. package/dist/webinar/index.js +1 -1
  53. package/package.json +16 -16
  54. package/src/constants.ts +2 -0
  55. package/src/locus-info/index.ts +84 -9
  56. package/src/locus-info/parser.ts +5 -1
  57. package/src/media/properties.ts +43 -0
  58. package/src/meeting/in-meeting-actions.ts +4 -0
  59. package/src/meeting/index.ts +91 -4
  60. package/src/meeting/muteState.ts +2 -6
  61. package/src/meeting/request.ts +23 -0
  62. package/src/meeting/util.ts +41 -20
  63. package/src/meeting-info/meeting-info-v2.ts +24 -5
  64. package/src/meetings/index.ts +9 -3
  65. package/src/member/types.ts +1 -0
  66. package/src/members/collection.ts +11 -0
  67. package/src/members/index.ts +38 -5
  68. package/src/members/util.ts +18 -2
  69. package/src/metrics/constants.ts +1 -0
  70. package/src/reachability/index.ts +3 -3
  71. package/test/unit/spec/locus-info/index.js +30 -15
  72. package/test/unit/spec/locus-info/parser.js +3 -2
  73. package/test/unit/spec/media/properties.ts +137 -0
  74. package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
  75. package/test/unit/spec/meeting/index.js +255 -27
  76. package/test/unit/spec/meeting/muteState.js +32 -6
  77. package/test/unit/spec/meeting/request.js +21 -0
  78. package/test/unit/spec/meeting/utils.js +45 -16
  79. package/test/unit/spec/meeting-info/meetinginfov2.js +8 -3
  80. package/test/unit/spec/meetings/index.js +10 -1
  81. package/test/unit/spec/members/collection.js +120 -0
  82. package/test/unit/spec/members/index.js +72 -3
  83. package/test/unit/spec/members/request.js +55 -0
  84. package/test/unit/spec/members/utils.js +116 -14
  85. package/test/unit/spec/reachability/index.ts +158 -3
@@ -74,7 +74,11 @@ import {Invitee} from '../meeting/type';
74
74
  * @memberof Members
75
75
  */
76
76
 
77
- type UpdatedMembers = {added: Array<Member>; updated: Array<Member>};
77
+ type UpdatedMembers = {
78
+ added: Array<Member>;
79
+ updated: Array<Member>;
80
+ removedIds?: Array<string>; // removed member ids
81
+ };
78
82
  /**
79
83
  * @class Members
80
84
  */
@@ -384,12 +388,18 @@ export default class Members extends StatelessWebexPlugin {
384
388
  * when new participant updates come in, both delta and full participants, update them in members collection
385
389
  * delta object in the event will have {updated, added} and full will be the full membersCollection
386
390
  * @param {Object} payload
387
- * @param {Object} payload.participants
391
+ * @param {Object} payload.participants new/updated participants
392
+ * @param {Boolean} payload.isReplace whether to replace the whole members collection
393
+ * @param {Object} payload.removedParticipantIds ids of the removed participants
388
394
  * @returns {undefined}
389
395
  * @private
390
396
  * @memberof Members
391
397
  */
392
- locusParticipantsUpdate(payload: {participants: object; isReplace?: boolean}) {
398
+ locusParticipantsUpdate(payload: {
399
+ participants: object;
400
+ isReplace?: boolean;
401
+ removedParticipantIds?: Array<string>;
402
+ }) {
393
403
  if (payload) {
394
404
  if (payload.isReplace) {
395
405
  this.clearMembers();
@@ -547,10 +557,22 @@ export default class Members extends StatelessWebexPlugin {
547
557
  private handleMembersUpdate(membersUpdate: UpdatedMembers) {
548
558
  this.constructMembers(membersUpdate.updated, true);
549
559
  this.constructMembers(membersUpdate.added, false);
560
+ this.removeMembers(membersUpdate.removedIds);
550
561
 
551
562
  return this.membersCollection.getAll();
552
563
  }
553
564
 
565
+ /**
566
+ * removes members from the collection
567
+ * @param {Array<string>} removedMembers removed members ids
568
+ * @returns {void}
569
+ */
570
+ private removeMembers(removedMembers: Array<string>) {
571
+ removedMembers.forEach((memberId) => {
572
+ this.membersCollection.remove(memberId);
573
+ });
574
+ }
575
+
554
576
  /**
555
577
  * set members to the member collection from each updated/added lists as passed in
556
578
  * @param {Array} list
@@ -600,6 +622,10 @@ export default class Members extends StatelessWebexPlugin {
600
622
  }
601
623
  const memberUpdate = this.update(payload.participants);
602
624
 
625
+ // this code depends on memberIds being the same as participantIds
626
+ // if MemberUtil.extractId() ever changes, this will need to be updated
627
+ memberUpdate.removedIds = payload.removedParticipantIds || [];
628
+
603
629
  return memberUpdate;
604
630
  }
605
631
 
@@ -1181,11 +1207,17 @@ export default class Members extends StatelessWebexPlugin {
1181
1207
  * @param {string} memberId - id of the participant who is receiving request
1182
1208
  * @param {string} requestingParticipantId - id of the participant who is sending request (optional)
1183
1209
  * @param {string} [alias] - alias name
1210
+ * @param {string} [suffix] - name suffix (optional)
1184
1211
  * @returns {Promise}
1185
1212
  * @public
1186
1213
  * @memberof Members
1187
1214
  */
1188
- public editDisplayName(memberId: string, requestingParticipantId: string, alias: string) {
1215
+ public editDisplayName(
1216
+ memberId: string,
1217
+ requestingParticipantId: string,
1218
+ alias: string,
1219
+ suffix?: string
1220
+ ) {
1189
1221
  if (!this.locusUrl) {
1190
1222
  return Promise.reject(
1191
1223
  new ParameterError(
@@ -1205,7 +1237,8 @@ export default class Members extends StatelessWebexPlugin {
1205
1237
  memberId,
1206
1238
  requestingParticipantId,
1207
1239
  alias,
1208
- locusUrl
1240
+ locusUrl,
1241
+ suffix
1209
1242
  );
1210
1243
 
1211
1244
  return this.membersRequest.editDisplayNameMember(options);
@@ -190,13 +190,21 @@ const MembersUtil = {
190
190
  * @param {String} requestingParticipantId id of the participant who is sending request (optional)
191
191
  * @param {String} alias alias name
192
192
  * @param {String} locusUrl url
193
+ * @param {String} suffix optional suffix
193
194
  * @returns {Object} consists of {memberID: string, requestingParticipantId: string, alias: string, locusUrl: string}
194
195
  */
195
- generateEditDisplayNameMemberOptions: (memberId, requestingParticipantId, alias, locusUrl) => ({
196
+ generateEditDisplayNameMemberOptions: (
196
197
  memberId,
197
198
  requestingParticipantId,
198
199
  alias,
199
200
  locusUrl,
201
+ suffix
202
+ ) => ({
203
+ memberId,
204
+ requestingParticipantId,
205
+ alias,
206
+ locusUrl,
207
+ suffix,
200
208
  }),
201
209
 
202
210
  getMuteMemberRequestParams: (options) => {
@@ -301,10 +309,18 @@ const MembersUtil = {
301
309
  * @returns {Object} request parameters (method, uri, body) needed to make a editDisplayName request
302
310
  */
303
311
  editDisplayNameMemberRequestParams: (options) => {
304
- const body = {
312
+ const body: {
313
+ aliasValue: string;
314
+ requestingParticipantId: string;
315
+ suffixValue?: string;
316
+ } = {
305
317
  aliasValue: options.alias,
306
318
  requestingParticipantId: options.requestingParticipantId,
307
319
  };
320
+
321
+ if (options.suffix !== undefined) {
322
+ body.suffixValue = options.suffix;
323
+ }
308
324
  const uri = `${options.locusUrl}/${PARTICIPANT}/${options.memberId}/${ALIAS}`;
309
325
 
310
326
  return {
@@ -86,6 +86,7 @@ const BEHAVIORAL_METRICS = {
86
86
  VERIFY_REGISTRATION_ID_SUCCESS: 'js_sdk_verify_registrationId_success',
87
87
  VERIFY_REGISTRATION_ID_ERROR: 'js_sdk_verify_registrationId_error',
88
88
  JOIN_FORBIDDEN_ERROR: 'js_sdk_join_forbidden_error',
89
+ MEDIA_ISSUE_DETECTED: 'js_sdk_media_issue_detected',
89
90
  };
90
91
 
91
92
  export {BEHAVIORAL_METRICS as default};
@@ -923,10 +923,10 @@ export default class Reachability extends EventsScope {
923
923
 
924
924
  // update expected results counters to include this cluster
925
925
  this.expectedResultsCount[cluster.isVideoMesh ? 'videoMesh' : 'public'].udp +=
926
- cluster.udp.length;
926
+ cluster.udp.length > 0 ? 1 : 0;
927
927
  if (!cluster.isVideoMesh) {
928
- this.expectedResultsCount.public.tcp += cluster.tcp.length;
929
- this.expectedResultsCount.public.xtls += cluster.xtls.length;
928
+ this.expectedResultsCount.public.tcp += cluster.tcp.length > 0 ? 1 : 0;
929
+ this.expectedResultsCount.public.xtls += cluster.xtls.length > 0 ? 1 : 0;
930
930
  }
931
931
  });
932
932
 
@@ -772,7 +772,7 @@ describe('plugin-meetings', () => {
772
772
  },
773
773
  };
774
774
  locusInfo.emitScoped = sinon.stub();
775
- locusInfo.updateParticipants({});
775
+ locusInfo.updateParticipants({}, []);
776
776
 
777
777
  // if this assertion fails, double-check the attributes used in
778
778
  // the updateParticipants function in locus-info/index.js
@@ -790,6 +790,7 @@ describe('plugin-meetings', () => {
790
790
  selfId: '2',
791
791
  hostId: '3',
792
792
  isReplace: undefined,
793
+ removedParticipantIds: [],
793
794
  }
794
795
  );
795
796
  // note: in a real use case, recordingId, selfId, and hostId would all be the same
@@ -814,7 +815,7 @@ describe('plugin-meetings', () => {
814
815
  };
815
816
 
816
817
  locusInfo.emitScoped = sinon.stub();
817
- locusInfo.updateParticipants({}, true);
818
+ locusInfo.updateParticipants({}, [], true);
818
819
 
819
820
  assert.calledWith(
820
821
  locusInfo.emitScoped,
@@ -830,6 +831,7 @@ describe('plugin-meetings', () => {
830
831
  selfId: '2',
831
832
  hostId: '3',
832
833
  isReplace: true,
834
+ removedParticipantIds: [],
833
835
  }
834
836
  );
835
837
  });
@@ -847,7 +849,7 @@ describe('plugin-meetings', () => {
847
849
  ];
848
850
 
849
851
  locusInfo.emitScoped = sinon.stub();
850
- locusInfo.updateParticipants(failureParticipant);
852
+ locusInfo.updateParticipants(failureParticipant, []);
851
853
  assert.calledWith(
852
854
  locusInfo.emitScoped,
853
855
  {
@@ -2038,6 +2040,18 @@ describe('plugin-meetings', () => {
2038
2040
  });
2039
2041
  });
2040
2042
 
2043
+ describe('#handleLocusAPIResponse', () => {
2044
+ it('calls handleLocusDelta', () => {
2045
+ const fakeLocus = {eventType: LOCUSEVENT.DIFFERENCE};
2046
+
2047
+ sinon.stub(locusInfo, 'handleLocusDelta');
2048
+
2049
+ locusInfo.handleLocusAPIResponse(mockMeeting, {locus: fakeLocus});
2050
+
2051
+ assert.calledWith(locusInfo.handleLocusDelta, fakeLocus, mockMeeting);
2052
+ });
2053
+ });
2054
+
2041
2055
  describe('#LocusDeltaEvents', () => {
2042
2056
  const fakeMeeting = 'fakeMeeting';
2043
2057
  let sandbox = null;
@@ -2050,7 +2064,7 @@ describe('plugin-meetings', () => {
2050
2064
 
2051
2065
  fakeLocus = {
2052
2066
  meeting: true,
2053
- participants: true,
2067
+ participants: [],
2054
2068
  url: 'newLocusUrl',
2055
2069
  syncUrl: 'newSyncUrl',
2056
2070
  };
@@ -2369,23 +2383,23 @@ describe('plugin-meetings', () => {
2369
2383
 
2370
2384
  it('applyLocusDeltaData handles LOCUS_URL_CHANGED action correctly', () => {
2371
2385
  const {LOCUS_URL_CHANGED} = LocusDeltaParser.loci;
2372
- const fakeDeltaLocus = {id: 'fake delta locus'};
2386
+ const fakeFullLocus = {
2387
+ url: 'new full loci url',
2388
+ };
2373
2389
  const meeting = {
2374
2390
  meetingRequest: {
2375
- getLocusDTO: sandbox.stub().resolves({body: fakeDeltaLocus}),
2391
+ getLocusDTO: sandbox.stub().resolves({body: fakeFullLocus}),
2376
2392
  },
2377
2393
  locusInfo: {
2378
2394
  handleLocusDelta: sandbox.stub(),
2379
2395
  },
2380
- locusUrl: 'current locus url',
2396
+ locusUrl: 'current BO session locus url',
2381
2397
  };
2382
2398
 
2383
- locusInfo.locusParser.workingCopy = {
2384
- syncUrl: 'current sync url',
2385
- };
2399
+ locusInfo.locusParser.workingCopy = null;
2386
2400
 
2387
2401
  locusInfo.applyLocusDeltaData(LOCUS_URL_CHANGED, fakeLocus, meeting);
2388
- assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {url: 'current sync url'});
2402
+ assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {url: fakeLocus.url});
2389
2403
  });
2390
2404
 
2391
2405
  describe('edge cases for sync failing', () => {
@@ -2525,7 +2539,7 @@ describe('plugin-meetings', () => {
2525
2539
  });
2526
2540
 
2527
2541
  it('onDeltaLocus handle delta data', () => {
2528
- fakeLocus.participants = {};
2542
+ fakeLocus.participants = [];
2529
2543
  const fakeBreakout = {
2530
2544
  sessionId: 'sessionId',
2531
2545
  groupId: 'groupId',
@@ -2542,11 +2556,11 @@ describe('plugin-meetings', () => {
2542
2556
  };
2543
2557
  locusInfo.updateParticipants = sinon.stub();
2544
2558
  locusInfo.onDeltaLocus(fakeLocus);
2545
- assert.calledWith(locusInfo.updateParticipants, {}, false);
2559
+ assert.calledWith(locusInfo.updateParticipants, [], undefined, false);
2546
2560
 
2547
2561
  fakeLocus.controls.breakout.sessionId = 'sessionId2';
2548
2562
  locusInfo.onDeltaLocus(fakeLocus);
2549
- assert.calledWith(locusInfo.updateParticipants, {}, true);
2563
+ assert.calledWith(locusInfo.updateParticipants, [], undefined, true);
2550
2564
  });
2551
2565
 
2552
2566
  it('onDeltaLocus merges delta participants with existing participants', () => {
@@ -2563,7 +2577,7 @@ describe('plugin-meetings', () => {
2563
2577
  existingParticipants,
2564
2578
  FAKE_DELTA_PARTICIPANTS
2565
2579
  );
2566
- assert.calledWith(locusInfo.updateParticipants, FAKE_DELTA_PARTICIPANTS, false);
2580
+ assert.calledWith(locusInfo.updateParticipants, FAKE_DELTA_PARTICIPANTS, undefined, false);
2567
2581
  });
2568
2582
 
2569
2583
  [true, false].forEach((isDelta) =>
@@ -3074,6 +3088,7 @@ describe('plugin-meetings', () => {
3074
3088
  id: 'test person id',
3075
3089
  },
3076
3090
  },
3091
+ participants: [],
3077
3092
  });
3078
3093
 
3079
3094
  updateLocusInfoStub.resetHistory();
@@ -253,7 +253,8 @@ describe('locus-info/parser', () => {
253
253
  });
254
254
 
255
255
  it('replaces current loci when the locus URL changes and incoming sequence is later, even when baseSequence doesn\'t match', () => {
256
- const {USE_INCOMING} = LocusDeltaParser.loci;
256
+ const {LOCUS_URL_CHANGED} = LocusDeltaParser.loci;
257
+ sandbox.stub(LocusDeltaParser, 'compare').returns(LOCUS_URL_CHANGED);
257
258
 
258
259
  parser.queue.dequeue = sandbox.stub().returns(NEW_LOCI);
259
260
  parser.onDeltaAction = sandbox.stub();
@@ -262,7 +263,7 @@ describe('locus-info/parser', () => {
262
263
 
263
264
  parser.processDeltaEvent();
264
265
 
265
- assert.equal(parser.workingCopy, NEW_LOCI);
266
+ assert.equal(parser.workingCopy, null);
266
267
  });
267
268
 
268
269
  it('does not replace current loci when the locus URL changes but incoming sequence is not later', () => {
@@ -6,6 +6,8 @@ import * as tsSdpModule from '@webex/ts-sdp';
6
6
  import MediaProperties from '@webex/plugin-meetings/src/media/properties';
7
7
  import {Defer} from '@webex/common';
8
8
  import MediaConnectionAwaiter from '../../../../src/media/MediaConnectionAwaiter';
9
+ import Metrics from '../../../../src/metrics';
10
+ import BEHAVIORAL_METRICS from '../../../../src/metrics/constants';
9
11
 
10
12
  describe('MediaProperties', () => {
11
13
  let mediaProperties;
@@ -389,4 +391,139 @@ describe('MediaProperties', () => {
389
391
  });
390
392
  });
391
393
  });
394
+
395
+ // issue types and subtypes used in these tests are just examples
396
+ // they don't reflect real issue types/subtypes used in production
397
+ describe('sendMediaIssueMetric', () => {
398
+ let sendBehavioralMetricStub;
399
+ let clock;
400
+
401
+ beforeEach(() => {
402
+ clock = sinon.useFakeTimers();
403
+ sendBehavioralMetricStub = sinon.stub(Metrics, 'sendBehavioralMetric');
404
+ });
405
+
406
+ afterEach(() => {
407
+ clock.restore();
408
+ });
409
+
410
+ it('should send a behavioral metric with correct parameters', () => {
411
+ const issueType = 'audio';
412
+ const issueSubType = 'packet-loss';
413
+ const correlationId = 'test-correlation-id-123';
414
+
415
+ mediaProperties.sendMediaIssueMetric(issueType, issueSubType, correlationId);
416
+
417
+ assert.calledOnce(sendBehavioralMetricStub);
418
+ assert.calledWith(sendBehavioralMetricStub, BEHAVIORAL_METRICS.MEDIA_ISSUE_DETECTED, {
419
+ correlationId,
420
+ 'audio_packet-loss': 1,
421
+ });
422
+ });
423
+
424
+ it('should increment count while being throttled and reset it once metric goes out', () => {
425
+ const issueType = 'video';
426
+ const issueSubType = 'freeze';
427
+ const correlationId = 'test-correlation-id';
428
+
429
+ // Call multiple times with same issue type/subtype
430
+ mediaProperties.sendMediaIssueMetric(issueType, issueSubType, correlationId);
431
+ mediaProperties.sendMediaIssueMetric(issueType, issueSubType, correlationId);
432
+ mediaProperties.sendMediaIssueMetric(issueType, issueSubType, correlationId);
433
+
434
+ // First call should go through immediately, subsequent calls are throttled
435
+ assert.calledOnce(sendBehavioralMetricStub);
436
+ assert.calledWith(sendBehavioralMetricStub, BEHAVIORAL_METRICS.MEDIA_ISSUE_DETECTED, {
437
+ correlationId,
438
+ video_freeze: 1, // Only the first call goes through due to throttling
439
+ });
440
+ sendBehavioralMetricStub.resetHistory();
441
+
442
+ assert.equal(mediaProperties.mediaIssueCounters['video_freeze'], 2); // counter should be reset after the first metric goes out, hence only 2 not 3 here
443
+
444
+ clock.tick(5 * 60 * 1000); // Advance time by 5 minutes to expire throttle
445
+
446
+ assert.calledOnceWithExactly(
447
+ sendBehavioralMetricStub,
448
+ BEHAVIORAL_METRICS.MEDIA_ISSUE_DETECTED,
449
+ {
450
+ correlationId,
451
+ video_freeze: 2,
452
+ }
453
+ );
454
+ });
455
+
456
+ it('should track different issue types separately in counters', () => {
457
+ const correlationId = 'test-correlation-id';
458
+
459
+ // Send different issue types
460
+ mediaProperties.sendMediaIssueMetric('audio', 'packet-loss', correlationId);
461
+ mediaProperties.sendMediaIssueMetric('video', 'freeze', correlationId);
462
+ mediaProperties.sendMediaIssueMetric('audio', 'packet-loss', correlationId);
463
+ mediaProperties.sendMediaIssueMetric('audio', 'packet-loss', correlationId);
464
+ mediaProperties.sendMediaIssueMetric('audio', 'packet-loss', correlationId);
465
+ mediaProperties.sendMediaIssueMetric('video', 'freeze', correlationId);
466
+
467
+ // First call should go through immediately, subsequent calls are throttled
468
+ assert.calledOnceWithExactly(
469
+ sendBehavioralMetricStub,
470
+ BEHAVIORAL_METRICS.MEDIA_ISSUE_DETECTED,
471
+ {
472
+ correlationId,
473
+ 'audio_packet-loss': 1,
474
+ }
475
+ );
476
+
477
+ // But the counters should be tracked separately
478
+ assert.equal(mediaProperties.mediaIssueCounters['audio_packet-loss'], 3);
479
+ assert.equal(mediaProperties.mediaIssueCounters['video_freeze'], 2);
480
+
481
+ sendBehavioralMetricStub.resetHistory();
482
+
483
+ clock.tick(5 * 60 * 1000); // Advance time by 5 minutes to expire throttle
484
+
485
+ assert.calledOnceWithExactly(
486
+ sendBehavioralMetricStub,
487
+ BEHAVIORAL_METRICS.MEDIA_ISSUE_DETECTED,
488
+ {
489
+ correlationId,
490
+ video_freeze: 2,
491
+ 'audio_packet-loss': 3,
492
+ }
493
+ );
494
+ });
495
+
496
+ it('should flush throttled metrics when unsetPeerConnection is called', () => {
497
+ const issueType = 'share';
498
+ const issueSubType = 'connection-lost';
499
+ const correlationId = 'test-correlation-id';
500
+
501
+ // Send metrics multiple times
502
+ mediaProperties.sendMediaIssueMetric(issueType, issueSubType, correlationId);
503
+ mediaProperties.sendMediaIssueMetric(issueType, issueSubType, correlationId);
504
+
505
+ // First call should go through immediately
506
+ assert.calledOnceWithExactly(
507
+ sendBehavioralMetricStub,
508
+ BEHAVIORAL_METRICS.MEDIA_ISSUE_DETECTED,
509
+ {
510
+ correlationId,
511
+ 'share_connection-lost': 1,
512
+ }
513
+ );
514
+ sendBehavioralMetricStub.resetHistory();
515
+
516
+ // Call unsetPeerConnection which should flush throttled metrics
517
+ mediaProperties.unsetPeerConnection();
518
+
519
+ assert.calledOnceWithExactly(
520
+ sendBehavioralMetricStub,
521
+ BEHAVIORAL_METRICS.MEDIA_ISSUE_DETECTED,
522
+ {
523
+ correlationId,
524
+ 'share_connection-lost': 1,
525
+ }
526
+ );
527
+ });
528
+ });
392
529
  });
@@ -38,6 +38,7 @@ describe('plugin-meetings', () => {
38
38
  isManualCaptionActive: null,
39
39
  isPremiseRecordingEnabled: null,
40
40
  isSaveTranscriptsEnabled: null,
41
+ isSpokenLanguageAutoDetectionEnabled: null,
41
42
  isWebexAssistantActive: null,
42
43
  canViewCaptionPanel: null,
43
44
  isRealTimeTranslationEnabled: null,
@@ -151,6 +152,7 @@ describe('plugin-meetings', () => {
151
152
  'isManualCaptionActive',
152
153
  'isPremiseRecordingEnabled',
153
154
  'isSaveTranscriptsEnabled',
155
+ 'isSpokenLanguageAutoDetectionEnabled',
154
156
  'isWebexAssistantActive',
155
157
  'canViewCaptionPanel',
156
158
  'isRealTimeTranslationEnabled',