@webex/plugin-meetings 3.11.0-next.43 → 3.11.0-next.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,198 @@ 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
+ });
237
+
238
+ it('no-ops when practice session join eligibility is false', async () => {
239
+ webinar.practiceSessionEnabled = false;
240
+ const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();
241
+
242
+ const result = await webinar.updatePSDataChannel();
243
+
244
+ assert.isUndefined(result);
245
+ assert.calledOnceWithExactly(cleanupPSDataChannelStub);
246
+ assert.notCalled(webex.internal.llm.registerAndConnect);
247
+ });
248
+
249
+ it('no-ops when meeting is not joined', async () => {
250
+ meeting.isJoined.returns(false);
251
+ const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();
252
+
253
+ const result = await webinar.updatePSDataChannel();
254
+
255
+ assert.isUndefined(result);
256
+ assert.calledOnceWithExactly(cleanupPSDataChannelStub);
257
+ assert.notCalled(webex.internal.llm.registerAndConnect);
258
+ });
259
+
260
+ it('no-ops when practiceSessionDatachannelUrl is missing', async () => {
261
+ meeting.locusInfo.info.practiceSessionDatachannelUrl = undefined;
262
+
263
+ const result = await webinar.updatePSDataChannel();
264
+
265
+ assert.isUndefined(result);
266
+ assert.notCalled(webex.internal.llm.registerAndConnect);
267
+ });
268
+
269
+ it('no-ops when already connected to the same endpoints', async () => {
270
+ webex.internal.llm.isConnected.returns(true);
271
+ webex.internal.llm.getLocusUrl.returns('locus-url');
272
+ webex.internal.llm.getDatachannelUrl.returns('dc-url');
273
+ const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();
274
+
275
+ const result = await webinar.updatePSDataChannel();
276
+
277
+ assert.isUndefined(result);
278
+ assert.notCalled(cleanupPSDataChannelStub);
279
+ assert.notCalled(webex.internal.llm.registerAndConnect);
280
+ });
281
+
282
+ it('connects when eligible', async () => {
283
+ const result = await webinar.updatePSDataChannel();
284
+
285
+ assert.calledOnceWithExactly(
286
+ webex.internal.llm.setDatachannelToken,
287
+ 'ps-token',
288
+ DataChannelTokenType.PracticeSession
289
+ );
290
+ assert.calledOnce(webex.internal.llm.registerAndConnect);
291
+ assert.calledWith(
292
+ webex.internal.llm.registerAndConnect,
293
+ 'locus-url',
294
+ 'dc-url',
295
+ 'ps-token',
296
+ LLM_PRACTICE_SESSION
297
+ );
298
+ assert.calledOnceWithExactly(webex.internal.voicea.announce);
299
+ assert.equal(result, 'REGISTER_AND_CONNECT_RESULT');
300
+ });
301
+
302
+ it('uses cached token when available', async () => {
303
+ webex.internal.llm.getDatachannelToken.returns('cached-token');
304
+
305
+ await webinar.updatePSDataChannel();
306
+
307
+ assert.calledWithExactly(
308
+ webex.internal.llm.getDatachannelToken,
309
+ DataChannelTokenType.PracticeSession
310
+ );
311
+ assert.notCalled(webex.internal.llm.setDatachannelToken);
312
+ assert.calledWith(
313
+ webex.internal.llm.registerAndConnect,
314
+ 'locus-url',
315
+ 'dc-url',
316
+ 'cached-token',
317
+ LLM_PRACTICE_SESSION
318
+ );
319
+ });
320
+
321
+ it('cleans up the existing practice session channel before reconnecting to new endpoints', async () => {
322
+ webex.internal.llm.isConnected.returns(true);
323
+ const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();
324
+
325
+ await webinar.updatePSDataChannel();
326
+
327
+ assert.calledOnceWithExactly(cleanupPSDataChannelStub);
328
+ assert.calledOnce(webex.internal.llm.registerAndConnect);
329
+ });
330
+
331
+ it('rebinds relay listener after successful connect', async () => {
332
+ await webinar.updatePSDataChannel();
333
+
334
+ assert.calledWith(
335
+ webex.internal.llm.off,
336
+ `event:relay.event:${LLM_PRACTICE_SESSION}`,
337
+ processRelayEvent
338
+ );
339
+ assert.calledWith(
340
+ webex.internal.llm.on,
341
+ `event:relay.event:${LLM_PRACTICE_SESSION}`,
342
+ processRelayEvent
343
+ );
344
+ });
150
345
  });
151
346
 
