@webex/plugin-meetings 3.8.1-web-workers-keepalive.1 → 3.9.0-next.1

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 (87) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.js +24 -2
  4. package/dist/constants.js.map +1 -1
  5. package/dist/interpretation/index.js +1 -1
  6. package/dist/interpretation/siLanguage.js +1 -1
  7. package/dist/locus-info/index.js +39 -85
  8. package/dist/locus-info/index.js.map +1 -1
  9. package/dist/meeting/brbState.js +14 -12
  10. package/dist/meeting/brbState.js.map +1 -1
  11. package/dist/meeting/in-meeting-actions.js +6 -0
  12. package/dist/meeting/in-meeting-actions.js.map +1 -1
  13. package/dist/meeting/index.js +274 -140
  14. package/dist/meeting/index.js.map +1 -1
  15. package/dist/meeting/request.js +19 -0
  16. package/dist/meeting/request.js.map +1 -1
  17. package/dist/meeting/request.type.js.map +1 -1
  18. package/dist/meeting/type.js +7 -0
  19. package/dist/meeting/type.js.map +1 -0
  20. package/dist/meeting/util.js +68 -2
  21. package/dist/meeting/util.js.map +1 -1
  22. package/dist/meetings/index.js +35 -33
  23. package/dist/meetings/index.js.map +1 -1
  24. package/dist/member/index.js.map +1 -1
  25. package/dist/members/index.js +11 -9
  26. package/dist/members/index.js.map +1 -1
  27. package/dist/members/request.js +3 -3
  28. package/dist/members/request.js.map +1 -1
  29. package/dist/members/util.js +18 -6
  30. package/dist/members/util.js.map +1 -1
  31. package/dist/multistream/mediaRequestManager.js +1 -1
  32. package/dist/multistream/mediaRequestManager.js.map +1 -1
  33. package/dist/multistream/remoteMedia.js +34 -5
  34. package/dist/multistream/remoteMedia.js.map +1 -1
  35. package/dist/multistream/remoteMediaGroup.js +42 -2
  36. package/dist/multistream/remoteMediaGroup.js.map +1 -1
  37. package/dist/multistream/sendSlotManager.js +32 -2
  38. package/dist/multistream/sendSlotManager.js.map +1 -1
  39. package/dist/types/constants.d.ts +22 -0
  40. package/dist/types/locus-info/index.d.ts +0 -9
  41. package/dist/types/meeting/brbState.d.ts +0 -1
  42. package/dist/types/meeting/in-meeting-actions.d.ts +6 -0
  43. package/dist/types/meeting/index.d.ts +40 -19
  44. package/dist/types/meeting/request.d.ts +9 -1
  45. package/dist/types/meeting/request.type.d.ts +74 -0
  46. package/dist/types/meeting/type.d.ts +9 -0
  47. package/dist/types/meeting/util.d.ts +3 -0
  48. package/dist/types/members/index.d.ts +10 -7
  49. package/dist/types/members/request.d.ts +1 -1
  50. package/dist/types/members/util.d.ts +7 -3
  51. package/dist/types/multistream/remoteMedia.d.ts +20 -1
  52. package/dist/types/multistream/remoteMediaGroup.d.ts +11 -0
  53. package/dist/types/multistream/sendSlotManager.d.ts +16 -0
  54. package/dist/webinar/index.js +1 -1
  55. package/package.json +22 -23
  56. package/src/constants.ts +23 -2
  57. package/src/locus-info/index.ts +48 -86
  58. package/src/meeting/brbState.ts +9 -7
  59. package/src/meeting/in-meeting-actions.ts +13 -0
  60. package/src/meeting/index.ts +165 -38
  61. package/src/meeting/request.ts +16 -0
  62. package/src/meeting/request.type.ts +64 -0
  63. package/src/meeting/type.ts +9 -0
  64. package/src/meeting/util.ts +73 -2
  65. package/src/meetings/index.ts +3 -2
  66. package/src/member/index.ts +1 -0
  67. package/src/members/index.ts +13 -10
  68. package/src/members/request.ts +2 -2
  69. package/src/members/util.ts +16 -4
  70. package/src/multistream/mediaRequestManager.ts +7 -7
  71. package/src/multistream/remoteMedia.ts +34 -4
  72. package/src/multistream/remoteMediaGroup.ts +37 -2
  73. package/src/multistream/sendSlotManager.ts +34 -2
  74. package/test/unit/spec/locus-info/index.js +199 -83
  75. package/test/unit/spec/meeting/brbState.ts +9 -9
  76. package/test/unit/spec/meeting/in-meeting-actions.ts +6 -0
  77. package/test/unit/spec/meeting/index.js +729 -80
  78. package/test/unit/spec/meeting/request.js +71 -0
  79. package/test/unit/spec/meeting/utils.js +122 -1
  80. package/test/unit/spec/meetings/index.js +2 -0
  81. package/test/unit/spec/members/index.js +68 -9
  82. package/test/unit/spec/members/request.js +2 -2
  83. package/test/unit/spec/members/utils.js +27 -7
  84. package/test/unit/spec/multistream/mediaRequestManager.ts +19 -6
  85. package/test/unit/spec/multistream/remoteMedia.ts +66 -2
  86. package/test/unit/spec/multistream/sendSlotManager.ts +59 -0
  87. package/test/unit/spec/reachability/index.ts +3 -1
@@ -56,6 +56,7 @@ import * as MeetingRequestImport from '@webex/plugin-meetings/src/meeting/reques
56
56
  import LocusInfo from '@webex/plugin-meetings/src/locus-info';
57
57
  import MediaProperties from '@webex/plugin-meetings/src/media/properties';
58
58
  import MeetingUtil from '@webex/plugin-meetings/src/meeting/util';
59
+ import MembersUtil from '@webex/plugin-meetings/src/members/util';
59
60
  import MeetingsUtil from '@webex/plugin-meetings/src/meetings/util';
60
61
  import Media from '@webex/plugin-meetings/src/media/index';
61
62
  import ReconnectionManager from '@webex/plugin-meetings/src/reconnection-manager';
@@ -244,6 +245,7 @@ describe('plugin-meetings', () => {
244
245
  });
245
246
 
246
247
  webex.internal.newMetrics.callDiagnosticMetrics.clearErrorCache = sinon.stub();
248
+ webex.internal.newMetrics.callDiagnosticMetrics.clearEventLimitsForCorrelationId = sinon.stub();
247
249
  webex.internal.support.submitLogs = sinon.stub().returns(Promise.resolve());
248
250
  webex.internal.services = {get: sinon.stub().returns('locus-url')};
249
251
  webex.credentials.getOrgId = sinon.stub().returns('fake-org-id');
@@ -368,6 +370,35 @@ describe('plugin-meetings', () => {
368
370
  assert.instanceOf(meeting.simultaneousInterpretation, SimultaneousInterpretation);
369
371
  assert.instanceOf(meeting.webinar, Webinar);
370
372
  });
