@webex/plugin-meetings 3.9.0-webinar5k.1 → 3.9.0

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 (83) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.js +16 -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 +40 -328
  8. package/dist/locus-info/index.js.map +1 -1
  9. package/dist/meeting/in-meeting-actions.js +6 -0
  10. package/dist/meeting/in-meeting-actions.js.map +1 -1
  11. package/dist/meeting/index.js +196 -160
  12. package/dist/meeting/index.js.map +1 -1
  13. package/dist/meeting/muteState.js +5 -2
  14. package/dist/meeting/muteState.js.map +1 -1
  15. package/dist/meeting/type.js +7 -0
  16. package/dist/meeting/type.js.map +1 -0
  17. package/dist/meeting/util.js +79 -10
  18. package/dist/meeting/util.js.map +1 -1
  19. package/dist/meetings/index.js +37 -39
  20. package/dist/meetings/index.js.map +1 -1
  21. package/dist/member/types.js.map +1 -1
  22. package/dist/members/collection.js +0 -13
  23. package/dist/members/collection.js.map +1 -1
  24. package/dist/members/index.js +21 -40
  25. package/dist/members/index.js.map +1 -1
  26. package/dist/members/util.js.map +1 -1
  27. package/dist/multistream/mediaRequestManager.js +1 -1
  28. package/dist/multistream/mediaRequestManager.js.map +1 -1
  29. package/dist/multistream/remoteMedia.js +34 -5
  30. package/dist/multistream/remoteMedia.js.map +1 -1
  31. package/dist/multistream/remoteMediaGroup.js +42 -2
  32. package/dist/multistream/remoteMediaGroup.js.map +1 -1
  33. package/dist/types/constants.d.ts +16 -0
  34. package/dist/types/locus-info/index.d.ts +3 -102
  35. package/dist/types/meeting/in-meeting-actions.d.ts +6 -0
  36. package/dist/types/meeting/index.d.ts +23 -28
  37. package/dist/types/meeting/type.d.ts +9 -0
  38. package/dist/types/meeting/util.d.ts +6 -3
  39. package/dist/types/member/types.d.ts +0 -1
  40. package/dist/types/members/collection.d.ts +0 -6
  41. package/dist/types/members/index.d.ts +7 -16
  42. package/dist/types/members/util.d.ts +2 -1
  43. package/dist/types/multistream/remoteMedia.d.ts +20 -1
  44. package/dist/types/multistream/remoteMediaGroup.d.ts +11 -0
  45. package/dist/webinar/index.js +1 -1
  46. package/package.json +22 -24
  47. package/src/constants.ts +16 -2
  48. package/src/locus-info/index.ts +39 -409
  49. package/src/meeting/in-meeting-actions.ts +13 -0
  50. package/src/meeting/index.ts +92 -63
  51. package/src/meeting/muteState.ts +6 -2
  52. package/src/meeting/type.ts +9 -0
  53. package/src/meeting/util.ts +93 -19
  54. package/src/meetings/index.ts +6 -19
  55. package/src/member/types.ts +0 -1
  56. package/src/members/collection.ts +0 -11
  57. package/src/members/index.ts +10 -33
  58. package/src/members/util.ts +2 -1
  59. package/src/multistream/mediaRequestManager.ts +7 -7
  60. package/src/multistream/remoteMedia.ts +34 -4
  61. package/src/multistream/remoteMediaGroup.ts +37 -2
  62. package/test/unit/spec/locus-info/index.js +8 -365
  63. package/test/unit/spec/meeting/in-meeting-actions.ts +6 -0
  64. package/test/unit/spec/meeting/index.js +254 -38
  65. package/test/unit/spec/meeting/utils.js +122 -1
  66. package/test/unit/spec/meetings/index.js +2 -0
  67. package/test/unit/spec/members/index.js +37 -1
  68. package/test/unit/spec/multistream/mediaRequestManager.ts +19 -6
  69. package/test/unit/spec/multistream/remoteMedia.ts +66 -2
  70. package/dist/hashTree/constants.js +0 -23
  71. package/dist/hashTree/constants.js.map +0 -1
  72. package/dist/hashTree/hashTree.js +0 -516
  73. package/dist/hashTree/hashTree.js.map +0 -1
  74. package/dist/hashTree/hashTreeParser.js +0 -521
  75. package/dist/hashTree/hashTreeParser.js.map +0 -1
  76. package/dist/types/hashTree/constants.d.ts +0 -8
  77. package/dist/types/hashTree/hashTree.d.ts +0 -128
  78. package/dist/types/hashTree/hashTreeParser.d.ts +0 -152
  79. package/src/hashTree/constants.ts +0 -12
  80. package/src/hashTree/hashTree.ts +0 -460
  81. package/src/hashTree/hashTreeParser.ts +0 -556
  82. package/test/unit/spec/hashTree/hashTree.ts +0 -394
  83. package/test/unit/spec/hashTree/hashTreeParser.ts +0 -156
