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

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 (55) 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/meeting/in-meeting-actions.js +2 -0
  10. package/dist/meeting/in-meeting-actions.js.map +1 -1
  11. package/dist/meeting/index.js +155 -103
  12. package/dist/meeting/index.js.map +1 -1
  13. package/dist/meeting-info/meeting-info-v2.js +3 -0
  14. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  15. package/dist/meeting-info/utilv2.js +14 -29
  16. package/dist/meeting-info/utilv2.js.map +1 -1
  17. package/dist/meetings/collection.js +17 -0
  18. package/dist/meetings/collection.js.map +1 -1
  19. package/dist/meetings/index.js +13 -0
  20. package/dist/meetings/index.js.map +1 -1
  21. package/dist/metrics/constants.js +1 -0
  22. package/dist/metrics/constants.js.map +1 -1
  23. package/dist/reconnection-manager/index.js +26 -26
  24. package/dist/reconnection-manager/index.js.map +1 -1
  25. package/dist/rtcMetrics/index.js +25 -0
  26. package/dist/rtcMetrics/index.js.map +1 -1
  27. package/dist/statsAnalyzer/index.js +21 -1
  28. package/dist/statsAnalyzer/index.js.map +1 -1
  29. package/dist/statsAnalyzer/mqaUtil.js +16 -16
  30. package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
  31. package/dist/webinar/index.js +1 -1
  32. package/package.json +21 -22
  33. package/src/constants.ts +10 -4
  34. package/src/controls-options-manager/enums.ts +2 -0
  35. package/src/meeting/in-meeting-actions.ts +4 -0
  36. package/src/meeting/index.ts +140 -92
  37. package/src/meeting-info/meeting-info-v2.ts +4 -0
  38. package/src/meeting-info/utilv2.ts +6 -19
  39. package/src/meetings/collection.ts +13 -0
  40. package/src/meetings/index.ts +11 -0
  41. package/src/metrics/constants.ts +1 -0
  42. package/src/reconnection-manager/index.ts +62 -66
  43. package/src/rtcMetrics/index.ts +24 -0
  44. package/src/statsAnalyzer/index.ts +30 -1
  45. package/src/statsAnalyzer/mqaUtil.ts +17 -14
  46. package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
  47. package/test/unit/spec/meeting/index.js +1058 -158
  48. package/test/unit/spec/meeting/muteState.js +2 -1
  49. package/test/unit/spec/meeting-info/meetinginfov2.js +28 -0
  50. package/test/unit/spec/meetings/collection.js +12 -0
  51. package/test/unit/spec/meetings/index.js +306 -118
  52. package/test/unit/spec/member/util.js +0 -31
  53. package/test/unit/spec/reconnection-manager/index.js +25 -10
  54. package/test/unit/spec/rtcMetrics/index.ts +20 -0
  55. 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 {
@@ -827,11 +828,11 @@ describe('plugin-meetings', () => {
827
828
  });
828
829
 
829
830
  it('should join the meeting and return promise', async () => {
830
- const join = meeting.join();
831
+ const join = meeting.join({pstnAudioType: 'dial-in'});
831
832
 
832
- assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
833
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
833
834
  name: 'client.call.initiated',
834
- payload: {trigger: 'user-interaction', isRoapCallEnabled: true},
835
+ payload: {trigger: 'user-interaction', isRoapCallEnabled: true, pstnAudioType: 'dial-in'},
835
836
  options: {meetingId: meeting.id},
836
837
  });
837
838
 
@@ -1791,17 +1792,26 @@ describe('plugin-meetings', () => {
1791
1792
  }]);
1792
1793
 
1793
1794
  const sendBehavioralMetricCalls = Metrics.sendBehavioralMetric.getCalls();
