@webex/plugin-meetings 3.8.1-next.42 → 3.8.1-next.44

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.
@@ -197,6 +197,17 @@ const MeetingUtil = {
197
197
  });
198
198
 
199
199
  return parsed;
200
+ })
201
+ .catch((err) => {
202
+ webex.internal.newMetrics.submitClientEvent({
203
+ name: 'client.locus.join.response',
204
+ payload: {
205
+ identifiers: {meetingLookupUrl: meeting.meetingInfo?.meetingLookupUrl},
206
+ },
207
+ options: {meetingId: meeting.id, rawError: err},
208
+ });
209
+
210
+ throw err;
200
211
  });
201
212
  },
202
213
 
@@ -332,10 +343,57 @@ const MeetingUtil = {
332
343
  meeting.resourceId = meeting.resourceId || options.resourceId;
333
344
 
334
345
  if (meeting.requiredCaptcha) {
335
- return Promise.reject(new CaptchaError());
346
+ const errorToThrow = new CaptchaError();
347
+
348
+ // @ts-ignore
349
+ webex.internal.newMetrics.submitClientEvent({
350
+ name: 'client.meetinginfo.response',
351
+ options: {
352
+ meetingId: meeting.id,
353
+ },
354
+ payload: {
355
+ errors: [
356
+ {
357
+ fatal: false,
358
+ category: 'expected',
359
+ name: 'other',
360
+ shownToUser: false,
361
+ errorCode: errorToThrow.code,
362
+ errorDescription: errorToThrow.name,
363
+ rawErrorMessage: errorToThrow.sdkMessage,
364
+ },
365
+ ],
366
+ },
367
+ });
368
+
369
+ return Promise.reject(errorToThrow);
336
370
  }
371
+
337
372
  if (meeting.passwordStatus === PASSWORD_STATUS.REQUIRED) {
338
- return Promise.reject(new PasswordError());
373
+ const errorToThrow = new PasswordError();
374
+
375
+ // @ts-ignore
376
+ webex.internal.newMetrics.submitClientEvent({
377
+ name: 'client.meetinginfo.response',
378
+ options: {
379
+ meetingId: meeting.id,
380
+ },
381
+ payload: {
382
+ errors: [
383
+ {
384
+ fatal: false,
385
+ category: 'expected',
386
+ name: 'other',
387
+ shownToUser: false,
388
+ errorCode: errorToThrow.code,
389
+ errorDescription: errorToThrow.name,
390
+ rawErrorMessage: errorToThrow.sdkMessage,
391
+ },
392
+ ],
393
+ },
394
+ });
395
+
396
+ return Promise.reject(errorToThrow);
339
397
  }
340
398
 
341
399
  if (options.pin) {
@@ -485,6 +485,18 @@ describe('plugin-meetings', () => {
485
485
  });
486
486
  });
487
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
+
488
500
  describe('creates ReceiveSlot manager instance', () => {
489
501
  let mockReceiveSlotManagerCtor;
490
502
  let providedCreateSlotCallback;
@@ -1977,21 +1989,25 @@ describe('plugin-meetings', () => {
1977
1989
  });
1978
1990
  });
1979
1991
 
