@webex/plugin-meetings 3.11.0 → 3.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/dist/aiEnableRequest/index.js +184 -0
  2. package/dist/aiEnableRequest/index.js.map +1 -0
  3. package/dist/aiEnableRequest/utils.js +36 -0
  4. package/dist/aiEnableRequest/utils.js.map +1 -0
  5. package/dist/annotation/index.js +14 -5
  6. package/dist/annotation/index.js.map +1 -1
  7. package/dist/breakouts/breakout.js +1 -1
  8. package/dist/breakouts/index.js +1 -1
  9. package/dist/config.js +5 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/constants.js +28 -6
  12. package/dist/constants.js.map +1 -1
  13. package/dist/hashTree/constants.js +3 -1
  14. package/dist/hashTree/constants.js.map +1 -1
  15. package/dist/hashTree/hashTree.js +18 -0
  16. package/dist/hashTree/hashTree.js.map +1 -1
  17. package/dist/hashTree/hashTreeParser.js +709 -380
  18. package/dist/hashTree/hashTreeParser.js.map +1 -1
  19. package/dist/hashTree/types.js +4 -2
  20. package/dist/hashTree/types.js.map +1 -1
  21. package/dist/hashTree/utils.js +10 -0
  22. package/dist/hashTree/utils.js.map +1 -1
  23. package/dist/index.js +11 -2
  24. package/dist/index.js.map +1 -1
  25. package/dist/interceptors/constant.js +12 -0
  26. package/dist/interceptors/constant.js.map +1 -0
  27. package/dist/interceptors/dataChannelAuthToken.js +290 -0
  28. package/dist/interceptors/dataChannelAuthToken.js.map +1 -0
  29. package/dist/interceptors/index.js +7 -0
  30. package/dist/interceptors/index.js.map +1 -1
  31. package/dist/interceptors/utils.js +27 -0
  32. package/dist/interceptors/utils.js.map +1 -0
  33. package/dist/interpretation/index.js +2 -2
  34. package/dist/interpretation/index.js.map +1 -1
  35. package/dist/interpretation/siLanguage.js +1 -1
  36. package/dist/locus-info/controlsUtils.js +5 -3
  37. package/dist/locus-info/controlsUtils.js.map +1 -1
  38. package/dist/locus-info/index.js +217 -79
  39. package/dist/locus-info/index.js.map +1 -1
  40. package/dist/locus-info/selfUtils.js +1 -0
  41. package/dist/locus-info/selfUtils.js.map +1 -1
  42. package/dist/locus-info/types.js.map +1 -1
  43. package/dist/media/MediaConnectionAwaiter.js +57 -1
  44. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  45. package/dist/media/properties.js +4 -2
  46. package/dist/media/properties.js.map +1 -1
  47. package/dist/meeting/in-meeting-actions.js +7 -1
  48. package/dist/meeting/in-meeting-actions.js.map +1 -1
  49. package/dist/meeting/index.js +1082 -861
  50. package/dist/meeting/index.js.map +1 -1
  51. package/dist/meeting/request.js +50 -0
  52. package/dist/meeting/request.js.map +1 -1
  53. package/dist/meeting/request.type.js.map +1 -1
  54. package/dist/meeting/util.js +133 -3
  55. package/dist/meeting/util.js.map +1 -1
  56. package/dist/meetings/index.js +100 -45
  57. package/dist/meetings/index.js.map +1 -1
  58. package/dist/member/index.js +10 -0
  59. package/dist/member/index.js.map +1 -1
  60. package/dist/member/util.js +10 -0
  61. package/dist/member/util.js.map +1 -1
  62. package/dist/metrics/constants.js +2 -1
  63. package/dist/metrics/constants.js.map +1 -1
  64. package/dist/multistream/mediaRequestManager.js +9 -60
  65. package/dist/multistream/mediaRequestManager.js.map +1 -1
  66. package/dist/multistream/remoteMediaManager.js +11 -0
  67. package/dist/multistream/remoteMediaManager.js.map +1 -1
  68. package/dist/reachability/index.js +18 -10
  69. package/dist/reachability/index.js.map +1 -1
  70. package/dist/reactions/reactions.type.js.map +1 -1
  71. package/dist/reconnection-manager/index.js +0 -1
  72. package/dist/reconnection-manager/index.js.map +1 -1
  73. package/dist/types/aiEnableRequest/index.d.ts +5 -0
  74. package/dist/types/aiEnableRequest/utils.d.ts +2 -0
  75. package/dist/types/config.d.ts +3 -0
  76. package/dist/types/constants.d.ts +23 -1
  77. package/dist/types/hashTree/constants.d.ts +1 -0
  78. package/dist/types/hashTree/hashTree.d.ts +7 -0
  79. package/dist/types/hashTree/hashTreeParser.d.ts +99 -14
  80. package/dist/types/hashTree/types.d.ts +3 -0
  81. package/dist/types/hashTree/utils.d.ts +6 -0
  82. package/dist/types/index.d.ts +1 -0
  83. package/dist/types/interceptors/constant.d.ts +5 -0
  84. package/dist/types/interceptors/dataChannelAuthToken.d.ts +43 -0
  85. package/dist/types/interceptors/index.d.ts +2 -1
  86. package/dist/types/interceptors/utils.d.ts +1 -0
  87. package/dist/types/locus-info/index.d.ts +21 -2
  88. package/dist/types/locus-info/types.d.ts +1 -0
  89. package/dist/types/media/MediaConnectionAwaiter.d.ts +10 -1
  90. package/dist/types/media/properties.d.ts +2 -1
  91. package/dist/types/meeting/in-meeting-actions.d.ts +6 -0
  92. package/dist/types/meeting/index.d.ts +38 -6
  93. package/dist/types/meeting/request.d.ts +16 -1
  94. package/dist/types/meeting/request.type.d.ts +5 -0
  95. package/dist/types/meeting/util.d.ts +31 -0
  96. package/dist/types/meetings/index.d.ts +4 -2
  97. package/dist/types/member/index.d.ts +1 -0
  98. package/dist/types/member/util.d.ts +5 -0
  99. package/dist/types/metrics/constants.d.ts +1 -0
  100. package/dist/types/multistream/mediaRequestManager.d.ts +0 -23
  101. package/dist/types/reactions/reactions.type.d.ts +1 -0
  102. package/dist/types/webinar/utils.d.ts +6 -0
  103. package/dist/webinar/index.js +260 -90
  104. package/dist/webinar/index.js.map +1 -1
  105. package/dist/webinar/utils.js +25 -0
  106. package/dist/webinar/utils.js.map +1 -0
  107. package/package.json +24 -23
  108. package/src/aiEnableRequest/README.md +84 -0
  109. package/src/aiEnableRequest/index.ts +170 -0
  110. package/src/aiEnableRequest/utils.ts +25 -0
  111. package/src/annotation/index.ts +27 -7
  112. package/src/config.ts +3 -0
  113. package/src/constants.ts +29 -1
  114. package/src/hashTree/constants.ts +1 -0
  115. package/src/hashTree/hashTree.ts +17 -0
  116. package/src/hashTree/hashTreeParser.ts +627 -249
  117. package/src/hashTree/types.ts +4 -0
  118. package/src/hashTree/utils.ts +9 -0
  119. package/src/index.ts +8 -1
  120. package/src/interceptors/constant.ts +6 -0
  121. package/src/interceptors/dataChannelAuthToken.ts +170 -0
  122. package/src/interceptors/index.ts +2 -1
  123. package/src/interceptors/utils.ts +16 -0
  124. package/src/interpretation/index.ts +2 -2
  125. package/src/locus-info/controlsUtils.ts +11 -0
  126. package/src/locus-info/index.ts +231 -61
  127. package/src/locus-info/selfUtils.ts +1 -0
  128. package/src/locus-info/types.ts +1 -0
  129. package/src/media/MediaConnectionAwaiter.ts +41 -1
  130. package/src/media/properties.ts +3 -1
  131. package/src/meeting/in-meeting-actions.ts +12 -0
  132. package/src/meeting/index.ts +205 -44
  133. package/src/meeting/request.ts +42 -0
  134. package/src/meeting/request.type.ts +6 -0
  135. package/src/meeting/util.ts +160 -2
  136. package/src/meetings/index.ts +135 -41
  137. package/src/member/index.ts +10 -0
  138. package/src/member/util.ts +12 -0
  139. package/src/metrics/constants.ts +1 -0
  140. package/src/multistream/mediaRequestManager.ts +4 -54
  141. package/src/multistream/remoteMediaManager.ts +13 -0
  142. package/src/reachability/index.ts +9 -0
  143. package/src/reactions/reactions.type.ts +1 -0
  144. package/src/reconnection-manager/index.ts +0 -1
  145. package/src/webinar/index.ts +162 -5
  146. package/src/webinar/utils.ts +16 -0
  147. package/test/unit/spec/aiEnableRequest/index.ts +981 -0
  148. package/test/unit/spec/aiEnableRequest/utils.ts +130 -0
  149. package/test/unit/spec/annotation/index.ts +69 -7
  150. package/test/unit/spec/hashTree/hashTree.ts +66 -0
  151. package/test/unit/spec/hashTree/hashTreeParser.ts +1869 -189
  152. package/test/unit/spec/interceptors/dataChannelAuthToken.ts +210 -0
  153. package/test/unit/spec/interceptors/utils.ts +75 -0
  154. package/test/unit/spec/locus-info/controlsUtils.js +29 -0
  155. package/test/unit/spec/locus-info/index.js +383 -46
  156. package/test/unit/spec/media/MediaConnectionAwaiter.ts +41 -1
  157. package/test/unit/spec/media/properties.ts +12 -3
  158. package/test/unit/spec/meeting/in-meeting-actions.ts +8 -2
  159. package/test/unit/spec/meeting/index.js +716 -115
  160. package/test/unit/spec/meeting/request.js +70 -0
  161. package/test/unit/spec/meeting/utils.js +438 -26
  162. package/test/unit/spec/meetings/index.js +652 -31
  163. package/test/unit/spec/member/index.js +28 -4
  164. package/test/unit/spec/member/util.js +65 -27
  165. package/test/unit/spec/multistream/mediaRequestManager.ts +2 -85
  166. package/test/unit/spec/multistream/remoteMediaManager.ts +30 -0
  167. package/test/unit/spec/reachability/index.ts +23 -0
  168. package/test/unit/spec/reconnection-manager/index.js +4 -8
  169. package/test/unit/spec/webinar/index.ts +348 -36
  170. package/test/unit/spec/webinar/utils.ts +39 -0