@@ -56,6 +56,7 @@ import * as MeetingRequestImport from '@webex/plugin-meetings/src/meeting/reques
56
56
  import LocusInfo from '@webex/plugin-meetings/src/locus-info';
57
57
  import MediaProperties from '@webex/plugin-meetings/src/media/properties';
58
58
  import MeetingUtil from '@webex/plugin-meetings/src/meeting/util';
59
+ import MembersUtil from '@webex/plugin-meetings/src/members/util';
59
60
  import MeetingsUtil from '@webex/plugin-meetings/src/meetings/util';
60
61
  import Media from '@webex/plugin-meetings/src/media/index';
61
62
  import ReconnectionManager from '@webex/plugin-meetings/src/reconnection-manager';
@@ -244,6 +245,7 @@ describe('plugin-meetings', () => {
244
245
  });
245
246
 
246
247
  webex.internal.newMetrics.callDiagnosticMetrics.clearErrorCache = sinon.stub();
248
+ webex.internal.newMetrics.callDiagnosticMetrics.clearEventLimitsForCorrelationId = sinon.stub();
247
249
  webex.internal.support.submitLogs = sinon.stub().returns(Promise.resolve());
248
250
  webex.internal.services = {get: sinon.stub().returns('locus-url')};
249
251
  webex.credentials.getOrgId = sinon.stub().returns('fake-org-id');
@@ -368,6 +370,35 @@ describe('plugin-meetings', () => {
368
370
  assert.instanceOf(meeting.simultaneousInterpretation, SimultaneousInterpretation);
369
371
  assert.instanceOf(meeting.webinar, Webinar);
370
372
  });
373
+
374
+ it('should call the callback with the meeting that has id already set', () => {
375
+ let meetingIdFromCallback;
376
+ // check that the meeting id is already set correctly at the time when the callback is called
377
+ const meetingCreationCallback = sinon.stub().callsFake((meeting) => {
378
+ meetingIdFromCallback = meeting.id;
379
+ });
380
+
381
+ meeting = new Meeting(
382
+ {
383
+ userId: uuid1,
384
+ resource: uuid2,
385
+ deviceUrl: uuid3,
386
+ locus: {url: url1},
387
+ destination: testDestination,
388
+ destinationType: DESTINATION_TYPE.MEETING_ID,
389
+ correlationId,
390
+ selfId: uuid1,
391
+ },
392
+ {
393
+ parent: webex,
394
+ },
395
+ meetingCreationCallback
396
+ );
397
+ assert.exists(meeting.id);
398
+ assert.calledOnceWithExactly(meetingCreationCallback, meeting);
399
+ assert.equal(meeting.id, meetingIdFromCallback);
400
+ });
401
+
371
402
  it('creates MediaRequestManager instances', () => {
372
403
  assert.instanceOf(meeting.mediaRequestManagers.audio, MediaRequestManager);
373
404
  assert.instanceOf(meeting.mediaRequestManagers.video, MediaRequestManager);
@@ -454,6 +485,18 @@ describe('plugin-meetings', () => {
454
485
  });
455
486
  });
456
487
 
488
+ it('pstnCorrelationId getter/setter should work correctly', () => {
489
+ const testPstnCorrelationId = uuid.v4();
490
+
491
+ meeting.pstnCorrelationId = testPstnCorrelationId;
492
+ assert.equal(meeting.pstnCorrelationId, testPstnCorrelationId);
493
+ assert.equal(meeting.callStateForMetrics.pstnCorrelationId, testPstnCorrelationId);
494
+
495
+ meeting.pstnCorrelationId = undefined;
496
+ assert.equal(meeting.pstnCorrelationId, undefined);
497
+ assert.equal(meeting.callStateForMetrics.pstnCorrelationId, undefined);
498
+ });
499
+
457
500
  describe('creates ReceiveSlot manager instance', () => {
458
501
  let mockReceiveSlotManagerCtor;
459
502
  let providedCreateSlotCallback;
@@ -581,7 +624,6 @@ describe('plugin-meetings', () => {
581
624
  assert.isFalse(meeting.isLocusCall());
582
625
  });
583
626
  });
