@webex/plugin-meetings 3.11.0-next.3 → 3.11.0-next.30

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 (99) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/config.js +5 -1
  4. package/dist/config.js.map +1 -1
  5. package/dist/hashTree/hashTree.js +18 -0
  6. package/dist/hashTree/hashTree.js.map +1 -1
  7. package/dist/hashTree/hashTreeParser.js +603 -266
  8. package/dist/hashTree/hashTreeParser.js.map +1 -1
  9. package/dist/hashTree/types.js +4 -2
  10. package/dist/hashTree/types.js.map +1 -1
  11. package/dist/hashTree/utils.js +10 -0
  12. package/dist/hashTree/utils.js.map +1 -1
  13. package/dist/index.js +2 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/interceptors/constant.js +12 -0
  16. package/dist/interceptors/constant.js.map +1 -0
  17. package/dist/interceptors/dataChannelAuthToken.js +233 -0
  18. package/dist/interceptors/dataChannelAuthToken.js.map +1 -0
  19. package/dist/interceptors/index.js +7 -0
  20. package/dist/interceptors/index.js.map +1 -1
  21. package/dist/interpretation/index.js +1 -1
  22. package/dist/interpretation/siLanguage.js +1 -1
  23. package/dist/locus-info/index.js +80 -44
  24. package/dist/locus-info/index.js.map +1 -1
  25. package/dist/locus-info/types.js.map +1 -1
  26. package/dist/media/MediaConnectionAwaiter.js +57 -1
  27. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  28. package/dist/media/properties.js +4 -2
  29. package/dist/media/properties.js.map +1 -1
  30. package/dist/meeting/index.js +134 -40
  31. package/dist/meeting/index.js.map +1 -1
  32. package/dist/meeting/request.js +50 -0
  33. package/dist/meeting/request.js.map +1 -1
  34. package/dist/meeting/request.type.js.map +1 -1
  35. package/dist/meeting/util.js +108 -2
  36. package/dist/meeting/util.js.map +1 -1
  37. package/dist/meetings/index.js +76 -34
  38. package/dist/meetings/index.js.map +1 -1
  39. package/dist/metrics/constants.js +2 -1
  40. package/dist/metrics/constants.js.map +1 -1
  41. package/dist/multistream/mediaRequestManager.js +1 -1
  42. package/dist/multistream/mediaRequestManager.js.map +1 -1
  43. package/dist/multistream/remoteMediaManager.js +11 -0
  44. package/dist/multistream/remoteMediaManager.js.map +1 -1
  45. package/dist/reactions/reactions.type.js.map +1 -1
  46. package/dist/types/config.d.ts +3 -0
  47. package/dist/types/hashTree/hashTree.d.ts +7 -0
  48. package/dist/types/hashTree/hashTreeParser.d.ts +83 -12
  49. package/dist/types/hashTree/types.d.ts +3 -0
  50. package/dist/types/hashTree/utils.d.ts +6 -0
  51. package/dist/types/interceptors/constant.d.ts +5 -0
  52. package/dist/types/interceptors/dataChannelAuthToken.d.ts +35 -0
  53. package/dist/types/interceptors/index.d.ts +2 -1
  54. package/dist/types/locus-info/index.d.ts +9 -2
  55. package/dist/types/locus-info/types.d.ts +1 -0
  56. package/dist/types/media/MediaConnectionAwaiter.d.ts +10 -1
  57. package/dist/types/media/properties.d.ts +2 -1
  58. package/dist/types/meeting/index.d.ts +27 -5
  59. package/dist/types/meeting/request.d.ts +16 -1
  60. package/dist/types/meeting/request.type.d.ts +5 -0
  61. package/dist/types/meeting/util.d.ts +28 -0
  62. package/dist/types/meetings/index.d.ts +3 -1
  63. package/dist/types/metrics/constants.d.ts +1 -0
  64. package/dist/types/reactions/reactions.type.d.ts +1 -0
  65. package/dist/webinar/index.js +1 -1
  66. package/package.json +22 -22
  67. package/src/config.ts +3 -0
  68. package/src/hashTree/hashTree.ts +17 -0
  69. package/src/hashTree/hashTreeParser.ts +525 -188
  70. package/src/hashTree/types.ts +4 -0
  71. package/src/hashTree/utils.ts +9 -0
  72. package/src/index.ts +6 -1
  73. package/src/interceptors/constant.ts +6 -0
  74. package/src/interceptors/dataChannelAuthToken.ts +142 -0
  75. package/src/interceptors/index.ts +2 -1
  76. package/src/locus-info/index.ts +110 -35
  77. package/src/locus-info/types.ts +1 -0
  78. package/src/media/MediaConnectionAwaiter.ts +41 -1
  79. package/src/media/properties.ts +3 -1
  80. package/src/meeting/index.ts +101 -22
  81. package/src/meeting/request.ts +42 -0
  82. package/src/meeting/request.type.ts +6 -0
  83. package/src/meeting/util.ts +132 -1
  84. package/src/meetings/index.ts +88 -7
  85. package/src/metrics/constants.ts +1 -0
  86. package/src/multistream/mediaRequestManager.ts +1 -1
  87. package/src/multistream/remoteMediaManager.ts +13 -0
  88. package/src/reactions/reactions.type.ts +1 -0
  89. package/test/unit/spec/hashTree/hashTree.ts +66 -0
  90. package/test/unit/spec/hashTree/hashTreeParser.ts +1594 -162
  91. package/test/unit/spec/interceptors/dataChannelAuthToken.ts +141 -0
  92. package/test/unit/spec/locus-info/index.js +173 -45
  93. package/test/unit/spec/media/MediaConnectionAwaiter.ts +41 -1
  94. package/test/unit/spec/media/properties.ts +12 -3
  95. package/test/unit/spec/meeting/index.js +414 -62
  96. package/test/unit/spec/meeting/request.js +64 -0
  97. package/test/unit/spec/meeting/utils.js +294 -22
  98. package/test/unit/spec/meetings/index.js +550 -10
  99. package/test/unit/spec/multistream/remoteMediaManager.ts +30 -0