152
347
  describe('#updateStatusByRole', () => {
153
- let updateLLMConnection;
154
348
  let updateMediaShares;
155
349
  beforeEach(() => {
156
- // @ts-ignore
157
- updateLLMConnection = sinon.stub();
158
350
  updateMediaShares = sinon.stub()
159
351
  webinar.webex.meetings = {
160
352
  getMeetingByType: sinon.stub().returns({
161
353
  id: 'meeting-id',
162
- updateLLMConnection: updateLLMConnection,
163
- shareStatus: 'whiteboard_share_active',
354
+ updateLLMConnection: sinon.stub(),
355
+ shareStatus: SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE,
164
356
  locusInfo: {
165
357
  mediaShares: 'mediaShares',
166
358
  updateMediaShares: updateMediaShares
@@ -173,40 +365,20 @@ describe('plugin-meetings', () => {
173
365
  sinon.restore();
174
366
  });
175
367
 
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
368
  it('trigger updateMediaShares if promoted', () => {
197
369
 
198
370
  const roleChange = {isPromoted: true, isDemoted: false};
199
371
 
200
- const result = webinar.updateStatusByRole(roleChange);
372
+ webinar.updateStatusByRole(roleChange);
201
373
 
202
- assert.calledOnce(updateMediaShares);
374
+ assert.calledOnceWithExactly(updateMediaShares, 'mediaShares', true);
203
375
  });
204
376
 
205
377
  it('Not trigger updateMediaShares if no role change', () => {
206
378
 
207
379
  const roleChange = {isPromoted: false, isDemoted: false};
208
380
 
209
- const result = webinar.updateStatusByRole(roleChange);
381
+ webinar.updateStatusByRole(roleChange);
210
382
 
211
383
  assert.notCalled(updateMediaShares);
212
384
  });
@@ -214,18 +386,18 @@ describe('plugin-meetings', () => {
214
386
 
215
387
  const roleChange = {isPromoted: true, isDemoted: false};
216
388
 
217
- const result = webinar.updateStatusByRole(roleChange);
389
+ webinar.updateStatusByRole(roleChange);
218
390
 
219
- assert.calledOnce(updateMediaShares);
391
+ assert.calledOnceWithExactly(updateMediaShares, 'mediaShares', true);
220
392
  });
221
393
 
222
394
  it('trigger updateMediaShares if is attendee with whiteboard share', () => {
223
395
 
224
396
  const roleChange = {isPromoted: false, isDemoted: true};
225
397
 
226
- const result = webinar.updateStatusByRole(roleChange);
398
+ webinar.updateStatusByRole(roleChange);
227
399
 
228
- assert.calledOnce(updateMediaShares);
400
+ assert.calledOnceWithExactly(updateMediaShares, 'mediaShares', true);
229
401
  });
230
402
 
231
403
  it('Not trigger updateMediaShares if is attendee with screen share', () => {
@@ -233,8 +405,8 @@ describe('plugin-meetings', () => {
233
405
  webinar.webex.meetings = {
234
406
  getMeetingByType: sinon.stub().returns({
235
407
  id: 'meeting-id',
236
- updateLLMConnection: updateLLMConnection,
237
- shareStatus: 'remote_share_active',
408
+ updateLLMConnection: sinon.stub(),
409
+ shareStatus: SHARE_STATUS.REMOTE_SHARE_ACTIVE,
238
410
  locusInfo: {
239
411
  mediaShares: 'mediaShares',
240
412
  updateMediaShares: updateMediaShares
@@ -244,10 +416,18 @@ describe('plugin-meetings', () => {
244
416
 
245
417
  const roleChange = {isPromoted: false, isDemoted: true};
246
418
 
247
- const result = webinar.updateStatusByRole(roleChange);
419
+ webinar.updateStatusByRole(roleChange);
248
420
 
249
421
  assert.notCalled(updateMediaShares);
250
422
  });
423
+
424
+ it('updates PS data channel based on join eligibility', () => {
425
+ const updatePSDataChannelStub = sinon.stub(webinar, 'updatePSDataChannel').resolves();
426
+
427
+ webinar.updateStatusByRole({isPromoted: false, isDemoted: false});
428
+
429
+ assert.calledOnceWithExactly(updatePSDataChannelStub);
430
+ });
251
431
  });
252
432
 
253
433
  describe("#setPracticeSessionState", () => {
@@ -323,6 +503,14 @@ describe('plugin-meetings', () => {
323
503
 
324
504
  assert.equal(webinar.practiceSessionEnabled, false);
325
505
  });
506
+ it('triggers PS data channel update using computed eligibility', () => {
507
+ webinar.selfIsPanelist = true;
508
+ const updatePSDataChannelStub = sinon.stub(webinar, 'updatePSDataChannel').resolves();
509
+
510
+ webinar.updatePracticeSessionStatus({enabled: true});
511
+
512
+ assert.calledOnceWithExactly(updatePSDataChannelStub);
513
+ });
326
514
  });
327
515
 
328
516
  describe("#startWebcast", () => {