1794
- assert.equal(sendBehavioralMetricCalls.length, 2);
1795
- assert.deepEqual(sendBehavioralMetricCalls[0].args, [
1795
+ assert.equal(sendBehavioralMetricCalls.length, 3);
1796
+ assert.deepEqual(sendBehavioralMetricCalls[0].args, [
1797
+ BEHAVIORAL_METRICS.ADD_MEDIA_RETRY,
1798
+ {
1799
+ correlation_id: meeting.correlationId,
1800
+ state: meeting.state,
1801
+ meetingState: meeting.meetingState,
1802
+ reason: 'forcingTurnTls',
1803
+ },
1804
+ ]);
1805
+ assert.deepEqual(sendBehavioralMetricCalls[1].args, [
1796
1806
  BEHAVIORAL_METRICS.TURN_DISCOVERY_LATENCY,
1797
1807
  {
1798
1808
  correlation_id: meeting.correlationId,
1799
1809
  turnServerUsed: true,
1800
1810
  retriedWithTurnServer: true,
1801
1811
  latency: undefined,
1802
- }
1812
+ },
1803
1813
  ]);
1804
- assert.deepEqual(sendBehavioralMetricCalls[1].args, [
1814
+ assert.deepEqual(sendBehavioralMetricCalls[2].args, [
1805
1815
  BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE,
1806
1816
  {
1807
1817
  correlation_id: meeting.correlationId,
@@ -1816,7 +1826,7 @@ describe('plugin-meetings', () => {
1816
1826
  signalingState: 'unknown',
1817
1827
  connectionState: 'unknown',
1818
1828
  iceConnectionState: 'unknown',
1819
- }
1829
+ },
1820
1830
  ]);
1821
1831
 
1822
1832
  // Check that doTurnDiscovery is called with th4 correct value of isForced
@@ -1957,17 +1967,26 @@ describe('plugin-meetings', () => {
1957
1967
  }]);
1958
1968
 
1959
1969
  const sendBehavioralMetricCalls = Metrics.sendBehavioralMetric.getCalls();
1960
- assert.equal(sendBehavioralMetricCalls.length, 2);
1961
- assert.deepEqual(sendBehavioralMetricCalls[0].args, [
1970
+ assert.equal(sendBehavioralMetricCalls.length, 3);
1971
+ assert.deepEqual(sendBehavioralMetricCalls[0].args, [
1972
+ BEHAVIORAL_METRICS.ADD_MEDIA_RETRY,
1973
+ {
1974
+ correlation_id: meeting.correlationId,
1975
+ state: meeting.state,
1976
+ meetingState: meeting.meetingState,
1977
+ reason: 'forcingTurnTls',
1978
+ },
1979
+ ]);
1980
+ assert.deepEqual(sendBehavioralMetricCalls[1].args, [
1962
1981
  BEHAVIORAL_METRICS.TURN_DISCOVERY_LATENCY,
1963
1982
  {
1964
1983
  correlation_id: meeting.correlationId,
1965
1984
  turnServerUsed: true,
1966
1985
  retriedWithTurnServer: true,
1967
1986
  latency: undefined,
1968
- }
1987
+ },
1969
1988
  ]);
1970
- assert.deepEqual(sendBehavioralMetricCalls[1].args, [
1989
+ assert.deepEqual(sendBehavioralMetricCalls[2].args, [
1971
1990
  BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS,
1972
1991
  {
1973
1992
  correlation_id: meeting.correlationId,
@@ -1975,7 +1994,7 @@ describe('plugin-meetings', () => {
1975
1994
  connectionType: 'udp',
1976
1995
  isMultistream: false,
1977
1996
  retriedWithTurnServer: true,
1978
- }
1997
+ },
1979
1998
  ]);
1980
1999
  meeting.roap.doTurnDiscovery
1981
2000
 
@@ -1999,6 +2018,90 @@ describe('plugin-meetings', () => {
1999
2018
  assert.isNotOk(errorThrown);
2000
2019
  });
2001
2020
 
2021
+ it('should call join if state is LEFT after first media connection attempt', async () => {
2022
+ const FAKE_TURN_URL = 'turns:webex.com:3478';
2023
+ const FAKE_TURN_USER = 'some-turn-username';
2024
+ const FAKE_TURN_PASSWORD = 'some-password';
2025
+ let errorThrown = undefined;
2026
+
2027
+ meeting.meetingState = 'ACTIVE';
2028
+ meeting.state = 'LEFT';
2029
+ meeting.roap.doTurnDiscovery = sinon.stub().onFirstCall().returns({
2030
+ turnServerInfo: undefined,
2031
+ turnDiscoverySkippedReason: 'reachability',
2032
+ }).onSecondCall().returns({
2033
+ turnServerInfo: {
2034
+ url: FAKE_TURN_URL,
2035
+ username: FAKE_TURN_USER,
2036
+ password: FAKE_TURN_PASSWORD,
2037
+ },
2038
+ turnDiscoverySkippedReason: undefined,
2039
+ });
2040
+ meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().onFirstCall().rejects().onSecondCall().resolves();
2041
+ meeting.join = sinon.stub().resolves();
2042
+
2043
+ const closeMediaConnectionStub = sinon.stub();
2044
+ Media.createMediaConnection = sinon.stub().returns({
2045
+ close: closeMediaConnectionStub,
2046
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
2047
+ initiateOffer: sinon.stub().resolves({}),
2048
+ on: sinon.stub(),
2049
+ });
2050
+
2051
+ await meeting
2052
+ .addMedia({
2053
+ mediaSettings: {},
2054
+ })
2055
+ .catch((err) => {
2056
+ errorThrown = err;
2057
+ });
2058
+
2059
+ assert.isNotOk(errorThrown);
2060
+ assert.calledOnceWithExactly(meeting.join, {rejoin: true});
2061
+ });
2062
+
2063
+ it('should reject if join attempt fails if state is LEFT after first media connection attempt', async () => {
2064
+ const FAKE_TURN_URL = 'turns:webex.com:3478';
2065
+ const FAKE_TURN_USER = 'some-turn-username';
2066
+ const FAKE_TURN_PASSWORD = 'some-password';
2067
+ let errorThrown = undefined;
2068
+
2069
+ meeting.meetingState = 'ACTIVE';
2070
+ meeting.state = 'LEFT';
2071
+ meeting.roap.doTurnDiscovery = sinon.stub().onFirstCall().returns({
2072
+ turnServerInfo: undefined,
2073
+ turnDiscoverySkippedReason: 'reachability',
2074
+ }).onSecondCall().returns({
2075
+ turnServerInfo: {
2076
+ url: FAKE_TURN_URL,
2077
+ username: FAKE_TURN_USER,
2078
+ password: FAKE_TURN_PASSWORD,
2079
+ },
2080
+ turnDiscoverySkippedReason: undefined,
2081
+ });
2082
+ meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().onFirstCall().rejects().onSecondCall().resolves();
2083
+ meeting.join = sinon.stub().rejects();
2084
+
2085
+ const closeMediaConnectionStub = sinon.stub();
2086
+ Media.createMediaConnection = sinon.stub().returns({
2087
+ close: closeMediaConnectionStub,
2088
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
2089
+ initiateOffer: sinon.stub().resolves({}),
2090
+ on: sinon.stub(),
2091
+ });
2092
+
2093
+ await meeting
2094
+ .addMedia({
2095
+ mediaSettings: {},
2096
+ })
2097
+ .catch((err) => {
2098
+ errorThrown = err;
2099
+ });
2100
+
2101
+ assert.isOk(errorThrown);
2102
+ assert.calledOnceWithExactly(meeting.join, {rejoin: true});
2103
+ });
2104
+
2002
2105
  it('should send ADD_MEDIA_SUCCESS metrics', async () => {
2003
2106
  meeting.meetingState = 'ACTIVE';
2004
2107
  meeting.webex.meetings.reachability = {
@@ -2383,6 +2486,7 @@ describe('plugin-meetings', () => {
2383
2486
  category: 'media',
2384
2487
  errorCode: clientErrorCode,
2385
2488
  serviceErrorCode: undefined,
2489
+ rawErrorMessage: undefined,
2386
2490
  ...expectedErrorPayload,
2387
2491
  },
2388
2492
  ],
@@ -5436,31 +5540,62 @@ describe('plugin-meetings', () => {
5436
5540
  it('should have #reconnect', () => {
5437
5541
  assert.exists(meeting.reconnect);
5438
5542
  });
5543
+
5439
5544
  describe('successful reconnect', () => {
5545
+ let eventListeners;
5546
+
5440
5547
  beforeEach(() => {
5548
+ eventListeners = {};
5549
+ meeting.mediaProperties.webrtcMediaConnection = {
5550
+ // mock the on() method and store all the listeners
5551
+ on: sinon.stub().callsFake((event, listener) => {
5552
+ eventListeners[event] = listener;
5553
+ }),
5554
+ };
5555
+ meeting.setupMediaConnectionListeners();
5556
+ meeting.deferSDPAnswer = {
5557
+ resolve: sinon.stub(),
5558
+ reject: sinon.stub(),
5559
+ };
5560
+ meeting.sdpResponseTimer = '1234';
5561
+ meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
5562
+
5563
+ eventListeners[Event.REMOTE_SDP_ANSWER_PROCESSED]();
5441
5564
  meeting.config.reconnection.enabled = true;
5442
5565
  meeting.currentMediaStatus = {audio: true};
5443
5566
  meeting.reconnectionManager = new ReconnectionManager(meeting);
5444
5567
  meeting.reconnectionManager.reconnect = sinon.stub().returns(Promise.resolve());
5445
5568
  meeting.reconnectionManager.reset = sinon.stub().returns(true);
5446
5569
  meeting.reconnectionManager.cleanup = sinon.stub().returns(true);
5570
+ meeting.reconnectionManager.setStatus = sinon.stub();
5447
5571
  });
5448
5572
 
5449
- it('should throw error if media not established before trying reconenct', async () => {
5573
+ it('should throw error if media not established before trying reconnect', async () => {
5450
5574
  meeting.currentMediaStatus = null;
5451
5575
  await meeting.reconnect().catch((err) => {
5452
5576
  assert.instanceOf(err, ParameterError);
5453
5577
  });
5454
5578
  });
5455
5579
 
5456
- it('should trigger reconnection success', async () => {
5580
+ it('should trigger reconnection success and send CA metric', async () => {
5457
5581
  await meeting.reconnect();
5582
+
5458
5583
  assert.calledWith(
5459
5584
  TriggerProxy.trigger,
5460
5585
  sinon.match.instanceOf(Meeting),
5461
5586
  {file: 'meeting/index', function: 'reconnect'},
5462
5587
  'meeting:reconnectionSuccess'
5463
5588
  );
5589
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
5590
+ name: 'client.media.recovered',
5591
+ payload: {
5592
+ recoveredBy: 'new',
5593
+ },
5594
+ options: {
5595
+ meetingId: meeting.id,
5596
+ },
5597
+ });
5598
+ assert.calledOnceWithExactly(meeting.reconnectionManager.setStatus, RECONNECTION.STATE.COMPLETE);
5464
5599
  });
5465
5600
 
5466
5601
  it('should reset after reconnection success', async () => {
@@ -5613,37 +5748,42 @@ describe('plugin-meetings', () => {
5613
5748
  });
5614
5749
  });
5615
5750
 
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);
5751
+ describe('CONNECTION_STATE_CHANGED event when state = "Connecting"', () => {
5752
+ it('sends client.ice.start correctly when hasMediaConnectionConnectedAtLeastOnce = true', () => {
5753
+ meeting.hasMediaConnectionConnectedAtLeastOnce = true;
5622
5754
  meeting.setupMediaConnectionListeners();
5623
5755
  eventListeners[Event.CONNECTION_STATE_CHANGED]({
5624
- state: 'Failed',
5756
+ state: 'Connecting',
5757
+ });
5758
+
5759
+ assert.notCalled(webex.internal.newMetrics.submitClientEvent);
5760
+ })
5761
+
5762
+ it('sends client.ice.start correctly when hasMediaConnectionConnectedAtLeastOnce = false', () => {
5763
+ meeting.hasMediaConnectionConnectedAtLeastOnce = false;
5764
+ meeting.setupMediaConnectionListeners();
5765
+ eventListeners[Event.CONNECTION_STATE_CHANGED]({
5766
+ state: 'Connecting',
5625
5767
  });
5626
- assert.calledOnceWithExactly(getErrorPayloadForClientErrorCodeStub, {clientErrorCode: 2004});
5768
+
5627
5769
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
5628
5770
  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
- },
5771
+ name: 'client.ice.start',
5635
5772
  options: {
5636
5773
  meetingId: meeting.id,
5637
5774
  },
5638
5775
  });
5639
- });
5776
+ })
5640
5777
  });
5641
5778
 
5642
5779
  describe('submitClientEvent on connectionSuccess', () => {
5643
- it('sends client.ice.end when connectionSuccess on CONNECTION_STATE_CHANGED event', () => {
5780
+ let setNetworkStatusSpy;
5781
+
5782
+ const setupSpies = () => {
5783
+ setNetworkStatusSpy = sinon.spy(meeting, 'setNetworkStatus');
5784
+
5644
5785
  meeting.reconnectionManager = new ReconnectionManager(meeting);
5645
5786
  meeting.reconnectionManager.iceReconnected = sinon.stub().returns(undefined);
5646
- meeting.setNetworkStatus = sinon.stub().returns(undefined);
5647
5787
  meeting.statsAnalyzer = {startAnalyzer: sinon.stub()};
5648
5788
  meeting.mediaProperties.webrtcMediaConnection = {
5649
5789
  // mock the on() method and store all the listeners
@@ -5651,113 +5791,302 @@ describe('plugin-meetings', () => {
5651
5791
  eventListeners[event] = listener;
5652
5792
  }),
5653
5793
  };
5794
+ };
5654
5795
 
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
- });
5796
+ const checkExpectedSpies = (expected) => {
5797
+ if (expected.icePhase) {
5798
+ assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
5799
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
5800
+ name: 'client.ice.end',
5801
+ options: {
5802
+ meetingId: meeting.id,
5803
+ },
5804
+ payload: {
5805
+ canProceed: true,
5806
+ icePhase: expected.icePhase,
5807
+ },
5808
+ });
5809
+ } else {
5810
+ assert.notCalled(webex.internal.newMetrics.submitClientEvent);
5811
+ }
5666
5812
  assert.calledOnce(Metrics.sendBehavioralMetric);
5667
5813
  assert.calledWith(Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.CONNECTION_SUCCESS, {
5668
5814
  correlation_id: meeting.correlationId,
5669
5815
  locus_id: meeting.locusId,
5670
5816
  latency: undefined,
5671
5817
  });
5672
- assert.calledOnce(meeting.setNetworkStatus);
5673
- assert.calledWith(meeting.setNetworkStatus, NETWORK_STATUS.CONNECTED);
5818
+ assert.deepEqual(
5819
+ setNetworkStatusSpy.getCalls().map((call) => call.args[0]),
5820
+ expected.setNetworkStatusCallParams
5821
+ );
5674
5822
  assert.calledOnce(meeting.reconnectionManager.iceReconnected);
5675
5823
  assert.calledOnce(meeting.statsAnalyzer.startAnalyzer);
5676
5824
  assert.calledWith(
5677
5825
  meeting.statsAnalyzer.startAnalyzer,
5678
5826
  meeting.mediaProperties.webrtcMediaConnection
5679
5827
  );
5828
+ };
5829
+
5830
+ const resetSpies = () => {
5831
+ setNetworkStatusSpy.resetHistory();
5832
+ webex.internal.newMetrics.submitClientEvent.resetHistory();
5833
+ Metrics.sendBehavioralMetric.resetHistory();
5834
+ meeting.reconnectionManager.iceReconnected.resetHistory();
5835
+ meeting.statsAnalyzer.startAnalyzer.resetHistory();
5836
+ };
5837
+
5838
+ it('sends client.ice.end with the correct icePhase when we get ConnectionState.Connected on CONNECTION_STATE_CHANGED event', () => {
5839
+ setupSpies();
5840
+
5841
+ meeting.setupMediaConnectionListeners();
5842
+
5843
+ assert.equal(meeting.hasMediaConnectionConnectedAtLeastOnce, false);
5844
+
5845
+ // simulate first connection success
5846
+ eventListeners[Event.CONNECTION_STATE_CHANGED]({
5847
+ state: 'Connected',
5848
+ });
5849
+ checkExpectedSpies({
5850
+ icePhase: 'JOIN_MEETING_FINAL',
5851
+ setNetworkStatusCallParams: [NETWORK_STATUS.CONNECTED],
5852
+ });
5853
+ assert.equal(meeting.hasMediaConnectionConnectedAtLeastOnce, true);
5854
+
5855
+ // now simulate short connection loss, client.ice.end is not sent a second time as hasMediaConnectionConnectedAtLeastOnce = true
5856
+ resetSpies();
5857
+
5858
+ eventListeners[Event.CONNECTION_STATE_CHANGED]({
5859
+ state: 'Disconnected',
5860
+ });
5861
+ eventListeners[Event.CONNECTION_STATE_CHANGED]({
5862
+ state: 'Connected',
5863
+ });
5864
+
5865
+ checkExpectedSpies({
5866
+ setNetworkStatusCallParams: [NETWORK_STATUS.DISCONNECTED, NETWORK_STATUS.CONNECTED],
5867
+ });
5868
+
5869
+ resetSpies();
5870
+
5871
+ eventListeners[Event.CONNECTION_STATE_CHANGED]({
5872
+ state: 'Disconnected',
5873
+ });
5874
+ eventListeners[Event.CONNECTION_STATE_CHANGED]({
5875
+ state: 'Connected',
5876
+ });
5680
5877
  });
5681
5878
  });
5682
5879
 
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';
5687
-
5880
+ describe('CONNECTION_STATE_CHANGED event when state = "Disconnected"', () => {
5688
5881
  beforeEach(() => {
5689
- meeting.setupMediaConnectionListeners();
5882
+ meeting.reconnectionManager = new ReconnectionManager(meeting);
5883
+ meeting.reconnectionManager.iceReconnected = sinon.stub().returns(undefined);
5884
+ meeting.setNetworkStatus = sinon.stub().returns(undefined);
5885
+ meeting.statsAnalyzer = {startAnalyzer: sinon.stub()};
5886
+ meeting.mediaProperties.webrtcMediaConnection = {
5887
+ // mock the on() method and store all the listeners
5888
+ on: sinon.stub().callsFake((event, listener) => {
5889
+ eventListeners[event] = listener;
5890
+ }),
5891
+ };
5892
+ meeting.reconnect = sinon.stub().resolves();
5690
5893
  });
5691
5894
 
5692
- const checkMetricSent = (event, error) => {
5693
- assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
5694
- assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
5695
- name: event,
5696
- payload: {
5697
- canProceed: false,
5698
- },
5699
- options: {rawError: error, meetingId: meeting.id},
5895
+ const mockDisconnectedEvent = () => {
5896
+ meeting.setupMediaConnectionListeners();
5897
+ eventListeners[Event.CONNECTION_STATE_CHANGED]({
5898
+ state: 'Disconnected',
5700
5899
  });
5701
5900
  };
5702
5901
 
5703
- const checkBehavioralMetricSent = (
5704
- metricName,
5705
- expectedCode,
5706
- expectedReason,
5707
- expectedMetadataType
5708
- ) => {
5902
+ const checkBehavioralMetricSent = (hasMediaConnectionConnectedAtLeastOnce = false) => {
5709
5903
  assert.calledOnce(Metrics.sendBehavioralMetric);
5710
5904
  assert.calledWith(
5711
5905
  Metrics.sendBehavioralMetric,
5712
- metricName,
5906
+ BEHAVIORAL_METRICS.CONNECTION_FAILURE,
5713
5907
  {
5714
- code: expectedCode,
5715
5908
  correlation_id: meeting.correlationId,
5716
- reason: expectedReason,
5717
- stack: sinon.match.any,
5909
+ locus_id: meeting.locusUrl.split('/').pop(),
5910
+ networkStatus: meeting.networkStatus,
5911
+ hasMediaConnectionConnectedAtLeastOnce,
5718
5912
  },
5719
- {
5720
- type: expectedMetadataType,
5721
- }
5722
5913
  );
5723
5914
  };
5724
5915
 
5725
- it('should send metrics for SdpOfferCreationError error', () => {
5726
- const fakeError = new Errors.SdpOfferCreationError(fakeErrorMessage, {
5727
- name: fakeErrorName,
5728
- cause: {name: fakeRootCauseName},
5729
- });
5730
-
5731
- eventListeners[Event.ROAP_FAILURE](fakeError);
5916
+ it('handles "Disconnected" state correctly when waitForIceReconnect resolves', async () => {
5917
+ meeting.reconnectionManager.waitForIceReconnect = sinon.stub().resolves();
5732
5918
 
5733
- checkMetricSent('client.media-engine.local-sdp-generated', fakeError);
5734
- checkBehavioralMetricSent(
5735
- BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE,
5736
- Errors.ErrorCode.SdpOfferCreationError,
5737
- fakeErrorMessage,
5738
- fakeRootCauseName
5739
- );
5740
- });
5741
5919
 
5742
- it('should send metrics for SdpOfferHandlingError error', () => {
5743
- const fakeError = new Errors.SdpOfferHandlingError(fakeErrorMessage, {
5744
- name: fakeErrorName,
5745
- cause: {name: fakeRootCauseName},
5746
- });
5920
+ mockDisconnectedEvent();
5747
5921
 
5748
- eventListeners[Event.ROAP_FAILURE](fakeError);
5922
+ await testUtils.flushPromises();
5749
5923
 
5750
- checkMetricSent('client.media-engine.remote-sdp-received', fakeError);
5751
- checkBehavioralMetricSent(
5752
- BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE,
5753
- Errors.ErrorCode.SdpOfferHandlingError,
5754
- fakeErrorMessage,
5755
- fakeRootCauseName
5756
- );
5924
+ assert.calledOnce(meeting.setNetworkStatus);
5925
+ assert.calledWith(meeting.setNetworkStatus, NETWORK_STATUS.DISCONNECTED);
5926
+ assert.calledOnce(meeting.reconnectionManager.waitForIceReconnect);
5927
+ assert.notCalled(webex.internal.newMetrics.submitClientEvent);
5928
+ assert.notCalled(Metrics.sendBehavioralMetric);
5757
5929
  });
5758
5930
 
5759
- it('should send metrics for SdpAnswerHandlingError error', () => {
5760
- const fakeError = new Errors.SdpAnswerHandlingError(fakeErrorMessage, {
5931
+ it('handles "Disconnected" state correctly when waitForIceReconnect rejects and hasMediaConnectionConnectedAtLeastOnce = true', async () => {
5932
+ const FAKE_ERROR = {fatal: true};
5933
+ const getErrorPayloadForClientErrorCodeStub = webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode = sinon
5934
+ .stub()
5935
+ .returns(FAKE_ERROR);
5936
+ meeting.waitForMediaConnectionConnected = sinon.stub().resolves();
5937
+ meeting.reconnectionManager.waitForIceReconnect = sinon.stub().rejects();
5938
+ meeting.hasMediaConnectionConnectedAtLeastOnce = true;
5939
+
5940
+
5941
+ mockDisconnectedEvent();
5942
+
5943
+ await testUtils.flushPromises();
5944
+
5945
+ assert.calledOnce(meeting.setNetworkStatus);
5946
+ assert.calledWith(meeting.setNetworkStatus, NETWORK_STATUS.DISCONNECTED);
5947
+ assert.calledOnce(meeting.reconnectionManager.waitForIceReconnect);
5948
+ assert.notCalled(webex.internal.newMetrics.submitClientEvent);
5949
+ checkBehavioralMetricSent(true);
5950
+ });
5951
+
5952
+ it('handles "Disconnected" state correctly when waitForIceReconnect rejects and hasMediaConnectionConnectedAtLeastOnce = false', async () => {
5953
+ meeting.reconnectionManager.waitForIceReconnect = sinon.stub().rejects();
5954
+
5955
+
5956
+ mockDisconnectedEvent();
5957
+
5958
+ await testUtils.flushPromises();
5959
+
5960
+ assert.calledOnce(meeting.setNetworkStatus);
5961
+ assert.calledWith(meeting.setNetworkStatus, NETWORK_STATUS.DISCONNECTED);
5962
+ assert.calledOnce(meeting.reconnectionManager.waitForIceReconnect);
5963
+ assert.notCalled(webex.internal.newMetrics.submitClientEvent);
5964
+ checkBehavioralMetricSent();
5965
+ });
5966
+ });
5967
+
5968
+ describe('CONNECTION_STATE_CHANGED event when state = "Failed"', () => {
5969
+
5970
+ const mockFailedEvent = () => {
5971
+ meeting.setupMediaConnectionListeners();
5972
+ eventListeners[Event.CONNECTION_STATE_CHANGED]({
5973
+ state: 'Failed',
5974
+ });
5975
+ };
5976
+
5977
+ const checkBehavioralMetricSent = (hasMediaConnectionConnectedAtLeastOnce = false) => {
5978
+ assert.calledOnce(Metrics.sendBehavioralMetric);
5979
+ assert.calledWith(
5980
+ Metrics.sendBehavioralMetric,
5981
+ BEHAVIORAL_METRICS.CONNECTION_FAILURE,
5982
+ {
5983
+ correlation_id: meeting.correlationId,
5984
+ locus_id: meeting.locusUrl.split('/').pop(),
5985
+ networkStatus: meeting.networkStatus,
5986
+ hasMediaConnectionConnectedAtLeastOnce,
5987
+ },
5988
+ );
5989
+ };
5990
+
5991
+ it('handles "Failed" state correctly when hasMediaConnectionConnectedAtLeastOnce = false', async () => {
5992
+ meeting.waitForMediaConnectionConnected = sinon.stub().resolves();
5993
+
5994
+ mockFailedEvent();
5995
+
5996
+ assert.notCalled(webex.internal.newMetrics.submitClientEvent);
5997
+ checkBehavioralMetricSent();
5998
+ });
5999
+
6000
+ it('handles "Failed" state correctly when hasMediaConnectionConnectedAtLeastOnce = true', async () => {
6001
+ meeting.hasMediaConnectionConnectedAtLeastOnce = true;
6002
+
6003
+ mockFailedEvent();
6004
+
6005
+ assert.notCalled(webex.internal.newMetrics.submitClientEvent);
6006
+ checkBehavioralMetricSent(true);
6007
+ });
6008
+ });
6009
+
6010
+ describe('should send correct metrics for ROAP_FAILURE event', () => {
6011
+ const fakeErrorMessage = 'test error';
6012
+ const fakeRootCauseName = 'root cause name';
6013
+ const fakeErrorName = 'test error name';
6014
+
6015
+ beforeEach(() => {
6016
+ meeting.setupMediaConnectionListeners();
6017
+ webex.internal.newMetrics.submitClientEvent.resetHistory();
6018
+ Metrics.sendBehavioralMetric.resetHistory();
6019
+ });
6020
+
6021
+ const checkMetricSent = (event, error) => {
6022
+ assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
6023
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
6024
+ name: event,
6025
+ payload: {
6026
+ canProceed: false,
6027
+ },
6028
+ options: {rawError: error, meetingId: meeting.id},
6029
+ });
6030
+ };
6031
+
6032
+ const checkBehavioralMetricSent = (
6033
+ metricName,
6034
+ expectedCode,
6035
+ expectedReason,
6036
+ expectedMetadataType
6037
+ ) => {
6038
+ assert.calledOnce(Metrics.sendBehavioralMetric);
6039
+ assert.calledWith(
6040
+ Metrics.sendBehavioralMetric,
6041
+ metricName,
6042
+ {
6043
+ code: expectedCode,
6044
+ correlation_id: meeting.correlationId,
6045
+ reason: expectedReason,
6046
+ stack: sinon.match.any,
6047
+ },
6048
+ {
6049
+ type: expectedMetadataType,
6050
+ }
6051
+ );
6052
+ };
6053
+
6054
+ it('should send metrics for SdpOfferCreationError error', () => {
6055
+ const fakeError = new Errors.SdpOfferCreationError(fakeErrorMessage, {
6056
+ name: fakeErrorName,
6057
+ cause: {name: fakeRootCauseName},
6058
+ });
6059
+
6060
+ eventListeners[Event.ROAP_FAILURE](fakeError);
6061
+
6062
+ checkMetricSent('client.media-engine.local-sdp-generated', fakeError);
6063
+ checkBehavioralMetricSent(
6064
+ BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE,
6065
+ Errors.ErrorCode.SdpOfferCreationError,
6066
+ fakeErrorMessage,
6067
+ fakeRootCauseName
6068
+ );
6069
+ });
6070
+
6071
+ it('should send metrics for SdpOfferHandlingError error', () => {
6072
+ const fakeError = new Errors.SdpOfferHandlingError(fakeErrorMessage, {
6073
+ name: fakeErrorName,
6074
+ cause: {name: fakeRootCauseName},
6075
+ });
6076
+
6077
+ eventListeners[Event.ROAP_FAILURE](fakeError);
6078
+
6079
+ checkMetricSent('client.media-engine.remote-sdp-received', fakeError);
6080
+ checkBehavioralMetricSent(
6081
+ BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE,
6082
+ Errors.ErrorCode.SdpOfferHandlingError,
6083
+ fakeErrorMessage,
6084
+ fakeRootCauseName
6085
+ );
6086
+ });
6087
+
6088
+ it('should send metrics for SdpAnswerHandlingError error', () => {
6089
+ const fakeError = new Errors.SdpAnswerHandlingError(fakeErrorMessage, {
5761
6090
  name: fakeErrorName,
5762
6091
  cause: {name: fakeRootCauseName},
5763
6092
  });
@@ -6732,6 +7061,7 @@ describe('plugin-meetings', () => {
6732
7061
  done();
6733
7062
  });
6734
7063
  });
7064
+
6735
7065
  describe('#setUpLocusInfoMediaInactiveListener', () => {
6736
7066
  it('listens to disconnect due to un activity ', (done) => {
6737
7067
  TriggerProxy.trigger.reset();
@@ -6977,12 +7307,14 @@ describe('plugin-meetings', () => {
6977
7307
  });
6978
7308
  describe('#closePeerConnections', () => {
6979
7309
  it('should close the webrtc media connection, and return a promise', async () => {
7310
+ const setNetworkStatusSpy = sinon.spy(meeting, 'setNetworkStatus');
6980
7311
  meeting.mediaProperties.webrtcMediaConnection = {close: sinon.stub()};
6981
7312
  const pcs = meeting.closePeerConnections();
6982
7313
 
6983
7314
  assert.exists(pcs.then);
6984
7315
  await pcs;
6985
7316
  assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.close);
7317
+ assert.calledOnceWithExactly(setNetworkStatusSpy, undefined);
6986
7318
  });
6987
7319
  });
6988
7320
  describe('#unsetPeerConnections', () => {
@@ -7246,47 +7578,12 @@ describe('plugin-meetings', () => {
7246
7578
  });
7247
7579
 
7248
7580
  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
7581
  let locusInfoOnSpy;
7266
7582
  let handleDataChannelUrlChangeSpy;
7267
7583
  let updateMeetingActionsSpy;
7268
7584
 
7269
7585
  beforeEach(() => {
7270
7586
  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
7587
  handleDataChannelUrlChangeSpy = sinon.spy(meeting, 'handleDataChannelUrlChange');
7291
7588
  updateMeetingActionsSpy = sinon.spy(meeting, 'updateMeetingActions');
7292
7589
  });
@@ -7324,29 +7621,632 @@ describe('plugin-meetings', () => {
7324
7621
  assert.equal(locusInfoOnSpy.thirdCall.args[0], 'MEETING_INFO_UPDATED');
7325
7622
  const callback = locusInfoOnSpy.thirdCall.args[1];
7326
7623
 
7327
- const payload = {
7328
- info: {
7329
- userDisplayHints: ['LOCK_CONTROL_UNLOCK'],
7330
- },
7331
- };
7332
-
7333
7624
  callback();
7334
7625
 
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);
7626
+ assert.calledWith(updateMeetingActionsSpy);
7627
+ assert.calledWith(setRecordingDisplayHintsSpy, userDisplayHints);
7628
+ assert.calledWith(setUserPolicySpy, userDisplayPolicy);
7629
+ assert.calledWith(setControlsDisplayHintsSpy, userDisplayHints);
7630
+ assert.calledWith(handleDataChannelUrlChangeSpy, datachannelUrl);
7631
+ });
7632
+ });
7633
+
7634
+ describe('#updateMeetingActions', () => {
7635
+ let inMeetingActionsSetSpy;
7636
+ let canUserLockSpy;
7637
+ let canUserUnlockSpy;
7638
+ let canUserStartSpy;
7639
+ let canUserStopSpy;
7640
+ let canUserPauseSpy;
7641
+ let canUserResumeSpy;
7642
+ let canSetMuteOnEntrySpy;
7643
+ let canUnsetMuteOnEntrySpy;
7644
+ let canSetDisallowUnmuteSpy;
7645
+ let canUnsetDisallowUnmuteSpy;
7646
+ let canUserRaiseHandSpy;
7647
+ let bothLeaveAndEndMeetingAvailableSpy;
7648
+ let canUserLowerAllHandsSpy;
7649
+ let canUserLowerSomeoneElsesHandSpy;
7650
+ let waitingForOthersToJoinSpy;
7651
+ let canSendReactionsSpy;
7652
+ let canUserRenameSelfAndObservedSpy;
7653
+ let canUserRenameOthersSpy;
7654
+ let canShareWhiteBoardSpy;
7655
+ // Due to import tree issues, hasHints must be stubed within the scope of the `it`.
7656
+
7657
+ beforeEach(() => {
7658
+ canUserLockSpy = sinon.spy(MeetingUtil, 'canUserLock');
7659
+ canUserUnlockSpy = sinon.spy(MeetingUtil, 'canUserUnlock');
7660
+ canUserStartSpy = sinon.spy(RecordingUtil, 'canUserStart');
7661
+ canUserStopSpy = sinon.spy(RecordingUtil, 'canUserStop');
7662
+ canUserPauseSpy = sinon.spy(RecordingUtil, 'canUserPause');
7663
+ canUserResumeSpy = sinon.spy(RecordingUtil, 'canUserResume');
7664
+ canSetMuteOnEntrySpy = sinon.spy(ControlsOptionsUtil, 'canSetMuteOnEntry');
7665
+ canUnsetMuteOnEntrySpy = sinon.spy(ControlsOptionsUtil, 'canUnsetMuteOnEntry');
7666
+ canSetDisallowUnmuteSpy = sinon.spy(ControlsOptionsUtil, 'canSetDisallowUnmute');
7667
+ canUnsetDisallowUnmuteSpy = sinon.spy(ControlsOptionsUtil, 'canUnsetDisallowUnmute');
7668
+ inMeetingActionsSetSpy = sinon.spy(meeting.inMeetingActions, 'set');
7669
+ canUserRaiseHandSpy = sinon.spy(MeetingUtil, 'canUserRaiseHand');
7670
+ canUserLowerAllHandsSpy = sinon.spy(MeetingUtil, 'canUserLowerAllHands');
7671
+ bothLeaveAndEndMeetingAvailableSpy = sinon.spy(
7672
+ MeetingUtil,
7673
+ 'bothLeaveAndEndMeetingAvailable'
7674
+ );
7675
+ canUserLowerSomeoneElsesHandSpy = sinon.spy(MeetingUtil, 'canUserLowerSomeoneElsesHand');
7676
+ waitingForOthersToJoinSpy = sinon.spy(MeetingUtil, 'waitingForOthersToJoin');
7677
+ canSendReactionsSpy = sinon.spy(MeetingUtil, 'canSendReactions');
7678
+ canUserRenameSelfAndObservedSpy = sinon.spy(MeetingUtil, 'canUserRenameSelfAndObserved');
7679
+ canUserRenameOthersSpy = sinon.spy(MeetingUtil, 'canUserRenameOthers');
7680
+ canShareWhiteBoardSpy = sinon.spy(MeetingUtil, 'canShareWhiteBoard');
7681
+ });
7682
+
7683
+ afterEach(() => {
7684
+ inMeetingActionsSetSpy.restore();
7685
+ waitingForOthersToJoinSpy.restore();
7686
+ });
7687
+
7688
+ forEach(
7689
+ [
7690
+ {
7691
+ actionName: 'canShareApplication',
7692
+ expectedEnabled: true,
7693
+ arePolicyRestrictionsSupported: false,
7694
+ },
7695
+ {
7696
+ actionName: 'canShareApplication',
7697
+ expectedEnabled: false,
7698
+ arePolicyRestrictionsSupported: true,
7699
+ },
7700
+ {
7701
+ actionName: 'canShareDesktop',
7702
+ arePolicyRestrictionsSupported: false,
7703
+ expectedEnabled: true,
7704
+ },
7705
+ {
7706
+ actionName: 'canShareDesktop',
7707
+ arePolicyRestrictionsSupported: true,
7708
+ expectedEnabled: false,
7709
+ },
7710
+ {
7711
+ actionName: 'canShareContent',
7712
+ arePolicyRestrictionsSupported: false,
7713
+ expectedEnabled: true,
7714
+ },
7715
+ {
7716
+ actionName: 'canShareContent',
7717
+ arePolicyRestrictionsSupported: true,
7718
+ expectedEnabled: false,
7719
+ },
7720
+ {
7721
+ actionName: 'canUseVoip',
7722
+ expectedEnabled: true,
7723
+ arePolicyRestrictionsSupported: false,
7724
+ },
7725
+ {
7726
+ actionName: 'canUseVoip',
7727
+ expectedEnabled: false,
7728
+ arePolicyRestrictionsSupported: true,
7729
+ },
7730
+ ],
7731
+ ({actionName, arePolicyRestrictionsSupported, expectedEnabled}) => {
7732
+ it(`${actionName} is ${expectedEnabled} when the call type is ${arePolicyRestrictionsSupported}`, () => {
7733
+ meeting.userDisplayHints = [];
7734
+ meeting.meetingInfo = {some: 'info'};
7735
+ sinon
7736
+ .stub(meeting, 'arePolicyRestrictionsSupported')
7737
+ .returns(arePolicyRestrictionsSupported);
7738
+
7739
+ meeting.updateMeetingActions();
7740
+
7741
+ assert.equal(meeting.inMeetingActions.get()[actionName], expectedEnabled);
7742
+ });
7743
+ }
7744
+ );
7745
+
7746
+ forEach(
7747
+ [
7748
+ {
7749
+ actionName: 'canShareFile',
7750
+ requiredDisplayHints: [DISPLAY_HINTS.SHARE_FILE],
7751
+ requiredPolicies: [SELF_POLICY.SUPPORT_FILE_SHARE],
7752
+ },
7753
+ {
7754
+ actionName: 'canShareApplication',
7755
+ requiredDisplayHints: [DISPLAY_HINTS.SHARE_APPLICATION],
7756
+ requiredPolicies: [SELF_POLICY.SUPPORT_APP_SHARE],
7757
+ },
7758
+ {
7759
+ actionName: 'canShareCamera',
7760
+ requiredDisplayHints: [DISPLAY_HINTS.SHARE_CAMERA],
7761
+ requiredPolicies: [SELF_POLICY.SUPPORT_CAMERA_SHARE],
7762
+ },
7763
+ {
7764
+ actionName: 'canBroadcastMessageToBreakout',
7765
+ requiredDisplayHints: [DISPLAY_HINTS.BROADCAST_MESSAGE_TO_BREAKOUT],
7766
+ requiredPolicies: [SELF_POLICY.SUPPORT_BROADCAST_MESSAGE],
7767
+ },
7768
+ {
7769
+ actionName: 'canShareDesktop',
7770
+ requiredDisplayHints: [DISPLAY_HINTS.SHARE_DESKTOP],
7771
+ requiredPolicies: [SELF_POLICY.SUPPORT_DESKTOP_SHARE],
7772
+ },
7773
+ {
7774
+ actionName: 'canTransferFile',
7775
+ requiredDisplayHints: [],
7776
+ requiredPolicies: [SELF_POLICY.SUPPORT_FILE_TRANSFER],
7777
+ },
7778
+ {
7779
+ actionName: 'canChat',
7780
+ requiredDisplayHints: [],
7781
+ requiredPolicies: [SELF_POLICY.SUPPORT_CHAT],
7782
+ },
7783
+ {
7784
+ actionName: 'canShareDesktop',
7785
+ requiredDisplayHints: [DISPLAY_HINTS.SHARE_DESKTOP],
7786
+ requiredPolicies: [],
7787
+ enableUnifiedMeetings: false,
7788
+ arePolicyRestrictionsSupported: false,
7789
+ },
7790
+ {
7791
+ actionName: 'canShareApplication',
7792
+ requiredDisplayHints: [DISPLAY_HINTS.SHARE_APPLICATION],
7793
+ requiredPolicies: [],
7794
+ enableUnifiedMeetings: false,
7795
+ arePolicyRestrictionsSupported: false,
7796
+ },
7797
+ {
7798
+ actionName: 'canAnnotate',
7799
+ requiredDisplayHints: [],
7800
+ requiredPolicies: [SELF_POLICY.SUPPORT_ANNOTATION],
7801
+ },
7802
+ ],
7803
+ ({
7804
+ actionName,
7805
+ requiredDisplayHints,
7806
+ requiredPolicies,
7807
+ enableUnifiedMeetings,
7808
+ meetingInfo,
7809
+ arePolicyRestrictionsSupported,
7810
+ }) => {
7811
+ it(`${actionName} is enabled when the conditions are met`, () => {
7812
+ meeting.userDisplayHints = requiredDisplayHints;
7813
+ meeting.selfUserPolicies = undefined;
7814
+ sinon
7815
+ .stub(meeting, 'arePolicyRestrictionsSupported')
7816
+ .returns(arePolicyRestrictionsSupported);
7817
+
7818
+ meeting.config.experimental.enableUnifiedMeetings = isUndefined(enableUnifiedMeetings)
7819
+ ? true
7820
+ : enableUnifiedMeetings;
7821
+
7822
+ meeting.meetingInfo = isUndefined(meetingInfo) ? {some: 'info'} : meetingInfo;
7823
+
7824
+ if (requiredPolicies) {
7825
+ meeting.selfUserPolicies = {};
7826
+ }
7827
+ forEach(requiredPolicies, (policy) => {
7828
+ meeting.selfUserPolicies[policy] = true;
7829
+ });
7830
+
7831
+ meeting.updateMeetingActions();
7832
+
7833
+ assert.isTrue(meeting.inMeetingActions.get()[actionName]);
7834
+ });
7835
+
7836
+ if (requiredDisplayHints.length !== 0) {
7837
+ it(`${actionName} is disabled when the required display hints are missing`, () => {
7838
+ meeting.userDisplayHints = [];
7839
+ meeting.selfUserPolicies = undefined;
7840
+
7841
+ meeting.meetingInfo = isUndefined(meetingInfo) ? {some: 'info'} : meetingInfo;
7842
+
7843
+ if (requiredPolicies) {
7844
+ meeting.selfUserPolicies = {};
7845
+ }
7846
+ forEach(requiredPolicies, (policy) => {
7847
+ meeting.selfUserPolicies[policy] = true;
7848
+ });
7849
+
7850
+ meeting.updateMeetingActions();
7851
+
7852
+ assert.isFalse(meeting.inMeetingActions.get()[actionName]);
7853
+ });
7854
+ }
7855
+
7856
+ it(`${actionName} is disabled when the required policies are missing`, () => {
7857
+ meeting.userDisplayHints = requiredDisplayHints;
7858
+ meeting.selfUserPolicies = undefined;
7859
+
7860
+ if (requiredPolicies) {
7861
+ meeting.selfUserPolicies = {};
7862
+ }
7863
+ meeting.meetingInfo = isUndefined(meetingInfo) ? {some: 'info'} : meetingInfo;
7864
+
7865
+ meeting.updateMeetingActions();
7866
+
7867
+ assert.isFalse(meeting.inMeetingActions.get()[actionName]);
7868
+ });
7869
+ }
7870
+ );
7871
+
7872
+ forEach(
7873
+ [
7874
+ {
7875
+ meetingInfo: {
7876
+ video: {
7877
+ supportHDV: true,
7878
+ supportHQV: true,
7879
+ },
7880
+ },
7881
+ selfUserPolicies: {
7882
+ [SELF_POLICY.SUPPORT_HDV]: true,
7883
+ [SELF_POLICY.SUPPORT_HQV]: true,
7884
+ },
7885
+ expectedActions: {
7886
+ supportHQV: true,
7887
+ supportHDV: true,
7888
+ },
7889
+ },
7890
+ {
7891
+ meetingInfo: {
7892
+ video: {
7893
+ supportHDV: false,
7894
+ supportHQV: false,
7895
+ },
7896
+ },
7897
+ selfUserPolicies: {
7898
+ [SELF_POLICY.SUPPORT_HDV]: true,
7899
+ [SELF_POLICY.SUPPORT_HQV]: true,
7900
+ },
7901
+ expectedActions: {
7902
+ supportHQV: false,
7903
+ supportHDV: false,
7904
+ },
7905
+ },
7906
+ {
7907
+ meetingInfo: {
7908
+ video: {
7909
+ supportHDV: true,
7910
+ supportHQV: true,
7911
+ },
7912
+ },
7913
+ selfUserPolicies: {
7914
+ [SELF_POLICY.SUPPORT_HDV]: false,
7915
+ [SELF_POLICY.SUPPORT_HQV]: false,
7916
+ },
7917
+ expectedActions: {
7918
+ supportHQV: false,
7919
+ supportHDV: false,
7920
+ },
7921
+ },
7922
+ {
7923
+ meetingInfo: undefined,
7924
+ selfUserPolicies: {},
7925
+ expectedActions: {
7926
+ supportHQV: true,
7927
+ supportHDV: true,
7928
+ },
7929
+ },
7930
+ {
7931
+ meetingInfo: {some: 'data'},
7932
+ selfUserPolicies: undefined,
7933
+ expectedActions: {
7934
+ supportHQV: true,
7935
+ supportHDV: true,
7936
+ },
7937
+ },
7938
+ ],
7939
+ ({meetingInfo, selfUserPolicies, expectedActions}) => {
7940
+ it(`expectedActions are ${JSON.stringify(
7941
+ expectedActions
7942
+ )} when policies are ${JSON.stringify(
7943
+ selfUserPolicies
7944
+ )} and meetingInfo is ${JSON.stringify(meetingInfo)}`, () => {
7945
+ meeting.meetingInfo = meetingInfo;
7946
+ meeting.selfUserPolicies = selfUserPolicies;
7947
+
7948
+ meeting.updateMeetingActions();
7949
+
7950
+ assert.deepEqual(
7951
+ {
7952
+ supportHDV: meeting.inMeetingActions.supportHDV,
7953
+ supportHQV: meeting.inMeetingActions.supportHQV,
7954
+ },
7955
+ expectedActions
7956
+ );
7957
+ });
7958
+ }
7959
+ );
7960
+
7961
+ it('canUseVoip is disabled when the required policies are missing', () => {
7962
+ meeting.userDisplayHints = [DISPLAY_HINTS.VOIP_IS_ENABLED];
7963
+ meeting.selfUserPolicies = {};
7964
+ meeting.meetingInfo.supportVoIP = true;
7965
+
7966
+ meeting.updateMeetingActions();
7967
+
7968
+ assert.isFalse(meeting.inMeetingActions.get()['canUseVoip']);
7969
+ });
7970
+
7971
+ it('canUseVoip is enabled based on api info when the conditions are met', () => {
7972
+ meeting.userDisplayHints = undefined;
7973
+ meeting.selfUserPolicies = {[SELF_POLICY.SUPPORT_VOIP]: true};
7974
+ meeting.meetingInfo.supportVoIP = true;
7975
+
7976
+ meeting.updateMeetingActions();
7977
+
7978
+ assert.isTrue(meeting.inMeetingActions.get()['canUseVoip']);
7979
+ });
7980
+
7981
+ it('canUseVoip is enabled based on api info when the conditions are met - no display hints', () => {
7982
+ meeting.userDisplayHints = [];
7983
+ meeting.selfUserPolicies = {[SELF_POLICY.SUPPORT_VOIP]: true};
7984
+ meeting.meetingInfo.supportVoIP = true;
7985
+
7986
+ meeting.updateMeetingActions();
7987
+
7988
+ assert.isTrue(meeting.inMeetingActions.get()['canUseVoip']);
7989
+ });
7990
+
7991
+ it('canUseVoip is enabled when there is no meeting info', () => {
7992
+ meeting.updateMeetingActions();
7993
+
7994
+ assert.isTrue(meeting.inMeetingActions.get()['canUseVoip']);
7995
+ });
7996
+
7997
+ it('canUseVoip is enabled when it is a locus call', () => {
7998
+ meeting.meetingInfo = {some: 'info'};
7999
+ meeting.type = 'CALL';
8000
+
8001
+ meeting.updateMeetingActions();
8002
+
8003
+ assert.isTrue(meeting.inMeetingActions.get()['canUseVoip']);
8004
+ });
8005
+
8006
+ it('canUseVoip is disabled based on api info when supportVoip is false', () => {
8007
+ meeting.userDisplayHints = undefined;
8008
+ meeting.selfUserPolicies = {[SELF_POLICY.SUPPORT_VOIP]: true};
8009
+ meeting.meetingInfo.supportVoIP = false;
8010
+
8011
+ meeting.updateMeetingActions();
8012
+
8013
+ assert.isFalse(meeting.inMeetingActions.get()['canUseVoip']);
8014
+ });
8015
+
8016
+ it('canUseVoip is disabled based on api info when the required policies are missing', () => {
8017
+ meeting.userDisplayHints = undefined;
8018
+ meeting.selfUserPolicies = {};
8019
+ meeting.meetingInfo.supportVoIP = true;
8020
+
8021
+ meeting.updateMeetingActions();
8022
+
8023
+ assert.isFalse(meeting.inMeetingActions.get()['canUseVoip']);
8024
+ });
8025
+
8026
+ it('canUseVoip is enabled when there are no policies', () => {
8027
+ meeting.userDisplayHints = [DISPLAY_HINTS.VOIP_IS_ENABLED];
8028
+ meeting.selfUserPolicies = undefined;
8029
+ meeting.meetingInfo.supportVoIP = false;
8030
+
8031
+ meeting.updateMeetingActions();
8032
+
8033
+ assert.isTrue(meeting.inMeetingActions.get()['canUseVoip']);
8034
+ });
8035
+
8036
+ forEach(
8037
+ [
8038
+ {
8039
+ meetingInfo: {},
8040
+ selfUserPolicies: {
8041
+ [SELF_POLICY.SUPPORT_VIDEO]: true,
8042
+ },
8043
+ expectedActions: {
8044
+ canDoVideo: true,
8045
+ },
8046
+ },
8047
+ {
8048
+ meetingInfo: {},
8049
+ selfUserPolicies: {
8050
+ [SELF_POLICY.SUPPORT_VIDEO]: false,
8051
+ },
8052
+ expectedActions: {
8053
+ canDoVideo: true,
8054
+ },
8055
+ },
8056
+ {
8057
+ meetingInfo: {some: 'data'},
8058
+ selfUserPolicies: {
8059
+ [SELF_POLICY.SUPPORT_VIDEO]: true,
8060
+ },
8061
+ expectedActions: {
8062
+ canDoVideo: false,
8063
+ },
8064
+ },
8065
+ {
8066
+ meetingInfo: {some: 'data'},
8067
+ selfUserPolicies: undefined,
8068
+ expectedActions: {
8069
+ canDoVideo: true,
8070
+ },
8071
+ },
8072
+ {
8073
+ meetingInfo: {
8074
+ video: {},
8075
+ },
8076
+ selfUserPolicies: {
8077
+ [SELF_POLICY.SUPPORT_VIDEO]: true,
8078
+ },
8079
+ expectedActions: {
8080
+ canDoVideo: true,
8081
+ },
8082
+ },
8083
+ {
8084
+ meetingInfo: undefined,
8085
+ selfUserPolicies: {},
8086
+ expectedActions: {
8087
+ canDoVideo: true,
8088
+ },
8089
+ },
8090
+ {
8091
+ meetingInfo: {
8092
+ video: {},
8093
+ },
8094
+ selfUserPolicies: {
8095
+ [SELF_POLICY.SUPPORT_VIDEO]: false,
8096
+ },
8097
+ expectedActions: {
8098
+ canDoVideo: false,
8099
+ },
8100
+ },
8101
+ ],
8102
+ ({meetingInfo, selfUserPolicies, expectedActions}) => {
8103
+ it(`has expectedActions ${JSON.stringify(
8104
+ expectedActions
8105
+ )} when policies are ${JSON.stringify(
8106
+ selfUserPolicies
8107
+ )} and meetingInfo is ${JSON.stringify(meetingInfo)}`, () => {
8108
+ meeting.meetingInfo = meetingInfo;
8109
+ meeting.selfUserPolicies = selfUserPolicies;
8110
+ meeting.config.experimental.enableUnifiedMeetings = true;
8111
+
8112
+ meeting.updateMeetingActions();
8113
+
8114
+ assert.deepEqual(
8115
+ {
8116
+ canDoVideo: meeting.inMeetingActions.canDoVideo,
8117
+ },
8118
+ expectedActions
8119
+ );
8120
+ });
8121
+ }
8122
+ );
8123
+
8124
+ it('correctly updates the meeting actions', () => {
8125
+ // Due to import tree issues, hasHints must be stubed within the scope of the `it`.
8126
+ const restorableHasHints = ControlsOptionsUtil.hasHints;
8127
+ ControlsOptionsUtil.hasHints = sinon.stub().returns(true);
8128
+ ControlsOptionsUtil.hasPolicies = sinon.stub().returns(true);
8129
+
8130
+ const selfUserPolicies = {a: true};
8131
+ meeting.selfUserPolicies = {a: true};
8132
+ const userDisplayHints = ['LOCK_CONTROL_UNLOCK'];
8133
+ meeting.userDisplayHints = ['LOCK_CONTROL_UNLOCK'];
8134
+ meeting.meetingInfo.supportVoIP = true;
8135
+
8136
+ meeting.updateMeetingActions();
8137
+
8138
+ assert.calledWith(canUserLockSpy, userDisplayHints);
8139
+ assert.calledWith(canUserUnlockSpy, userDisplayHints);
8140
+ assert.calledWith(canUserStartSpy, userDisplayHints);
8141
+ assert.calledWith(canUserStopSpy, userDisplayHints);
8142
+ assert.calledWith(canUserPauseSpy, userDisplayHints);
8143
+ assert.calledWith(canUserResumeSpy, userDisplayHints);
8144
+ assert.calledWith(canSetMuteOnEntrySpy, userDisplayHints);
8145
+ assert.calledWith(canUnsetMuteOnEntrySpy, userDisplayHints);
8146
+ assert.calledWith(canSetDisallowUnmuteSpy, userDisplayHints);
8147
+ assert.calledWith(canUnsetDisallowUnmuteSpy, userDisplayHints);
8148
+ assert.calledWith(canUserRaiseHandSpy, userDisplayHints);
8149
+ assert.calledWith(bothLeaveAndEndMeetingAvailableSpy, userDisplayHints);
8150
+ assert.calledWith(canUserLowerAllHandsSpy, userDisplayHints);
8151
+ assert.calledWith(canUserLowerSomeoneElsesHandSpy, userDisplayHints);
8152
+ assert.calledWith(waitingForOthersToJoinSpy, userDisplayHints);
8153
+ assert.calledWith(canSendReactionsSpy, null, userDisplayHints);
8154
+ assert.calledWith(canUserRenameSelfAndObservedSpy, userDisplayHints);
8155
+ assert.calledWith(canUserRenameOthersSpy, userDisplayHints);
8156
+ assert.calledWith(canShareWhiteBoardSpy, userDisplayHints);
8157
+
8158
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8159
+ requiredHints: [DISPLAY_HINTS.MUTE_ALL],
8160
+ displayHints: userDisplayHints,
8161
+ });
8162
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8163
+ requiredHints: [DISPLAY_HINTS.UNMUTE_ALL],
8164
+ displayHints: userDisplayHints,
8165
+ });
8166
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8167
+ requiredHints: [DISPLAY_HINTS.ENABLE_HARD_MUTE],
8168
+ displayHints: userDisplayHints,
8169
+ });
8170
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8171
+ requiredHints: [DISPLAY_HINTS.DISABLE_HARD_MUTE],
8172
+ displayHints: userDisplayHints,
8173
+ });
8174
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8175
+ requiredHints: [DISPLAY_HINTS.ENABLE_MUTE_ON_ENTRY],
8176
+ displayHints: userDisplayHints,
8177
+ });
8178
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8179
+ requiredHints: [DISPLAY_HINTS.DISABLE_MUTE_ON_ENTRY],
8180
+ displayHints: userDisplayHints,
8181
+ });
8182
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8183
+ requiredHints: [DISPLAY_HINTS.ENABLE_REACTIONS],
8184
+ displayHints: userDisplayHints,
8185
+ });
8186
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8187
+ requiredHints: [DISPLAY_HINTS.DISABLE_REACTIONS],
8188
+ displayHints: userDisplayHints,
8189
+ });
8190
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8191
+ requiredHints: [DISPLAY_HINTS.ENABLE_SHOW_DISPLAY_NAME],
8192
+ displayHints: userDisplayHints,
8193
+ });
8194
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8195
+ requiredHints: [DISPLAY_HINTS.DISABLE_SHOW_DISPLAY_NAME],
8196
+ displayHints: userDisplayHints,
8197
+ });
8198
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8199
+ requiredHints: [DISPLAY_HINTS.SHARE_CONTROL],
8200
+ displayHints: userDisplayHints,
8201
+ });
8202
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8203
+ requiredHints: [DISPLAY_HINTS.ENABLE_VIEW_THE_PARTICIPANT_LIST],
8204
+ displayHints: userDisplayHints,
8205
+ });
8206
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8207
+ requiredHints: [DISPLAY_HINTS.DISABLE_VIEW_THE_PARTICIPANT_LIST],
8208
+ displayHints: userDisplayHints,
8209
+ });
8210
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8211
+ requiredHints: [DISPLAY_HINTS.SHARE_FILE],
8212
+ displayHints: userDisplayHints,
8213
+ });
8214
+ assert.calledWith(ControlsOptionsUtil.hasPolicies, {
8215
+ requiredPolicies: [SELF_POLICY.SUPPORT_FILE_SHARE],
8216
+ policies: selfUserPolicies,
8217
+ });
8218
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8219
+ requiredHints: [DISPLAY_HINTS.SHARE_APPLICATION],
8220
+ displayHints: userDisplayHints,
8221
+ });
8222
+ assert.calledWith(ControlsOptionsUtil.hasPolicies, {
8223
+ requiredPolicies: [SELF_POLICY.SUPPORT_APP_SHARE],
8224
+ policies: selfUserPolicies,
8225
+ });
8226
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8227
+ requiredHints: [DISPLAY_HINTS.SHARE_CAMERA],
8228
+ displayHints: userDisplayHints,
8229
+ });
8230
+ assert.calledWith(ControlsOptionsUtil.hasPolicies, {
8231
+ requiredPolicies: [SELF_POLICY.SUPPORT_CAMERA_SHARE],
8232
+ policies: selfUserPolicies,
8233
+ });
8234
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8235
+ requiredHints: [DISPLAY_HINTS.SHARE_DESKTOP],
8236
+ displayHints: userDisplayHints,
8237
+ });
8238
+ assert.calledWith(ControlsOptionsUtil.hasPolicies, {
8239
+ requiredPolicies: [SELF_POLICY.SUPPORT_DESKTOP_SHARE],
8240
+ policies: selfUserPolicies,
8241
+ });
8242
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
8243
+ requiredHints: [DISPLAY_HINTS.SHARE_CONTENT],
8244
+ displayHints: userDisplayHints,
8245
+ });
8246
+ assert.calledWith(ControlsOptionsUtil.hasPolicies, {
8247
+ requiredPolicies: [SELF_POLICY.SUPPORT_VOIP],
8248
+ policies: selfUserPolicies,
8249
+ });
7350
8250
 
7351
8251
  assert.calledWith(updateMeetingActionsSpy);
7352
8252
  assert.calledWith(setRecordingDisplayHintsSpy, userDisplayHints);