1980
- it('should post error event if failed', async () => {
1992
+ it('should handle join failure', async () => {
1981
1993
  MeetingUtil.isPinOrGuest = sinon.stub().returns(false);
1994
+ webex.internal.newMetrics.submitClientEvent = sinon.stub();
1995
+
1982
1996
  await meeting.join().catch(() => {
1983
- assert.deepEqual(
1984
- webex.internal.newMetrics.submitClientEvent.getCall(1).args[0].name,
1985
- 'client.locus.join.response'
1986
- );
1987
- assert.match(
1988
- 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,
1989
2003
  {
1990
- code: 2,
1991
- error: null,
1992
- joinOptions: {},
1993
- sdkMessage:
1994
- '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},
1995
2011
  }
1996
2012
  );
1997
2013
  });
@@ -6526,12 +6542,17 @@ describe('plugin-meetings', () => {
6526
6542
  const DIAL_IN_URL = meeting.dialInUrl;
6527
6543
 
6528
6544
  assert.calledWith(meeting.meetingRequest.dialIn, {
6529
- correlationId: meeting.correlationId,
6545
+ correlationId: meeting.pstnCorrelationId,
6530
6546
  dialInUrl: DIAL_IN_URL,
6531
6547
  locusUrl: meeting.locusUrl,
6532
6548
  clientUrl: meeting.deviceUrl,
6533
6549
  });
6534
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
6535
6556
 
6536
6557
  meeting.meetingRequest.dialIn.resetHistory();
6537
6558
 
@@ -6539,12 +6560,18 @@ describe('plugin-meetings', () => {
6539
6560
  await meeting.usePhoneAudio();
6540
6561
 
6541
6562
  assert.calledWith(meeting.meetingRequest.dialIn, {
6542
- correlationId: meeting.correlationId,
6563
+ correlationId: meeting.pstnCorrelationId,
6543
6564
  dialInUrl: DIAL_IN_URL,
6544
6565
  locusUrl: meeting.locusUrl,
6545
6566
  clientUrl: meeting.deviceUrl,
6546
6567
  });
6547
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
+ );
6548
6575
  });
6549
6576
 
6550
6577
  it('given a phone number, triggers dial-out, delegating request to meetingRequest correctly', async () => {
@@ -6554,7 +6581,7 @@ describe('plugin-meetings', () => {
6554
6581
  const DIAL_OUT_URL = meeting.dialOutUrl;
6555
6582
 
6556
6583
  assert.calledWith(meeting.meetingRequest.dialOut, {
6557
- correlationId: meeting.correlationId,
6584
+ correlationId: meeting.pstnCorrelationId,
6558
6585
  dialOutUrl: DIAL_OUT_URL,
6559
6586
  locusUrl: meeting.locusUrl,
6560
6587
  clientUrl: meeting.deviceUrl,
@@ -6562,49 +6589,126 @@ describe('plugin-meetings', () => {
6562
6589
  });
6563
6590
  assert.notCalled(meeting.meetingRequest.dialIn);
6564
6591
 
6592
+ // Verify pstnCorrelationId was set
6593
+ assert.exists(meeting.pstnCorrelationId);
6594
+ assert.notEqual(meeting.pstnCorrelationId, meeting.correlationId);
6595
+ const firstPstnCorrelationId = meeting.pstnCorrelationId;
6596
+
6565
6597
  meeting.meetingRequest.dialOut.resetHistory();
6566
6598
 
6567
6599
  // try again. the dial out urls should match
6568
6600
  await meeting.usePhoneAudio(phoneNumber);
6569
6601
 
6570
6602
  assert.calledWith(meeting.meetingRequest.dialOut, {
6571
- correlationId: meeting.correlationId,
6603
+ correlationId: meeting.pstnCorrelationId,
6572
6604
  dialOutUrl: DIAL_OUT_URL,
6573
6605
  locusUrl: meeting.locusUrl,
6574
6606
  clientUrl: meeting.deviceUrl,
6575
6607
  phoneNumber,
6576
6608
  });
6577
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
+ );
6578
6616
  });
6579
6617
 
6580
- it('rejects if the request failed (dial in)', () => {
6581
- 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'};
6582
6620
 
6583
6621
  meeting.meetingRequest.dialIn = sinon.stub().returns(Promise.reject(error));
6584
6622
 
6585
- return meeting
6586
- .usePhoneAudio()
6587
- .then(() => Promise.reject(new Error('Promise resolved when it should have rejected')))
6588
- .catch((e) => {
6589
- assert.equal(e, error);
6590
-
6591
- 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,
6592
6638
  });
6639
+
6640
+ // Verify pstnCorrelationId was cleared after error
6641
+ assert.equal(meeting.pstnCorrelationId, undefined);
6642
+ }
6593
6643
  });
6594
6644
 
6595
6645
  it('rejects if the request failed (dial out)', async () => {
6596
- const error = 'something bad happened';
6646
+ const error = {error: {message: 'dial out failed'}, stack: 'error stack'};
6597
6647
 
6598
6648
  meeting.meetingRequest.dialOut = sinon.stub().returns(Promise.reject(error));
6599
6649
 
6600
- return meeting
6601
- .usePhoneAudio('+441234567890')
6602
- .then(() => Promise.reject(new Error('Promise resolved when it should have rejected')))
6603
- .catch((e) => {
6604
- assert.equal(e, error);
6605
-
6606
- 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,
6607
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);
6608
6712
  });
6609
6713
  });
6610
6714
 
@@ -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;
@@ -638,6 +640,28 @@ describe('plugin-meetings', () => {
638
640
 
639
641
  assert.equal(parameter.locusClusterUrl, 'locusClusterUrl');
640
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
+ });
641
665
  });
642
666
 
643
667
  describe('joinMeetingOptions', () => {
@@ -677,6 +701,82 @@ describe('plugin-meetings', () => {
677
701
  joinMeetingSpy.restore();
678
702
  }
679
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
+ });
680
780
  });
681
781
 
682
782
  describe('getUserDisplayHintsFromLocusInfo', () => {