373
+
374
+ it('should call the callback with the meeting that has id already set', () => {
375
+ let meetingIdFromCallback;
376
+ // check that the meeting id is already set correctly at the time when the callback is called
377
+ const meetingCreationCallback = sinon.stub().callsFake((meeting) => {
378
+ meetingIdFromCallback = meeting.id;
379
+ });
380
+
381
+ meeting = new Meeting(
382
+ {
383
+ userId: uuid1,
384
+ resource: uuid2,
385
+ deviceUrl: uuid3,
386
+ locus: {url: url1},
387
+ destination: testDestination,
388
+ destinationType: DESTINATION_TYPE.MEETING_ID,
389
+ correlationId,
390
+ selfId: uuid1,
391
+ },
392
+ {
393
+ parent: webex,
394
+ },
395
+ meetingCreationCallback
396
+ );
397
+ assert.exists(meeting.id);
398
+ assert.calledOnceWithExactly(meetingCreationCallback, meeting);
399
+ assert.equal(meeting.id, meetingIdFromCallback);
400
+ });
401
+
371
402
  it('creates MediaRequestManager instances', () => {
372
403
  assert.instanceOf(meeting.mediaRequestManagers.audio, MediaRequestManager);
373
404
  assert.instanceOf(meeting.mediaRequestManagers.video, MediaRequestManager);
@@ -454,6 +485,18 @@ describe('plugin-meetings', () => {
454
485
  });
455
486
  });
456
487
 
488
+ it('pstnCorrelationId getter/setter should work correctly', () => {
489
+ const testPstnCorrelationId = uuid.v4();
490
+
491
+ meeting.pstnCorrelationId = testPstnCorrelationId;
492
+ assert.equal(meeting.pstnCorrelationId, testPstnCorrelationId);
493
+ assert.equal(meeting.callStateForMetrics.pstnCorrelationId, testPstnCorrelationId);
494
+
495
+ meeting.pstnCorrelationId = undefined;
496
+ assert.equal(meeting.pstnCorrelationId, undefined);
497
+ assert.equal(meeting.callStateForMetrics.pstnCorrelationId, undefined);
498
+ });
499
+
457
500
  describe('creates ReceiveSlot manager instance', () => {
458
501
  let mockReceiveSlotManagerCtor;
459
502
  let providedCreateSlotCallback;
@@ -581,7 +624,6 @@ describe('plugin-meetings', () => {
581
624
  assert.isFalse(meeting.isLocusCall());
582
625
  });
583
626
  });