@@ -0,0 +1,141 @@
1
+ import 'jsdom-global/register';
2
+ import {assert, expect} from '@webex/test-helper-chai';
3
+ import sinon from 'sinon';
4
+ import MockWebex from '@webex/test-helper-mock-webex';
5
+ import {WebexHttpError} from '@webex/webex-core';
6
+ import DataChannelAuthTokenInterceptor from '@webex/plugin-meetings/src/interceptors/dataChannelAuthToken';
7
+ import LoggerProxy from '@webex/plugin-meetings/src/common/logs/logger-proxy';
8
+ import {DATA_CHANNEL_AUTH_HEADER, MAX_RETRY} from '@webex/plugin-meetings/src/interceptors/constant';
9
+
10
+ describe('plugin-meetings', () => {
11
+ describe('Interceptors', () => {
12
+ describe('DataChannelAuthTokenInterceptor', () => {
13
+ let interceptor, webex, clock;
14
+
15
+ beforeEach(() => {
16
+ clock = sinon.useFakeTimers();
17
+
18
+ webex = new MockWebex({children: {}});
19
+ webex.request = sinon.stub().resolves({});
20
+
21
+ interceptor = Reflect.apply(DataChannelAuthTokenInterceptor.create, webex, []);
22
+
23
+ interceptor._refreshDataChannelToken = sinon.stub();
24
+ interceptor._isDataChannelTokenEnabled = sinon.stub().resolves(true);
25
+ });
26
+
27
+ afterEach(() => {
28
+ clock.restore();
29
+ });
30
+
31
+ const makeReason = (statusCode) =>
32
+ new WebexHttpError({
33
+ statusCode,
34
+ options: {headers: {}, uri: 'https://example.com'},
35
+ body: {},
36
+ });
37
+
38
+ describe('#onResponseError', () => {
39
+ it('rejects when no Data-Channel-Auth-Token header exists', async () => {
40
+ const options = {headers: {}};
41
+ const reason = makeReason(401);
42
+
43
+ await assert.isRejected(interceptor.onResponseError(options, reason), reason);
44
+ });
45
+
46
+ it('rejects when statusCode is not 401/403', async () => {
47
+ const options = {headers: {[DATA_CHANNEL_AUTH_HEADER]: 'abc'}};
48
+ const reason = makeReason(500);
49
+
50
+ await assert.isRejected(interceptor.onResponseError(options, reason), reason);
51
+ });
52
+
53
+ it('rejects when retry count exceeds MAX_RETRY', async () => {
54
+ const options = {headers: {[DATA_CHANNEL_AUTH_HEADER]: 'abc'}};
55
+ const reason = makeReason(401);
56
+
57
+ for (let i = 0; i < MAX_RETRY; i++) {
58
+ interceptor.onResponseError(options, reason).catch(() => {});
59
+ }
60
+
61
+ await assert.isRejected(interceptor.onResponseError(options, reason), reason);
62
+
63
+ sinon.assert.calledOnce(LoggerProxy.logger.error);
64
+ });
65
+
66
+ it('calls refreshTokenAndRetryWithDelay when eligible', async () => {
67
+ const options = {headers: {[DATA_CHANNEL_AUTH_HEADER]: 'abc'}};
68
+ const reason = makeReason(401);
69
+
70
+ interceptor._isDataChannelTokenEnabled.resolves(true);
71
+
72
+ const stub = sinon.stub(interceptor, 'refreshTokenAndRetryWithDelay').resolves('ok');
73
+
74
+ await interceptor.onResponseError(options, reason);
75
+
76
+ sinon.assert.calledOnceWithExactly(stub, options);
77
+ });
78
+
79
+ it('rejects when isDataChannelTokenEnabled is false', async () => {
80
+ const options = {headers: {[DATA_CHANNEL_AUTH_HEADER]: 'abc'}};
81
+ const reason = makeReason(401);
82
+
83
+ interceptor._isDataChannelTokenEnabled.resolves(false);
84
+
85
+ await assert.isRejected(interceptor.onResponseError(options, reason), reason);
86
+ });
87
+ });
88
+
89
+ describe('#refreshTokenAndRetryWithDelay', () => {
90
+ const options = {
91
+ headers: {[DATA_CHANNEL_AUTH_HEADER]: 'old-token'},
92
+ method: 'GET',
93
+ uri: 'https://example.com',
94
+ };
95
+
96
+ it('refreshes token and retries request successfully', async () => {
97
+ interceptor._refreshDataChannelToken.resolves('new-token');
98
+ webex.request.resolves('mock-response');
99
+
100
+ const promise = interceptor.refreshTokenAndRetryWithDelay(options);
101
+
102
+ clock.tick(2000);
103
+
104
+ const result = await promise;
105
+
106
+ expect(interceptor._refreshDataChannelToken.calledOnce).to.be.true;
107
+ expect(options.headers[DATA_CHANNEL_AUTH_HEADER]).to.equal('new-token');
108
+ expect(webex.request.calledOnceWith(options)).to.be.true;
109
+ expect(result).to.equal('mock-response');
110
+ });
111
+
112
+ it('rejects when refreshDataChannelToken fails', async () => {
113
+ interceptor._refreshDataChannelToken.rejects(new Error('refresh failed'));
114
+
115
+ const promise = interceptor.refreshTokenAndRetryWithDelay(options);
116
+
117
+ clock.tick(2000);
118
+
119
+ await assert.isRejected(
120
+ promise,
121
+ /DataChannel token refresh failed: refresh failed/
122
+ );
123
+ });
124
+
125
+ it('rejects when retry request fails', async () => {
126
+ interceptor._refreshDataChannelToken.resolves('new-token');
127
+ webex.request.rejects(new Error('request failed'));
128
+
129
+ const promise = interceptor.refreshTokenAndRetryWithDelay(options);
130
+
131
+ clock.tick(2000);
132
+
133
+ await assert.isRejected(
134
+ promise,
135
+ /DataChannel token refresh failed: request failed/
136
+ );
137
+ });
138
+ });
139
+ });
140
+ });
141
+ });
@@ -29,7 +29,8 @@ import {
29
29
  } from '../../../../src/constants';
