@webex/plugin-meetings 2.60.0-next.1 → 2.60.0-next.3

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 (62) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.js +1 -1
  4. package/dist/constants.js.map +1 -1
  5. package/dist/controls-options-manager/enums.js +2 -1
  6. package/dist/controls-options-manager/enums.js.map +1 -1
  7. package/dist/interpretation/index.js +1 -1
  8. package/dist/interpretation/siLanguage.js +1 -1
  9. package/dist/locus-info/parser.js +5 -5
  10. package/dist/locus-info/parser.js.map +1 -1
  11. package/dist/media/index.js +6 -5
  12. package/dist/media/index.js.map +1 -1
  13. package/dist/meeting/in-meeting-actions.js +4 -0
  14. package/dist/meeting/in-meeting-actions.js.map +1 -1
  15. package/dist/meeting/index.js +276 -150
  16. package/dist/meeting/index.js.map +1 -1
  17. package/dist/meeting-info/meeting-info-v2.js +3 -0
  18. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  19. package/dist/meeting-info/utilv2.js +14 -29
  20. package/dist/meeting-info/utilv2.js.map +1 -1
  21. package/dist/meetings/collection.js +17 -0
  22. package/dist/meetings/collection.js.map +1 -1
  23. package/dist/meetings/index.js +30 -9
  24. package/dist/meetings/index.js.map +1 -1
  25. package/dist/metrics/constants.js +3 -0
  26. package/dist/metrics/constants.js.map +1 -1
  27. package/dist/reconnection-manager/index.js +27 -28
  28. package/dist/reconnection-manager/index.js.map +1 -1
  29. package/dist/rtcMetrics/index.js +25 -0
  30. package/dist/rtcMetrics/index.js.map +1 -1
  31. package/dist/statsAnalyzer/index.js +21 -1
  32. package/dist/statsAnalyzer/index.js.map +1 -1
  33. package/dist/statsAnalyzer/mqaUtil.js +16 -16
  34. package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
  35. package/dist/webinar/index.js +1 -1
  36. package/package.json +21 -22
  37. package/src/constants.ts +10 -4
  38. package/src/controls-options-manager/enums.ts +2 -0
  39. package/src/locus-info/parser.ts +6 -6
  40. package/src/media/index.ts +5 -5
  41. package/src/meeting/in-meeting-actions.ts +8 -0
  42. package/src/meeting/index.ts +249 -120
  43. package/src/meeting-info/meeting-info-v2.ts +4 -0
  44. package/src/meeting-info/utilv2.ts +6 -19
  45. package/src/meetings/collection.ts +13 -0
  46. package/src/meetings/index.ts +28 -10
  47. package/src/metrics/constants.ts +3 -0
  48. package/src/reconnection-manager/index.ts +63 -68
  49. package/src/rtcMetrics/index.ts +24 -0
  50. package/src/statsAnalyzer/index.ts +30 -1
  51. package/src/statsAnalyzer/mqaUtil.ts +17 -14
  52. package/test/unit/spec/media/index.ts +20 -4
  53. package/test/unit/spec/meeting/in-meeting-actions.ts +4 -0
  54. package/test/unit/spec/meeting/index.js +1253 -157
  55. package/test/unit/spec/meeting/muteState.js +2 -1
  56. package/test/unit/spec/meeting-info/meetinginfov2.js +28 -0
  57. package/test/unit/spec/meetings/collection.js +12 -0
  58. package/test/unit/spec/meetings/index.js +382 -118
  59. package/test/unit/spec/member/util.js +0 -31
  60. package/test/unit/spec/reconnection-manager/index.js +42 -12
  61. package/test/unit/spec/rtcMetrics/index.ts +20 -0
  62. package/test/unit/spec/stats-analyzer/index.js +12 -2
@@ -33,6 +33,7 @@ import {
33
33
  NETWORK_STATUS,
34
34
  ONLINE,
35
35
  OFFLINE,
36
+ RECONNECTION,
36
37
  } from '@webex/plugin-meetings/src/constants';
37
38
  import * as InternalMediaCoreModule from '@webex/internal-media-core';