584
-
585
627
  describe('#invite', () => {
586
628
  it('should have #invite', () => {
587
629
  assert.exists(meeting.invite);
@@ -592,8 +634,6 @@ describe('plugin-meetings', () => {
592
634
  it('should proxy members #addMember and return a promise', async () => {
593
635
  const invite = meeting.invite(uuid1, false);
594
636
 
595
- assert.exists(invite.then);
596
- await invite;
597
637
  assert.calledOnce(meeting.members.addMember);
598
638
  assert.calledWith(meeting.members.addMember, uuid1, false);
599
639
  });
@@ -614,20 +654,20 @@ describe('plugin-meetings', () => {
614
654
  assert.calledWith(meeting.members.cancelPhoneInvite, uuid1);
615
655
  });
616
656
  });
617
- describe('#cancelSIPInvite', () => {
618
- it('should have #cancelSIPInvite', () => {
619
- assert.exists(meeting.cancelSIPInvite);
657
+ describe('#cancelInviteByMemberId', () => {
658
+ it('should have #cancelInviteByMemberId', () => {
659
+ assert.exists(meeting.cancelInviteByMemberId);
620
660
  });
621
661
  beforeEach(() => {
622
- meeting.members.cancelSIPInvite = sinon.stub().returns(Promise.resolve(test1));
662
+ meeting.members.cancelInviteByMemberId = sinon.stub().returns(Promise.resolve(test1));
623
663
  });
624
- it('should proxy members #cancelSIPInvite and return a promise', async () => {
625
- const cancel = meeting.cancelSIPInvite({memberId: uuid1});
664
+ it('should proxy members #cancelInviteByMemberId and return a promise', async () => {
665
+ const cancel = meeting.cancelInviteByMemberId({memberId: uuid1});
626
666
 
627
667
  assert.exists(cancel.then);
628
668
  await cancel;
629
- assert.calledOnce(meeting.members.cancelSIPInvite);
630
- assert.calledWith(meeting.members.cancelSIPInvite, {memberId: uuid1});
669
+ assert.calledOnce(meeting.members.cancelInviteByMemberId);
670
+ assert.calledWith(meeting.members.cancelInviteByMemberId, {memberId: uuid1});
631
671
  });
632
672
  });
633
673
  describe('#admit', () => {
@@ -1219,14 +1259,13 @@ describe('plugin-meetings', () => {
1219
1259
  allowMediaInLobby: true,
1220
1260
  },
1221
1261
  });
1222
-
1262
+
1223
1263
  assert.calledWithMatch(
1224
1264
  meeting.addMediaInternal,
1225
1265
  sinon.match.any,
1226
1266
  sinon.match.any,
1227
1267
  sinon.match.any,
1228
- sinon.match.has('videoEnabled', false)
1229
- .and(sinon.match.has('allowMediaInLobby', true))
1268
+ sinon.match.has('videoEnabled', false).and(sinon.match.has('allowMediaInLobby', true))
1230
1269
  );
1231
1270
  });
1232
1271
 
@@ -1235,23 +1274,21 @@ describe('plugin-meetings', () => {
1235
1274
  joinOptions,
1236
1275
  mediaOptions: {
1237
1276
  audioEnabled: false,
1238
- sendAudio: true,
1239
- receiveAudio: false,
1277
+ sendAudio: true,
1278
+ receiveAudio: false,
1240
1279
  allowMediaInLobby: true,
1241
1280
  },
1242
1281
  });
1243
-
1282
+
1244
1283
  assert.calledWithMatch(
1245
1284
  meeting.addMediaInternal,
1246
1285
  sinon.match.any,
1247
1286
  sinon.match.any,
1248
1287
  sinon.match.any,
1249
- sinon.match.has('audioEnabled', false)
1250
- .and(sinon.match.has('allowMediaInLobby', true))
1288
+ sinon.match.has('audioEnabled', false).and(sinon.match.has('allowMediaInLobby', true))
1251
1289
  );
1252
- });
1290
+ });
1253
1291
 
1254
-
1255
1292
  it('should use provided send/receive values when videoEnabled/audioEnabled are true or not set', async () => {
1256
1293
  await meeting.joinWithMedia({
1257
1294
  joinOptions,
@@ -1263,7 +1300,7 @@ describe('plugin-meetings', () => {
1263
1300
  allowMediaInLobby: true,
1264
1301
  },
1265
1302
  });
1266
-
1303
+
1267
1304
  assert.calledWith(
1268
1305
  meeting.addMediaInternal,
1269
1306
  sinon.match.any,
@@ -1301,12 +1338,11 @@ describe('plugin-meetings', () => {
1301
1338
  sinon.restore();
1302
1339
  });
1303
1340
  it('should call voicea.onSpokenLanguageUpdate when joined', async () => {
1304
-
1305
1341
  meeting.joinedWith = {state: 'JOINED'};
1306
1342
  await meeting.locusInfo.emitScoped(
1307
1343
  {function: 'test', file: 'test'},
1308
1344
  LOCUSINFO.EVENTS.CONTROLS_MEETING_TRANSCRIPTION_SPOKEN_LANGUAGE_UPDATED,
1309
- {spokenLanguage: 'fr'},
1345
+ {spokenLanguage: 'fr'}
1310
1346
  );
1311
1347
  assert.calledWith(webex.internal.voicea.onSpokenLanguageUpdate, 'fr', meeting.id);
1312
1348
  assert.equal(meeting.transcription.languageOptions.currentSpokenLanguage, 'fr');
@@ -1319,12 +1355,11 @@ describe('plugin-meetings', () => {
1319
1355
  });
1320
1356
 
1321
1357
  it('should also call voicea.onSpokenLanguageUpdate when not joined', async () => {
1322
-
1323
1358
  meeting.joinedWith = {state: 'NOT_JOINED'};
1324
1359
  await meeting.locusInfo.emitScoped(
1325
1360
  {function: 'test', file: 'test'},
1326
1361
  LOCUSINFO.EVENTS.CONTROLS_MEETING_TRANSCRIPTION_SPOKEN_LANGUAGE_UPDATED,
1327
- {spokenLanguage: 'de'},
1362
+ {spokenLanguage: 'de'}
1328
1363
  );
1329
1364
  assert.calledWith(webex.internal.voicea.onSpokenLanguageUpdate, 'de');
1330
1365
  assert.equal(meeting.transcription.languageOptions.currentSpokenLanguage, 'de');
@@ -1954,21 +1989,25 @@ describe('plugin-meetings', () => {
1954
1989
  });
1955
1990
  });
1956
1991
 
1957
- it('should post error event if failed', async () => {
1992
+ it('should handle join failure', async () => {
1958
1993
  MeetingUtil.isPinOrGuest = sinon.stub().returns(false);
1994
+ webex.internal.newMetrics.submitClientEvent = sinon.stub();
1995
+
1959
1996
  await meeting.join().catch(() => {
1960
- assert.deepEqual(
1961
- webex.internal.newMetrics.submitClientEvent.getCall(1).args[0].name,
1962
- 'client.locus.join.response'
1963
- );
1964
- assert.match(
1965
- webex.internal.newMetrics.submitClientEvent.getCall(1).args[0].options.rawError,
1997
+ assert.calledOnce(MeetingUtil.joinMeeting);
1998
+
1999
+ // Assert that client.locus.join.response error event is not sent from this function, it is now emitted from MeetingUtil.joinMeeting
2000
+ assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
2001
+ assert.calledWithMatch(
2002
+ webex.internal.newMetrics.submitClientEvent,
1966
2003
  {
1967
- code: 2,
1968
- error: null,
1969
- joinOptions: {},
1970
- sdkMessage:
1971
- 'There was an issue joining the meeting, meeting could be in a bad state.',
2004
+ name: 'client.call.initiated',
2005
+ payload: {
2006
+ trigger: 'user-interaction',
2007
+ isRoapCallEnabled: true,
2008
+ pstnAudioType: undefined
2009
+ },
2010
+ options: {meetingId: meeting.id},
1972
2011
  }
1973
2012
  );
1974
2013
  });
@@ -2169,14 +2208,12 @@ describe('plugin-meetings', () => {
2169
2208
  };
2170
2209
  meeting.mediaProperties.setMediaDirection = sinon.stub().returns(true);
2171
2210
  meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
2172
- meeting.mediaProperties.getCurrentConnectionInfo = sinon
2173
- .stub()
2174
- .resolves({
2175
- connectionType: 'udp',
2176
- selectedCandidatePairChanges: 2,
2177
- numTransports: 1,
2178
- ipVersion: 'IPv6',
2179
- });
2211
+ meeting.mediaProperties.getCurrentConnectionInfo = sinon.stub().resolves({
2212
+ connectionType: 'udp',
2213
+ selectedCandidatePairChanges: 2,
2214
+ numTransports: 1,
2215
+ ipVersion: 'IPv6',
2216
+ });
2180
2217
  meeting.audio = muteStateStub;
2181
2218
  meeting.video = muteStateStub;
2182
2219
  sinon.stub(Media, 'createMediaConnection').returns(fakeMediaConnection);
@@ -3401,8 +3438,8 @@ describe('plugin-meetings', () => {
3401
3438
  meeting.mediaConnections = [
3402
3439
  {
3403
3440
  mediaAgentCluster: 'some.cluster',
3404
- }
3405
- ]
3441
+ },
3442
+ ];
3406
3443
  meeting.iceCandidatesCount = 3;
3407
3444
  meeting.iceCandidateErrors.set('701_error', 3);
3408
3445
  meeting.iceCandidateErrors.set('701_turn_host_lookup_received_error', 1);
@@ -3583,12 +3620,12 @@ describe('plugin-meetings', () => {
3583
3620
  stopReachability: sinon.stub(),
3584
3621
  isSubnetReachable: sinon.stub().returns(false),
3585
3622
  };
3586
- meeting.mediaServerIp = '1.2.3.4';
3623
+ meeting.mediaServerIp = '1.2.3.4';
3587
3624
  meeting.mediaConnections = [
3588
3625
  {
3589
3626
  mediaAgentCluster: 'some.cluster',
3590
- }
3591
- ]
3627
+ },
3628
+ ];
3592
3629
 
3593
3630
  const forceRtcMetricsSend = sinon.stub().resolves();
3594
3631
  const closeMediaConnectionStub = sinon.stub();
@@ -3638,12 +3675,12 @@ describe('plugin-meetings', () => {
3638
3675
  stopReachability: sinon.stub(),
3639
3676
  isSubnetReachable: sinon.stub().returns(true),
3640
3677
  };
3641
- meeting.mediaServerIp = '1.2.3.4';
3678
+ meeting.mediaServerIp = '1.2.3.4';
3642
3679
  meeting.mediaConnections = [
3643
3680
  {
3644
3681
  mediaAgentCluster: 'some.cluster',
3645
- }
3646
- ]
3682
+ },
3683
+ ];
3647
3684
 
3648
3685
  const forceRtcMetricsSend = sinon.stub().resolves();
3649
3686
  const closeMediaConnectionStub = sinon.stub();
@@ -4355,9 +4392,7 @@ describe('plugin-meetings', () => {
4355
4392
  const error = new Error();
4356
4393
  meeting.meetingRequest.setBrb = sinon.stub().rejects(error);
4357
4394
 
4358
- await expect(
4359
- meeting.beRightBack(true)
4360
- ).to.be.rejectedWith(error);
4395
+ await expect(meeting.beRightBack(true)).to.be.rejectedWith(error);
4361
4396
 
4362
4397
  assert.isFalse(meeting.brbState.state.syncToServerInProgress);
4363
4398
  });
@@ -6507,12 +6542,17 @@ describe('plugin-meetings', () => {
6507
6542
  const DIAL_IN_URL = meeting.dialInUrl;
6508
6543
 
6509
6544
  assert.calledWith(meeting.meetingRequest.dialIn, {
6510
- correlationId: meeting.correlationId,
6545
+ correlationId: meeting.pstnCorrelationId,
6511
6546
  dialInUrl: DIAL_IN_URL,
6512
6547
  locusUrl: meeting.locusUrl,
6513
6548
  clientUrl: meeting.deviceUrl,
6514
6549
  });
6515
6550
  assert.notCalled(meeting.meetingRequest.dialOut);
6551
+
6552
+ // Verify pstnCorrelationId was set
6553
+ assert.exists(meeting.pstnCorrelationId);
6554
+ assert.notEqual(meeting.pstnCorrelationId, meeting.correlationId);
6555
+ const firstPstnCorrelationId = meeting.pstnCorrelationId
6516
6556
 
6517
6557
  meeting.meetingRequest.dialIn.resetHistory();
6518
6558
 
@@ -6520,12 +6560,18 @@ describe('plugin-meetings', () => {
6520
6560
  await meeting.usePhoneAudio();
6521
6561
 
6522
6562
  assert.calledWith(meeting.meetingRequest.dialIn, {
6523
- correlationId: meeting.correlationId,
6563
+ correlationId: meeting.pstnCorrelationId,
6524
6564
  dialInUrl: DIAL_IN_URL,
6525
6565
  locusUrl: meeting.locusUrl,
6526
6566
  clientUrl: meeting.deviceUrl,
6527
6567
  });
6528
6568
  assert.notCalled(meeting.meetingRequest.dialOut);
6569
+ // A new PSTN correlationId should be generated for the second attempt
6570
+ assert.notEqual(
6571
+ meeting.pstnCorrelationId,
6572
+ firstPstnCorrelationId,
6573
+ 'pstnCorrelationId should be regenerated on each dial-in attempt'
6574
+ );
6529
6575
  });
6530
6576
 
6531
6577
  it('given a phone number, triggers dial-out, delegating request to meetingRequest correctly', async () => {
@@ -6535,7 +6581,7 @@ describe('plugin-meetings', () => {
6535
6581
  const DIAL_OUT_URL = meeting.dialOutUrl;
6536
6582
 
6537
6583
  assert.calledWith(meeting.meetingRequest.dialOut, {
6538
- correlationId: meeting.correlationId,
6584
+ correlationId: meeting.pstnCorrelationId,
6539
6585
  dialOutUrl: DIAL_OUT_URL,
6540
6586
  locusUrl: meeting.locusUrl,
6541
6587
  clientUrl: meeting.deviceUrl,
@@ -6543,49 +6589,126 @@ describe('plugin-meetings', () => {
6543
6589
  });
6544
6590
  assert.notCalled(meeting.meetingRequest.dialIn);
6545
6591
 
6592
+ // Verify pstnCorrelationId was set
6593
+ assert.exists(meeting.pstnCorrelationId);
6594
+ assert.notEqual(meeting.pstnCorrelationId, meeting.correlationId);
6595
+ const firstPstnCorrelationId = meeting.pstnCorrelationId;
6596
+
6546
6597
  meeting.meetingRequest.dialOut.resetHistory();
6547
6598
 
6548
6599
  // try again. the dial out urls should match
6549
6600
  await meeting.usePhoneAudio(phoneNumber);
6550
6601
 
6551
6602
  assert.calledWith(meeting.meetingRequest.dialOut, {
6552
- correlationId: meeting.correlationId,
6603
+ correlationId: meeting.pstnCorrelationId,
6553
6604
  dialOutUrl: DIAL_OUT_URL,
6554
6605
  locusUrl: meeting.locusUrl,
6555
6606
  clientUrl: meeting.deviceUrl,
6556
6607
  phoneNumber,
6557
6608
  });
6558
6609
  assert.notCalled(meeting.meetingRequest.dialIn);
6610
+ // A new PSTN correlationId should be generated for the second attempt
6611
+ assert.notEqual(
6612
+ meeting.pstnCorrelationId,
6613
+ firstPstnCorrelationId,
6614
+ 'pstnCorrelationId should be regenerated on each dial-out attempt'
6615
+ );
6559
6616
  });
6560
6617
 
6561
- it('rejects if the request failed (dial in)', () => {
6562
- const error = 'something bad happened';
6618
+ it('rejects if the request failed (dial in)', async () => {
6619
+ const error = {error: {message: 'dial in failed'}, stack: 'error stack'};
6563
6620
 
6564
6621
  meeting.meetingRequest.dialIn = sinon.stub().returns(Promise.reject(error));
6565
6622
 
6566
- return meeting
6567
- .usePhoneAudio()
6568
- .then(() => Promise.reject(new Error('Promise resolved when it should have rejected')))
6569
- .catch((e) => {
6570
- assert.equal(e, error);
6571
-
6572
- return Promise.resolve();
6623
+ try {
6624
+ await meeting.usePhoneAudio();
6625
+ throw new Error('Promise resolved when it should have rejected');
6626
+ } catch (e) {
6627
+ assert.equal(e, error);
6628
+
6629
+ // Verify behavioral metric was sent with dial_in_correlation_id
6630
+ assert.calledWith(Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.ADD_DIAL_IN_FAILURE, {
6631
+ correlation_id: meeting.correlationId,
6632
+ dial_in_url: meeting.dialInUrl,
6633
+ dial_in_correlation_id: sinon.match.string,
6634
+ locus_id: meeting.locusUrl.split('/').pop(),
6635
+ client_url: meeting.deviceUrl,
6636
+ reason: error.error.message,
6637
+ stack: error.stack,
6573
6638
  });
6639
+
6640
+ // Verify pstnCorrelationId was cleared after error
6641
+ assert.equal(meeting.pstnCorrelationId, undefined);
6642
+ }
6574
6643
  });
6575
6644
 
6576
6645
  it('rejects if the request failed (dial out)', async () => {
6577
- const error = 'something bad happened';
6646
+ const error = {error: {message: 'dial out failed'}, stack: 'error stack'};
6578
6647
 
6579
6648
  meeting.meetingRequest.dialOut = sinon.stub().returns(Promise.reject(error));
6580
6649
 
6581
- return meeting
6582
- .usePhoneAudio('+441234567890')
6583
- .then(() => Promise.reject(new Error('Promise resolved when it should have rejected')))
6584
- .catch((e) => {
6585
- assert.equal(e, error);
6586
-
6587
- return Promise.resolve();
6650
+ try {
6651
+ await meeting.usePhoneAudio('+441234567890');
6652
+ throw new Error('Promise resolved when it should have rejected');
6653
+ } catch (e) {
6654
+ assert.equal(e, error);
6655
+
6656
+ // Verify behavioral metric was sent with dial_out_correlation_id
6657
+ assert.calledWith(Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.ADD_DIAL_OUT_FAILURE, {
6658
+ correlation_id: meeting.correlationId,
6659
+ dial_out_url: meeting.dialOutUrl,
6660
+ dial_out_correlation_id: sinon.match.string,
6661
+ locus_id: meeting.locusUrl.split('/').pop(),
6662
+ client_url: meeting.deviceUrl,
6663
+ reason: error.error.message,
6664
+ stack: error.stack,
6588
6665
  });
6666
+
6667
+ // Verify pstnCorrelationId was cleared after error
6668
+ assert.equal(meeting.pstnCorrelationId, undefined);
6669
+ }
6670
+ });
6671
+ });
6672
+
6673
+ describe('#disconnectPhoneAudio', () => {
6674
+ beforeEach(() => {
6675
+ // Mock the MeetingUtil.disconnectPhoneAudio method
6676
+ sinon.stub(MeetingUtil, 'disconnectPhoneAudio').resolves();
6677
+ meeting.dialInUrl = 'dialin:///test-dial-in-url';
6678
+ meeting.dialOutUrl = 'dialout:///test-dial-out-url';
6679
+ meeting.dialInDeviceStatus = 'JOINED';
6680
+ meeting.dialOutDeviceStatus = 'JOINED';
6681
+ });
6682
+
6683
+ afterEach(() => {
6684
+ MeetingUtil.disconnectPhoneAudio.restore();
6685
+ });
6686
+
6687
+ it('should disconnect phone audio and clear pstnCorrelationId', async () => {
6688
+ meeting.pstnCorrelationId = 'test-pstn-correlation-id';
6689
+
6690
+ await meeting.disconnectPhoneAudio();
6691
+
6692
+ // Verify that pstnCorrelationId is cleared
6693
+ assert.equal(meeting.pstnCorrelationId, undefined);
6694
+
6695
+ // Verify that MeetingUtil.disconnectPhoneAudio was called for both dial-in and dial-out
6696
+ assert.calledTwice(MeetingUtil.disconnectPhoneAudio);
6697
+ assert.calledWith(MeetingUtil.disconnectPhoneAudio, meeting, meeting.dialInUrl);
6698
+ assert.calledWith(MeetingUtil.disconnectPhoneAudio, meeting, meeting.dialOutUrl);
6699
+ });
6700
+
6701
+ it('should handle case when no PSTN connection is active', async () => {
6702
+ meeting.dialInDeviceStatus = 'IDLE';
6703
+ meeting.dialOutDeviceStatus = 'IDLE';
6704
+ meeting.pstnCorrelationId = 'test-pstn-correlation-id';
6705
+
6706
+ await meeting.disconnectPhoneAudio();
6707
+
6708
+ // Verify that pstnCorrelationId is still cleared even when no phone connection is active
6709
+ assert.equal(meeting.pstnCorrelationId, undefined);
6710
+ // And verify no disconnect was attempted
6711
+ assert.notCalled(MeetingUtil.disconnectPhoneAudio);
6589
6712
  });
6590
6713
  });
6591
6714
 
@@ -8016,11 +8139,13 @@ describe('plugin-meetings', () => {
8016
8139
  meeting.isoLocalClientMeetingJoinTime = undefined;
8017
8140
  assert.equal(meeting.isoLocalClientMeetingJoinTime, currentSystemTime);
8018
8141
  });
8142
+
8019
8143
  it('should fallback to system clock ISO string when given an invalid value', () => {
8020
8144
  const currentSystemTime = new Date().toISOString();
8021
8145
  meeting.isoLocalClientMeetingJoinTime = 'invalid-date';
8022
8146
  assert.equal(meeting.isoLocalClientMeetingJoinTime, currentSystemTime);
8023
8147
  });
8148
+
8024
8149
  it('should set the isoLocalClientMeetingJoinTime correctly for a valid date string', () => {
8025
8150
  const validDateString = 'Tue, 01 Apr 2025 13:00:36 GMT';
8026
8151
  const expectedISOString = new Date(validDateString).toISOString();
@@ -8100,6 +8225,7 @@ describe('plugin-meetings', () => {
8100
8225
 
8101
8226
  meeting.requestScreenShareFloor = sinon.stub().resolves({});
8102
8227
  meeting.releaseScreenShareFloor = sinon.stub().resolves({});
8228
+ webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
8103
8229
  meeting.mediaProperties.mediaDirection = {
8104
8230
  sendAudio: 'fake value', // using non-boolean here so that we can check that these values are untouched in tests
8105
8231
  sendVideo: 'fake value',
@@ -8181,6 +8307,12 @@ describe('plugin-meetings', () => {
8181
8307
  payload: {mediaType: 'share', shareInstanceId: meeting.localShareInstanceId},
8182
8308
  options: {meetingId: meeting.id},
8183
8309
  });
8310
+
8311
+ // ensure the share start timestamp is saved
8312
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
8313
+ key: 'internal.client.share.initiated',
8314
+ });
8315
+
8184
8316
  assert.equal(meeting.mediaProperties.mediaDirection.sendShare, true);
8185
8317
 
8186
8318
  assert.equal(meeting.shareCAEventSentStatus.transmitStart, false);
@@ -8199,6 +8331,11 @@ describe('plugin-meetings', () => {
8199
8331
  options: {meetingId: meeting.id},
8200
8332
  });
8201
8333
 
8334
+ // ensure the share start timestamp is saved
8335
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
8336
+ key: 'internal.client.share.initiated',
8337
+ });
8338
+
8202
8339
  assert.calledWith(
8203
8340
  meeting.sendSlotManager.getSlot(MediaType.AudioSlides).publishStream,
8204
8341
  stream
@@ -9194,6 +9331,7 @@ describe('plugin-meetings', () => {
9194
9331
  meeting.deferSDPAnswer = {
9195
9332
  reject: sinon.stub(),
9196
9333
  };
9334
+
9197
9335
  const clearTimeoutSpy = sinon.spy(clock, 'clearTimeout');
9198
9336
 
9199
9337
  const fakeError = new Errors.SdpAnswerHandlingError(fakeErrorMessage, {
@@ -10471,6 +10609,8 @@ describe('plugin-meetings', () => {
10471
10609
  meeting.mediaProperties = {mediaDirection: {sendShare: true}};
10472
10610
  meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
10473
10611
  (meeting.deviceUrl = 'deviceUrl.com'), (meeting.localShareInstanceId = '1234-5678');
10612
+ webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
10613
+ webex.internal.newMetrics.callDiagnosticLatencies.getShareDuration = sinon.stub().returns(1000);
10474
10614
  });
10475
10615
  it('should call changeMeetingFloor()', async () => {
10476
10616
  meeting.screenShareFloorState = 'GRANTED';
@@ -10488,6 +10628,22 @@ describe('plugin-meetings', () => {
10488
10628
  assert.exists(share.then);
10489
10629
  await share;
10490
10630
  assert.calledOnce(meeting.meetingRequest.changeMeetingFloor);
10631
+
10632
+ // ensure the share stop timestamp is saved
10633
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
10634
+ key: 'internal.client.share.stopped',
10635
+ });
10636
+
10637
+ // ensure the CA share stopped metric is submitted with duration
10638
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
10639
+ name: 'client.share.stopped',
10640
+ payload: {
10641
+ mediaType: 'share',
10642
+ shareInstanceId: meeting.localShareInstanceId,
10643
+ shareDuration: 1000,
10644
+ },
10645
+ options: {meetingId: meeting.id},
10646
+ });
10491
10647
  });
10492
10648
  it('should not call changeMeetingFloor() if someone else already has the floor', async () => {
10493
10649
  // change selfId so that it doesn't match the beneficiary id from meeting.locusInfo.mediaShares
@@ -12060,6 +12216,7 @@ describe('plugin-meetings', () => {
12060
12216
  meeting.locusInfo.self = {url: url1};
12061
12217
  meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
12062
12218
  meeting.deviceUrl = 'deviceUrl.com';
12219
+ webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
12063
12220
  });
12064
12221
  it('should have #startWhiteboardShare', () => {
12065
12222
  assert.exists(meeting.startWhiteboardShare);
@@ -12087,6 +12244,11 @@ describe('plugin-meetings', () => {
12087
12244
  payload: {mediaType: 'whiteboard'},
12088
12245
  options: {meetingId: meeting.id},
12089
12246
  });
12247
+
12248
+ // ensure the share start timestamp is saved
12249
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
12250
+ key: 'internal.client.share.initiated',
12251
+ });
12090
12252
  });
12091
12253
  });
12092
12254
  describe('#stopWhiteboardShare', () => {
@@ -12098,6 +12260,8 @@ describe('plugin-meetings', () => {
12098
12260
  meeting.locusInfo.self = {url: url1};
12099
12261
  meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
12100
12262
  meeting.deviceUrl = 'deviceUrl.com';
12263
+ webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
12264
+ webex.internal.newMetrics.callDiagnosticLatencies.getShareDuration = sinon.stub().returns(1000);
12101
12265
  });
12102
12266
  it('should stop the whiteboard share', async () => {
12103
12267
  const whiteboardShare = meeting.stopWhiteboardShare();
@@ -12112,6 +12276,21 @@ describe('plugin-meetings', () => {
12112
12276
  uri: url1,
12113
12277
  });
12114
12278
  assert.calledOnce(meeting.meetingRequest.changeMeetingFloor);
12279
+
12280
+ // ensure the share stop timestamp is saved
12281
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
12282
+ key: 'internal.client.share.stopped',
12283
+ });
12284
+
12285
+ // ensure the CA share stopped metric is submitted with duration
12286
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
12287
+ name: 'client.share.stopped',
12288
+ payload: {
12289
+ mediaType: 'whiteboard',
12290
+ shareDuration: 1000,
12291
+ },
12292
+ options: {meetingId: meeting.id},
12293
+ });
12115
12294
  });
12116
12295
  });
12117
12296
  });
@@ -12410,7 +12589,7 @@ describe('plugin-meetings', () => {
12410
12589
  activeSharingId.whiteboard = beneficiaryId;
12411
12590
 
12412
12591
  eventTrigger.share.push(
12413
- meeting.webinar.selfIsAttendee
12592
+ meeting.webinar.selfIsAttendee || meeting.guest
12414
12593
  ? {
12415
12594
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
12416
12595
  functionName: 'remoteShare',
@@ -12429,7 +12608,8 @@ describe('plugin-meetings', () => {
12429
12608
  }
12430
12609
  );
12431
12610
 
12432
- shareStatus = meeting.webinar.selfIsAttendee
12611
+ shareStatus =
12612
+ meeting.webinar.selfIsAttendee || meeting.guest
12433
12613
  ? SHARE_STATUS.REMOTE_SHARE_ACTIVE
12434
12614
  : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
12435
12615
  }
@@ -12647,6 +12827,36 @@ describe('plugin-meetings', () => {
12647
12827
  });
12648
12828
  });
12649
12829
 
12830
+ describe('Whiteboard Share - User is guest', () => {
12831
+ it('User receives a remote share instead of whiteboard share', () => {
12832
+ // Set the guest flag
12833
+ meeting.guest = true;
12834
+
12835
+ // Step 1: Start sharing whiteboard A
12836
+ const data1 = generateData(
12837
+ blankPayload, // Initial payload
12838
+ true, // isGranting: Granting share
12839
+ false, // isContent: Whiteboard (not content)
12840
+ USER_IDS.REMOTE_A, // Beneficiary ID: Remote user A
12841
+ RESOURCE_URLS.WHITEBOARD_A // Resource URL: Whiteboard A
12842
+ );
12843
+
12844
+ // Step 2: Stop sharing whiteboard A
12845
+ const data2 = generateData(
12846
+ data1.payload, // Updated payload from Step 1
12847
+ false, // isGranting: Stopping share
12848
+ false, // isContent: Whiteboard
12849
+ USER_IDS.REMOTE_A // Beneficiary ID: Remote user A
12850
+ );
12851
+
12852
+ // Validate the payload changes and status updates
12853
+ payloadTestHelper([data1]);
12854
+
12855
+ // Specific assertions for guest
12856
+ assert.equal(meeting.shareStatus, SHARE_STATUS.REMOTE_SHARE_ACTIVE);
12857
+ });
12858
+ });
12859
+
12650
12860
  describe('Whiteboard A --> Whiteboard B', () => {
12651
12861
  it('Scenario #1: you share both whiteboards', () => {
12652
12862
  const data1 = generateData(
@@ -14091,4 +14301,443 @@ describe('plugin-meetings', () => {
14091
14301
  assert.equal(result.failureReason, MEETING_INFO_FAILURE_REASON.WRONG_CAPTCHA);
14092
14302
  });
14093
14303
  });
14304
+
14305
+ describe('#setStage', () => {
14306
+ const check = async (options, expectedVideoLayout) => {
14307
+ const locusUrl = `https://locus-test.wbx2.com/locus/api/v1/loci/${uuidv4()}`;
14308
+ meeting.locusUrl = locusUrl;
14309
+
14310
+ const setStagePromise = meeting.setStage(options);
14311
+
14312
+ assert.exists(setStagePromise.then);
14313
+ await setStagePromise;
14314
+
14315
+ assert.calledOnceWithExactly(
14316
+ meeting.meetingRequest.synchronizeStage,
14317
+ locusUrl,
14318
+ expectedVideoLayout
14319
+ );
14320
+ };
14321
+
14322
+ beforeEach(() => {
14323
+ meeting.meetingRequest.synchronizeStage = sinon.stub().returns(Promise.resolve());
14324
+ });
14325
+
14326
+ it('sends the expected request when no options are provided', async () => {
14327
+ await check(undefined, {
14328
+ overrideDefault: true,
14329
+ lockAttendeeViewOnStageOnly: false,
14330
+ stageParameters: {
14331
+ activeSpeakerProportion: 0.5,
14332
+ showActiveSpeaker: {show: false, order: 0},
14333
+ stageManagerType: 0,
14334
+ },
14335
+ });
14336
+ });
14337
+
14338
+ it('sends the expected request when empty options are provided', async () => {
14339
+ await check(
14340
+ {},
14341
+ {
14342
+ overrideDefault: true,
14343
+ lockAttendeeViewOnStageOnly: false,
14344
+ stageParameters: {
14345
+ activeSpeakerProportion: 0.5,
14346
+ showActiveSpeaker: {show: false, order: 0},
14347
+ stageManagerType: 0,
14348
+ },
14349
+ }
14350
+ );
14351
+ });
14352
+
14353
+ [0.25, 0.5, 0.75].forEach((activeSpeakerProportion) => {
14354
+ it(`sends the expected request when only the active speaker proportion option is provided as ${activeSpeakerProportion}`, async () => {
14355
+ await check(
14356
+ {activeSpeakerProportion},
14357
+ {
14358
+ overrideDefault: true,
14359
+ lockAttendeeViewOnStageOnly: false,
14360
+ stageParameters: {
14361
+ activeSpeakerProportion,
14362
+ showActiveSpeaker: {show: false, order: 0},
14363
+ stageManagerType: 0,
14364
+ },
14365
+ }
14366
+ );
14367
+ });
14368
+ });
14369
+
14370
+ it('sends the expected request when only the custom background option is provided', async () => {
14371
+ const customBackground = {
14372
+ url: `https://test.wbx2.com/background/${uuidv4()}.jpg`,
14373
+ };
14374
+
14375
+ await check(
14376
+ {customBackground},
14377
+ {
14378
+ overrideDefault: true,
14379
+ lockAttendeeViewOnStageOnly: false,
14380
+ stageParameters: {
14381
+ activeSpeakerProportion: 0.5,
14382
+ showActiveSpeaker: {show: false, order: 0},
14383
+ stageManagerType: 2,
14384
+ },
14385
+ customLayouts: {background: customBackground},
14386
+ }
14387
+ );
14388
+ });
14389
+
14390
+ it('sends the expected request when only the custom logo option is provided', async () => {
14391
+ const customLogo = {
14392
+ url: `https://test.wbx2.com/logo/${uuidv4()}.png`,
14393
+ position: 'LowerRight',
14394
+ };
14395
+
14396
+ await check(
14397
+ {customLogo},
14398
+ {
14399
+ overrideDefault: true,
14400
+ lockAttendeeViewOnStageOnly: false,
14401
+ stageParameters: {
14402
+ activeSpeakerProportion: 0.5,
14403
+ showActiveSpeaker: {show: false, order: 0},
14404
+ stageManagerType: 1,
14405
+ },
14406
+ customLayouts: {logo: customLogo},
14407
+ }
14408
+ );
14409
+ });
14410
+
14411
+ it('sends the expected request when only the custom name label option is provided', async () => {
14412
+ const customNameLabel = {
14413
+ accentColor: '#0A7806',
14414
+ background: {color: 'rgba(255, 255, 255, 1)'},
14415
+ border: {color: 'rgba(255, 255, 255, 1)'},
14416
+ content: {
14417
+ displayName: {color: 'rgba(0, 0, 0, 0.95)'},
14418
+ subtitle: {color: 'rgba(0, 0, 0, 0.6)'},
14419
+ },
14420
+ decoration: {color: 'rgba(10, 120, 6, 1)'},
14421
+ fadeOut: {delay: 15},
14422
+ type: 'Primary',
14423
+ };
14424
+
14425
+ await check(
14426
+ {customNameLabel},
14427
+ {
14428
+ overrideDefault: true,
14429
+ lockAttendeeViewOnStageOnly: false,
14430
+ stageParameters: {
14431
+ activeSpeakerProportion: 0.5,
14432
+ showActiveSpeaker: {show: false, order: 0},
14433
+ stageManagerType: 4,
14434
+ },
14435
+ nameLabelStyle: customNameLabel,
14436
+ }
14437
+ );
14438
+ });
14439
+
14440
+ it('sends the expected request when only the custom background and logo options are provided', async () => {
14441
+ const customBackground = {
14442
+ url: `https://test.wbx2.com/background/${uuidv4()}.jpg`,
14443
+ };
14444
+ const customLogo = {
14445
+ url: `https://test.wbx2.com/logo/${uuidv4()}.png`,
14446
+ position: 'UpperRight',
14447
+ };
14448
+
14449
+ await check(
14450
+ {customBackground, customLogo},
14451
+ {
14452
+ overrideDefault: true,
14453
+ lockAttendeeViewOnStageOnly: false,
14454
+ stageParameters: {
14455
+ activeSpeakerProportion: 0.5,
14456
+ showActiveSpeaker: {show: false, order: 0},
14457
+ stageManagerType: 3,
14458
+ },
14459
+ customLayouts: {background: customBackground, logo: customLogo},
14460
+ }
14461
+ );
14462
+ });
14463
+
14464
+ it('sends the expected request when only the custom background and name label options are provided', async () => {
14465
+ const customBackground = {
14466
+ url: `https://test.wbx2.com/background/${uuidv4()}.jpg`,
14467
+ };
14468
+ const customNameLabel = {
14469
+ accentColor: '#00A3FF',
14470
+ background: {color: 'rgba(0, 163, 255, 1)'},
14471
+ border: {color: 'rgba(0, 163, 255, 1)'},
14472
+ content: {
14473
+ displayName: {color: 'rgba(255, 255, 255, 0.95)'},
14474
+ subtitle: {color: 'rgba(255, 255, 255, 0.7)'},
14475
+ },
14476
+ decoration: {color: 'rgba(255, 255, 255, 0.95)'},
14477
+ fadeOut: {delay: 15},
14478
+ type: 'PrimaryInverted',
14479
+ };
14480
+
14481
+ await check(
14482
+ {customBackground, customNameLabel},
14483
+ {
14484
+ overrideDefault: true,
14485
+ lockAttendeeViewOnStageOnly: false,
14486
+ stageParameters: {
14487
+ activeSpeakerProportion: 0.5,
14488
+ showActiveSpeaker: {show: false, order: 0},
14489
+ stageManagerType: 6,
14490
+ },
14491
+ customLayouts: {background: customBackground},
14492
+ nameLabelStyle: customNameLabel,
14493
+ }
14494
+ );
14495
+ });
14496
+
14497
+ it('sends the expected request when only the custom logo and name label options are provided', async () => {
14498
+ const customLogo = {
14499
+ url: `https://test.wbx2.com/logo/${uuidv4()}.png`,
14500
+ position: 'UpperLeft',
14501
+ };
14502
+ const customNameLabel = {
14503
+ accentColor: '#942B2B',
14504
+ background: {color: 'rgba(255, 255, 255, 1)'},
14505
+ border: {color: 'rgba(148, 43, 43, 1)'},
14506
+ content: {
14507
+ displayName: {color: 'rgba(0, 0, 0, 0.95)'},
14508
+ subtitle: {color: 'rgba(0, 0, 0, 0.6)'},
14509
+ },
14510
+ decoration: {color: 'rgba(0, 0, 0, 0)'},
14511
+ fadeOut: {delay: 15},
14512
+ type: 'Secondary',
14513
+ };
14514
+
14515
+ await check(
14516
+ {customLogo, customNameLabel},
14517
+ {
14518
+ overrideDefault: true,
14519
+ lockAttendeeViewOnStageOnly: false,
14520
+ stageParameters: {
14521
+ activeSpeakerProportion: 0.5,
14522
+ showActiveSpeaker: {show: false, order: 0},
14523
+ stageManagerType: 5,
14524
+ },
14525
+ customLayouts: {logo: customLogo},
14526
+ nameLabelStyle: customNameLabel,
14527
+ }
14528
+ );
14529
+ });
14530
+
14531
+ it('sends the expected request when only the custom background, logo, name label options are provided', async () => {
14532
+ const customBackground = {
14533
+ url: `https://test.wbx2.com/background/${uuidv4()}.jpg`,
14534
+ };
14535
+ const customLogo = {
14536
+ url: `https://test.wbx2.com/logo/${uuidv4()}.png`,
14537
+ position: 'LowerLeft',
14538
+ };
14539
+ const customNameLabel = {
14540
+ accentColor: '#EBD960',
14541
+ background: {color: 'rgba(235, 217, 96, 0.55)'},
14542
+ border: {color: 'rgba(235, 217, 96, 0.55)'},
14543
+ content: {
14544
+ displayName: {color: 'rgba(255, 255, 255, 0.95)'},
14545
+ subtitle: {color: 'rgba(255, 255, 255, 0.7)'},
14546
+ },
14547
+ decoration: {color: 'rgba(0, 0, 0, 0)'},
14548
+ fadeOut: {delay: 15},
14549
+ type: 'SecondaryInverted',
14550
+ };
14551
+
14552
+ await check(
14553
+ {customBackground, customLogo, customNameLabel},
14554
+ {
14555
+ overrideDefault: true,
14556
+ lockAttendeeViewOnStageOnly: false,
14557
+ stageParameters: {
14558
+ activeSpeakerProportion: 0.5,
14559
+ showActiveSpeaker: {show: false, order: 0},
14560
+ stageManagerType: 7,
14561
+ },
14562
+ customLayouts: {background: customBackground, logo: customLogo},
14563
+ nameLabelStyle: customNameLabel,
14564
+ }
14565
+ );
14566
+ });
14567
+
14568
+ it('sends the expected request when only the important participants option is provided as empty', async () => {
14569
+ await check(
14570
+ {importantParticipants: []},
14571
+ {
14572
+ overrideDefault: true,
14573
+ lockAttendeeViewOnStageOnly: false,
14574
+ stageParameters: {
14575
+ activeSpeakerProportion: 0.5,
14576
+ showActiveSpeaker: {show: false, order: 0},
14577
+ stageManagerType: 0,
14578
+ },
14579
+ }
14580
+ );
14581
+ });
14582
+
14583
+ it('sends the expected request when only the important participants option is provided as populated', async () => {
14584
+ const importantParticipants = [
14585
+ {mainCsi: 11111111, participantId: uuidv4()},
14586
+ {mainCsi: 22222222, participantId: uuidv4()},
14587
+ {mainCsi: 33333333, participantId: uuidv4()},
14588
+ {mainCsi: 44444444, participantId: uuidv4()},
14589
+ {mainCsi: 55555555, participantId: uuidv4()},
14590
+ {mainCsi: 66666666, participantId: uuidv4()},
14591
+ {mainCsi: 77777777, participantId: uuidv4()},
14592
+ {mainCsi: 88888888, participantId: uuidv4()},
14593
+ ];
14594
+
14595
+ await check(
14596
+ {importantParticipants},
14597
+ {
14598
+ overrideDefault: true,
14599
+ lockAttendeeViewOnStageOnly: false,
14600
+ stageParameters: {
14601
+ activeSpeakerProportion: 0.5,
14602
+ importantParticipants: [
14603
+ {...importantParticipants[0], order: 1},
14604
+ {...importantParticipants[1], order: 2},
14605
+ {...importantParticipants[2], order: 3},
14606
+ {...importantParticipants[3], order: 4},
14607
+ {...importantParticipants[4], order: 5},
14608
+ {...importantParticipants[5], order: 6},
14609
+ {...importantParticipants[6], order: 7},
14610
+ {...importantParticipants[7], order: 8},
14611
+ ],
14612
+ showActiveSpeaker: {show: false, order: 0},
14613
+ stageManagerType: 0,
14614
+ },
14615
+ }
14616
+ );
14617
+ });
14618
+
14619
+ [false, true].forEach((lockAttendeeViewOnStage) => {
14620
+ it(`sends the expected request when only the lock attendee view on stage option is provided as ${lockAttendeeViewOnStage}`, async () => {
14621
+ await check(
14622
+ {lockAttendeeViewOnStage},
14623
+ {
14624
+ overrideDefault: true,
14625
+ lockAttendeeViewOnStageOnly: lockAttendeeViewOnStage,
14626
+ stageParameters: {
14627
+ activeSpeakerProportion: 0.5,
14628
+ showActiveSpeaker: {show: false, order: 0},
14629
+ stageManagerType: 0,
14630
+ },
14631
+ }
14632
+ );
14633
+ });
14634
+ });
14635
+
14636
+ [false, true].forEach((showActiveSpeaker) => {
14637
+ it(`sends the expected request when only the show active speaker option is provided as ${showActiveSpeaker}`, async () => {
14638
+ await check(
14639
+ {showActiveSpeaker},
14640
+ {
14641
+ overrideDefault: true,
14642
+ lockAttendeeViewOnStageOnly: false,
14643
+ stageParameters: {
14644
+ activeSpeakerProportion: 0.5,
14645
+ showActiveSpeaker: {show: showActiveSpeaker, order: 0},
14646
+ stageManagerType: 0,
14647
+ },
14648
+ }
14649
+ );
14650
+ });
14651
+ });
14652
+
14653
+ it('sends the expected request when all options are provided', async () => {
14654
+ const activeSpeakerProportion = 0.6;
14655
+ const customBackground = {
14656
+ url: `https://test.wbx2.com/background/${uuidv4()}.jpg`,
14657
+ };
14658
+ const customLogo = {
14659
+ url: `https://test.wbx2.com/logo/${uuidv4()}.png`,
14660
+ position: 'UpperMiddle',
14661
+ };
14662
+ const customNameLabel = {
14663
+ accentColor: '#0A7806',
14664
+ background: {color: 'rgba(255, 255, 255, 1)'},
14665
+ border: {color: 'rgba(255, 255, 255, 1)'},
14666
+ content: {
14667
+ displayName: {color: 'rgba(0, 0, 0, 0.95)'},
14668
+ subtitle: {color: 'rgba(0, 0, 0, 0.6)'},
14669
+ },
14670
+ decoration: {color: 'rgba(10, 120, 6, 1)'},
14671
+ fadeOut: {delay: 15},
14672
+ type: 'Primary',
14673
+ };
14674
+ const importantParticipants = [
14675
+ {mainCsi: 11111111, participantId: uuidv4()},
14676
+ {mainCsi: 22222222, participantId: uuidv4()},
14677
+ {mainCsi: 33333333, participantId: uuidv4()},
14678
+ {mainCsi: 44444444, participantId: uuidv4()},
14679
+ {mainCsi: 55555555, participantId: uuidv4()},
14680
+ {mainCsi: 66666666, participantId: uuidv4()},
14681
+ {mainCsi: 77777777, participantId: uuidv4()},
14682
+ {mainCsi: 88888888, participantId: uuidv4()},
14683
+ ];
14684
+ const lockAttendeeViewOnStage = true;
14685
+ const showActiveSpeaker = true;
14686
+
14687
+ await check(
14688
+ {
14689
+ activeSpeakerProportion,
14690
+ customBackground,
14691
+ customLogo,
14692
+ customNameLabel,
14693
+ importantParticipants,
14694
+ lockAttendeeViewOnStage,
14695
+ showActiveSpeaker,
14696
+ },
14697
+ {
14698
+ overrideDefault: true,
14699
+ lockAttendeeViewOnStageOnly: lockAttendeeViewOnStage,
14700
+ stageParameters: {
14701
+ activeSpeakerProportion,
14702
+ importantParticipants: [
14703
+ {...importantParticipants[0], order: 1},
14704
+ {...importantParticipants[1], order: 2},
14705
+ {...importantParticipants[2], order: 3},
14706
+ {...importantParticipants[3], order: 4},
14707
+ {...importantParticipants[4], order: 5},
14708
+ {...importantParticipants[5], order: 6},
14709
+ {...importantParticipants[6], order: 7},
14710
+ {...importantParticipants[7], order: 8},
14711
+ ],
14712
+ showActiveSpeaker: {show: showActiveSpeaker, order: 0},
14713
+ stageManagerType: 7,
14714
+ },
14715
+ customLayouts: {background: customBackground, logo: customLogo},
14716
+ nameLabelStyle: customNameLabel,
14717
+ }
14718
+ );
14719
+ });
14720
+ });
14721
+
14722
+ describe('#unsetStage', () => {
14723
+ beforeEach(() => {
14724
+ meeting.meetingRequest.synchronizeStage = sinon.stub().returns(Promise.resolve());
14725
+ });
14726
+
14727
+ it('sends the expected request', async () => {
14728
+ const locusUrl = `https://locus-test.wbx2.com/locus/api/v1/loci/${uuidv4()}`;
14729
+ meeting.locusUrl = locusUrl;
14730
+
14731
+ const unsetStagePromise = meeting.unsetStage();
14732
+
14733
+ assert.exists(unsetStagePromise.then);
14734
+ await unsetStagePromise;
14735
+
14736
+ assert.calledOnceWithExactly(
14737
+ meeting.meetingRequest.synchronizeStage,
14738
+ locusUrl,
14739
+ {overrideDefault: false}
14740
+ );
14741
+ });
14742
+ });
14094
14743
  });