30
30
 
31
31
  import {self, selfWithInactivity} from './selfConstant';
32
- import { MEETING_REMOVED_REASON } from '@webex/plugin-meetings/src/constants';
32
+ import {MEETING_REMOVED_REASON} from '@webex/plugin-meetings/src/constants';
33
+ import LoggerProxy from '@webex/plugin-meetings/src/common/logs/logger-proxy';
33
34
 
34
35
  describe('plugin-meetings', () => {
35
36
  describe('LocusInfo index', () => {
@@ -106,7 +107,7 @@ describe('plugin-meetings', () => {
106
107
  const createHashTreeMessage = (visibleDataSets) => ({
107
108
  locusStateElements: [
108
109
  {
109
- htMeta: {elementId: {type: 'self'}},
110
+ htMeta: {elementId: {type: 'metadata'}},
110
111
  data: {visibleDataSets},
111
112
  },
112
113
  ],
@@ -136,9 +137,13 @@ describe('plugin-meetings', () => {
136
137
  HashTreeParserStub,
137
138
  sinon.match({
138
139
  initialLocus: {
139
- locus: {self: {visibleDataSets}},
140
+ locus: null,
140
141
  dataSets: [],
141
142
  },
143
+ metadata: {
144
+ htMeta: hashTreeMessage.locusStateElements[0].htMeta,
145
+ visibleDataSets,
146
+ },
142
147
  webexRequest: sinon.match.func,
143
148
  locusInfoUpdateCallback: sinon.match.func,
144
149
  debugId: sinon.match.string,
@@ -169,11 +174,16 @@ describe('plugin-meetings', () => {
169
174
  const visibleDataSets = ['dataset1', 'dataset2'];
170
175
  const locus = createLocusWithVisibleDataSets(visibleDataSets);
171
176
  const dataSets = [{name: 'dataset1', url: 'http://dataset-url.com'}];
177
+ const metadata = {
178
+ htMeta: {elementId: {type: 'metadata'}},
179
+ visibleDataSets,
180
+ };
172
181
 
173
182
  await locusInfo.initialSetup({
174
183
  trigger: 'join-response',
175
184
  locus,
176
185
  dataSets,
186
+ metadata,
177
187
  });
178
188
 
179
189
  assert.calledOnceWithExactly(
@@ -183,6 +193,7 @@ describe('plugin-meetings', () => {
183
193
  locus,
184
194
  dataSets,
185
195
  },
196
+ metadata,
186
197
  webexRequest: sinon.match.func,
187
198
  locusInfoUpdateCallback: sinon.match.func,
188
199
  debugId: sinon.match.string,
@@ -220,12 +231,13 @@ describe('plugin-meetings', () => {
220
231
  HashTreeParserStub,
221
232
  sinon.match({
222
233
  initialLocus: {
223
- locus: {self: {visibleDataSets}},
234
+ locus: null,
224
235
  dataSets: [],
225
236
  },
226
237
  webexRequest: sinon.match.func,
227
238
  locusInfoUpdateCallback: sinon.match.func,
228
239
  debugId: sinon.match.string,
240
+ metadata: null,
229
241
  })
230
242
  );
231
243
  assert.calledOnceWithExactly(mockHashTreeParser.initializeFromGetLociResponse, locus);
@@ -249,6 +261,30 @@ describe('plugin-meetings', () => {
249
261
  assert.isTrue(locusInfo.emitChange);
250
262
  });
251
263
 
264
+ it('throws if called with "locus-message" and Metadata object without visibleDataSets', async () => {
265
+ const hashTreeMessage = {
266
+ locusStateElements: [
267
+ {
268
+ htMeta: {elementId: {type: 'Metadata'}},
269
+ data: {},
270
+ },
271
+ ],
272
+ dataSets: [{name: 'dataset1', url: 'test-url'}],
273
+ };
274
+ try {
275
+ await locusInfo.initialSetup({
276
+ trigger: 'locus-message',
277
+ hashTreeMessage,
278
+ });
279
+ assert.fail('should have thrown an error');
280
+ } catch (error) {
281
+ assert.equal(
282
+ error.message,
283
+ 'Metadata object with visibleDataSets is missing in the message'
284
+ );
285
+ }
286
+ });
287
+
252
288
  describe('should setup correct locusInfoUpdateCallback when creating HashTreeParser', () => {
253
289
  const OBJECTS_UPDATED = HashTreeParserModule.LocusInfoUpdateType.OBJECTS_UPDATED;
254
290
  const MEETING_ENDED = HashTreeParserModule.LocusInfoUpdateType.MEETING_ENDED;
@@ -265,8 +301,8 @@ describe('plugin-meetings', () => {
265
301
  hashTreeMessage: {
266
302
  locusStateElements: [
267
303
  {
268
- htMeta: {elementId: {type: 'self'}},
269
- data: {visibleDataSets: ['dataset1']},
304
+ htMeta: {elementId: {type: 'Metadata'}},
305
+ data: {visibleDataSets: [{name: 'dataset1', url: 'test-url'}]},
270
306
  },
271
307
  ],
272
308
  dataSets: [{name: 'dataset1', url: 'test-url'}],
@@ -293,6 +329,16 @@ describe('plugin-meetings', () => {
293
329
  htMeta: {elementId: {type: 'mediashare', id: 'fake-ht-mediaShare-2', version: 1}},
294
330
  },
295
331
  ];
332
+ locusInfo.embeddedApps = [
333
+ {
334
+ id: 'fake-embedded-app-1',
335
+ htMeta: {elementId: {type: 'embeddedapp', id: 'fake-ht-embeddedApp-1', version: 1}},
336
+ },
337
+ {
338
+ id: 'fake-embedded-app-2',
339
+ htMeta: {elementId: {type: 'embeddedapp', id: 'fake-ht-embeddedApp-2', version: 1}},
340
+ },
341
+ ];
296
342
  locusInfo.meetings = {id: 'fake-meetings'};
297
343
  locusInfo.participants = [
298
344
  {id: 'fake-participant-1', name: 'Participant One'},
@@ -328,6 +374,16 @@ describe('plugin-meetings', () => {
328
374
  htMeta: {elementId: {type: 'mediashare', id: 'fake-ht-mediaShare-2', version: 1}},
329
375
  },
330
376
  ],
377
+ embeddedApps: [
378
+ {
379
+ id: 'fake-embedded-app-1',
380
+ htMeta: {elementId: {type: 'embeddedapp', id: 'fake-ht-embeddedApp-1', version: 1}},
381
+ },
382
+ {
383
+ id: 'fake-embedded-app-2',
384
+ htMeta: {elementId: {type: 'embeddedapp', id: 'fake-ht-embeddedApp-2', version: 1}},
385
+ },
386
+ ],
331
387
  meetings: {id: 'fake-meetings'},
332
388
  jsSdkMeta: {removedParticipantIds: []},
333
389
  participants: [], // empty means there were no participant updates
@@ -504,6 +560,7 @@ describe('plugin-meetings', () => {
504
560
  self: {id: 'fake-self'},
505
561
  links: {id: 'fake-links'},
506
562
  mediaShares: expectedLocusInfo.mediaShares,
563
+ embeddedApps: expectedLocusInfo.embeddedApps,
507
564
  // and now the new fields
508
565
  ...newLocus,
509
566
  htMeta: newLocusHtMeta,
@@ -536,6 +593,7 @@ describe('plugin-meetings', () => {
536
593
  self: 'new-self',
537
594
  participants: 'new-participants',
538
595
  mediaShares: 'new-mediaShares',
596
+ embeddedApps: 'new-embeddedApps',
539
597
  },
540
598
  },
541
599
  ],
@@ -551,6 +609,7 @@ describe('plugin-meetings', () => {
551
609
  self: {id: 'fake-self'},
552
610
  links: {id: 'fake-links'},
553
611
  mediaShares: expectedLocusInfo.mediaShares,
612
+ embeddedApps: expectedLocusInfo.embeddedApps,
554
613
  participants: [], // empty means there were no participant updates
555
614
  jsSdkMeta: {removedParticipantIds: []}, // no participants were removed
556
615
  ...newLocus,
@@ -586,6 +645,7 @@ describe('plugin-meetings', () => {
586
645
  self: {id: 'fake-self'},
587
646
  links: {id: 'fake-links'},
588
647
  mediaShares: expectedLocusInfo.mediaShares,
648
+ embeddedApps: expectedLocusInfo.embeddedApps,
589
649
  // and now the new fields
590
650
  ...newLocus,
591
651
  htMeta: newLocusHtMeta,
@@ -725,6 +785,39 @@ describe('plugin-meetings', () => {
725
785
  });
726
786
  });
727
787
 
788
+ it('should process locus update correctly when called with updated EMBEDDEDAPP objects', () => {
789
+ const newEmbeddedApp = {
790
+ id: 'new-embedded-app-3',
791
+ htMeta: {elementId: {type: 'embeddedapp', id: 'fake-ht-embeddedApp-3', version: 100}},
792
+ };
793
+ const updatedEmbeddedApp2 = {
794
+ id: 'fake-embedded-app-2',
795
+ someNewProp: 'newValue',
796
+ htMeta: {elementId: {type: 'embeddedapp', id: 'fake-ht-embeddedApp-2', version: 100}},
797
+ };
798
+ // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
799
+ // with 1 embedded app added, 1 updated, and 1 removed
800
+ locusInfoUpdateCallback(OBJECTS_UPDATED, {
801
+ updatedObjects: [
802
+ {htMeta: {elementId: {type: 'embeddedapp', id: 'fake-ht-embeddedApp-1'}}, data: null},
803
+ {
804
+ htMeta: {elementId: {type: 'embeddedapp', id: 'fake-ht-embeddedApp-2'}},
805
+ data: updatedEmbeddedApp2,
806
+ },
807
+ {
808
+ htMeta: {elementId: {type: 'embeddedapp', id: 'fake-ht-embeddedApp-3'}},
809
+ data: newEmbeddedApp,
810
+ },
811
+ ],
812
+ });
813
+
814
+ // check onDeltaLocus() was called with correctly updated locus info
815
+ assert.calledOnceWithExactly(onDeltaLocusStub, {
816
+ ...expectedLocusInfo,
817
+ embeddedApps: [updatedEmbeddedApp2, newEmbeddedApp],
818
+ });
819
+ });
820
+
728
821
  it('should process locus update correctly when called with a combination of various updated objects', () => {
729
822
  const newSelf = {
730
823
  id: 'new-self',
@@ -1076,7 +1169,7 @@ describe('plugin-meetings', () => {
1076
1169
  it('should trigger the CONTROLS_POLLING_QA_CHANGED event when necessary', () => {
1077
1170
  locusInfo.controls = {};
1078
1171
  locusInfo.emitScoped = sinon.stub();
1079
- newControls.pollingQAControl = { enabled: true };
1172
+ newControls.pollingQAControl = {enabled: true};
1080
1173
  locusInfo.updateControls(newControls);
1081
1174
 
1082
1175
  assert.calledWith(
@@ -1631,7 +1724,6 @@ describe('plugin-meetings', () => {
1631
1724
  );
1632
1725
  });
1633
1726
 
1634
-
1635
1727
  it('should call with participant display name', () => {
1636
1728
  const failureParticipant = [
1637
1729
  {
@@ -1656,7 +1748,7 @@ describe('plugin-meetings', () => {
1656
1748
  displayName: 'Test User',
1657
1749
  }
1658
1750
  );
1659
- })
1751
+ });
1660
1752
  });
1661
1753
 
1662
1754
  describe('#updateSelf', () => {
@@ -2457,8 +2549,8 @@ describe('plugin-meetings', () => {
2457
2549
  {
2458
2550
  isInitializing: !self,
2459
2551
  }
2460
- );
2461
- });
2552
+ );
2553
+ });
2462
2554
 
2463
2555
  const checkMeetingInfoUpdatedCalled = (expected, payload) => {
2464
2556
  const expectedArgs = [
@@ -2923,28 +3015,28 @@ describe('plugin-meetings', () => {
2923
3015
  assert.isFunction(locusParser.onDeltaAction);
2924
3016
  });
2925
3017
 
2926
- it("#updateLocusInfo invokes updateLocusUrl before updateMeetingInfo", () => {
3018
+ it('#updateLocusInfo invokes updateLocusUrl before updateMeetingInfo', () => {
2927
3019
  const callOrder = [];
2928
- sinon.stub(locusInfo, "updateControls");
2929
- sinon.stub(locusInfo, "updateConversationUrl");
2930
- sinon.stub(locusInfo, "updateCreated");
2931
- sinon.stub(locusInfo, "updateFullState");
2932
- sinon.stub(locusInfo, "updateHostInfo");
2933
- sinon.stub(locusInfo, "updateMeetingInfo").callsFake(() => {
2934
- callOrder.push("updateMeetingInfo");
3020
+ sinon.stub(locusInfo, 'updateControls');
3021
+ sinon.stub(locusInfo, 'updateConversationUrl');
3022
+ sinon.stub(locusInfo, 'updateCreated');
3023
+ sinon.stub(locusInfo, 'updateFullState');
3024
+ sinon.stub(locusInfo, 'updateHostInfo');
3025
+ sinon.stub(locusInfo, 'updateMeetingInfo').callsFake(() => {
3026
+ callOrder.push('updateMeetingInfo');
2935
3027
  });
2936
- sinon.stub(locusInfo, "updateMediaShares");
2937
- sinon.stub(locusInfo, "updateReplaces");
2938
- sinon.stub(locusInfo, "updateSelf");
2939
- sinon.stub(locusInfo, "updateLocusUrl").callsFake(() => {
2940
- callOrder.push("updateLocusUrl");
3028
+ sinon.stub(locusInfo, 'updateMediaShares');
3029
+ sinon.stub(locusInfo, 'updateReplaces');
3030
+ sinon.stub(locusInfo, 'updateSelf');
3031
+ sinon.stub(locusInfo, 'updateLocusUrl').callsFake(() => {
3032
+ callOrder.push('updateLocusUrl');
2941
3033
  });
2942
- sinon.stub(locusInfo, "updateAclUrl");
2943
- sinon.stub(locusInfo, "updateBasequence");
2944
- sinon.stub(locusInfo, "updateSequence");
2945
- sinon.stub(locusInfo, "updateEmbeddedApps");
2946
- sinon.stub(locusInfo, "updateLinks");
2947
- sinon.stub(locusInfo, "compareAndUpdate");
3034
+ sinon.stub(locusInfo, 'updateAclUrl');
3035
+ sinon.stub(locusInfo, 'updateBasequence');
3036
+ sinon.stub(locusInfo, 'updateSequence');
3037
+ sinon.stub(locusInfo, 'updateEmbeddedApps');
3038
+ sinon.stub(locusInfo, 'updateLinks');
3039
+ sinon.stub(locusInfo, 'compareAndUpdate');
2948
3040
 
2949
3041
  locusInfo.updateLocusInfo(locus);
2950
3042
 
@@ -3000,7 +3092,7 @@ describe('plugin-meetings', () => {
3000
3092
  it('#updateLocusInfo puts the Locus DTO top level properties at the right place in LocusInfo class', () => {
3001
3093
  // this test verifies that the top-level properties of Locus DTO are copied
3002
3094
  // into LocusInfo class and set as top level properties too
3003
- // this is important, because the code handling Locus hass trees relies on it, see updateFromHashTree()
3095
+ // this is important, because the code handling Locus hash trees relies on it, see updateFromHashTree()
3004
3096
  const info = {id: 'info id'};
3005
3097
  const fullState = {id: 'fullState id'};
3006
3098
  const links = {services: {id: 'service links'}, resources: {id: 'resource links'}};
@@ -3039,7 +3131,7 @@ describe('plugin-meetings', () => {
3039
3131
  sandbox.stub(locusInfo, 'handleOneOnOneEvent');
3040
3132
  sandbox.stub(locusParser, 'isNewFullLocus').returns(true);
3041
3133
 
3042
- locusInfo.onFullLocus(fakeLocus, eventType);
3134
+ locusInfo.onFullLocus('test', fakeLocus, eventType);
3043
3135
 
3044
3136
  assert.equal(fakeLocus, locusParser.workingCopy);
3045
3137
  });
@@ -3060,7 +3152,7 @@ describe('plugin-meetings', () => {
3060
3152
 
3061
3153
  sandbox.stub(locusParser, 'isNewFullLocus').returns(false);
3062
3154
 
3063
- locusInfo.onFullLocus(fakeLocus, eventType);
3155
+ locusInfo.onFullLocus('test', fakeLocus, eventType);
3064
3156
 
3065
3157
  spies.forEach((spy) => {
3066
3158
  assert.notCalled(spy);
@@ -3210,7 +3302,11 @@ describe('plugin-meetings', () => {
3210
3302
  }).then(() => {
3211
3303
  assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {url: 'oldLocusUrl'});
3212
3304
 
3213
- assert.calledOnceWithExactly(meeting.locusInfo.onFullLocus, fakeFullLocusDto);
3305
+ assert.calledOnceWithExactly(
3306
+ meeting.locusInfo.onFullLocus,
3307
+ 'classic Locus sync',
3308
+ fakeFullLocusDto
3309
+ );
3214
3310
  assert.calledOnce(locusInfo.locusParser.resume);
3215
3311
  });
3216
3312
  });
@@ -3308,7 +3404,11 @@ describe('plugin-meetings', () => {
3308
3404
  });
3309
3405
 
3310
3406
  assert.notCalled(meeting.locusInfo.handleLocusDelta);
3311
- assert.calledOnceWithExactly(meeting.locusInfo.onFullLocus, fakeFullLocusDto);
3407
+ assert.calledOnceWithExactly(
3408
+ meeting.locusInfo.onFullLocus,
3409
+ 'classic Locus sync',
3410
+ fakeFullLocusDto
3411
+ );
3312
3412
  assert.calledOnce(locusInfo.locusParser.resume);
3313
3413
  });
3314
3414
  });
@@ -3484,7 +3584,11 @@ describe('plugin-meetings', () => {
3484
3584
  url: 'fake locus DELTA url',
3485
3585
  });
3486
3586
  assert.notCalled(meeting.locusInfo.handleLocusDelta);
3487
- assert.calledOnceWithExactly(meeting.locusInfo.onFullLocus, fakeFullLocusDto);
3587
+ assert.calledOnceWithExactly(
3588
+ meeting.locusInfo.onFullLocus,
3589
+ 'classic Locus sync',
3590
+ fakeFullLocusDto
3591
+ );
3488
3592
  assert.calledOnce(locusInfo.locusParser.resume);
3489
3593
  });
3490
3594
  });
@@ -3856,7 +3960,7 @@ describe('plugin-meetings', () => {
3856
3960
 
3857
3961
  describe('#updateLocusUrl', () => {
3858
3962
  it('trigger LOCUS_INFO_UPDATE_URL event with isMainLocus is true as default', () => {
3859
- const fakeUrl = "https://fake.com/locus";
3963
+ const fakeUrl = 'https://fake.com/locus';
3860
3964
  locusInfo.emitScoped = sinon.stub();
3861
3965
  locusInfo.updateLocusUrl(fakeUrl);
3862
3966
 
@@ -3869,12 +3973,12 @@ describe('plugin-meetings', () => {
3869
3973
  EVENTS.LOCUS_INFO_UPDATE_URL,
3870
3974
  {
3871
3975
  url: fakeUrl,
3872
- isMainLocus: true
3873
- },
3976
+ isMainLocus: true,
3977
+ }
3874
3978
  );
3875
3979
  });
3876
3980
  it('trigger LOCUS_INFO_UPDATE_URL event with isMainLocus is false', () => {
3877
- const fakeUrl = "https://fake.com/locus";
3981
+ const fakeUrl = 'https://fake.com/locus';
3878
3982
  locusInfo.emitScoped = sinon.stub();
3879
3983
  locusInfo.updateLocusUrl(fakeUrl, false);
3880
3984
 
@@ -3887,8 +3991,8 @@ describe('plugin-meetings', () => {
3887
3991
  EVENTS.LOCUS_INFO_UPDATE_URL,
3888
3992
  {
3889
3993
  url: fakeUrl,
3890
- isMainLocus: false
3891
- },
3994
+ isMainLocus: false,
3995
+ }
3892
3996
  );
3893
3997
  });
3894
3998
  });
@@ -3940,8 +4044,8 @@ describe('plugin-meetings', () => {
3940
4044
 
3941
4045
  sinon.stub(locusInfo, 'updateParticipants');
3942
4046
  sinon.stub(locusInfo, 'isMeetingActive');
3943
- sinon.stub(locusInfo, 'handleOneOnOneEvent');
3944
- (updateLocusInfoStub = sinon.stub(locusInfo, 'updateLocusInfo'));
4047
+ sinon.stub(locusInfo, 'handleOneOnOneEvent');
4048
+ updateLocusInfoStub = sinon.stub(locusInfo, 'updateLocusInfo');
3945
4049
  syncRequestStub = sinon.stub().resolves({body: {}});
3946
4050
 
3947
4051
  mockMeeting.locusInfo = locusInfo;
@@ -3950,7 +4054,7 @@ describe('plugin-meetings', () => {
3950
4054
  getLocusDTO: syncRequestStub,
3951
4055
  };
3952
4056
 
3953
- locusInfo.onFullLocus({
4057
+ locusInfo.onFullLocus('test', {
3954
4058
  sequence: {
3955
4059
  rangeStart: 0,
3956
4060
  rangeEnd: 0,
@@ -4213,6 +4317,30 @@ describe('plugin-meetings', () => {
4213
4317
 
4214
4318
  assert.calledOnceWithExactly(mockHashTreeParser.handleMessage, fakeHashTreeMessage);
4215
4319
  });
4320
+
4321
+ it('ignores hash tree event when hashTreeParser is not created yet', () => {
4322
+ const data = {
4323
+ eventType: LOCUSEVENT.HASH_TREE_DATA_UPDATED,
4324
+ stateElementsMessage: {
4325
+ locusStateElements: [],
4326
+ dataSets: [],
4327
+ },
4328
+ };
4329
+
4330
+ const loggerSpy = sinon.spy(LoggerProxy.logger, 'info');
4331
+ const getTheLocusToUpdateStub = sinon.stub(locusInfo, 'getTheLocusToUpdate');
4332
+
4333
+ // Ensure we're not using hash trees
4334
+ assert.isUndefined(locusInfo.hashTreeParser);
4335
+
4336
+ locusInfo.parse(mockMeeting, data);
4337
+
4338
+ assert.calledWith(
4339
+ loggerSpy,
4340
+ 'Locus-info:index#parse --> received locus hash tree event before hashTreeParser is created'
4341
+ );
4342
+ assert.notCalled(getTheLocusToUpdateStub);
4343
+ });
4216
4344
  });
4217
4345
  });
4218
4346
  });
@@ -5,6 +5,8 @@ import {ConnectionState, MediaConnectionEventNames} from '@webex/internal-media-
5
5
  import testUtils from '../../../utils/testUtils';
6
6
  import {ICE_AND_DTLS_CONNECTION_TIMEOUT} from '@webex/plugin-meetings/src/constants';
7
7
  import MediaConnectionAwaiter from '../../../../src/media/MediaConnectionAwaiter';
8
+ import Metrics from '../../../../src/metrics';
9
+ import BEHAVIORAL_METRICS from '../../../../src/metrics/constants';
8
10
 
9
11
  describe('MediaConnectionAwaiter', () => {
10
12
  let mediaConnectionAwaiter;
@@ -14,18 +16,34 @@ describe('MediaConnectionAwaiter', () => {
14
16
  beforeEach(() => {
15
17
  clock = sinon.useFakeTimers();
16
18
 
19
+ const mockTransportReport = {
20
+ type: 'transport',
21
+ dtlsState: 'connecting',
22
+ iceState: 'checking',
23
+ packetsSent: 10,
24
+ packetsReceived: 5,
25
+ };
26
+
17
27
  mockMC = {
18
- getStats: sinon.stub().resolves([]),
28
+ getStats: sinon.stub().resolves({
29
+ values: () => [mockTransportReport],
30
+ }),
19
31
  on: sinon.stub(),
20
32
  off: sinon.stub(),
21
33
  getConnectionState: sinon.stub().returns(ConnectionState.New),
22
34
  getIceGatheringState: sinon.stub().returns('new'),
23
35
  getIceConnectionState: sinon.stub().returns('new'),
24
36
  getPeerConnectionState: sinon.stub().returns('new'),
37
+ multistreamConnection: {
38
+ dataChannel: {
39
+ readyState: 'open',
40
+ },
41
+ },
25
42
  };
26
43
 
27
44
  mediaConnectionAwaiter = new MediaConnectionAwaiter({
28
45
  webrtcMediaConnection: mockMC,
46
+ correlationId: 'test-correlation-id',
29
47
  });
30
48
  });
31
49
 
@@ -44,6 +62,8 @@ describe('MediaConnectionAwaiter', () => {
44
62
  });
45
63
 
46
64
  it('rejects after timeout if ice state is not connected', async () => {
65
+ const sendMetricSpy = sinon.spy(Metrics, 'sendBehavioralMetric');
66
+
47
67
  mockMC.getConnectionState.returns(ConnectionState.Connecting);
48
68
  mockMC.getIceGatheringState.returns('gathering');
49
69
 
@@ -83,6 +103,18 @@ describe('MediaConnectionAwaiter', () => {
83
103
  assert.equal(promiseRejected, true);
84
104
 
85
105
  assert.calledThrice(mockMC.off);
106
+
107
+ assert.calledOnceWithExactly(sendMetricSpy, BEHAVIORAL_METRICS.MEDIA_STILL_NOT_CONNECTED, {
108
+ correlation_id: 'test-correlation-id',
109
+ numTransports: 1,
110
+ dtlsState: 'connecting',
111
+ iceState: 'checking',
112
+ packetsSent: 10,
113
+ packetsReceived: 5,
114
+ dataChannelState: 'open',
115
+ });
116
+
117
+ sendMetricSpy.restore();
86
118
  });
87
119
 
88
120
  it('rejects immediately if ice state is FAILED', async () => {
@@ -351,6 +383,8 @@ describe('MediaConnectionAwaiter', () => {
351
383
  });
352
384
 
353
385
  it(`reject with restart timer once if gathering state is not complete`, async () => {
386
+ const sendMetricSpy = sinon.spy(Metrics, 'sendBehavioralMetric');
387
+
354
388
  mockMC.getConnectionState.returns(ConnectionState.Connecting);
355
389
  mockMC.getIceGatheringState.returns('new');
356
390
 
@@ -390,6 +424,12 @@ describe('MediaConnectionAwaiter', () => {
390
424
 
391
425
  assert.calledOnce(clearTimeoutSpy);
392
426
  assert.calledTwice(setTimeoutSpy);
427
+
428
+ // verify sendMetric was called twice (once for each timeout)
429
+ assert.calledTwice(sendMetricSpy);
430
+ assert.calledWith(sendMetricSpy, BEHAVIORAL_METRICS.MEDIA_STILL_NOT_CONNECTED);
431
+
432
+ sendMetricSpy.restore();
393
433
  });
394
434
 
395
435
  it(`resolves gathering and connection state complete right after`, async () => {