@@ -1,9 +1,11 @@
1
- import {assert, expect} from '@webex/test-helper-chai';
1
+ import {assert} from '@webex/test-helper-chai';
2
2
  import LoggerProxy from '@webex/plugin-meetings/src/common/logs/logger-proxy';
3
3
  import Webinar from '@webex/plugin-meetings/src/webinar';
4
4
  import MockWebex from '@webex/test-helper-mock-webex';
5
5
  import uuid from 'uuid';
6
6
  import sinon from 'sinon';
7
+ import {DataChannelTokenType} from '@webex/internal-plugin-llm';
8
+ import {LLM_PRACTICE_SESSION, SHARE_STATUS} from '@webex/plugin-meetings/src/constants';
7
9
 
8
10
  describe('plugin-meetings', () => {
9
11
  describe('Webinar', () => {
@@ -26,7 +28,19 @@ describe('plugin-meetings', () => {
26
28
  webex.meetings = {};
27
29
  webex.credentials.getUserToken = getUserTokenStub;
28
30
  webex.meetings.getMeetingByType = sinon.stub();
31
+ webex.internal.voicea.announce = sinon.stub();
29
32
 
33
+ webex.internal.llm = {
34
+ getDatachannelToken: sinon.stub().returns(undefined),
35
+ setDatachannelToken: sinon.stub(),
36
+ isConnected: sinon.stub().returns(false),
37
+ disconnectLLM: sinon.stub().resolves(),
38
+ off: sinon.stub(),
39
+ on: sinon.stub(),
40
+ getLocusUrl: sinon.stub().returns('old-locus-url'),
41
+ getDatachannelUrl: sinon.stub().returns('old-dc-url'),
42
+ registerAndConnect: sinon.stub().resolves('REGISTER_AND_CONNECT_RESULT'),
43
+ };
30
44
  });
31
45
 
32
46
  afterEach(() => {
@@ -147,20 +161,216 @@ describe('plugin-meetings', () => {
147
161
  assert.equal(result.isPromoted, false, 'should not indicate promotion');
148
162
  assert.equal(result.isDemoted, false, 'should not indicate demotion');
149
163
  });
164
+
165
+ it('handles missing role payload safely', () => {
166
+ const updateStatusByRoleStub = sinon.stub(webinar, 'updateStatusByRole');
167
+
168
+ const result = webinar.updateRoleChanged(undefined);
169
+
170
+ assert.equal(webinar.selfIsPanelist, false);
171
+ assert.equal(webinar.selfIsAttendee, false);
172
+ assert.equal(webinar.canManageWebcast, false);
173
+ assert.deepEqual(result, {isPromoted: false, isDemoted: false});
174
+ assert.calledOnceWithExactly(updateStatusByRoleStub, {isPromoted: false, isDemoted: false});
175
+ });
176
+ });
177
+
178
+ describe('#cleanUp', () => {
179
+ it('delegates to cleanupPSDataChannel', () => {
180
+ const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();
181
+
182
+ webinar.cleanUp();
183
+
184
+ assert.calledOnceWithExactly(cleanupPSDataChannelStub);
185
+ });
186
+ });
187
+
188
+ describe('#cleanupPSDataChannel', () => {
189
+ let meeting;
190
+
191
+ beforeEach(() => {
192
+ meeting = {
193
+ processRelayEvent: sinon.stub(),
194
+ };
195
+
196
+ webex.meetings.getMeetingByType = sinon.stub().returns(meeting);
197
+ });
198
+
199
+ it('disconnects the practice session channel and removes the relay listener', async () => {
200
+ await webinar.cleanupPSDataChannel();
201
+
202
+ assert.calledOnceWithExactly(
203
+ webex.internal.llm.disconnectLLM,
204
+ {code: 3050, reason: 'done (permanent)'},
205
+ LLM_PRACTICE_SESSION
206
+ );
207
+ assert.calledOnceWithExactly(
208
+ webex.internal.llm.off,
209
+ `event:relay.event:${LLM_PRACTICE_SESSION}`,
210
+ meeting.processRelayEvent
211
+ );
212
+ });
213
+ });
214
+
215
+ describe('#updatePSDataChannel', () => {
216
+ let meeting;
217
+ let processRelayEvent;
218
+
219
+ beforeEach(() => {
220
+ processRelayEvent = sinon.stub();
221
+ meeting = {
222
+ isJoined: sinon.stub().returns(true),
223
+ processRelayEvent,
224
+ locusInfo: {
225
+ url: 'locus-url',
226
+ info: {practiceSessionDatachannelUrl: 'dc-url'},
227
+ self: {practiceSessionDatachannelToken: 'ps-token'},
228
+ },
229
+ };
230
+
231
+ webex.meetings.getMeetingByType = sinon.stub().returns(meeting);
232
+
233
+ // Ensure connect path is eligible
234
+ webinar.selfIsPanelist = true;
235
+ webinar.practiceSessionEnabled = true;
236
+ webex.internal.voicea.getIsCaptionBoxOn = sinon.stub().returns(false);
237
+ webex.internal.voicea.updateSubchannelSubscriptions = sinon.stub();
238
+ });
239
+
240
+ it('no-ops when practice session join eligibility is false', async () => {
241
+ webinar.practiceSessionEnabled = false;
242
+ const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();
243
+
244
+ const result = await webinar.updatePSDataChannel();
245
+
246
+ assert.isUndefined(result);
247
+ assert.calledOnceWithExactly(cleanupPSDataChannelStub);
248
+ assert.notCalled(webex.internal.llm.registerAndConnect);
249
+ });
250
+
251
+ it('no-ops when meeting is not joined', async () => {
252
+ meeting.isJoined.returns(false);
253
+ const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();
254
+
255
+ const result = await webinar.updatePSDataChannel();
256
+
257
+ assert.isUndefined(result);
258
+ assert.calledOnceWithExactly(cleanupPSDataChannelStub);
259
+ assert.notCalled(webex.internal.llm.registerAndConnect);
260
+ });
261
+
262
+ it('no-ops when practiceSessionDatachannelUrl is missing', async () => {
263
+ meeting.locusInfo.info.practiceSessionDatachannelUrl = undefined;
264
+
265
+ const result = await webinar.updatePSDataChannel();
266
+
267
+ assert.isUndefined(result);
268
+ assert.notCalled(webex.internal.llm.registerAndConnect);
269
+ });
270
+
271
+ it('no-ops when already connected to the same endpoints', async () => {
272
+ webex.internal.llm.isConnected.returns(true);
273
+ webex.internal.llm.getLocusUrl.returns('locus-url');
274
+ webex.internal.llm.getDatachannelUrl.returns('dc-url');
275
+ const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();
276
+
277
+ const result = await webinar.updatePSDataChannel();
278
+
279
+ assert.isUndefined(result);
280
+ assert.notCalled(cleanupPSDataChannelStub);
281
+ assert.notCalled(webex.internal.llm.registerAndConnect);
282
+ });
283
+
284
+ it('connects when eligible', async () => {
285
+ const result = await webinar.updatePSDataChannel();
286
+
287
+ assert.calledOnceWithExactly(
288
+ webex.internal.llm.setDatachannelToken,
289
+ 'ps-token',
290
+ DataChannelTokenType.PracticeSession
291
+ );
292
+ assert.calledOnce(webex.internal.llm.registerAndConnect);
293
+ assert.calledWith(
294
+ webex.internal.llm.registerAndConnect,
295
+ 'locus-url',
296
+ 'dc-url',
297
+ 'ps-token',
298
+ LLM_PRACTICE_SESSION
299
+ );
300
+ assert.calledOnceWithExactly(webex.internal.voicea.announce);
301
+ assert.equal(result, 'REGISTER_AND_CONNECT_RESULT');
302
+ });
303
+
304
+ it('uses cached token when available', async () => {
305
+ webex.internal.llm.getDatachannelToken.returns('cached-token');
306
+
307
+ await webinar.updatePSDataChannel();
308
+
309
+ assert.calledWithExactly(
310
+ webex.internal.llm.getDatachannelToken,
311
+ DataChannelTokenType.PracticeSession
312
+ );
313
+ assert.notCalled(webex.internal.llm.setDatachannelToken);
314
+ assert.calledWith(
315
+ webex.internal.llm.registerAndConnect,
316
+ 'locus-url',
317
+ 'dc-url',
318
+ 'cached-token',
319
+ LLM_PRACTICE_SESSION
320
+ );
321
+ });
322
+
323
+ it('cleans up the existing practice session channel before reconnecting to new endpoints', async () => {
324
+ webex.internal.llm.isConnected.returns(true);
325
+ const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();
326
+
327
+ await webinar.updatePSDataChannel();
328
+
329
+ assert.calledOnceWithExactly(cleanupPSDataChannelStub);
330
+ assert.calledOnce(webex.internal.llm.registerAndConnect);
331
+ });
332
+
333
+ it('rebinds relay listener after successful connect', async () => {
334
+ await webinar.updatePSDataChannel();
335
+
336
+ assert.calledWith(
337
+ webex.internal.llm.off,
338
+ `event:relay.event:${LLM_PRACTICE_SESSION}`,
339
+ processRelayEvent
340
+ );
341
+ assert.calledWith(
342
+ webex.internal.llm.on,
343
+ `event:relay.event:${LLM_PRACTICE_SESSION}`,
344
+ processRelayEvent
345
+ );
346
+ });
347
+
348
+ it('subscribes to transcription when caption intent is enabled', async () => {
349
+ webex.internal.voicea.getIsCaptionBoxOn = sinon.stub().returns(true);
350
+
351
+ await webinar.updatePSDataChannel();
352
+
353
+ assert.calledOnceWithExactly(webex.internal.voicea.updateSubchannelSubscriptions, { subscribe: ['transcription'] });
354
+ });
355
+
356
+ it('does not subscribe to transcription when caption intent is disabled', async () => {
357
+ webex.internal.voicea.getIsCaptionBoxOn = sinon.stub().returns(false);
358
+
359
+ await webinar.updatePSDataChannel();
360
+
361
+ assert.notCalled(webex.internal.voicea.updateSubchannelSubscriptions);
362
+ });
150
363
  });
151
364
 
152
365
  describe('#updateStatusByRole', () => {
153
- let updateLLMConnection;
154
366
  let updateMediaShares;
155
367
  beforeEach(() => {
156
- // @ts-ignore
157
- updateLLMConnection = sinon.stub();
158
368
  updateMediaShares = sinon.stub()
159
369
  webinar.webex.meetings = {
160
370
  getMeetingByType: sinon.stub().returns({
161
371
  id: 'meeting-id',
162
- updateLLMConnection: updateLLMConnection,
163
- shareStatus: 'whiteboard_share_active',
372
+ updateLLMConnection: sinon.stub(),
373
+ shareStatus: SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE,
164
374
  locusInfo: {
165
375
  mediaShares: 'mediaShares',
166
376
  updateMediaShares: updateMediaShares
@@ -173,40 +383,20 @@ describe('plugin-meetings', () => {
173
383
  sinon.restore();
174
384
  });
175
385
 
176
- it('trigger updateLLMConnection if PS started', () => {
177
-
178
- webinar.practiceSessionEnabled = true;
179
- const roleChange = {isPromoted: true, isDemoted: false};
180
-
181
- const result = webinar.updateStatusByRole(roleChange);
182
-
183
- assert.calledOnce(updateLLMConnection);
184
- });
185
-
186
- it('Not trigger updateLLMConnection if PS not started', () => {
187
-
188
- webinar.practiceSessionEnabled = false;
189
- const roleChange = {isPromoted: true, isDemoted: false};
190
-
191
- const result = webinar.updateStatusByRole(roleChange);
192
-
193
- assert.notCalled(updateLLMConnection);
194
- });
195
-
196
386
  it('trigger updateMediaShares if promoted', () => {
197
387
 
198
388
  const roleChange = {isPromoted: true, isDemoted: false};
199
389
 
200
- const result = webinar.updateStatusByRole(roleChange);
390
+ webinar.updateStatusByRole(roleChange);
201
391
 
202
- assert.calledOnce(updateMediaShares);
392
+ assert.calledOnceWithExactly(updateMediaShares, 'mediaShares', true);
203
393
  });
204
394
 
205
395
  it('Not trigger updateMediaShares if no role change', () => {
206
396
 
207
397
  const roleChange = {isPromoted: false, isDemoted: false};
208
398
 
209
- const result = webinar.updateStatusByRole(roleChange);
399
+ webinar.updateStatusByRole(roleChange);
210
400
 
211
401
  assert.notCalled(updateMediaShares);
212
402
  });
@@ -214,18 +404,18 @@ describe('plugin-meetings', () => {
214
404
 
215
405
  const roleChange = {isPromoted: true, isDemoted: false};
216
406
 
217
- const result = webinar.updateStatusByRole(roleChange);
407
+ webinar.updateStatusByRole(roleChange);
218
408
 
219
- assert.calledOnce(updateMediaShares);
409
+ assert.calledOnceWithExactly(updateMediaShares, 'mediaShares', true);
220
410
  });
221
411
 
222
412
  it('trigger updateMediaShares if is attendee with whiteboard share', () => {
223
413
 
224
414
  const roleChange = {isPromoted: false, isDemoted: true};
225
415
 
226
- const result = webinar.updateStatusByRole(roleChange);
416
+ webinar.updateStatusByRole(roleChange);
227
417
 
228
- assert.calledOnce(updateMediaShares);
418
+ assert.calledOnceWithExactly(updateMediaShares, 'mediaShares', true);
229
419
  });
230
420
 
231
421
  it('Not trigger updateMediaShares if is attendee with screen share', () => {
@@ -233,8 +423,8 @@ describe('plugin-meetings', () => {
233
423
  webinar.webex.meetings = {
234
424
  getMeetingByType: sinon.stub().returns({
235
425
  id: 'meeting-id',
236
- updateLLMConnection: updateLLMConnection,
237
- shareStatus: 'remote_share_active',
426
+ updateLLMConnection: sinon.stub(),
427
+ shareStatus: SHARE_STATUS.REMOTE_SHARE_ACTIVE,
238
428
  locusInfo: {
239
429
  mediaShares: 'mediaShares',
240
430
  updateMediaShares: updateMediaShares
@@ -244,10 +434,18 @@ describe('plugin-meetings', () => {
244
434
 
245
435
  const roleChange = {isPromoted: false, isDemoted: true};
246
436
 
247
- const result = webinar.updateStatusByRole(roleChange);
437
+ webinar.updateStatusByRole(roleChange);
248
438
 
249
439
  assert.notCalled(updateMediaShares);
250
440
  });
441
+
442
+ it('updates PS data channel based on join eligibility', () => {
443
+ const updatePSDataChannelStub = sinon.stub(webinar, 'updatePSDataChannel').resolves();
444
+
445
+ webinar.updateStatusByRole({isPromoted: false, isDemoted: false});
446
+
447
+ assert.calledOnceWithExactly(updatePSDataChannelStub);
448
+ });
251
449
  });
252
450
 
253
451
  describe("#setPracticeSessionState", () => {
@@ -323,6 +521,14 @@ describe('plugin-meetings', () => {
323
521
 
324
522
  assert.equal(webinar.practiceSessionEnabled, false);
325
523
  });
524
+ it('triggers PS data channel update using computed eligibility', () => {
525
+ webinar.selfIsPanelist = true;
526
+ const updatePSDataChannelStub = sinon.stub(webinar, 'updatePSDataChannel').resolves();
527
+
528
+ webinar.updatePracticeSessionStatus({enabled: true});
529
+
530
+ assert.calledOnceWithExactly(updatePSDataChannelStub);
531
+ });
326
532
  });
327
533
 
328
534
  describe("#startWebcast", () => {
@@ -631,5 +837,111 @@ describe('plugin-meetings', () => {
631
837
  }
632
838
  });
633
839
  });
840
+
841
+ describe("#searchLargeScaleWebinarAttendees", () => {
842
+ const attendeeSearchUrl = 'https://locusUrl/attendees/search';
843
+ const params = {
844
+ queryString: 'queryString',
845
+ limit: 50,
846
+ next: null,
847
+ };
848
+ beforeEach(() => {
849
+ // @ts-ignore
850
+ webinar.webex.meetings = {
851
+ getMeetingByType: sinon.stub().returns({
852
+ id: 'meeting-id',
853
+ locusInfo: {
854
+ links:{
855
+ resources: {
856
+ attendeeSearch: {
857
+ url: attendeeSearchUrl
858
+ }
859
+ }
860
+ }
861
+ }
862
+ })
863
+ };
864
+ });
865
+
866
+ it('throws an error if attendeeSearchUrl is not available', async () => {
867
+ webinar.webex.meetings = {
868
+ getMeetingByType: sinon.stub().returns({
869
+ id: 'meeting-id',
870
+ locusInfo: {
871
+ links:{
872
+ resources: {
873
+ attendeeSearch: {
874
+ url: null
875
+ }
876
+ }
877
+ }
878
+ }
879
+ })
880
+ };
881
+ try {
882
+ await webinar.searchLargeScaleWebinarAttendees(params);
883
+ assert.fail('searchLargeScaleWebinarAttendees should throw an error');
884
+ } catch (error) {
885
+ assert.equal(error.message,'Meeting:webinar5k#Attendee search url is not available', 'should throw the correct error');
886
+ }
887
+ });
888
+
889
+ it('sends a GET request to search the large scale webinar attendees', async () => {
890
+ const result = await webinar.searchLargeScaleWebinarAttendees(params);
891
+ assert.calledOnce(webex.request);
892
+ assert.calledWith(webex.request, {
893
+ method: 'GET',
894
+ uri: `${attendeeSearchUrl}?search_text=${encodeURIComponent(params.queryString)}&limit=50`,
895
+ headers: {
896
+ authorization: 'test-token',
897
+ trackingId: 'webex-js-sdk_test-uuid',
898
+ },
899
+ });
900
+ assert.equal(
901
+ result,
902
+ 'REQUEST_RETURN_VALUE',
903
+ 'should return the resolved value from the request'
904
+ );
905
+ });
906
+
907
+ it('queryString is empty string', async () => {
908
+ params.queryString = '';
909
+ const result = await webinar.searchLargeScaleWebinarAttendees(params);
910
+ assert.calledOnce(webex.request);
911
+ assert.calledWith(webex.request, {
912
+ method: 'GET',
913
+ uri: `${attendeeSearchUrl}?limit=50`,
914
+ headers: {
915
+ authorization: 'test-token',
916
+ trackingId: 'webex-js-sdk_test-uuid',
917
+ },
918
+ });
919
+ assert.equal(
920
+ result,
921
+ 'REQUEST_RETURN_VALUE',
922
+ 'should return the resolved value from the request'
923
+ );
924
+ });
925
+
926
+ it('handles API call failures gracefully', async () => {
927
+ webex.request.rejects(new Error('API_ERROR'));
928
+ const errorLogger = sinon.stub(LoggerProxy.logger, 'error');
929
+
930
+ try {
931
+ await webinar.searchLargeScaleWebinarAttendees(params);
932
+ assert.fail('searchLargeScaleWebinarAttendees should throw an error');
933
+ } catch (error) {
934
+ assert.equal(error.message, 'API_ERROR', 'should throw the correct error');
935
+ assert.calledOnce(errorLogger);
936
+ assert.calledWith(
937
+ errorLogger,
938
+ 'Meeting:webinar5k#searchLargeScaleWebinarAttendees failed',
939
+ sinon.match.instanceOf(Error)
940
+ );
941
+ } finally {
942
+ errorLogger.restore();
943
+ }
944
+ });
945
+ });
634
946
  })
635
947
  })
@@ -0,0 +1,39 @@
1
+ import chai from 'chai';
2
+ import {sanitizeParams} from '@webex/plugin-meetings/src/webinar/utils';
3
+
4
+ const {assert} = chai;
5
+
6
+ describe('plugin-meetings', () => {
7
+ describe('webinar utils', () => {
8
+ describe('#sanitizeParams', () => {
9
+ it('sanitizes params by removing undefined, "", or null values', () => {
10
+ const input = {
11
+ a: 1,
12
+ b: undefined,
13
+ c: null,
14
+ d: 'test',
15
+ e: false,
16
+ f: '',
17
+ };
18
+ const expectedOutput = {
19
+ a: 1,
20
+ d: 'test',
21
+ e: false,
22
+ };
23
+ const result = sanitizeParams(input);
24
+ assert.deepEqual(result, expectedOutput);
25
+ });
26
+
27
+ it('returns an empty object when all values are invalid', () => {
28
+ const input = {
29
+ a: undefined,
30
+ b: null,
31
+ c: '',
32
+ };
33
+ const expectedOutput = {};
34
+ const result = sanitizeParams(input);
35
+ assert.deepEqual(result, expectedOutput);
36
+ });
37
+ });
38
+ });
39
+ });