38
39
  import {
@@ -307,6 +308,7 @@ describe('plugin-meetings', () => {
307
308
  assert.equal(meeting.resource, uuid2);
308
309
  assert.equal(meeting.deviceUrl, uuid3);
309
310
  assert.equal(meeting.correlationId, correlationId);
311
+ assert.deepEqual(meeting.callStateForMetrics, {correlationId});
310
312
  assert.deepEqual(meeting.meetingInfo, {});
311
313
  assert.instanceOf(meeting.members, Members);
312
314
  assert.calledOnceWithExactly(
@@ -374,6 +376,34 @@ describe('plugin-meetings', () => {
374
376
  }
375
377
  );
376
378
  assert.equal(newMeeting.correlationId, newMeeting.id);
379
+ assert.deepEqual(newMeeting.callStateForMetrics, {correlationId: newMeeting.id});
380
+ });
381
+
382
+ it('correlationId can be provided in callStateForMetrics', () => {
383
+ const newMeeting = new Meeting(
384
+ {
385
+ userId: uuid1,
386
+ resource: uuid2,
387
+ deviceUrl: uuid3,
388
+ locus: {url: url1},
389
+ destination: testDestination,
390
+ destinationType: _MEETING_ID_,
391
+ callStateForMetrics: {
392
+ correlationId: uuid4,
393
+ joinTrigger: 'fake-join-trigger',
394
+ loginType: 'fake-login-type',
395
+ }
396
+ },
397
+ {
398
+ parent: webex,
399
+ }
400
+ );
401
+ assert.equal(newMeeting.correlationId, uuid4);
402
+ assert.deepEqual(newMeeting.callStateForMetrics, {
403
+ correlationId: uuid4,
404
+ joinTrigger: 'fake-join-trigger',
405
+ loginType: 'fake-login-type',
406
+ });
377
407
  });
378
408
 
379
409
  describe('creates ReceiveSlot manager instance', () => {
@@ -827,11 +857,11 @@ describe('plugin-meetings', () => {
827
857
  });
828
858
 
829
859
  it('should join the meeting and return promise', async () => {
830
- const join = meeting.join();
860
+ const join = meeting.join({pstnAudioType: 'dial-in'});
831
861
 
832
- assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
862
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
833
863
  name: 'client.call.initiated',
834
- payload: {trigger: 'user-interaction', isRoapCallEnabled: true},
864
+ payload: {trigger: 'user-interaction', isRoapCallEnabled: true, pstnAudioType: 'dial-in'},
835
865
  options: {meetingId: meeting.id},
836
866
  });
837
867
 
@@ -850,6 +880,17 @@ describe('plugin-meetings', () => {
850
880
  assert.calledOnce(meeting.startTranscription);
851
881
  });
852
882
 
883
+ it('should take trigger from meeting joinTrigger if available', () => {
884
+ meeting.updateCallStateForMetrics({joinTrigger: 'fake-join-trigger'});
885
+ const join = meeting.join();
886
+
887
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
888
+ name: 'client.call.initiated',
889
+ payload: {trigger: 'fake-join-trigger', isRoapCallEnabled: true},
890
+ options: {meetingId: meeting.id},
891
+ });
892
+ });
893
+
853
894
  it('should not create new correlation ID on join immediately after create', async () => {
854
895
  await meeting.join();
855
896
  sinon.assert.notCalled(setCorrelationIdSpy);
@@ -1172,8 +1213,8 @@ describe('plugin-meetings', () => {
1172
1213
  assert.exists(meeting.addMedia);
1173
1214
  });
1174
1215
 
1175
- it('should reject promise if meeting is not active', async () => {
1176
- const result = await assert.isRejected(meeting.addMedia());
1216
+ it('should reject promise if meeting is not active and the meeting in lobby is not enabled', async () => {
1217
+ const result = await assert.isRejected(meeting.addMedia({allowMediaInLobby: false}));
1177
1218
 
1178
1219
  assert.instanceOf(result, MeetingNotActiveError);
1179
1220
  });
@@ -1791,17 +1832,26 @@ describe('plugin-meetings', () => {
1791
1832
  }]);
1792
1833
 
1793
1834
  const sendBehavioralMetricCalls = Metrics.sendBehavioralMetric.getCalls();
1794
- assert.equal(sendBehavioralMetricCalls.length, 2);
1795
- assert.deepEqual(sendBehavioralMetricCalls[0].args, [
1835
+ assert.equal(sendBehavioralMetricCalls.length, 3);
1836
+ assert.deepEqual(sendBehavioralMetricCalls[0].args, [
1837
+ BEHAVIORAL_METRICS.ADD_MEDIA_RETRY,
1838
+ {
1839
+ correlation_id: meeting.correlationId,
1840
+ state: meeting.state,
1841
+ meetingState: meeting.meetingState,
1842
+ reason: 'forcingTurnTls',
1843
+ },
1844
+ ]);
1845
+ assert.deepEqual(sendBehavioralMetricCalls[1].args, [
1796
1846
  BEHAVIORAL_METRICS.TURN_DISCOVERY_LATENCY,
1797
1847
  {
1798
1848
  correlation_id: meeting.correlationId,
1799
1849
  turnServerUsed: true,
1800
1850
  retriedWithTurnServer: true,
1801
1851
  latency: undefined,
1802
- }
1852
+ },
1803
1853
  ]);
1804
- assert.deepEqual(sendBehavioralMetricCalls[1].args, [
1854
+ assert.deepEqual(sendBehavioralMetricCalls[2].args, [
1805
1855
  BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE,
1806
1856
  {
1807
1857
  correlation_id: meeting.correlationId,
@@ -1816,7 +1866,7 @@ describe('plugin-meetings', () => {
1816
1866
  signalingState: 'unknown',
1817
1867
  connectionState: 'unknown',
1818
1868
  iceConnectionState: 'unknown',
1819
- }
1869
+ },
1820
1870
  ]);
1821
1871
 
1822
1872
  // Check that doTurnDiscovery is called with th4 correct value of isForced
@@ -1957,17 +2007,26 @@ describe('plugin-meetings', () => {
1957
2007
  }]);
1958
2008
 
1959
2009
  const sendBehavioralMetricCalls = Metrics.sendBehavioralMetric.getCalls();
1960
- assert.equal(sendBehavioralMetricCalls.length, 2);
1961
- assert.deepEqual(sendBehavioralMetricCalls[0].args, [
2010
+ assert.equal(sendBehavioralMetricCalls.length, 3);
2011
+ assert.deepEqual(sendBehavioralMetricCalls[0].args, [
2012
+ BEHAVIORAL_METRICS.ADD_MEDIA_RETRY,
2013
+ {
2014
+ correlation_id: meeting.correlationId,
2015
+ state: meeting.state,
2016
+ meetingState: meeting.meetingState,
2017
+ reason: 'forcingTurnTls',
2018
+ },
2019
+ ]);
2020
+ assert.deepEqual(sendBehavioralMetricCalls[1].args, [
1962
2021
  BEHAVIORAL_METRICS.TURN_DISCOVERY_LATENCY,
1963
2022
  {
1964
2023
  correlation_id: meeting.correlationId,
1965
2024
  turnServerUsed: true,
1966
2025
  retriedWithTurnServer: true,
1967
2026
  latency: undefined,
1968
- }
2027
+ },
1969
2028
  ]);
1970
- assert.deepEqual(sendBehavioralMetricCalls[1].args, [
2029
+ assert.deepEqual(sendBehavioralMetricCalls[2].args, [
1971
2030
  BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS,
1972
2031
  {
1973
2032
  correlation_id: meeting.correlationId,
@@ -1975,7 +2034,7 @@ describe('plugin-meetings', () => {
1975
2034
  connectionType: 'udp',
1976
2035
  isMultistream: false,
1977
2036
  retriedWithTurnServer: true,
1978
- }
2037
+ },
1979
2038
  ]);
1980
2039
  meeting.roap.doTurnDiscovery
1981
2040
 
@@ -1999,6 +2058,90 @@ describe('plugin-meetings', () => {
1999
2058
  assert.isNotOk(errorThrown);
2000
2059
  });
2001
2060
 
2061
+ it('should call join if state is LEFT after first media connection attempt', async () => {
2062
+ const FAKE_TURN_URL = 'turns:webex.com:3478';
2063
+ const FAKE_TURN_USER = 'some-turn-username';
2064
+ const FAKE_TURN_PASSWORD = 'some-password';
2065
+ let errorThrown = undefined;
2066
+
2067
+ meeting.meetingState = 'ACTIVE';
2068
+ meeting.state = 'LEFT';
2069
+ meeting.roap.doTurnDiscovery = sinon.stub().onFirstCall().returns({
2070
+ turnServerInfo: undefined,
2071
+ turnDiscoverySkippedReason: 'reachability',
2072
+ }).onSecondCall().returns({
2073
+ turnServerInfo: {
2074
+ url: FAKE_TURN_URL,
2075
+ username: FAKE_TURN_USER,
2076
+ password: FAKE_TURN_PASSWORD,
2077
+ },
2078
+ turnDiscoverySkippedReason: undefined,
2079
+ });
2080
+ meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().onFirstCall().rejects().onSecondCall().resolves();
2081
+ meeting.join = sinon.stub().resolves();
2082
+
2083
+ const closeMediaConnectionStub = sinon.stub();
2084
+ Media.createMediaConnection = sinon.stub().returns({
2085
+ close: closeMediaConnectionStub,
2086
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
2087
+ initiateOffer: sinon.stub().resolves({}),
2088
+ on: sinon.stub(),
2089
+ });
2090
+
2091
+ await meeting
2092
+ .addMedia({
2093
+ mediaSettings: {},
2094
+ })
2095
+ .catch((err) => {
2096
+ errorThrown = err;
2097
+ });
2098
+
2099
+ assert.isNotOk(errorThrown);
2100
+ assert.calledOnceWithExactly(meeting.join, {rejoin: true});
2101
+ });
2102
+
2103
+ it('should reject if join attempt fails if state is LEFT after first media connection attempt', async () => {
2104
+ const FAKE_TURN_URL = 'turns:webex.com:3478';
2105
+ const FAKE_TURN_USER = 'some-turn-username';
2106
+ const FAKE_TURN_PASSWORD = 'some-password';
2107
+ let errorThrown = undefined;
2108
+
2109
+ meeting.meetingState = 'ACTIVE';
2110
+ meeting.state = 'LEFT';
2111
+ meeting.roap.doTurnDiscovery = sinon.stub().onFirstCall().returns({
2112
+ turnServerInfo: undefined,
2113
+ turnDiscoverySkippedReason: 'reachability',
2114
+ }).onSecondCall().returns({
2115
+ turnServerInfo: {
2116
+ url: FAKE_TURN_URL,
2117
+ username: FAKE_TURN_USER,
2118
+ password: FAKE_TURN_PASSWORD,
2119
+ },
2120
+ turnDiscoverySkippedReason: undefined,
2121
+ });
2122
+ meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().onFirstCall().rejects().onSecondCall().resolves();
2123
+ meeting.join = sinon.stub().rejects();
2124
+
2125
+ const closeMediaConnectionStub = sinon.stub();
2126
+ Media.createMediaConnection = sinon.stub().returns({
2127
+ close: closeMediaConnectionStub,
2128
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
2129
+ initiateOffer: sinon.stub().resolves({}),
2130
+ on: sinon.stub(),
2131
+ });
2132
+
2133
+ await meeting
2134
+ .addMedia({
2135
+ mediaSettings: {},
2136
+ })
2137
+ .catch((err) => {
2138
+ errorThrown = err;
2139
+ });
2140
+
2141
+ assert.isOk(errorThrown);
2142
+ assert.calledOnceWithExactly(meeting.join, {rejoin: true});
2143
+ });
2144
+
2002
2145
  it('should send ADD_MEDIA_SUCCESS metrics', async () => {
2003
2146
  meeting.meetingState = 'ACTIVE';
2004
2147
  meeting.webex.meetings.reachability = {
@@ -2383,6 +2526,7 @@ describe('plugin-meetings', () => {
2383
2526
  category: 'media',
2384
2527
  errorCode: clientErrorCode,
2385
2528
  serviceErrorCode: undefined,
2529
+ rawErrorMessage: undefined,
2386
2530
  ...expectedErrorPayload,
2387
2531
  },
2388
2532
  ],
@@ -2479,8 +2623,12 @@ describe('plugin-meetings', () => {
2479
2623
  setUnmuteAllowed: sinon.stub(),
2480
2624
  setMuted: sinon.stub(),
2481
2625
  setServerMuted: sinon.stub(),
2482
- outputTrack: {
2483
- id: 'fake mic'
2626
+ outputStream: {
2627
+ getTracks: () => {
2628
+ return [{
2629
+ id: 'fake mic'
2630
+ }];
2631
+ }
2484
2632
  }
2485
2633
  }
2486
2634
 
@@ -2691,10 +2839,10 @@ describe('plugin-meetings', () => {
2691
2839
  assert.calledOnceWithExactly(roapMediaConnectionConstructorStub, mediaConnectionConfig,
2692
2840
  {
2693
2841
  localTracks: {
2694
- audio: localStreams.audio?.outputTrack,
2695
- video: localStreams.video?.outputTrack,
2696
- screenShareVideo: localStreams.screenShareVideo?.outputTrack,
2697
- screenShareAudio: localStreams.screenShareAudio?.outputTrack,
2842
+ audio: localStreams.audio?.outputStream?.getTracks()[0],
2843
+ video: localStreams.video?.outputStream?.getTracks()[0],
2844
+ screenShareVideo: localStreams.screenShareVideo?.outputStream?.getTracks()[0],
2845
+ screenShareAudio: localStreams.screenShareAudio?.outputStream?.getTracks()[0],
2698
2846
  },
2699
2847
  direction: {audio: direction.audio, video: direction.video, screenShareVideo: direction.screenShare},
2700
2848
  remoteQualityLevel,
@@ -2969,7 +3117,7 @@ describe('plugin-meetings', () => {
2969
3117
  assert.calledOnceWithExactly(meeting.sendSlotManager.getSlot(MediaType.AudioMain).publishStream, fakeMicrophoneStream);
2970
3118
  } else {
2971
3119
  assert.calledOnceWithExactly(fakeRoapMediaConnection.update, {
2972
- localTracks: { audio: fakeMicrophoneStream.outputTrack, video: null, screenShareVideo: null, screenShareAudio: null },
3120
+ localTracks: { audio: fakeMicrophoneStream.outputStream.getTracks()[0], video: null, screenShareVideo: null, screenShareAudio: null },
2973
3121
  direction: {
2974
3122
  audio: expected.direction,
2975
3123
  video: 'sendrecv',
@@ -2995,8 +3143,12 @@ describe('plugin-meetings', () => {
2995
3143
  muted: false,
2996
3144
  setUnmuteAllowed: sinon.stub(),
2997
3145
  setMuted: sinon.stub(),
2998
- outputTrack:{
2999
- id: 'fake mic 2',
3146
+ outputStream: {
3147
+ getTracks: () => {
3148
+ return [{
3149
+ id: 'fake mic 2',
3150
+ }];
3151
+ }
3000
3152
  }
3001
3153
  }
3002
3154
 
@@ -3008,7 +3160,7 @@ describe('plugin-meetings', () => {
3008
3160
  assert.calledOnceWithExactly(meeting.sendSlotManager.getSlot(MediaType.AudioMain).publishStream, fakeMicrophoneStream2);
3009
3161
  } else {
3010
3162
  assert.calledOnceWithExactly(fakeRoapMediaConnection.update, {
3011
- localTracks: { audio: fakeMicrophoneStream2.outputTrack, video: null, screenShareVideo: null, screenShareAudio: null },
3163
+ localTracks: { audio: fakeMicrophoneStream2.outputStream.getTracks()[0], video: null, screenShareVideo: null, screenShareAudio: null },
3012
3164
  direction: {
3013
3165
  audio: expected.direction,
3014
3166
  video: 'sendrecv',
@@ -3079,7 +3231,7 @@ describe('plugin-meetings', () => {
3079
3231
  assert.equal(meeting.sendSlotManager.getSlot(MediaType.AudioMain).active, expectedDirection !== 'inactive');
3080
3232
  } else {
3081
3233
  assert.calledOnceWithExactly(fakeRoapMediaConnection.update, {
3082
- localTracks: { audio: expectedStream?.outputTrack ?? null, video: null, screenShareVideo: null, screenShareAudio: null },
3234
+ localTracks: { audio: expectedStream?.outputStream.getTracks()[0] ?? null, video: null, screenShareVideo: null, screenShareAudio: null },
3083
3235
  direction: {
3084
3236
  audio: expectedDirection,
3085
3237
  video: 'sendrecv',
@@ -3529,7 +3681,11 @@ describe('plugin-meetings', () => {
3529
3681
  let sandbox;
3530
3682
 
3531
3683
  const createFakeLocalStream = () => ({
3532
- outputTrack: {id: 'fake underlying track'},
3684
+ outputStream: {
3685
+ getTracks: () => {
3686
+ return [{id: 'fake underlying track'}];
3687
+ }
3688
+ }
3533
3689
  });
3534
3690
  beforeEach(() => {
3535
3691
  sandbox = sinon.createSandbox();
@@ -3612,10 +3768,10 @@ describe('plugin-meetings', () => {
3612
3768
  meeting.mediaProperties.webrtcMediaConnection.update,
3613
3769
  {
3614
3770
  localTracks: {
3615
- audio: meeting.mediaProperties.audioStream.outputTrack,
3616
- video: meeting.mediaProperties.videoStream.outputTrack,
3617
- screenShareVideo: meeting.mediaProperties.shareVideoStream.outputTrack,
3618
- screenShareAudio: meeting.mediaProperties.shareVideoStream.outputTrack,
3771
+ audio: meeting.mediaProperties.audioStream.outputStream.getTracks()[0],
3772
+ video: meeting.mediaProperties.videoStream.outputStream.getTracks()[0],
3773
+ screenShareVideo: meeting.mediaProperties.shareVideoStream.outputStream.getTracks()[0],
3774
+ screenShareAudio: meeting.mediaProperties.shareVideoStream.outputStream.getTracks()[0],
3619
3775
  },
3620
3776
  direction: {
3621
3777
  audio: 'inactive',
@@ -3644,7 +3800,13 @@ describe('plugin-meetings', () => {
3644
3800
  meeting.mediaProperties.mediaDirection = mediaDirection;
3645
3801
  meeting.mediaProperties.remoteVideoStream = sinon
3646
3802
  .stub()
3647
- .returns({outputTrack: {id: 'some mock id'}});
3803
+ .returns({
3804
+ outputStream: {
3805
+ getTracks: () => {
3806
+ id: 'some mock id'
3807
+ }
3808
+ }
3809
+ });
3648
3810
 
3649
3811
  meeting.meetingRequest.changeVideoLayoutDebounced = sinon
3650
3812
  .stub()
@@ -4469,7 +4631,7 @@ describe('plugin-meetings', () => {
4469
4631
  };
4470
4632
  const FAKE_MEETING_INFO_LOOKUP_URL = 'meetingLookupUrl';
4471
4633
  const FAKE_PERMISSION_TOKEN = {someField: 'some value'};
4472
- const FAKE_TTL = 13;
4634
+ const FAKE_TIMESTAMPS = {timeLeft: 13, expiryTime: 123456, currentTime: 123478};
4473
4635
 
4474
4636
  beforeEach(() => {
4475
4637
  meeting.locusId = 'locus-id';
@@ -4479,7 +4641,7 @@ describe('plugin-meetings', () => {
4479
4641
  meeting.destination = 'meeting-destination';
4480
4642
  meeting.destinationType = 'meeting-destination-type';
4481
4643
  meeting.updateMeetingActions = sinon.stub().returns(undefined);
4482
- meeting.getPermissionTokenTimeLeftInSec = sinon.stub().returns(FAKE_TTL);
4644
+
4483
4645
  meeting.meetingInfoExtraParams = {
4484
4646
  extraParam1: 'value1'
4485
4647
  };
@@ -4500,6 +4662,50 @@ describe('plugin-meetings', () => {
4500
4662
  });
4501
4663
 
4502
4664
  it('calls meetingInfoProvider.fetchMeetingInfo() with the right params', async () => {
4665
+ meeting.getPermissionTokenExpiryInfo = sinon.stub().returns(FAKE_TIMESTAMPS);
4666
+ await meeting.refreshPermissionToken('fake reason');
4667
+
4668
+ assert.calledOnceWithExactly(
4669
+ meeting.attrs.meetingInfoProvider.fetchMeetingInfo,
4670
+ 'meeting-destination',
4671
+ 'meeting-destination-type',
4672
+ null,
4673
+ null,
4674
+ 'fake-installed-org-id',
4675
+ 'locus-id',
4676
+ {extraParam1: 'value1', permissionToken: FAKE_PERMISSION_TOKEN},
4677
+ {meetingId: meeting.id, sendCAevents: true}
4678
+ );
4679
+ assert.deepEqual(meeting.meetingInfo, {
4680
+ ...FAKE_MEETING_INFO,
4681
+ meetingLookupUrl: FAKE_MEETING_INFO_LOOKUP_URL
4682
+ });
4683
+ assert.equal(meeting.meetingInfoFailureReason, MEETING_INFO_FAILURE_REASON.NONE);
4684
+ assert.equal(meeting.requiredCaptcha, null);
4685
+ assert.equal(meeting.passwordStatus, PASSWORD_STATUS.NOT_REQUIRED);
4686
+
4687
+ assert.calledWith(
4688
+ TriggerProxy.trigger,
4689
+ meeting,
4690
+ {file: 'meetings', function: 'fetchMeetingInfo'},
4691
+ 'meeting:meetingInfoAvailable'
4692
+ );
4693
+ assert.calledWith(meeting.updateMeetingActions);
4694
+
4695
+ assert.calledWith(
4696
+ Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.PERMISSION_TOKEN_REFRESH, {
4697
+ correlationId: meeting.correlationId,
4698
+ timeLeft: FAKE_TIMESTAMPS.timeLeft,
4699
+ expiryTime: FAKE_TIMESTAMPS.expiryTime,
4700
+ currentTime: FAKE_TIMESTAMPS.currentTime,
4701
+ reason: 'fake reason',
4702
+ destinationType: 'meeting-destination-type',
4703
+ }
4704
+ );
4705
+ });
4706
+
4707
+ it('calls meetingInfoProvider.fetchMeetingInfo() with the right params when getPermissionTokenExpiryInfo returns undefined', async () => {
4708
+ meeting.getPermissionTokenExpiryInfo = sinon.stub().returns(undefined);
4503
4709
  await meeting.refreshPermissionToken('fake reason');
4504
4710
 
4505
4711
  assert.calledOnceWithExactly(
@@ -4532,7 +4738,9 @@ describe('plugin-meetings', () => {
4532
4738
  assert.calledWith(
4533
4739
  Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.PERMISSION_TOKEN_REFRESH, {
4534
4740
  correlationId: meeting.correlationId,
4535
- timeLeft: FAKE_TTL,
4741
+ timeLeft: undefined,
4742
+ expiryTime: undefined,
4743
+ currentTime: undefined,
4536
4744
  reason: 'fake reason',
4537
4745
  destinationType: 'meeting-destination-type',
4538
4746
  }
@@ -4540,6 +4748,7 @@ describe('plugin-meetings', () => {
4540
4748
  });
4541
4749
 
4542
4750
  it('calls meetingInfoProvider.fetchMeetingInfo() with the right params when we are starting an instant space meeting', async () => {
4751
+ meeting.getPermissionTokenExpiryInfo = sinon.stub().returns(FAKE_TIMESTAMPS);
4543
4752
  meeting.destination = 'some-convo-url';
4544
4753
  meeting.destinationType = 'CONVERSATION_URL';
4545
4754
  meeting.config.experimental = {enableAdhocMeetings: true};
@@ -4581,7 +4790,9 @@ describe('plugin-meetings', () => {
4581
4790
  assert.calledWith(
4582
4791
  Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.PERMISSION_TOKEN_REFRESH, {
4583
4792
  correlationId: meeting.correlationId,
4584
- timeLeft: FAKE_TTL,
4793
+ timeLeft: FAKE_TIMESTAMPS.timeLeft,
4794
+ expiryTime: FAKE_TIMESTAMPS.expiryTime,
4795
+ currentTime: FAKE_TIMESTAMPS.currentTime,
4585
4796
  reason: 'some reason',
4586
4797
  destinationType: 'MEETING_LINK',
4587
4798
  }
@@ -5074,6 +5285,31 @@ describe('plugin-meetings', () => {
5074
5285
  }
5075
5286
  });
5076
5287
  });
5288
+
5289
+ describe('#setCorrelationId', () => {
5290
+ it('should set the correlationId and return undefined', () => {
5291
+ assert.equal(meeting.correlationId, correlationId);
5292
+ assert.deepEqual(meeting.callStateForMetrics, {correlationId});
5293
+ meeting.setCorrelationId(uuid1);
5294
+ assert.equal(meeting.correlationId, uuid1);
5295
+ assert.deepEqual(meeting.callStateForMetrics, {correlationId: uuid1});
5296
+ });
5297
+ });
5298
+
5299
+ describe('#updateCallStateForMetrics', () => {
5300
+ it('should update the callState, overriding existing values', () => {
5301
+ assert.deepEqual(meeting.callStateForMetrics, {correlationId});
5302
+ meeting.updateCallStateForMetrics({correlationId: uuid1, joinTrigger: 'jt', loginType: 'lt'});
5303
+ assert.deepEqual(meeting.callStateForMetrics, {correlationId: uuid1, joinTrigger: 'jt', loginType: 'lt'});
5304
+ });
5305
+
5306
+ it('should update the callState, keeping non-supplied values', () => {
5307
+ assert.deepEqual(meeting.callStateForMetrics, {correlationId});
5308
+ meeting.updateCallStateForMetrics({joinTrigger: 'jt', loginType: 'lt'});
5309
+ assert.deepEqual(meeting.callStateForMetrics, {correlationId, joinTrigger: 'jt', loginType: 'lt'});
5310
+ });
5311
+ });
5312
+
5077
5313
  describe('Local tracks publishing', () => {
5078
5314
  let audioStream;
5079
5315
  let videoStream;
@@ -5436,31 +5672,72 @@ describe('plugin-meetings', () => {
5436
5672
  it('should have #reconnect', () => {
5437
5673
  assert.exists(meeting.reconnect);
5438
5674
  });
5675
+
5439
5676
  describe('successful reconnect', () => {
5677
+ let eventListeners;
5678
+
5440
5679
  beforeEach(() => {
5680
+ eventListeners = {};
5681
+ meeting.mediaProperties.webrtcMediaConnection = {
5682
+ // mock the on() method and store all the listeners
5683
+ on: sinon.stub().callsFake((event, listener) => {
5684
+ eventListeners[event] = listener;
5685
+ }),
5686
+ };
5687
+ meeting.setupMediaConnectionListeners();
5688
+ meeting.deferSDPAnswer = {
5689
+ resolve: sinon.stub(),
5690
+ reject: sinon.stub(),
5691
+ };
5692
+ meeting.sdpResponseTimer = '1234';
5693
+ meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
5694
+
5695
+ eventListeners[Event.REMOTE_SDP_ANSWER_PROCESSED]();
5441
5696
  meeting.config.reconnection.enabled = true;
5442
5697
  meeting.currentMediaStatus = {audio: true};
5443
5698
  meeting.reconnectionManager = new ReconnectionManager(meeting);
5444
5699
  meeting.reconnectionManager.reconnect = sinon.stub().returns(Promise.resolve());
5445
5700
  meeting.reconnectionManager.reset = sinon.stub().returns(true);
5446
5701
  meeting.reconnectionManager.cleanup = sinon.stub().returns(true);
5702
+ meeting.reconnectionManager.setStatus = sinon.stub();
5447
5703
  });
5448
5704
 
5449
- it('should throw error if media not established before trying reconenct', async () => {
5705
+ it('should throw error if media not established before trying reconnect', async () => {
5450
5706
  meeting.currentMediaStatus = null;
5451
5707
  await meeting.reconnect().catch((err) => {
5452
5708
  assert.instanceOf(err, ParameterError);
5453
5709
  });
5454
5710
  });
5455
5711
 
5456
- it('should trigger reconnection success', async () => {
5712
+ it('should reconnect successfully if reconnectionManager.cleanUp is called before reconnection attempt', async () => {
5713
+ meeting.reconnectionManager.cleanUp();
5714
+
5715
+ try {
5716
+ await meeting.reconnect();
5717
+ } catch (err) {
5718
+ assert.fail('reconnect should not error after clean up');
5719
+ }
5720
+ })
5721
+
5722
+ it('should trigger reconnection success and send CA metric', async () => {
5457
5723
  await meeting.reconnect();
5724
+
5458
5725
  assert.calledWith(
5459
5726
  TriggerProxy.trigger,
5460
5727
  sinon.match.instanceOf(Meeting),
5461
5728
  {file: 'meeting/index', function: 'reconnect'},
5462
5729
  'meeting:reconnectionSuccess'
5463
5730
  );
5731
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
5732
+ name: 'client.media.recovered',
5733
+ payload: {
5734
+ recoveredBy: 'new',
5735
+ },
5736
+ options: {
5737
+ meetingId: meeting.id,
5738
+ },
5739
+ });
5740
+ assert.calledOnceWithExactly(meeting.reconnectionManager.setStatus, RECONNECTION.STATE.COMPLETE);
5464
5741
  });
5465
5742
 
5466
5743
  it('should reset after reconnection success', async () => {
@@ -5527,7 +5804,7 @@ describe('plugin-meetings', () => {
5527
5804
  it('should stop remote tracks, and trigger a media:stopped event when the remote tracks are stopped', async () => {
5528
5805
  await meeting.closeRemoteStreams();
5529
5806
 
5530
- assert.equal(TriggerProxy.trigger.callCount, 6);
5807
+ assert.equal(TriggerProxy.trigger.callCount, 5);
5531
5808
  assert.calledWith(
5532
5809
  TriggerProxy.trigger,
5533
5810
  sinon.match.instanceOf(Meeting),
@@ -5613,37 +5890,42 @@ describe('plugin-meetings', () => {
5613
5890
  });
5614
5891
  });
5615
5892
 
5616
- describe('submitClientEvent on connectionFailed', () => {
5617
- it('sends client.ice.end when connectionFailed on CONNECTION_STATE_CHANGED event', () => {
5618
- const FAKE_ERROR = {fatal: true};
5619
- const getErrorPayloadForClientErrorCodeStub = webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode = sinon
5620
- .stub()
5621
- .returns(FAKE_ERROR);
5893
+ describe('CONNECTION_STATE_CHANGED event when state = "Connecting"', () => {
5894
+ it('sends client.ice.start correctly when hasMediaConnectionConnectedAtLeastOnce = true', () => {
5895
+ meeting.hasMediaConnectionConnectedAtLeastOnce = true;
5622
5896
  meeting.setupMediaConnectionListeners();
5623
5897
  eventListeners[Event.CONNECTION_STATE_CHANGED]({
5624
- state: 'Failed',
5898
+ state: 'Connecting',
5899
+ });
5900
+
5901
+ assert.notCalled(webex.internal.newMetrics.submitClientEvent);
5902
+ })
5903
+
5904
+ it('sends client.ice.start correctly when hasMediaConnectionConnectedAtLeastOnce = false', () => {
5905
+ meeting.hasMediaConnectionConnectedAtLeastOnce = false;
5906
+ meeting.setupMediaConnectionListeners();
5907
+ eventListeners[Event.CONNECTION_STATE_CHANGED]({
5908
+ state: 'Connecting',
5625
5909
  });
5626
- assert.calledOnceWithExactly(getErrorPayloadForClientErrorCodeStub, {clientErrorCode: 2004});
5910
+
5627
5911
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
5628
5912
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
5629
- name: 'client.ice.end',
5630
- payload: {
5631
- canProceed: false,
5632
- icePhase: 'IN_MEETING',
5633
- errors: [FAKE_ERROR],
5634
- },
5913
+ name: 'client.ice.start',
5635
5914
  options: {
5636
5915
  meetingId: meeting.id,
5637
5916
  },
5638
5917
  });
5639
- });
5918
+ })
5640
5919
  });
5641
5920
 
5642
5921
  describe('submitClientEvent on connectionSuccess', () => {
5643
- it('sends client.ice.end when connectionSuccess on CONNECTION_STATE_CHANGED event', () => {
5922
+ let setNetworkStatusSpy;
5923
+
5924
+ const setupSpies = () => {
5925
+ setNetworkStatusSpy = sinon.spy(meeting, 'setNetworkStatus');
5926
+
5644
5927
  meeting.reconnectionManager = new ReconnectionManager(meeting);
5645
5928
  meeting.reconnectionManager.iceReconnected = sinon.stub().returns(undefined);
5646
- meeting.setNetworkStatus = sinon.stub().returns(undefined);
5647
5929
  meeting.statsAnalyzer = {startAnalyzer: sinon.stub()};
5648
5930
  meeting.mediaProperties.webrtcMediaConnection = {
5649
5931
  // mock the on() method and store all the listeners
@@ -5651,45 +5933,234 @@ describe('plugin-meetings', () => {
5651
5933
  eventListeners[event] = listener;
5652
5934
  }),
5653
5935
  };
5936
+ };
5654
5937
 
5655
- meeting.setupMediaConnectionListeners();
5656
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
5657
- state: 'Connected',
5658
- });
5659
- assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
5660
- assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
5661
- name: 'client.ice.end',
5662
- options: {
5663
- meetingId: meeting.id,
5664
- },
5665
- });
5938
+ const checkExpectedSpies = (expected) => {
5939
+ if (expected.icePhase) {
5940
+ assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
5941
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
5942
+ name: 'client.ice.end',
5943
+ options: {
5944
+ meetingId: meeting.id,
5945
+ },
5946
+ payload: {
5947
+ canProceed: true,
5948
+ icePhase: expected.icePhase,
5949
+ },
5950
+ });
5951
+ } else {
5952
+ assert.notCalled(webex.internal.newMetrics.submitClientEvent);
5953
+ }
5666
5954
  assert.calledOnce(Metrics.sendBehavioralMetric);
5667
5955
  assert.calledWith(Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.CONNECTION_SUCCESS, {
5668
5956
  correlation_id: meeting.correlationId,
5669
5957
  locus_id: meeting.locusId,
5670
5958
  latency: undefined,
5671
5959
  });
5672
- assert.calledOnce(meeting.setNetworkStatus);
5673
- assert.calledWith(meeting.setNetworkStatus, NETWORK_STATUS.CONNECTED);
5960
+ assert.deepEqual(
5961
+ setNetworkStatusSpy.getCalls().map((call) => call.args[0]),
5962
+ expected.setNetworkStatusCallParams
5963
+ );
5674
5964
  assert.calledOnce(meeting.reconnectionManager.iceReconnected);
5675
5965
  assert.calledOnce(meeting.statsAnalyzer.startAnalyzer);
5676
5966
  assert.calledWith(
5677
5967
  meeting.statsAnalyzer.startAnalyzer,
5678
5968
  meeting.mediaProperties.webrtcMediaConnection
5679
5969
  );
5680
- });
5681
- });
5970
+ };
5682
5971
 
5683
- describe('should send correct metrics for ROAP_FAILURE event', () => {
5684
- const fakeErrorMessage = 'test error';
5685
- const fakeRootCauseName = 'root cause name';
5686
- const fakeErrorName = 'test error name';
5972
+ const resetSpies = () => {
5973
+ setNetworkStatusSpy.resetHistory();
5974
+ webex.internal.newMetrics.submitClientEvent.resetHistory();
5975
+ Metrics.sendBehavioralMetric.resetHistory();
5976
+ meeting.reconnectionManager.iceReconnected.resetHistory();
5977
+ meeting.statsAnalyzer.startAnalyzer.resetHistory();
5978
+ };
5979
+
5980
+ it('sends client.ice.end with the correct icePhase when we get ConnectionState.Connected on CONNECTION_STATE_CHANGED event', () => {
5981
+ setupSpies();
5687
5982
 
5688
- beforeEach(() => {
5689
5983
  meeting.setupMediaConnectionListeners();
5690
- });
5691
5984
 
5692
- const checkMetricSent = (event, error) => {
5985
+ assert.equal(meeting.hasMediaConnectionConnectedAtLeastOnce, false);
5986
+
5987
+ // simulate first connection success
5988
+ eventListeners[Event.CONNECTION_STATE_CHANGED]({
5989
+ state: 'Connected',
5990
+ });
5991
+ checkExpectedSpies({
5992
+ icePhase: 'JOIN_MEETING_FINAL',
5993
+ setNetworkStatusCallParams: [NETWORK_STATUS.CONNECTED],
5994
+ });
5995
+ assert.equal(meeting.hasMediaConnectionConnectedAtLeastOnce, true);
5996
+
5997
+ // now simulate short connection loss, client.ice.end is not sent a second time as hasMediaConnectionConnectedAtLeastOnce = true
5998
+ resetSpies();
5999
+
6000
+ eventListeners[Event.CONNECTION_STATE_CHANGED]({
6001
+ state: 'Disconnected',
6002
+ });
6003
+ eventListeners[Event.CONNECTION_STATE_CHANGED]({
6004
+ state: 'Connected',
6005
+ });
6006
+
6007
+ checkExpectedSpies({
6008
+ setNetworkStatusCallParams: [NETWORK_STATUS.DISCONNECTED, NETWORK_STATUS.CONNECTED],
6009
+ });
6010
+
6011
+ resetSpies();
6012
+
6013
+ eventListeners[Event.CONNECTION_STATE_CHANGED]({
6014
+ state: 'Disconnected',
6015
+ });
6016
+ eventListeners[Event.CONNECTION_STATE_CHANGED]({
6017
+ state: 'Connected',
6018
+ });
6019
+ });
6020
+ });
6021
+
6022
+ describe('CONNECTION_STATE_CHANGED event when state = "Disconnected"', () => {
6023
+ beforeEach(() => {
6024
+ meeting.reconnectionManager = new ReconnectionManager(meeting);
6025
+ meeting.reconnectionManager.iceReconnected = sinon.stub().returns(undefined);
6026
+ meeting.setNetworkStatus = sinon.stub().returns(undefined);
6027
+ meeting.statsAnalyzer = {startAnalyzer: sinon.stub()};
6028
+ meeting.mediaProperties.webrtcMediaConnection = {
6029
+ // mock the on() method and store all the listeners
6030
+ on: sinon.stub().callsFake((event, listener) => {
6031
+ eventListeners[event] = listener;
6032
+ }),
6033
+ };
6034
+ meeting.reconnect = sinon.stub().resolves();
6035
+ });
6036
+
6037
+ const mockDisconnectedEvent = () => {
6038
+ meeting.setupMediaConnectionListeners();
6039
+ eventListeners[Event.CONNECTION_STATE_CHANGED]({
6040
+ state: 'Disconnected',
6041
+ });
6042
+ };
6043
+
6044
+ const checkBehavioralMetricSent = (hasMediaConnectionConnectedAtLeastOnce = false) => {
6045
+ assert.calledOnce(Metrics.sendBehavioralMetric);
6046
+ assert.calledWith(
6047
+ Metrics.sendBehavioralMetric,
6048
+ BEHAVIORAL_METRICS.CONNECTION_FAILURE,
6049
+ {
6050
+ correlation_id: meeting.correlationId,
6051
+ locus_id: meeting.locusUrl.split('/').pop(),
6052
+ networkStatus: meeting.networkStatus,
6053
+ hasMediaConnectionConnectedAtLeastOnce,
6054
+ },
6055
+ );
6056
+ };
6057
+
6058
+ it('handles "Disconnected" state correctly when waitForIceReconnect resolves', async () => {
6059
+ meeting.reconnectionManager.waitForIceReconnect = sinon.stub().resolves();
6060
+
6061
+
6062
+ mockDisconnectedEvent();
6063
+
6064
+ await testUtils.flushPromises();
6065
+
6066
+ assert.calledOnce(meeting.setNetworkStatus);
6067
+ assert.calledWith(meeting.setNetworkStatus, NETWORK_STATUS.DISCONNECTED);
6068
+ assert.calledOnce(meeting.reconnectionManager.waitForIceReconnect);
6069
+ assert.notCalled(webex.internal.newMetrics.submitClientEvent);
6070
+ assert.notCalled(Metrics.sendBehavioralMetric);
6071
+ });
6072
+
6073
+ it('handles "Disconnected" state correctly when waitForIceReconnect rejects and hasMediaConnectionConnectedAtLeastOnce = true', async () => {
6074
+ const FAKE_ERROR = {fatal: true};
6075
+ const getErrorPayloadForClientErrorCodeStub = webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode = sinon
6076
+ .stub()
6077
+ .returns(FAKE_ERROR);
6078
+ meeting.waitForMediaConnectionConnected = sinon.stub().resolves();
6079
+ meeting.reconnectionManager.waitForIceReconnect = sinon.stub().rejects();
6080
+ meeting.hasMediaConnectionConnectedAtLeastOnce = true;
6081
+
6082
+
6083
+ mockDisconnectedEvent();
6084
+
6085
+ await testUtils.flushPromises();
6086
+
6087
+ assert.calledOnce(meeting.setNetworkStatus);
6088
+ assert.calledWith(meeting.setNetworkStatus, NETWORK_STATUS.DISCONNECTED);
6089
+ assert.calledOnce(meeting.reconnectionManager.waitForIceReconnect);
6090
+ assert.notCalled(webex.internal.newMetrics.submitClientEvent);
6091
+ checkBehavioralMetricSent(true);
6092
+ });
6093
+
6094
+ it('handles "Disconnected" state correctly when waitForIceReconnect rejects and hasMediaConnectionConnectedAtLeastOnce = false', async () => {
6095
+ meeting.reconnectionManager.waitForIceReconnect = sinon.stub().rejects();
6096
+
6097
+
6098
+ mockDisconnectedEvent();
6099
+
6100
+ await testUtils.flushPromises();
6101
+
6102
+ assert.calledOnce(meeting.setNetworkStatus);
6103
+ assert.calledWith(meeting.setNetworkStatus, NETWORK_STATUS.DISCONNECTED);
6104
+ assert.calledOnce(meeting.reconnectionManager.waitForIceReconnect);
6105
+ assert.notCalled(webex.internal.newMetrics.submitClientEvent);
6106
+ checkBehavioralMetricSent();
6107
+ });
6108
+ });
6109
+
6110
+ describe('CONNECTION_STATE_CHANGED event when state = "Failed"', () => {
6111
+
6112
+ const mockFailedEvent = () => {
6113
+ meeting.setupMediaConnectionListeners();
6114
+ eventListeners[Event.CONNECTION_STATE_CHANGED]({
6115
+ state: 'Failed',
6116
+ });
6117
+ };
6118
+
6119
+ const checkBehavioralMetricSent = (hasMediaConnectionConnectedAtLeastOnce = false) => {
6120
+ assert.calledOnce(Metrics.sendBehavioralMetric);
6121
+ assert.calledWith(
6122
+ Metrics.sendBehavioralMetric,
6123
+ BEHAVIORAL_METRICS.CONNECTION_FAILURE,
6124
+ {
6125
+ correlation_id: meeting.correlationId,
6126
+ locus_id: meeting.locusUrl.split('/').pop(),
6127
+ networkStatus: meeting.networkStatus,
6128
+ hasMediaConnectionConnectedAtLeastOnce,
6129
+ },
6130
+ );
6131
+ };
6132
+
6133
+ it('handles "Failed" state correctly when hasMediaConnectionConnectedAtLeastOnce = false', async () => {
6134
+ meeting.waitForMediaConnectionConnected = sinon.stub().resolves();
6135
+
6136
+ mockFailedEvent();
6137
+
6138
+ assert.notCalled(webex.internal.newMetrics.submitClientEvent);
6139
+ checkBehavioralMetricSent();
6140
+ });
6141
+
6142
+ it('handles "Failed" state correctly when hasMediaConnectionConnectedAtLeastOnce = true', async () => {
6143
+ meeting.hasMediaConnectionConnectedAtLeastOnce = true;
6144
+
6145
+ mockFailedEvent();
6146
+
6147
+ assert.notCalled(webex.internal.newMetrics.submitClientEvent);
6148
+ checkBehavioralMetricSent(true);
6149
+ });
6150
+ });
6151
+
6152
+ describe('should send correct metrics for ROAP_FAILURE event', () => {
6153
+ const fakeErrorMessage = 'test error';
6154
+ const fakeRootCauseName = 'root cause name';
6155
+ const fakeErrorName = 'test error name';
6156
+
6157
+ beforeEach(() => {
6158
+ meeting.setupMediaConnectionListeners();
6159
+ webex.internal.newMetrics.submitClientEvent.resetHistory();
6160
+ Metrics.sendBehavioralMetric.resetHistory();
6161
+ });
6162
+
6163
+ const checkMetricSent = (event, error) => {
5693
6164
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
5694
6165
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
5695
6166
  name: event,
@@ -6732,6 +7203,7 @@ describe('plugin-meetings', () => {
6732
7203
  done();
6733
7204
  });
6734
7205
  });
7206
+
6735
7207
  describe('#setUpLocusInfoMediaInactiveListener', () => {
6736
7208
  it('listens to disconnect due to un activity ', (done) => {
6737
7209
  TriggerProxy.trigger.reset();
@@ -6919,12 +7391,18 @@ describe('plugin-meetings', () => {
6919
7391
  it('sets correctly when policy data is present in token', () => {
6920
7392
  assert.notOk(meeting.selfUserPolicies);
6921
7393
 
6922
- const policyData = {permission: {userPolicies: {a: true}}};
7394
+ const testUrl = 'https://example.com';
7395
+
7396
+ const policyData = {permission: {
7397
+ userPolicies: {a: true},
7398
+ enforceVBGImagesURL: testUrl
7399
+ }};
6923
7400
  meeting.permissionTokenPayload = policyData;
6924
7401
 
6925
7402
  meeting.setSelfUserPolicies();
6926
7403
 
6927
7404
  assert.deepEqual(meeting.selfUserPolicies, policyData.permission.userPolicies);
7405
+ assert.equal(meeting.enforceVBGImagesURL, testUrl);
6928
7406
  });
6929
7407
 
6930
7408
  it('handles missing permission data', () => {
@@ -6977,12 +7455,14 @@ describe('plugin-meetings', () => {
6977
7455
  });
6978
7456
  describe('#closePeerConnections', () => {
6979
7457
  it('should close the webrtc media connection, and return a promise', async () => {
7458
+ const setNetworkStatusSpy = sinon.spy(meeting, 'setNetworkStatus');
6980
7459
  meeting.mediaProperties.webrtcMediaConnection = {close: sinon.stub()};
6981
7460
  const pcs = meeting.closePeerConnections();
6982
7461
 
6983
7462
  assert.exists(pcs.then);
6984
7463
  await pcs;
6985
7464
  assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.close);
7465
+ assert.calledOnceWithExactly(setNetworkStatusSpy, undefined);
6986
7466
  });
6987
7467
  });
6988
7468
  describe('#unsetPeerConnections', () => {
@@ -7190,14 +7670,6 @@ describe('plugin-meetings', () => {
7190
7670
  });
7191
7671
  });
7192
7672
 
7193
- describe('#setCorrelationId', () => {
7194
- it('should set the correlationId and return undefined', () => {
7195
- assert.ok(meeting.correlationId);
7196
- meeting.setCorrelationId(uuid1);
7197
- assert.equal(meeting.correlationId, uuid1);
7198
- });
7199
- });
7200
-
7201
7673
  describe('#setUpLocusInfoAssignHostListener', () => {
7202
7674
  let locusInfoOnSpy;
7203
7675
  let inMeetingActionsSetSpy;
@@ -7246,47 +7718,12 @@ describe('plugin-meetings', () => {
7246
7718
  });
7247
7719
 
7248
7720
  describe('#setUpLocusInfoMeetingInfoListener', () => {
7249
- let inMeetingActionsSetSpy;
7250
- let canUserLockSpy;
7251
- let canUserUnlockSpy;
7252
- let canUserStartSpy;
7253
- let canUserStopSpy;
7254
- let canUserPauseSpy;
7255
- let canUserResumeSpy;
7256
- let canSetMuteOnEntrySpy;
7257
- let canUnsetMuteOnEntrySpy;
7258
- let canSetDisallowUnmuteSpy;
7259
- let canUnsetDisallowUnmuteSpy;
7260
- let canUserRaiseHandSpy;
7261
- let bothLeaveAndEndMeetingAvailableSpy;
7262
- let canUserLowerAllHandsSpy;
7263
- let canUserLowerSomeoneElsesHandSpy;
7264
- let waitingForOthersToJoinSpy;
7265
7721
  let locusInfoOnSpy;
7266
7722
  let handleDataChannelUrlChangeSpy;
7267
7723
  let updateMeetingActionsSpy;
7268
7724
 
7269
7725
  beforeEach(() => {
7270
7726
  locusInfoOnSpy = sinon.spy(meeting.locusInfo, 'on');
7271
- canUserLockSpy = sinon.spy(MeetingUtil, 'canUserLock');
7272
- canUserUnlockSpy = sinon.spy(MeetingUtil, 'canUserUnlock');
7273
- canUserStartSpy = sinon.spy(RecordingUtil, 'canUserStart');
7274
- canUserStopSpy = sinon.spy(RecordingUtil, 'canUserStop');
7275
- canUserPauseSpy = sinon.spy(RecordingUtil, 'canUserPause');
7276
- canUserResumeSpy = sinon.spy(RecordingUtil, 'canUserResume');
7277
- canSetMuteOnEntrySpy = sinon.spy(ControlsOptionsUtil, 'canSetMuteOnEntry');
7278
- canUnsetMuteOnEntrySpy = sinon.spy(ControlsOptionsUtil, 'canUnsetMuteOnEntry');
7279
- canSetDisallowUnmuteSpy = sinon.spy(ControlsOptionsUtil, 'canSetDisallowUnmute');
7280
- canUnsetDisallowUnmuteSpy = sinon.spy(ControlsOptionsUtil, 'canUnsetDisallowUnmute');
7281
- inMeetingActionsSetSpy = sinon.spy(meeting.inMeetingActions, 'set');
7282
- canUserRaiseHandSpy = sinon.spy(MeetingUtil, 'canUserRaiseHand');
7283
- canUserLowerAllHandsSpy = sinon.spy(MeetingUtil, 'canUserLowerAllHands');
7284
- bothLeaveAndEndMeetingAvailableSpy = sinon.spy(
7285
- MeetingUtil,
7286
- 'bothLeaveAndEndMeetingAvailable'
7287
- );
7288
- canUserLowerSomeoneElsesHandSpy = sinon.spy(MeetingUtil, 'canUserLowerSomeoneElsesHand');
7289
- waitingForOthersToJoinSpy = sinon.spy(MeetingUtil, 'waitingForOthersToJoin');
7290
7727
  handleDataChannelUrlChangeSpy = sinon.spy(meeting, 'handleDataChannelUrlChange');
7291
7728
  updateMeetingActionsSpy = sinon.spy(meeting, 'updateMeetingActions');
7292
7729
  });
@@ -7324,29 +7761,686 @@ describe('plugin-meetings', () => {
7324
7761
  assert.equal(locusInfoOnSpy.thirdCall.args[0], 'MEETING_INFO_UPDATED');
7325
7762
  const callback = locusInfoOnSpy.thirdCall.args[1];
7326
7763
 
7327
- const payload = {
7328
- info: {
7329
- userDisplayHints: ['LOCK_CONTROL_UNLOCK'],
7330
- },
7331
- };
7332
-
7333
7764
  callback();
7334
7765
 
7335
- assert.calledWith(canUserLockSpy, payload.info.userDisplayHints);
7336
- assert.calledWith(canUserUnlockSpy, payload.info.userDisplayHints);
7337
- assert.calledWith(canUserStartSpy, payload.info.userDisplayHints);
7338
- assert.calledWith(canUserStopSpy, payload.info.userDisplayHints);
7339
- assert.calledWith(canUserPauseSpy, payload.info.userDisplayHints);
7340
- assert.calledWith(canUserResumeSpy, payload.info.userDisplayHints);
7341
- assert.calledWith(canSetMuteOnEntrySpy, payload.info.userDisplayHints);
7342
- assert.calledWith(canUnsetMuteOnEntrySpy, payload.info.userDisplayHints);
7343
- assert.calledWith(canSetDisallowUnmuteSpy, payload.info.userDisplayHints);
7344
- assert.calledWith(canUnsetDisallowUnmuteSpy, payload.info.userDisplayHints);
7345
- assert.calledWith(canUserRaiseHandSpy, payload.info.userDisplayHints);
7346
- assert.calledWith(bothLeaveAndEndMeetingAvailableSpy, payload.info.userDisplayHints);
7347
- assert.calledWith(canUserLowerAllHandsSpy, payload.info.userDisplayHints);
7348
- assert.calledWith(canUserLowerSomeoneElsesHandSpy, payload.info.userDisplayHints);
7349
- assert.calledWith(waitingForOthersToJoinSpy, payload.info.userDisplayHints);
7766
+ assert.calledWith(updateMeetingActionsSpy);
7767
+ assert.calledWith(setRecordingDisplayHintsSpy, userDisplayHints);
7768
+ assert.calledWith(setUserPolicySpy, userDisplayPolicy);
7769
+ assert.calledWith(setControlsDisplayHintsSpy, userDisplayHints);
7770
+ assert.calledWith(handleDataChannelUrlChangeSpy, datachannelUrl);
7771
+ });
7772
+ });
7773
+
7774
+ describe('#updateMeetingActions', () => {
7775
+ let inMeetingActionsSetSpy;
7776
+ let canUserLockSpy;
7777
+ let canUserUnlockSpy;
7778
+ let canUserStartSpy;
7779
+ let canUserStopSpy;
7780
+ let canUserPauseSpy;
7781
+ let canUserResumeSpy;
7782
+ let canSetMuteOnEntrySpy;
7783
+ let canUnsetMuteOnEntrySpy;
7784
+ let canSetDisallowUnmuteSpy;
7785
+ let canUnsetDisallowUnmuteSpy;
7786
+ let canUserRaiseHandSpy;
7787
+ let bothLeaveAndEndMeetingAvailableSpy;
7788
+ let canUserLowerAllHandsSpy;
7789
+ let canUserLowerSomeoneElsesHandSpy;
7790
+ let waitingForOthersToJoinSpy;
7791
+ let canSendReactionsSpy;
7792
+ let canUserRenameSelfAndObservedSpy;
7793
+ let canUserRenameOthersSpy;
7794
+ let canShareWhiteBoardSpy;
7795
+ // Due to import tree issues, hasHints must be stubed within the scope of the `it`.
7796
+
7797
+ beforeEach(() => {
7798
+ canUserLockSpy = sinon.spy(MeetingUtil, 'canUserLock');
7799
+ canUserUnlockSpy = sinon.spy(MeetingUtil, 'canUserUnlock');
7800
+ canUserStartSpy = sinon.spy(RecordingUtil, 'canUserStart');
7801
+ canUserStopSpy = sinon.spy(RecordingUtil, 'canUserStop');
7802
+ canUserPauseSpy = sinon.spy(RecordingUtil, 'canUserPause');
7803
+ canUserResumeSpy = sinon.spy(RecordingUtil, 'canUserResume');
7804
+ canSetMuteOnEntrySpy = sinon.spy(ControlsOptionsUtil, 'canSetMuteOnEntry');
7805
+ canUnsetMuteOnEntrySpy = sinon.spy(ControlsOptionsUtil, 'canUnsetMuteOnEntry');
7806
+ canSetDisallowUnmuteSpy = sinon.spy(ControlsOptionsUtil, 'canSetDisallowUnmute');
7807
+ canUnsetDisallowUnmuteSpy = sinon.spy(ControlsOptionsUtil, 'canUnsetDisallowUnmute');
7808
+ inMeetingActionsSetSpy = sinon.spy(meeting.inMeetingActions, 'set');
7809
+ canUserRaiseHandSpy = sinon.spy(MeetingUtil, 'canUserRaiseHand');
7810
+ canUserLowerAllHandsSpy = sinon.spy(MeetingUtil, 'canUserLowerAllHands');
7811
+ bothLeaveAndEndMeetingAvailableSpy = sinon.spy(
7812
+ MeetingUtil,
7813
+ 'bothLeaveAndEndMeetingAvailable'
7814
+ );
7815
+ canUserLowerSomeoneElsesHandSpy = sinon.spy(MeetingUtil, 'canUserLowerSomeoneElsesHand');
7816
+ waitingForOthersToJoinSpy = sinon.spy(MeetingUtil, 'waitingForOthersToJoin');
7817
+ canSendReactionsSpy = sinon.spy(MeetingUtil, 'canSendReactions');
7818
+ canUserRenameSelfAndObservedSpy = sinon.spy(MeetingUtil, 'canUserRenameSelfAndObserved');
7819
+ canUserRenameOthersSpy = sinon.spy(MeetingUtil, 'canUserRenameOthers');
7820
+ canShareWhiteBoardSpy = sinon.spy(MeetingUtil, 'canShareWhiteBoard');
7821
+ });
7822
+
7823
+ afterEach(() => {
7824
+ inMeetingActionsSetSpy.restore();
7825
+ waitingForOthersToJoinSpy.restore();
7826
+ });
7827
+
7828
+ forEach(
7829
+ [
7830
+ {
7831
+ actionName: 'canShareApplication',
7832
+ expectedEnabled: true,
7833
+ arePolicyRestrictionsSupported: false,
7834
+ },
7835
+ {
7836
+ actionName: 'canShareApplication',
7837
+ expectedEnabled: false,
7838
+ arePolicyRestrictionsSupported: true,
7839
+ },
7840
+ {
7841
+ actionName: 'canShareDesktop',
7842
+ arePolicyRestrictionsSupported: false,
7843
+ expectedEnabled: true,
7844
+ },
7845
+ {
7846
+ actionName: 'canShareDesktop',
7847
+ arePolicyRestrictionsSupported: true,
7848
+ expectedEnabled: false,
7849
+ },
7850
+ {
7851
+ actionName: 'canShareContent',
7852
+ arePolicyRestrictionsSupported: false,
7853
+ expectedEnabled: true,
7854
+ },
7855
+ {
7856
+ actionName: 'canShareContent',
7857
+ arePolicyRestrictionsSupported: true,
7858
+ expectedEnabled: false,
7859
+ },
7860
+ {
7861
+ actionName: 'canUseVoip',
7862
+ expectedEnabled: true,
7863
+ arePolicyRestrictionsSupported: false,
7864
+ },
7865
+ {
7866
+ actionName: 'canUseVoip',
7867
+ expectedEnabled: false,
7868
+ arePolicyRestrictionsSupported: true,
7869
+ },
7870
+ ],
7871
+ ({actionName, arePolicyRestrictionsSupported, expectedEnabled}) => {
7872
+ it(`${actionName} is ${expectedEnabled} when the call type is ${arePolicyRestrictionsSupported}`, () => {
7873
+ meeting.userDisplayHints = [];
7874
+ meeting.meetingInfo = {some: 'info'};
7875
+ sinon
7876
+ .stub(meeting, 'arePolicyRestrictionsSupported')
7877
+ .returns(arePolicyRestrictionsSupported);
7878
+
7879
+ meeting.updateMeetingActions();
7880
+
7881
+ assert.equal(meeting.inMeetingActions.get()[actionName], expectedEnabled);
7882
+ });
7883
+ }
7884
+ );
7885
+
7886
+ forEach(
7887
+ [
7888
+ {
7889
+ actionName: 'canShareFile',
7890
+ requiredDisplayHints: [DISPLAY_HINTS.SHARE_FILE],
7891
+ requiredPolicies: [SELF_POLICY.SUPPORT_FILE_SHARE],
7892
+ },
7893
+ {
7894
+ actionName: 'canShareApplication',
7895
+ requiredDisplayHints: [DISPLAY_HINTS.SHARE_APPLICATION],
7896
+ requiredPolicies: [SELF_POLICY.SUPPORT_APP_SHARE],
7897
+ },
7898
+ {
7899
+ actionName: 'canShareCamera',
7900
+ requiredDisplayHints: [DISPLAY_HINTS.SHARE_CAMERA],
7901
+ requiredPolicies: [SELF_POLICY.SUPPORT_CAMERA_SHARE],
7902
+ },
7903
+ {
7904
+ actionName: 'canBroadcastMessageToBreakout',
7905
+ requiredDisplayHints: [DISPLAY_HINTS.BROADCAST_MESSAGE_TO_BREAKOUT],
7906
+ requiredPolicies: [SELF_POLICY.SUPPORT_BROADCAST_MESSAGE],
7907
+ },
7908
+ {
7909
+ actionName: 'canShareDesktop',
7910
+ requiredDisplayHints: [DISPLAY_HINTS.SHARE_DESKTOP],
7911
+ requiredPolicies: [SELF_POLICY.SUPPORT_DESKTOP_SHARE],
7912
+ },
7913
+ {
7914
+ actionName: 'canTransferFile',
7915
+ requiredDisplayHints: [],
7916
+ requiredPolicies: [SELF_POLICY.SUPPORT_FILE_TRANSFER],
7917
+ },
7918
+ {
7919
+ actionName: 'canChat',
7920
+ requiredDisplayHints: [],
7921
+ requiredPolicies: [SELF_POLICY.SUPPORT_CHAT],
7922
+ },
7923
+ {
7924
+ actionName: 'canShareDesktop',
7925
+ requiredDisplayHints: [DISPLAY_HINTS.SHARE_DESKTOP],
7926
+ requiredPolicies: [],
7927
+ enableUnifiedMeetings: false,
7928
+ arePolicyRestrictionsSupported: false,
7929
+ },
7930
+ {
7931
+ actionName: 'canShareApplication',
7932
+ requiredDisplayHints: [DISPLAY_HINTS.SHARE_APPLICATION],
7933
+ requiredPolicies: [],
7934
+ enableUnifiedMeetings: false,
7935
+ arePolicyRestrictionsSupported: false,
7936
+ },
7937
+ {
7938
+ actionName: 'canAnnotate',
7939
+ requiredDisplayHints: [],
7940
+ requiredPolicies: [SELF_POLICY.SUPPORT_ANNOTATION],
7941
+ },
7942
+ ],
7943
+ ({
7944
+ actionName,
7945
+ requiredDisplayHints,
7946
+ requiredPolicies,
7947
+ enableUnifiedMeetings,
7948
+ meetingInfo,
7949
+ arePolicyRestrictionsSupported,
7950
+ }) => {
7951
+ it(`${actionName} is enabled when the conditions are met`, () => {
7952
+ meeting.userDisplayHints = requiredDisplayHints;
7953
+ meeting.selfUserPolicies = undefined;
7954
+ sinon
7955
+ .stub(meeting, 'arePolicyRestrictionsSupported')
7956
+ .returns(arePolicyRestrictionsSupported);
7957
+
7958
+ meeting.config.experimental.enableUnifiedMeetings = isUndefined(enableUnifiedMeetings)
7959
+ ? true
7960
+ : enableUnifiedMeetings;
7961
+
7962
+ meeting.meetingInfo = isUndefined(meetingInfo) ? {some: 'info'} : meetingInfo;
7963
+
7964
+ if (requiredPolicies) {
7965
+ meeting.selfUserPolicies = {};
7966
+ }
7967
+ forEach(requiredPolicies, (policy) => {
7968
+ meeting.selfUserPolicies[policy] = true;
7969
+ });
7970
+
7971
+ meeting.updateMeetingActions();
7972
+
7973
+ assert.isTrue(meeting.inMeetingActions.get()[actionName]);
7974
+ });
7975
+
7976
+ if (requiredDisplayHints.length !== 0) {
7977
+ it(`${actionName} is disabled when the required display hints are missing`, () => {
7978
+ meeting.userDisplayHints = [];
7979
+ meeting.selfUserPolicies = undefined;
7980
+
7981
+ meeting.meetingInfo = isUndefined(meetingInfo) ? {some: 'info'} : meetingInfo;
7982
+
7983
+ if (requiredPolicies) {
7984
+ meeting.selfUserPolicies = {};
7985
+ }
7986
+ forEach(requiredPolicies, (policy) => {
7987
+ meeting.selfUserPolicies[policy] = true;
7988
+ });
7989
+
7990
+ meeting.updateMeetingActions();
7991
+
7992
+ assert.isFalse(meeting.inMeetingActions.get()[actionName]);
7993
+ });
7994
+ }
7995
+
7996
+ it(`${actionName} is disabled when the required policies are missing`, () => {
7997
+ meeting.userDisplayHints = requiredDisplayHints;
7998
+ meeting.selfUserPolicies = undefined;
7999
+
8000
+ if (requiredPolicies) {
8001
+ meeting.selfUserPolicies = {};
8002
+ }
8003
+ meeting.meetingInfo = isUndefined(meetingInfo) ? {some: 'info'} : meetingInfo;
8004
+
8005
+ meeting.updateMeetingActions();
8006
+
8007
+ assert.isFalse(meeting.inMeetingActions.get()[actionName]);
8008
+ });
8009
+ }
8010
+ );
8011
+
8012
+ forEach(
8013
+ [
8014
+ {
8015
+ meetingInfo: {
8016
+ video: {
8017
+ supportHDV: true,
8018
+ supportHQV: true,
8019
+ },
8020
+ },
8021
+ selfUserPolicies: {
8022
+ [SELF_POLICY.SUPPORT_HDV]: true,
8023
+ [SELF_POLICY.SUPPORT_HQV]: true,
8024
+ },
8025
+ expectedActions: {
8026
+ supportHQV: true,
8027
+ supportHDV: true,
8028
+ },
8029
+ },
8030
+ {
8031
+ meetingInfo: {
8032
+ video: {
8033
+ supportHDV: false,
8034
+ supportHQV: false,
8035
+ },
8036
+ },
8037
+ selfUserPolicies: {
8038
+ [SELF_POLICY.SUPPORT_HDV]: true,
8039
+ [SELF_POLICY.SUPPORT_HQV]: true,
8040
+ },
8041
+ expectedActions: {
8042
+ supportHQV: false,
8043
+ supportHDV: false,
8044
+ },
8045
+ },
8046
+ {
8047
+ meetingInfo: {
8048
+ video: {
8049
+ supportHDV: true,
8050
+ supportHQV: true,
8051
+ },
8052
+ },
8053
+ selfUserPolicies: {
8054
+ [SELF_POLICY.SUPPORT_HDV]: false,
8055
+ [SELF_POLICY.SUPPORT_HQV]: false,
8056
+ },
8057
+ expectedActions: {
8058
+ supportHQV: false,
8059
+ supportHDV: false,
8060
+ },
8061
+ },
8062
+ {
8063
+ meetingInfo: undefined,
8064
+ selfUserPolicies: {},
8065
+ expectedActions: {
8066
+ supportHQV: true,
8067
+ supportHDV: true,
8068
+ },
8069
+ },
8070
+ {
8071
+ meetingInfo: {some: 'data'},
8072
+ selfUserPolicies: undefined,
8073
+ expectedActions: {
8074
+ supportHQV: true,
8075
+ supportHDV: true,
8076
+ },
8077
+ },
8078
+ ],
8079
+ ({meetingInfo, selfUserPolicies, expectedActions}) => {
8080
+ it(`expectedActions are ${JSON.stringify(
8081
+ expectedActions
8082
+ )} when policies are ${JSON.stringify(
8083
+ selfUserPolicies
8084
+ )} and meetingInfo is ${JSON.stringify(meetingInfo)}`, () => {
8085
+ meeting.meetingInfo = meetingInfo;
8086
+ meeting.selfUserPolicies = selfUserPolicies;
8087
+
8088
+ meeting.updateMeetingActions();
8089
+
8090
+ assert.deepEqual(
8091
+ {
8092
+ supportHDV: meeting.inMeetingActions.supportHDV,
8093
+ supportHQV: meeting.inMeetingActions.supportHQV,
8094
+ },
8095
+ expectedActions
8096
+ );
8097
+ });
8098
+ }
8099
+ );
8100
+
8101
+ forEach(
8102
+ [
8103
+ // policies supported and enforce is true
8104
+ {
8105
+ meetingInfo: {video: {}},
8106
+ selfUserPolicies: {
8107
+ [SELF_POLICY.ENFORCE_VIRTUAL_BACKGROUND]: true,
8108
+ },
8109
+ expectedActions: {
8110
+ enforceVirtualBackground: true,
8111
+ },
8112
+ },
8113
+ // policies supported and enforce is false
8114
+ {
8115
+ meetingInfo: {video: {}},
8116
+ selfUserPolicies: {
8117
+ [SELF_POLICY.ENFORCE_VIRTUAL_BACKGROUND]: false,
8118
+ },
8119
+ expectedActions: {
8120
+ enforceVirtualBackground: false,
8121
+ },
8122
+ },
8123
+ // policies not supported but enforce is true
8124
+ {
8125
+ meetingInfo: undefined,
8126
+ selfUserPolicies: {
8127
+ [SELF_POLICY.ENFORCE_VIRTUAL_BACKGROUND]: true,
8128
+ },
8129
+ expectedActions: {
8130
+ enforceVirtualBackground: false,
8131
+ },
8132
+ },
8133
+ ],
8134
+ ({meetingInfo, selfUserPolicies, expectedActions}) => {
8135
+ it(`expectedActions are ${JSON.stringify(
8136
+ expectedActions
8137
+ )} when policies are ${JSON.stringify(
8138
+ selfUserPolicies
8139
+ )} and meetingInfo is ${JSON.stringify(meetingInfo)}`, () => {
8140
+ meeting.meetingInfo = meetingInfo;
8141
+ meeting.selfUserPolicies = selfUserPolicies;
8142
+
8143
+ meeting.updateMeetingActions();
8144
+
8145
+ assert.deepEqual(
8146
+ {
8147
+ enforceVirtualBackground: meeting.inMeetingActions.enforceVirtualBackground,
8148
+ },
8149
+ expectedActions
8150
+ );
8151
+ });
8152
+ }
8153
+ );
8154
+
8155
+ it('canUseVoip is disabled when the required policies are missing', () => {
8156
+ meeting.userDisplayHints = [DISPLAY_HINTS.VOIP_IS_ENABLED];
8157
+ meeting.selfUserPolicies = {};
8158
+ meeting.meetingInfo.supportVoIP = true;
8159
+
8160
+ meeting.updateMeetingActions();
8161
+
8162
+ assert.isFalse(meeting.inMeetingActions.get()['canUseVoip']);
8163
+ });
8164
+
8165
+ it('canUseVoip is enabled based on api info when the conditions are met', () => {
8166
+ meeting.userDisplayHints = undefined;
8167
+ meeting.selfUserPolicies = {[SELF_POLICY.SUPPORT_VOIP]: true};
8168
+ meeting.meetingInfo.supportVoIP = true;
8169
+
8170
+ meeting.updateMeetingActions();
8171
+
8172
+ assert.isTrue(meeting.inMeetingActions.get()['canUseVoip']);
8173
+ });
8174
+
8175
+ it('canUseVoip is enabled based on api info when the conditions are met - no display hints', () => {
8176
+ meeting.userDisplayHints = [];
8177
+ meeting.selfUserPolicies = {[SELF_POLICY.SUPPORT_VOIP]: true};
8178
+ meeting.meetingInfo.supportVoIP = true;
8179
+
8180
+ meeting.updateMeetingActions();
8181
+
8182
+ assert.isTrue(meeting.inMeetingActions.get()['canUseVoip']);
8183
+ });
8184
+
8185
+ it('canUseVoip is enabled when there is no meeting info', () => {
8186
+ meeting.updateMeetingActions();
8187
+
8188
+ assert.isTrue(meeting.inMeetingActions.get()['canUseVoip']);
8189
+ });
8190
+
8191
+ it('canUseVoip is enabled when it is a locus call', () => {
8192
+ meeting.meetingInfo = {some: 'info'};
8193
+ meeting.type = 'CALL';
8194
+
8195
+ meeting.updateMeetingActions();
8196
+
8197
+ assert.isTrue(meeting.inMeetingActions.get()['canUseVoip']);
8198
+ });
8199
+
8200
+ it('canUseVoip is disabled based on api info when supportVoip is false', () => {
8201
+ meeting.userDisplayHints = undefined;
8202
+ meeting.selfUserPolicies = {[SELF_POLICY.SUPPORT_VOIP]: true};
8203
+ meeting.meetingInfo.supportVoIP = false;
8204
+
8205
+ meeting.updateMeetingActions();
8206
+
8207
+ assert.isFalse(meeting.inMeetingActions.get()['canUseVoip']);
8208
+ });
8209
+
8210
+ it('canUseVoip is disabled based on api info when the required policies are missing', () => {
8211
+ meeting.userDisplayHints = undefined;
8212
+ meeting.selfUserPolicies = {};
8213
+ meeting.meetingInfo.supportVoIP = true;
8214
+
8215
+ meeting.updateMeetingActions();
8216
+
8217
+ assert.isFalse(meeting.inMeetingActions.get()['canUseVoip']);
8218
+ });
8219
+
8220
+ it('canUseVoip is enabled when there are no policies', () => {
8221
+ meeting.userDisplayHints = [DISPLAY_HINTS.VOIP_IS_ENABLED];
8222
+ meeting.selfUserPolicies = undefined;
8223
+ meeting.meetingInfo.supportVoIP = false;
8224
+
8225
+ meeting.updateMeetingActions();
8226
+
8227
+ assert.isTrue(meeting.inMeetingActions.get()['canUseVoip']);
8228
+ });
8229
+
8230
+ forEach(
8231
+ [
8232
+ {
8233
+ meetingInfo: {},
8234
+ selfUserPolicies: {
8235
+ [SELF_POLICY.SUPPORT_VIDEO]: true,
8236
+ },
8237
+ expectedActions: {
8238
+ canDoVideo: true,
8239
+ },
8240
+ },
8241
+ {
8242
+ meetingInfo: {},
8243
+ selfUserPolicies: {
8244
+ [SELF_POLICY.SUPPORT_VIDEO]: false,
8245
+ },
8246
+ expectedActions: {
8247
+ canDoVideo: true,
8248
+ },
8249
+ },
8250
+ {
8251
+ meetingInfo: {some: 'data'},
8252
+ selfUserPolicies: {
8253
+ [SELF_POLICY.SUPPORT_VIDEO]: true,
8254
+ },
8255
+ expectedActions: {
8256
+ canDoVideo: false,
8257
+ },
8258
+ },
8259
+ {
8260
+ meetingInfo: {some: 'data'},
8261
+ selfUserPolicies: undefined,
8262
+ expectedActions: {
8263
+ canDoVideo: true,
8264
+ },
8265
+ },
8266
+ {
8267
+ meetingInfo: {
8268
+ video: {},
8269
+ },
8270
+ selfUserPolicies: {
8271
+ [SELF_POLICY.SUPPORT_VIDEO]: true,
8272
+ },
8273
+ expectedActions: {
8274
+ canDoVideo: true,
8275
+ },
8276
+ },
8277
+ {
8278
+ meetingInfo: undefined,
8279
+ selfUserPolicies: {},
8280
+ expectedActions: {
8281
+ canDoVideo: true,
8282
+ },
8283
+ },
8284
+ {
8285
+ meetingInfo: {
8286
+ video: {},
8287
+ },
8288
+ selfUserPolicies: {
8289
+ [SELF_POLICY.SUPPORT_VIDEO]: false,
8290
+ },
8291
+ expectedActions: {
8292
+ canDoVideo: false,
8293
+ },
8294
+ },
8295
+ ],
8296
+ ({meetingInfo, selfUserPolicies, expectedActions}) => {
8297
+ it(`has expectedActions ${JSON.stringify(
8298
+ expectedActions
8299
+ )} when policies are ${JSON.stringify(
8300
+ selfUserPolicies
8301
+ )} and meetingInfo is ${JSON.stringify(meetingInfo)}`, () => {
8302
+ meeting.meetingInfo = meetingInfo;
8303
+ meeting.selfUserPolicies = selfUserPolicies;
8304
+ meeting.config.experimental.enableUnifiedMeetings = true;
8305
+
8306
+ meeting.updateMeetingActions();
8307
+
8308
+ assert.deepEqual(
8309
+ {
8310
+ canDoVideo: meeting.inMeetingActions.canDoVideo,
8311
+ },
8312
+ expectedActions
8313
+ );
8314
+ });
8315
+ }
8316
+ );
8317
+
8318
+ it('correctly updates the meeting actions', () => {
8319
+ // Due to import tree issues, hasHints must be stubed within the scope of the `it`.
8320
+ const restorableHasHints = ControlsOptionsUtil.hasHints;
8321
+ ControlsOptionsUtil.hasHints = sinon.stub().returns(true);
8322
+ ControlsOptionsUtil.hasPolicies = sinon.stub().returns(true);
8323
+
8324
+ const selfUserPolicies = {a: true};
8325
+ meeting.selfUserPolicies = {a: true};
8326
+ const userDisplayHints = ['LOCK_CONTROL_UNLOCK'];
8327
+ meeting.userDisplayHints = ['LOCK_CONTROL_UNLOCK'];
8328
+ meeting.meetingInfo.supportVoIP = true;
8329
+
8330
+ meeting.updateMeetingActions();
8331
+
8332
+ assert.calledWith(canUserLockSpy, userDisplayHints);
8333
+ assert.calledWith(canUserUnlockSpy, userDisplayHints);
8334
+ assert.calledWith(canUserStartSpy, userDisplayHints);
8335
+ assert.calledWith(canUserStopSpy, userDisplayHints);
8336
+ assert.calledWith(canUserPauseSpy, userDisplayHints);
8337
+ assert.calledWith(canUserResumeSpy, userDisplayHints);
8338
+ assert.calledWith(canSetMuteOnEntrySpy, userDisplayHints);
8339
+ assert.calledWith(canUnsetMuteOnEntrySpy, userDisplayHints);
8340
+ assert.calledWith(canSetDisallowUnmuteSpy, userDisplayHints);
8341
+ assert.calledWith(canUnsetDisallowUnmuteSpy, userDisplayHints);
8342
+ assert.calledWith(canUserRaiseHandSpy, userDisplayHints);
8343
+ assert.calledWith(bothLeaveAndEndMeetingAvailableSpy, userDisplayHints);
8344
+ assert.calledWith(canUserLowerAllHandsSpy, userDisplayHints);
8345
+ assert.calledWith(canUserLowerSomeoneElsesHandSpy, userDisplayHints);
8346
+ assert.calledWith(waitingForOthersToJoinSpy, userDisplayHints);
8347
+ assert.calledWith(canSendReactionsSpy, null, userDisplayHints);
8348
+ assert.calledWith(canUserRenameSelfAndObservedSpy, userDisplayHints);
8349
+ assert.calledWith(canUserRenameOthersSpy, userDisplayHints);
8350
+ assert.calledWith(canShareWhiteBoardSpy, userDisplayHints);
8351
+
8352
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8353
+ requiredHints: [DISPLAY_HINTS.MUTE_ALL],
8354
+ displayHints: userDisplayHints,
8355
+ });
8356
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8357
+ requiredHints: [DISPLAY_HINTS.UNMUTE_ALL],
8358
+ displayHints: userDisplayHints,
8359
+ });
8360
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8361
+ requiredHints: [DISPLAY_HINTS.ENABLE_HARD_MUTE],
8362
+ displayHints: userDisplayHints,
8363
+ });
8364
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8365
+ requiredHints: [DISPLAY_HINTS.DISABLE_HARD_MUTE],
8366
+ displayHints: userDisplayHints,
8367
+ });
8368
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8369
+ requiredHints: [DISPLAY_HINTS.ENABLE_MUTE_ON_ENTRY],
8370
+ displayHints: userDisplayHints,
8371
+ });
8372
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8373
+ requiredHints: [DISPLAY_HINTS.DISABLE_MUTE_ON_ENTRY],
8374
+ displayHints: userDisplayHints,
8375
+ });
8376
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8377
+ requiredHints: [DISPLAY_HINTS.ENABLE_REACTIONS],
8378
+ displayHints: userDisplayHints,
8379
+ });
8380
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8381
+ requiredHints: [DISPLAY_HINTS.DISABLE_REACTIONS],
8382
+ displayHints: userDisplayHints,
8383
+ });
8384
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8385
+ requiredHints: [DISPLAY_HINTS.ENABLE_SHOW_DISPLAY_NAME],
8386
+ displayHints: userDisplayHints,
8387
+ });
8388
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8389
+ requiredHints: [DISPLAY_HINTS.DISABLE_SHOW_DISPLAY_NAME],
8390
+ displayHints: userDisplayHints,
8391
+ });
8392
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8393
+ requiredHints: [DISPLAY_HINTS.SHARE_CONTROL],
8394
+ displayHints: userDisplayHints,
8395
+ });
8396
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8397
+ requiredHints: [DISPLAY_HINTS.ENABLE_VIEW_THE_PARTICIPANT_LIST],
8398
+ displayHints: userDisplayHints,
8399
+ });
8400
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8401
+ requiredHints: [DISPLAY_HINTS.DISABLE_VIEW_THE_PARTICIPANT_LIST],
8402
+ displayHints: userDisplayHints,
8403
+ });
8404
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8405
+ requiredHints: [DISPLAY_HINTS.SHARE_FILE],
8406
+ displayHints: userDisplayHints,
8407
+ });
8408
+ assert.calledWith(ControlsOptionsUtil.hasPolicies, {
8409
+ requiredPolicies: [SELF_POLICY.SUPPORT_FILE_SHARE],
8410
+ policies: selfUserPolicies,
8411
+ });
8412
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8413
+ requiredHints: [DISPLAY_HINTS.SHARE_APPLICATION],
8414
+ displayHints: userDisplayHints,
8415
+ });
8416
+ assert.calledWith(ControlsOptionsUtil.hasPolicies, {
8417
+ requiredPolicies: [SELF_POLICY.SUPPORT_APP_SHARE],
8418
+ policies: selfUserPolicies,
8419
+ });
8420
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8421
+ requiredHints: [DISPLAY_HINTS.SHARE_CAMERA],
8422
+ displayHints: userDisplayHints,
8423
+ });
8424
+ assert.calledWith(ControlsOptionsUtil.hasPolicies, {
8425
+ requiredPolicies: [SELF_POLICY.SUPPORT_CAMERA_SHARE],
8426
+ policies: selfUserPolicies,
8427
+ });
8428
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8429
+ requiredHints: [DISPLAY_HINTS.SHARE_DESKTOP],
8430
+ displayHints: userDisplayHints,
8431
+ });
8432
+ assert.calledWith(ControlsOptionsUtil.hasPolicies, {
8433
+ requiredPolicies: [SELF_POLICY.SUPPORT_DESKTOP_SHARE],
8434
+ policies: selfUserPolicies,
8435
+ });
8436
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8437
+ requiredHints: [DISPLAY_HINTS.SHARE_CONTENT],
8438
+ displayHints: userDisplayHints,
8439
+ });
8440
+ assert.calledWith(ControlsOptionsUtil.hasPolicies, {
8441
+ requiredPolicies: [SELF_POLICY.SUPPORT_VOIP],
8442
+ policies: selfUserPolicies,
8443
+ });
7350
8444
 
7351
8445
  assert.calledWith(updateMeetingActionsSpy);
7352
8446
  assert.calledWith(setRecordingDisplayHintsSpy, userDisplayHints);
@@ -9465,7 +10559,7 @@ describe('plugin-meetings', () => {
9465
10559
  });
9466
10560
  });
9467
10561
 
9468
- describe('#getPermissionTokenTimeLeftInSec', () => {
10562
+ describe('#getPermissionTokenExpiryInfo', () => {
9469
10563
  let now;
9470
10564
  let clock;
9471
10565
 
@@ -9481,63 +10575,65 @@ describe('plugin-meetings', () => {
9481
10575
  })
9482
10576
 
9483
10577
  it('should return undefined if exp is undefined', () => {
9484
- assert.equal(meeting.getPermissionTokenTimeLeftInSec(), undefined)
10578
+ assert.equal(meeting.getPermissionTokenExpiryInfo(), undefined)
9485
10579
  });
9486
10580
 
9487
10581
  it('should return the expected positive exp', () => {
9488
10582
  // set permission token as now + 1 sec
9489
- meeting.permissionTokenPayload = {exp: (now + 1000).toString()};
9490
- assert.equal(meeting.getPermissionTokenTimeLeftInSec(), 1);
10583
+ const expiryTime = now + 1000;
10584
+ meeting.permissionTokenPayload = {exp: (expiryTime).toString()};
10585
+ assert.deepEqual(meeting.getPermissionTokenExpiryInfo(), {timeLeft: 1, expiryTime: Number(expiryTime), currentTime: now});
9491
10586
  });
9492
10587
 
9493
10588
  it('should return the expected negative exp', () => {
9494
10589
  // set permission token as now - 1 sec
9495
- meeting.permissionTokenPayload = {exp: (now - 1000).toString()};
9496
- assert.equal(meeting.getPermissionTokenTimeLeftInSec(), -1);
10590
+ const expiryTime = now - 1000;
10591
+ meeting.permissionTokenPayload = {exp: (expiryTime).toString()};
10592
+ assert.deepEqual(meeting.getPermissionTokenExpiryInfo(), {timeLeft: -1, expiryTime: Number(expiryTime), currentTime: now});
9497
10593
  });
9498
10594
  });
9499
10595
 
9500
10596
  describe('#checkAndRefreshPermissionToken', () => {
9501
10597
  it('should not fire refreshPermissionToken if permissionToken is not defined', async() => {
9502
- meeting.getPermissionTokenTimeLeftInSec = sinon.stub().returns(undefined)
10598
+ meeting.getPermissionTokenExpiryInfo = sinon.stub().returns(undefined)
9503
10599
  meeting.refreshPermissionToken = sinon.stub().returns(Promise.resolve('test return value'));
9504
10600
 
9505
10601
  const returnValue = await meeting.checkAndRefreshPermissionToken(10, 'ttl-join');
9506
10602
 
9507
- assert.calledOnce(meeting.getPermissionTokenTimeLeftInSec);
10603
+ assert.calledOnce(meeting.getPermissionTokenExpiryInfo);
9508
10604
  assert.notCalled(meeting.refreshPermissionToken);
9509
10605
  assert.equal(returnValue, undefined);
9510
10606
  });
9511
10607
 
9512
10608
  it('should fire refreshPermissionToken if time left is below 10sec', async() => {
9513
- meeting.getPermissionTokenTimeLeftInSec = sinon.stub().returns(9)
10609
+ meeting.getPermissionTokenExpiryInfo = sinon.stub().returns({timeLeft: 9, expiryTime: 122132, currentTime: Date.now()})
9514
10610
  meeting.refreshPermissionToken = sinon.stub().returns(Promise.resolve('test return value'));
9515
10611
 
9516
10612
  const returnValue = await meeting.checkAndRefreshPermissionToken(10, 'ttl-join');
9517
10613
 
9518
- assert.calledOnce(meeting.getPermissionTokenTimeLeftInSec);
10614
+ assert.calledOnce(meeting.getPermissionTokenExpiryInfo);
9519
10615
  assert.calledOnceWithExactly(meeting.refreshPermissionToken, 'ttl-join');
9520
10616
  assert.equal(returnValue, 'test return value');
9521
10617
  });
9522
10618
 
9523
10619
  it('should fire refreshPermissionToken if time left is equal 10sec', async () => {
9524
- meeting.getPermissionTokenTimeLeftInSec = sinon.stub().returns(10)
10620
+ meeting.getPermissionTokenExpiryInfo = sinon.stub().returns({timeLeft: 10, expiryTime: 122132, currentTime: Date.now()})
9525
10621
  meeting.refreshPermissionToken = sinon.stub().returns(Promise.resolve('test return value'));
9526
10622
 
9527
10623
  const returnValue = await meeting.checkAndRefreshPermissionToken(10, 'ttl-join');
9528
10624
 
9529
- assert.calledOnce(meeting.getPermissionTokenTimeLeftInSec);
10625
+ assert.calledOnce(meeting.getPermissionTokenExpiryInfo);
9530
10626
  assert.calledOnceWithExactly(meeting.refreshPermissionToken, 'ttl-join');
9531
10627
  assert.equal(returnValue, 'test return value');
9532
10628
  });
9533
10629
 
9534
10630
  it('should not fire refreshPermissionToken if time left is higher than 10sec', async () => {
9535
- meeting.getPermissionTokenTimeLeftInSec = sinon.stub().returns(11)
10631
+ meeting.getPermissionTokenExpiryInfo = sinon.stub().returns({timeLeft: 11, expiryTime: 122132, currentTime: Date.now()})
9536
10632
  meeting.refreshPermissionToken = sinon.stub().returns(Promise.resolve('test return value'));
9537
10633
 
9538
10634
  const returnValue = await meeting.checkAndRefreshPermissionToken(10, 'ttl-join');
9539
10635
 
9540
- assert.calledOnce(meeting.getPermissionTokenTimeLeftInSec);
10636
+ assert.calledOnce(meeting.getPermissionTokenExpiryInfo);
9541
10637
  assert.notCalled(meeting.refreshPermissionToken);
9542
10638
  assert.equal(returnValue, undefined);
9543
10639
  });