584
-
585
627
  describe('#invite', () => {
586
628
  it('should have #invite', () => {
587
629
  assert.exists(meeting.invite);
@@ -592,8 +634,6 @@ describe('plugin-meetings', () => {
592
634
  it('should proxy members #addMember and return a promise', async () => {
593
635
  const invite = meeting.invite(uuid1, false);
594
636
 
595
- assert.exists(invite.then);
596
- await invite;
597
637
  assert.calledOnce(meeting.members.addMember);
598
638
  assert.calledWith(meeting.members.addMember, uuid1, false);
599
639
  });
@@ -1949,21 +1989,25 @@ describe('plugin-meetings', () => {
1949
1989
  });
1950
1990
  });
1951
1991
 
1952
- it('should post error event if failed', async () => {
1992
+ it('should handle join failure', async () => {
1953
1993
  MeetingUtil.isPinOrGuest = sinon.stub().returns(false);
1994
+ webex.internal.newMetrics.submitClientEvent = sinon.stub();
1995
+
1954
1996
  await meeting.join().catch(() => {
1955
- assert.deepEqual(
1956
- webex.internal.newMetrics.submitClientEvent.getCall(1).args[0].name,
1957
- 'client.locus.join.response'
1958
- );
1959
- assert.match(
1960
- webex.internal.newMetrics.submitClientEvent.getCall(1).args[0].options.rawError,
1997
+ assert.calledOnce(MeetingUtil.joinMeeting);
1998
+
1999
+ // Assert that client.locus.join.response error event is not sent from this function, it is now emitted from MeetingUtil.joinMeeting
2000
+ assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
2001
+ assert.calledWithMatch(
2002
+ webex.internal.newMetrics.submitClientEvent,
1961
2003
  {
1962
- code: 2,
1963
- error: null,
1964
- joinOptions: {},
1965
- sdkMessage:
1966
- 'There was an issue joining the meeting, meeting could be in a bad state.',
2004
+ name: 'client.call.initiated',
2005
+ payload: {
2006
+ trigger: 'user-interaction',
2007
+ isRoapCallEnabled: true,
2008
+ pstnAudioType: undefined
2009
+ },
2010
+ options: {meetingId: meeting.id},
1967
2011
  }
1968
2012
  );
1969
2013
  });
@@ -6498,12 +6542,17 @@ describe('plugin-meetings', () => {
6498
6542
  const DIAL_IN_URL = meeting.dialInUrl;
6499
6543
 
6500
6544
  assert.calledWith(meeting.meetingRequest.dialIn, {
6501
- correlationId: meeting.correlationId,
6545
+ correlationId: meeting.pstnCorrelationId,
6502
6546
  dialInUrl: DIAL_IN_URL,
6503
6547
  locusUrl: meeting.locusUrl,
6504
6548
  clientUrl: meeting.deviceUrl,
6505
6549
  });
6506
6550
  assert.notCalled(meeting.meetingRequest.dialOut);
6551
+
6552
+ // Verify pstnCorrelationId was set
6553
+ assert.exists(meeting.pstnCorrelationId);
6554
+ assert.notEqual(meeting.pstnCorrelationId, meeting.correlationId);
6555
+ const firstPstnCorrelationId = meeting.pstnCorrelationId
6507
6556
 
6508
6557
  meeting.meetingRequest.dialIn.resetHistory();
6509
6558
 
@@ -6511,12 +6560,18 @@ describe('plugin-meetings', () => {
6511
6560
  await meeting.usePhoneAudio();
6512
6561
 
6513
6562
  assert.calledWith(meeting.meetingRequest.dialIn, {
6514
- correlationId: meeting.correlationId,
6563
+ correlationId: meeting.pstnCorrelationId,
6515
6564
  dialInUrl: DIAL_IN_URL,
6516
6565
  locusUrl: meeting.locusUrl,
6517
6566
  clientUrl: meeting.deviceUrl,
6518
6567
  });
6519
6568
  assert.notCalled(meeting.meetingRequest.dialOut);
6569
+ // A new PSTN correlationId should be generated for the second attempt
6570
+ assert.notEqual(
6571
+ meeting.pstnCorrelationId,
6572
+ firstPstnCorrelationId,
6573
+ 'pstnCorrelationId should be regenerated on each dial-in attempt'
6574
+ );
6520
6575
  });
6521
6576
 
6522
6577
  it('given a phone number, triggers dial-out, delegating request to meetingRequest correctly', async () => {
@@ -6526,7 +6581,7 @@ describe('plugin-meetings', () => {
6526
6581
  const DIAL_OUT_URL = meeting.dialOutUrl;
6527
6582
 
6528
6583
  assert.calledWith(meeting.meetingRequest.dialOut, {
6529
- correlationId: meeting.correlationId,
6584
+ correlationId: meeting.pstnCorrelationId,
6530
6585
  dialOutUrl: DIAL_OUT_URL,
6531
6586
  locusUrl: meeting.locusUrl,
6532
6587
  clientUrl: meeting.deviceUrl,
@@ -6534,49 +6589,126 @@ describe('plugin-meetings', () => {
6534
6589
  });
6535
6590
  assert.notCalled(meeting.meetingRequest.dialIn);
6536
6591
 
6592
+ // Verify pstnCorrelationId was set
6593
+ assert.exists(meeting.pstnCorrelationId);
6594
+ assert.notEqual(meeting.pstnCorrelationId, meeting.correlationId);
6595
+ const firstPstnCorrelationId = meeting.pstnCorrelationId;
6596
+
6537
6597
  meeting.meetingRequest.dialOut.resetHistory();
6538
6598
 
6539
6599
  // try again. the dial out urls should match
6540
6600
  await meeting.usePhoneAudio(phoneNumber);
6541
6601
 
6542
6602
  assert.calledWith(meeting.meetingRequest.dialOut, {
6543
- correlationId: meeting.correlationId,
6603
+ correlationId: meeting.pstnCorrelationId,
6544
6604
  dialOutUrl: DIAL_OUT_URL,
6545
6605
  locusUrl: meeting.locusUrl,
6546
6606
  clientUrl: meeting.deviceUrl,
6547
6607
  phoneNumber,
6548
6608
  });
6549
6609
  assert.notCalled(meeting.meetingRequest.dialIn);
6610
+ // A new PSTN correlationId should be generated for the second attempt
6611
+ assert.notEqual(
6612
+ meeting.pstnCorrelationId,
6613
+ firstPstnCorrelationId,
6614
+ 'pstnCorrelationId should be regenerated on each dial-out attempt'
6615
+ );
6550
6616
  });
6551
6617
 
6552
- it('rejects if the request failed (dial in)', () => {
6553
- const error = 'something bad happened';
6618
+ it('rejects if the request failed (dial in)', async () => {
6619
+ const error = {error: {message: 'dial in failed'}, stack: 'error stack'};
6554
6620
 
6555
6621
  meeting.meetingRequest.dialIn = sinon.stub().returns(Promise.reject(error));
6556
6622
 
6557
- return meeting
6558
- .usePhoneAudio()
6559
- .then(() => Promise.reject(new Error('Promise resolved when it should have rejected')))
6560
- .catch((e) => {
6561
- assert.equal(e, error);
6562
-
6563
- return Promise.resolve();
6623
+ try {
6624
+ await meeting.usePhoneAudio();
6625
+ throw new Error('Promise resolved when it should have rejected');
6626
+ } catch (e) {
6627
+ assert.equal(e, error);
6628
+
6629
+ // Verify behavioral metric was sent with dial_in_correlation_id
6630
+ assert.calledWith(Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.ADD_DIAL_IN_FAILURE, {
6631
+ correlation_id: meeting.correlationId,
6632
+ dial_in_url: meeting.dialInUrl,
6633
+ dial_in_correlation_id: sinon.match.string,
6634
+ locus_id: meeting.locusUrl.split('/').pop(),
6635
+ client_url: meeting.deviceUrl,
6636
+ reason: error.error.message,
6637
+ stack: error.stack,
6564
6638
  });
6639
+
6640
+ // Verify pstnCorrelationId was cleared after error
6641
+ assert.equal(meeting.pstnCorrelationId, undefined);
6642
+ }
6565
6643
  });
6566
6644
 
6567
6645
  it('rejects if the request failed (dial out)', async () => {
6568
- const error = 'something bad happened';
6646
+ const error = {error: {message: 'dial out failed'}, stack: 'error stack'};
6569
6647
 
6570
6648
  meeting.meetingRequest.dialOut = sinon.stub().returns(Promise.reject(error));
6571
6649
 
6572
- return meeting
6573
- .usePhoneAudio('+441234567890')
6574
- .then(() => Promise.reject(new Error('Promise resolved when it should have rejected')))
6575
- .catch((e) => {
6576
- assert.equal(e, error);
6577
-
6578
- return Promise.resolve();
6650
+ try {
6651
+ await meeting.usePhoneAudio('+441234567890');
6652
+ throw new Error('Promise resolved when it should have rejected');
6653
+ } catch (e) {
6654
+ assert.equal(e, error);
6655
+
6656
+ // Verify behavioral metric was sent with dial_out_correlation_id
6657
+ assert.calledWith(Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.ADD_DIAL_OUT_FAILURE, {
6658
+ correlation_id: meeting.correlationId,
6659
+ dial_out_url: meeting.dialOutUrl,
6660
+ dial_out_correlation_id: sinon.match.string,
6661
+ locus_id: meeting.locusUrl.split('/').pop(),
6662
+ client_url: meeting.deviceUrl,
6663
+ reason: error.error.message,
6664
+ stack: error.stack,
6579
6665
  });
6666
+
6667
+ // Verify pstnCorrelationId was cleared after error
6668
+ assert.equal(meeting.pstnCorrelationId, undefined);
6669
+ }
6670
+ });
6671
+ });
6672
+
6673
+ describe('#disconnectPhoneAudio', () => {
6674
+ beforeEach(() => {
6675
+ // Mock the MeetingUtil.disconnectPhoneAudio method
6676
+ sinon.stub(MeetingUtil, 'disconnectPhoneAudio').resolves();
6677
+ meeting.dialInUrl = 'dialin:///test-dial-in-url';
6678
+ meeting.dialOutUrl = 'dialout:///test-dial-out-url';
6679
+ meeting.dialInDeviceStatus = 'JOINED';
6680
+ meeting.dialOutDeviceStatus = 'JOINED';
6681
+ });
6682
+
6683
+ afterEach(() => {
6684
+ MeetingUtil.disconnectPhoneAudio.restore();
6685
+ });
6686
+
6687
+ it('should disconnect phone audio and clear pstnCorrelationId', async () => {
6688
+ meeting.pstnCorrelationId = 'test-pstn-correlation-id';
6689
+
6690
+ await meeting.disconnectPhoneAudio();
6691
+
6692
+ // Verify that pstnCorrelationId is cleared
6693
+ assert.equal(meeting.pstnCorrelationId, undefined);
6694
+
6695
+ // Verify that MeetingUtil.disconnectPhoneAudio was called for both dial-in and dial-out
6696
+ assert.calledTwice(MeetingUtil.disconnectPhoneAudio);
6697
+ assert.calledWith(MeetingUtil.disconnectPhoneAudio, meeting, meeting.dialInUrl);
6698
+ assert.calledWith(MeetingUtil.disconnectPhoneAudio, meeting, meeting.dialOutUrl);
6699
+ });
6700
+
6701
+ it('should handle case when no PSTN connection is active', async () => {
6702
+ meeting.dialInDeviceStatus = 'IDLE';
6703
+ meeting.dialOutDeviceStatus = 'IDLE';
6704
+ meeting.pstnCorrelationId = 'test-pstn-correlation-id';
6705
+
6706
+ await meeting.disconnectPhoneAudio();
6707
+
6708
+ // Verify that pstnCorrelationId is still cleared even when no phone connection is active
6709
+ assert.equal(meeting.pstnCorrelationId, undefined);
6710
+ // And verify no disconnect was attempted
6711
+ assert.notCalled(MeetingUtil.disconnectPhoneAudio);
6580
6712
  });
6581
6713
  });
6582
6714
 
@@ -8093,6 +8225,7 @@ describe('plugin-meetings', () => {
8093
8225
 
8094
8226
  meeting.requestScreenShareFloor = sinon.stub().resolves({});
8095
8227
  meeting.releaseScreenShareFloor = sinon.stub().resolves({});
8228
+ webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
8096
8229
  meeting.mediaProperties.mediaDirection = {
8097
8230
  sendAudio: 'fake value', // using non-boolean here so that we can check that these values are untouched in tests
8098
8231
  sendVideo: 'fake value',
@@ -8174,6 +8307,12 @@ describe('plugin-meetings', () => {
8174
8307
  payload: {mediaType: 'share', shareInstanceId: meeting.localShareInstanceId},
8175
8308
  options: {meetingId: meeting.id},
8176
8309
  });
8310
+
8311
+ // ensure the share start timestamp is saved
8312
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
8313
+ key: 'internal.client.share.initiated',
8314
+ });
8315
+
8177
8316
  assert.equal(meeting.mediaProperties.mediaDirection.sendShare, true);
8178
8317
 
8179
8318
  assert.equal(meeting.shareCAEventSentStatus.transmitStart, false);
@@ -8192,6 +8331,11 @@ describe('plugin-meetings', () => {
8192
8331
  options: {meetingId: meeting.id},
8193
8332
  });
8194
8333
 
8334
+ // ensure the share start timestamp is saved
8335
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
8336
+ key: 'internal.client.share.initiated',
8337
+ });
8338
+
8195
8339
  assert.calledWith(
8196
8340
  meeting.sendSlotManager.getSlot(MediaType.AudioSlides).publishStream,
8197
8341
  stream
@@ -10465,6 +10609,8 @@ describe('plugin-meetings', () => {
10465
10609
  meeting.mediaProperties = {mediaDirection: {sendShare: true}};
10466
10610
  meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
10467
10611
  (meeting.deviceUrl = 'deviceUrl.com'), (meeting.localShareInstanceId = '1234-5678');
10612
+ webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
10613
+ webex.internal.newMetrics.callDiagnosticLatencies.getShareDuration = sinon.stub().returns(1000);
10468
10614
  });
10469
10615
  it('should call changeMeetingFloor()', async () => {
10470
10616
  meeting.screenShareFloorState = 'GRANTED';
@@ -10482,6 +10628,22 @@ describe('plugin-meetings', () => {
10482
10628
  assert.exists(share.then);
10483
10629
  await share;
10484
10630
  assert.calledOnce(meeting.meetingRequest.changeMeetingFloor);
10631
+
10632
+ // ensure the share stop timestamp is saved
10633
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
10634
+ key: 'internal.client.share.stopped',
10635
+ });
10636
+
10637
+ // ensure the CA share stopped metric is submitted with duration
10638
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
10639
+ name: 'client.share.stopped',
10640
+ payload: {
10641
+ mediaType: 'share',
10642
+ shareInstanceId: meeting.localShareInstanceId,
10643
+ shareDuration: 1000,
10644
+ },
10645
+ options: {meetingId: meeting.id},
10646
+ });
10485
10647
  });
10486
10648
  it('should not call changeMeetingFloor() if someone else already has the floor', async () => {
10487
10649
  // change selfId so that it doesn't match the beneficiary id from meeting.locusInfo.mediaShares
@@ -12054,6 +12216,7 @@ describe('plugin-meetings', () => {
12054
12216
  meeting.locusInfo.self = {url: url1};
12055
12217
  meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
12056
12218
  meeting.deviceUrl = 'deviceUrl.com';
12219
+ webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
12057
12220
  });
12058
12221
  it('should have #startWhiteboardShare', () => {
12059
12222
  assert.exists(meeting.startWhiteboardShare);
@@ -12081,6 +12244,11 @@ describe('plugin-meetings', () => {
12081
12244
  payload: {mediaType: 'whiteboard'},
12082
12245
  options: {meetingId: meeting.id},
12083
12246
  });
12247
+
12248
+ // ensure the share start timestamp is saved
12249
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
12250
+ key: 'internal.client.share.initiated',
12251
+ });
12084
12252
  });
12085
12253
  });
12086
12254
  describe('#stopWhiteboardShare', () => {
@@ -12092,6 +12260,8 @@ describe('plugin-meetings', () => {
12092
12260
  meeting.locusInfo.self = {url: url1};
12093
12261
  meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
12094
12262
  meeting.deviceUrl = 'deviceUrl.com';
12263
+ webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
12264
+ webex.internal.newMetrics.callDiagnosticLatencies.getShareDuration = sinon.stub().returns(1000);
12095
12265
  });
12096
12266
  it('should stop the whiteboard share', async () => {
12097
12267
  const whiteboardShare = meeting.stopWhiteboardShare();
@@ -12106,6 +12276,21 @@ describe('plugin-meetings', () => {
12106
12276
  uri: url1,
12107
12277
  });
12108
12278
  assert.calledOnce(meeting.meetingRequest.changeMeetingFloor);
12279
+
12280
+ // ensure the share stop timestamp is saved
12281
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
12282
+ key: 'internal.client.share.stopped',
12283
+ });
12284
+
12285
+ // ensure the CA share stopped metric is submitted with duration
12286
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
12287
+ name: 'client.share.stopped',
12288
+ payload: {
12289
+ mediaType: 'whiteboard',
12290
+ shareDuration: 1000,
12291
+ },
12292
+ options: {meetingId: meeting.id},
12293
+ });
12109
12294
  });
12110
12295
  });
12111
12296
  });
@@ -12404,7 +12589,7 @@ describe('plugin-meetings', () => {
12404
12589
  activeSharingId.whiteboard = beneficiaryId;
12405
12590
 
12406
12591
  eventTrigger.share.push(
12407
- meeting.webinar.selfIsAttendee
12592
+ meeting.webinar.selfIsAttendee || meeting.guest
12408
12593
  ? {
12409
12594
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
12410
12595
  functionName: 'remoteShare',
@@ -12423,7 +12608,8 @@ describe('plugin-meetings', () => {
12423
12608
  }
12424
12609
  );
12425
12610
 
12426
- shareStatus = meeting.webinar.selfIsAttendee
12611
+ shareStatus =
12612
+ meeting.webinar.selfIsAttendee || meeting.guest
12427
12613
  ? SHARE_STATUS.REMOTE_SHARE_ACTIVE
12428
12614
  : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
12429
12615
  }
@@ -12641,6 +12827,36 @@ describe('plugin-meetings', () => {
12641
12827
  });
12642
12828
  });
12643
12829
 
12830
+ describe('Whiteboard Share - User is guest', () => {
12831
+ it('User receives a remote share instead of whiteboard share', () => {
12832
+ // Set the guest flag
12833
+ meeting.guest = true;
12834
+
12835
+ // Step 1: Start sharing whiteboard A
12836
+ const data1 = generateData(
12837
+ blankPayload, // Initial payload
12838
+ true, // isGranting: Granting share
12839
+ false, // isContent: Whiteboard (not content)
12840
+ USER_IDS.REMOTE_A, // Beneficiary ID: Remote user A
12841
+ RESOURCE_URLS.WHITEBOARD_A // Resource URL: Whiteboard A
12842
+ );
12843
+
12844
+ // Step 2: Stop sharing whiteboard A
12845
+ const data2 = generateData(
12846
+ data1.payload, // Updated payload from Step 1
12847
+ false, // isGranting: Stopping share
12848
+ false, // isContent: Whiteboard
12849
+ USER_IDS.REMOTE_A // Beneficiary ID: Remote user A
12850
+ );
12851
+
12852
+ // Validate the payload changes and status updates
12853
+ payloadTestHelper([data1]);
12854
+
12855
+ // Specific assertions for guest
12856
+ assert.equal(meeting.shareStatus, SHARE_STATUS.REMOTE_SHARE_ACTIVE);
12857
+ });
12858
+ });
12859
+
12644
12860
  describe('Whiteboard A --> Whiteboard B', () => {
12645
12861
  it('Scenario #1: you share both whiteboards', () => {
12646
12862
  const data1 = generateData(
@@ -3,12 +3,14 @@ import sinon from 'sinon';
3
3
  import {assert} from '@webex/test-helper-chai';
4
4
  import Meetings from '@webex/plugin-meetings';
5
5
  import MeetingUtil from '@webex/plugin-meetings/src/meeting/util';
6
- import {LOCAL_SHARE_ERRORS} from '@webex/plugin-meetings/src/constants';
6
+ import {LOCAL_SHARE_ERRORS, PASSWORD_STATUS} from '@webex/plugin-meetings/src/constants';
7
7
  import LoggerProxy from '@webex/plugin-meetings/src/common/logs/logger-proxy';
8
8
  import LoggerConfig from '@webex/plugin-meetings/src/common/logs/logger-config';
9
9
  import {SELF_POLICY, IP_VERSION} from '@webex/plugin-meetings/src/constants';
10
10
  import MockWebex from '@webex/test-helper-mock-webex';
11
11
  import * as BrowserDetectionModule from '@webex/plugin-meetings/src/common/browser-detection';
12
+ import PasswordError from '@webex/plugin-meetings/src/common/errors/password-error';
13
+ import CaptchaError from '@webex/plugin-meetings/src/common/errors/captcha-error';
12
14
 
13
15
  describe('plugin-meetings', () => {
14
16
  let webex;
@@ -57,6 +59,10 @@ describe('plugin-meetings', () => {
57
59
  meeting.getWebexObject = sinon.stub().returns(webex);
58
60
  meeting.simultaneousInterpretation = {cleanUp: sinon.stub()};
59
61
  meeting.trigger = sinon.stub();
62
+ meeting.webex = webex;
63
+ meeting.webex.internal.newMetrics.callDiagnosticMetrics =
64
+ meeting.webex.internal.newMetrics.callDiagnosticMetrics || {};
65
+ meeting.webex.internal.newMetrics.callDiagnosticMetrics.clearEventLimitsForCorrelationId = sinon.stub();
60
66
  });
61
67
 
62
68
  afterEach(() => {
@@ -81,6 +87,10 @@ describe('plugin-meetings', () => {
81
87
  assert.calledOnce(meeting.breakouts.cleanUp);
82
88
  assert.calledOnce(meeting.simultaneousInterpretation.cleanUp);
83
89
  assert.calledOnce(webex.internal.device.meetingEnded);
90
+ assert.calledOnceWithExactly(
91
+ meeting.webex.internal.newMetrics.callDiagnosticMetrics.clearEventLimitsForCorrelationId,
92
+ meeting.correlationId
93
+ );
84
94
  });
85
95
 
86
96
  it('do clean up on meeting object with LLM disabled', async () => {
@@ -98,6 +108,10 @@ describe('plugin-meetings', () => {
98
108
  assert.calledOnce(meeting.breakouts.cleanUp);
99
109
  assert.calledOnce(meeting.simultaneousInterpretation.cleanUp);
100
110
  assert.calledOnce(webex.internal.device.meetingEnded);
111
+ assert.calledOnceWithExactly(
112
+ meeting.webex.internal.newMetrics.callDiagnosticMetrics.clearEventLimitsForCorrelationId,
113
+ meeting.correlationId
114
+ );
101
115
  });
102
116
 
103
117
  it('do clean up on meeting object with no config', async () => {
@@ -114,6 +128,10 @@ describe('plugin-meetings', () => {
114
128
  assert.calledOnce(meeting.breakouts.cleanUp);
115
129
  assert.calledOnce(meeting.simultaneousInterpretation.cleanUp);
116
130
  assert.calledOnce(webex.internal.device.meetingEnded);
131
+ assert.calledOnceWithExactly(
132
+ meeting.webex.internal.newMetrics.callDiagnosticMetrics.clearEventLimitsForCorrelationId,
133
+ meeting.correlationId
134
+ );
117
135
  });
118
136
  });
119
137
 
@@ -622,6 +640,28 @@ describe('plugin-meetings', () => {
622
640
 
623
641
  assert.equal(parameter.locusClusterUrl, 'locusClusterUrl');
624
642
  });
643
+
644
+ it('should post client event with error when join fails', async () => {
645
+ const joinError = new Error('Join failed');
646
+ meeting.meetingRequest.joinMeeting.rejects(joinError);
647
+ meeting.meetingInfo = { meetingLookupUrl: 'test-lookup-url' };
648
+
649
+ try {
650
+ await MeetingUtil.joinMeeting(meeting, {});
651
+ assert.fail('Expected joinMeeting to throw an error');
652
+ } catch (error) {
653
+ assert.equal(error, joinError);
654
+
655
+ // Verify error client event was submitted
656
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
657
+ name: 'client.locus.join.response',
658
+ payload: {
659
+ identifiers: { meetingLookupUrl: 'test-lookup-url' },
660
+ },
661
+ options: { meetingId: meeting.id, rawError: joinError },
662
+ });
663
+ }
664
+ });
625
665
  });
626
666
 
627
667
  describe('joinMeetingOptions', () => {
@@ -661,6 +701,82 @@ describe('plugin-meetings', () => {
661
701
  joinMeetingSpy.restore();
662
702
  }
663
703
  });
704
+
705
+ it('should submit client event and reject with PasswordError when password is required', async () => {
706
+ const meeting = {
707
+ id: 'meeting-id',
708
+ passwordStatus: PASSWORD_STATUS.REQUIRED,
709
+ resourceId: null,
710
+ requiredCaptcha: null,
711
+ getWebexObject: sinon.stub().returns(webex),
712
+ };
713
+
714
+ try {
715
+ await MeetingUtil.joinMeetingOptions(meeting, {});
716
+ assert.fail('Expected joinMeetingOptions to throw PasswordError');
717
+ } catch (error) {
718
+ assert.instanceOf(error, PasswordError);
719
+
720
+ // Verify client event was submitted with error details
721
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
722
+ name: 'client.meetinginfo.response',
723
+ options: {
724
+ meetingId: meeting.id,
725
+ },
726
+ payload: {
727
+ errors: [
728
+ {
729
+ fatal: false,
730
+ category: 'expected',
731
+ name: 'other',
732
+ shownToUser: false,
733
+ errorCode: error.code,
734
+ errorDescription: error.name,
735
+ rawErrorMessage: error.sdkMessage,
736
+ },
737
+ ],
738
+ },
739
+ });
740
+ }
741
+ });
742
+
743
+ it('should submit client event and reject with CaptchaError when captcha is required', async () => {
744
+ const meeting = {
745
+ id: 'meeting-id',
746
+ passwordStatus: null,
747
+ resourceId: null,
748
+ requiredCaptcha: {captchaId: 'test-captcha'},
749
+ getWebexObject: sinon.stub().returns(webex),
750
+ };
751
+
752
+ try {
753
+ await MeetingUtil.joinMeetingOptions(meeting, {});
754
+ assert.fail('Expected joinMeetingOptions to throw CaptchaError');
755
+ } catch (error) {
756
+ assert.instanceOf(error, CaptchaError);
757
+
758
+ // Verify client event was submitted with error details
759
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
760
+ name: 'client.meetinginfo.response',
761
+ options: {
762
+ meetingId: meeting.id,
763
+ },
764
+ payload: {
765
+ errors: [
766
+ {
767
+ fatal: false,
768
+ category: 'expected',
769
+ name: 'other',
770
+ shownToUser: false,
771
+ errorCode: error.code,
772
+ errorDescription: error.name,
773
+ rawErrorMessage: error.sdkMessage,
774
+ },
775
+ ],
776
+ },
777
+ });
778
+ }
779
+ });
664
780
  });
665
781
 
666
782
  describe('getUserDisplayHintsFromLocusInfo', () => {
@@ -850,6 +966,11 @@ describe('plugin-meetings', () => {
850
966
  {functionName: 'isClosedCaptionActive', displayHint: 'CAPTION_STATUS_ACTIVE'},
851
967
  {functionName: 'canStartManualCaption', displayHint: 'MANUAL_CAPTION_START'},
852
968
  {functionName: 'canStopManualCaption', displayHint: 'MANUAL_CAPTION_STOP'},
969
+
970
+ {functionName: 'isLocalRecordingStarted',displayHint:'LOCAL_RECORDING_STATUS_STARTED'},
971
+ {functionName: 'isLocalRecordingStopped', displayHint: 'LOCAL_RECORDING_STATUS_STOPPED'},
972
+ {functionName: 'isLocalRecordingPaused', displayHint: 'LOCAL_RECORDING_STATUS_PAUSED'},
973
+
853
974
  {functionName: 'isManualCaptionActive', displayHint: 'MANUAL_CAPTION_STATUS_ACTIVE'},
854
975
  {functionName: 'isWebexAssistantActive', displayHint: 'WEBEX_ASSISTANT_STATUS_ACTIVE'},
855
976
  {functionName: 'canViewCaptionPanel', displayHint: 'ENABLE_CAPTION_PANEL'},
@@ -189,6 +189,7 @@ describe('plugin-meetings', () => {
189
189
  },
190
190
  callDiagnosticMetrics: {
191
191
  clearErrorCache: sinon.stub(),
192
+ clearEventLimits: sinon.stub(),
192
193
  },
193
194
  },
194
195
  });
@@ -1716,6 +1717,7 @@ describe('plugin-meetings', () => {
1716
1717
  {file: 'meetings', function: 'fetchMeetingInfo'},
1717
1718
  'meeting:meetingInfoAvailable'
1718
1719
  );
1720
+ assert.equal(webex.meetings.meetingCollection.get(meeting.id), meeting);
1719
1721
  };
1720
1722
 
1721
1723
  it('creates the meeting from a successful meeting info fetch promise testing', async () => {