@webex/plugin-meetings 2.11.1 → 2.13.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.
- package/README.md +4 -0
- package/dist/constants.js +9 -2
- package/dist/constants.js.map +1 -1
- package/dist/meeting/index.js +230 -103
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting-info/meeting-info-v2.js +3 -1
- package/dist/meeting-info/meeting-info-v2.js.map +1 -1
- package/dist/members/index.js +23 -0
- package/dist/members/index.js.map +1 -1
- package/dist/members/request.js +11 -0
- package/dist/members/request.js.map +1 -1
- package/dist/members/util.js +22 -0
- package/dist/members/util.js.map +1 -1
- package/package.json +7 -7
- package/src/constants.js +7 -0
- package/src/meeting/index.js +74 -14
- package/src/meeting-info/meeting-info-v2.js +3 -1
- package/src/members/index.js +20 -0
- package/src/members/request.js +10 -0
- package/src/members/util.js +21 -0
- package/test/unit/spec/meeting/index.js +87 -29
- package/test/unit/spec/meeting-info/meetinginfov2.js +21 -5
- package/test/unit/spec/members/index.js +51 -0
- package/test/unit/spec/members/request.js +70 -1
- package/test/unit/spec/members/utils.js +39 -0
package/src/meeting/index.js
CHANGED
|
@@ -72,6 +72,7 @@ import {
|
|
|
72
72
|
STATS,
|
|
73
73
|
VIDEO_RESOLUTIONS,
|
|
74
74
|
VIDEO,
|
|
75
|
+
BNR_STATUS,
|
|
75
76
|
HTTP_VERBS
|
|
76
77
|
} from '../constants';
|
|
77
78
|
import BEHAVIORAL_METRICS from '../metrics/constants';
|
|
@@ -914,6 +915,14 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
914
915
|
*/
|
|
915
916
|
this.meetingInfoFailureReason = undefined;
|
|
916
917
|
|
|
918
|
+
/**
|
|
919
|
+
* Indicates the current status of BNR in the meeting
|
|
920
|
+
* @type {BNR_STATUS}
|
|
921
|
+
* @private
|
|
922
|
+
* @memberof Meeting
|
|
923
|
+
*/
|
|
924
|
+
this.bnrStatus = BNR_STATUS.NOT_ENABLED;
|
|
925
|
+
|
|
917
926
|
this.setUpLocusInfoListeners();
|
|
918
927
|
this.locusInfo.init(attrs.locus ? attrs.locus : {});
|
|
919
928
|
this.hasJoinedOnce = false;
|
|
@@ -4482,18 +4491,23 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4482
4491
|
* @public
|
|
4483
4492
|
* @memberof Meeting
|
|
4484
4493
|
*/
|
|
4485
|
-
updateAudio(options) {
|
|
4494
|
+
async updateAudio(options) {
|
|
4486
4495
|
if (!this.canUpdateMedia()) {
|
|
4487
4496
|
return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.AUDIO, options);
|
|
4488
4497
|
}
|
|
4489
4498
|
const {sendAudio, receiveAudio, stream} = options;
|
|
4490
4499
|
const {audioTransceiver} = this.mediaProperties.peerConnection;
|
|
4491
|
-
|
|
4500
|
+
let track = MeetingUtil.getTrack(stream).audioTrack;
|
|
4492
4501
|
|
|
4493
4502
|
if (typeof sendAudio !== 'boolean' || typeof receiveAudio !== 'boolean') {
|
|
4494
4503
|
return Promise.reject(new ParameterError('Pass sendAudio and receiveAudio parameter'));
|
|
4495
4504
|
}
|
|
4496
4505
|
|
|
4506
|
+
if (sendAudio && !this.isAudioMuted() && (this.bnrStatus === BNR_STATUS.ENABLED || this.bnrStatus === BNR_STATUS.SHOULD_ENABLE)) {
|
|
4507
|
+
track = await this.internal_enableBNR(track);
|
|
4508
|
+
this.bnrStatus = BNR_STATUS.ENABLED;
|
|
4509
|
+
}
|
|
4510
|
+
|
|
4497
4511
|
return MeetingUtil.validateOptions({sendAudio, localStream: stream})
|
|
4498
4512
|
.then(() => {
|
|
4499
4513
|
let previousMediaDirection = {};
|
|
@@ -5750,6 +5764,38 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5750
5764
|
}
|
|
5751
5765
|
};
|
|
5752
5766
|
|
|
5767
|
+
/**
|
|
5768
|
+
* Internal API to return status of BNR
|
|
5769
|
+
* @returns {Boolean}
|
|
5770
|
+
* @public
|
|
5771
|
+
* @memberof Meeting
|
|
5772
|
+
*/
|
|
5773
|
+
isBnrEnabled() {
|
|
5774
|
+
return this.bnrStatus === BNR_STATUS.ENABLED;
|
|
5775
|
+
}
|
|
5776
|
+
|
|
5777
|
+
/**
|
|
5778
|
+
* Internal API to obtain BNR enabled MediaStream
|
|
5779
|
+
* @returns {Promise<MediaStreamTrack>}
|
|
5780
|
+
* @private
|
|
5781
|
+
* @param {MedaiStreamTrack} audioTrack from updateAudio
|
|
5782
|
+
* @memberof Meeting
|
|
5783
|
+
*/
|
|
5784
|
+
async internal_enableBNR(audioTrack) {
|
|
5785
|
+
try {
|
|
5786
|
+
LoggerProxy.logger.info('Meeting:index#internal_enableBNR. Internal enable BNR called');
|
|
5787
|
+
const bnrAudioTrack = await WebRTCMedia.Effects.BNR.enableBNR(audioTrack);
|
|
5788
|
+
|
|
5789
|
+
LoggerProxy.logger.info('Meeting:index#internal_enableBNR. BNR enabled track obtained from WebRTC & returned as stream');
|
|
5790
|
+
|
|
5791
|
+
return bnrAudioTrack;
|
|
5792
|
+
}
|
|
5793
|
+
catch (error) {
|
|
5794
|
+
LoggerProxy.logger.error('Meeting:index#internal_enableBNR.', error);
|
|
5795
|
+
throw error;
|
|
5796
|
+
}
|
|
5797
|
+
}
|
|
5798
|
+
|
|
5753
5799
|
/**
|
|
5754
5800
|
* enableBNR API
|
|
5755
5801
|
* @returns {Promise<Boolean>}
|
|
@@ -5764,24 +5810,29 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5764
5810
|
if (typeof this.mediaProperties === 'undefined' || typeof this.mediaProperties.audioTrack === 'undefined') {
|
|
5765
5811
|
throw new Error("Meeting doesn't have an audioTrack attached");
|
|
5766
5812
|
}
|
|
5767
|
-
|
|
5813
|
+
else if (this.isAudioMuted()) {
|
|
5814
|
+
throw new Error('Cannot enable BNR while meeting is muted');
|
|
5815
|
+
}
|
|
5816
|
+
|
|
5817
|
+
|
|
5818
|
+
this.bnrStatus = BNR_STATUS.SHOULD_ENABLE;
|
|
5819
|
+
|
|
5768
5820
|
const audioStream = MediaUtil.createMediaStream([this.mediaProperties.audioTrack]);
|
|
5769
5821
|
|
|
5770
|
-
LoggerProxy.logger.info('Meeting:index#enableBNR.
|
|
5822
|
+
LoggerProxy.logger.info('Meeting:index#enableBNR. MediaStream created from meeting & sent to updateAudio');
|
|
5771
5823
|
await this.updateAudio({
|
|
5772
5824
|
sendAudio: true,
|
|
5773
|
-
receiveAudio:
|
|
5825
|
+
receiveAudio: this.mediaProperties.mediaDirection.receiveAudio,
|
|
5774
5826
|
stream: audioStream
|
|
5775
5827
|
});
|
|
5776
|
-
this.
|
|
5828
|
+
this.bnrStatus = BNR_STATUS.ENABLED;
|
|
5777
5829
|
isSuccess = true;
|
|
5778
5830
|
Metrics.sendBehavioralMetric(
|
|
5779
5831
|
BEHAVIORAL_METRICS.ENABLE_BNR_SUCCESS,
|
|
5780
5832
|
);
|
|
5781
5833
|
}
|
|
5782
5834
|
catch (error) {
|
|
5783
|
-
|
|
5784
|
-
|
|
5835
|
+
this.bnrStatus = BNR_STATUS.NOT_ENABLED;
|
|
5785
5836
|
Metrics.sendBehavioralMetric(
|
|
5786
5837
|
BEHAVIORAL_METRICS.ENABLE_BNR_FAILURE,
|
|
5787
5838
|
{
|
|
@@ -5789,7 +5840,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5789
5840
|
stack: error.stack
|
|
5790
5841
|
}
|
|
5791
5842
|
);
|
|
5792
|
-
|
|
5843
|
+
LoggerProxy.logger.error('Meeting:index#enableBNR.', error);
|
|
5793
5844
|
throw error;
|
|
5794
5845
|
}
|
|
5795
5846
|
|
|
@@ -5807,19 +5858,27 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5807
5858
|
let isSuccess = false;
|
|
5808
5859
|
|
|
5809
5860
|
try {
|
|
5810
|
-
if (
|
|
5861
|
+
if (!this.isBnrEnabled()) {
|
|
5862
|
+
throw new Error('Can not disable as BNR is not enabled');
|
|
5863
|
+
}
|
|
5864
|
+
else if (typeof this.mediaProperties === 'undefined' || typeof this.mediaProperties.audioTrack === 'undefined') {
|
|
5811
5865
|
throw new Error("Meeting doesn't have an audioTrack attached");
|
|
5812
5866
|
}
|
|
5813
|
-
|
|
5814
|
-
const audioStream = MediaUtil.createMediaStream([
|
|
5867
|
+
const audioTrack = WebRTCMedia.Effects.BNR.disableBNR(this.mediaProperties.audioTrack);
|
|
5868
|
+
const audioStream = MediaUtil.createMediaStream([audioTrack]);
|
|
5815
5869
|
|
|
5816
5870
|
LoggerProxy.logger.info('Meeting:index#disableBNR. Raw media track obtained from WebRTC & sent to updateAudio');
|
|
5871
|
+
|
|
5872
|
+
this.bnrStatus = BNR_STATUS.SHOULD_DISABLE;
|
|
5873
|
+
|
|
5817
5874
|
await this.updateAudio({
|
|
5818
5875
|
sendAudio: true,
|
|
5819
|
-
receiveAudio:
|
|
5876
|
+
receiveAudio: this.mediaProperties.mediaDirection.receiveAudio,
|
|
5820
5877
|
stream: audioStream
|
|
5821
5878
|
});
|
|
5822
|
-
|
|
5879
|
+
|
|
5880
|
+
this.bnrStatus = BNR_STATUS.NOT_ENABLED;
|
|
5881
|
+
|
|
5823
5882
|
isSuccess = true;
|
|
5824
5883
|
|
|
5825
5884
|
Metrics.sendBehavioralMetric(
|
|
@@ -5827,6 +5886,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5827
5886
|
);
|
|
5828
5887
|
}
|
|
5829
5888
|
catch (error) {
|
|
5889
|
+
this.bnrStatus = BNR_STATUS.ENABLED;
|
|
5830
5890
|
LoggerProxy.logger.error(`Meeting:index#disableBNR. ${error}`);
|
|
5831
5891
|
|
|
5832
5892
|
Metrics.sendBehavioralMetric(
|
|
@@ -205,10 +205,12 @@ export default class MeetingInfoV2 {
|
|
|
205
205
|
if (directURI) options.directURI = directURI;
|
|
206
206
|
|
|
207
207
|
return this.webex.request(options)
|
|
208
|
-
.then(() => {
|
|
208
|
+
.then((response) => {
|
|
209
209
|
Metrics.sendBehavioralMetric(
|
|
210
210
|
BEHAVIORAL_METRICS.FETCH_MEETING_INFO_V1_SUCCESS
|
|
211
211
|
);
|
|
212
|
+
|
|
213
|
+
return response;
|
|
212
214
|
})
|
|
213
215
|
.catch((err) => {
|
|
214
216
|
if (err?.statusCode === 403) {
|
package/src/members/index.js
CHANGED
|
@@ -759,6 +759,26 @@ export default class Members extends StatelessWebexPlugin {
|
|
|
759
759
|
return this.membersRequest.raiseOrLowerHandMember(options);
|
|
760
760
|
}
|
|
761
761
|
|
|
762
|
+
/**
|
|
763
|
+
* Lower all hands of members in a meeting
|
|
764
|
+
* @param {String} requestingMemberId - id of the participant which requested the lower all hands
|
|
765
|
+
* @returns {Promise}
|
|
766
|
+
* @public
|
|
767
|
+
* @memberof Members
|
|
768
|
+
*/
|
|
769
|
+
lowerAllHands(requestingMemberId) {
|
|
770
|
+
if (!this.locusUrl) {
|
|
771
|
+
return Promise.reject(new ParameterError('The associated locus url for this meetings members object must be defined.'));
|
|
772
|
+
}
|
|
773
|
+
if (!requestingMemberId) {
|
|
774
|
+
return Promise.reject(new ParameterError('The requestingMemberId must be defined to lower all hands in a meeting.'));
|
|
775
|
+
}
|
|
776
|
+
const options = MembersUtil.generateLowerAllHandsMemberOptions(requestingMemberId, this.locusUrl);
|
|
777
|
+
|
|
778
|
+
return this.membersRequest.lowerAllHandsMember(options);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
|
|
762
782
|
/**
|
|
763
783
|
* Transfers the host to another member
|
|
764
784
|
* @param {String} memberId
|
package/src/members/request.js
CHANGED
|
@@ -74,6 +74,16 @@ export default class MembersRequest extends StatelessWebexPlugin {
|
|
|
74
74
|
return this.request(requestParams);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
lowerAllHandsMember(options) {
|
|
78
|
+
if (!options || !options.locusUrl || !options.requestingParticipantId) {
|
|
79
|
+
throw new ParameterError('requestingParticipantId must be defined, and the associated locus url for this meeting object must be defined.');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const requestParams = MembersUtil.getLowerAllHandsMemberRequestParams(options);
|
|
83
|
+
|
|
84
|
+
return this.request(requestParams);
|
|
85
|
+
}
|
|
86
|
+
|
|
77
87
|
transferHostToMember(options) {
|
|
78
88
|
if (!options || !options.locusUrl || !options.memberId || !options.moderator) {
|
|
79
89
|
throw new ParameterError('memberId must be defined, the associated locus url, and the moderator for this meeting object must be defined.');
|
package/src/members/util.js
CHANGED
|
@@ -136,6 +136,11 @@ MembersUtil.generateRaiseHandMemberOptions = (memberId, status, locusUrl) => ({
|
|
|
136
136
|
locusUrl
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
+
MembersUtil.generateLowerAllHandsMemberOptions = (requestingParticipantId, locusUrl) => ({
|
|
140
|
+
requestingParticipantId,
|
|
141
|
+
locusUrl
|
|
142
|
+
});
|
|
143
|
+
|
|
139
144
|
MembersUtil.getMuteMemberRequestParams = (options) => {
|
|
140
145
|
const body = {
|
|
141
146
|
audio: {
|
|
@@ -166,6 +171,22 @@ MembersUtil.getRaiseHandMemberRequestParams = (options) => {
|
|
|
166
171
|
};
|
|
167
172
|
};
|
|
168
173
|
|
|
174
|
+
MembersUtil.getLowerAllHandsMemberRequestParams = (options) => {
|
|
175
|
+
const body = {
|
|
176
|
+
hand: {
|
|
177
|
+
raised: false
|
|
178
|
+
},
|
|
179
|
+
requestingParticipantId: options.requestingParticipantId
|
|
180
|
+
};
|
|
181
|
+
const uri = `${options.locusUrl}/${CONTROLS}`;
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
method: HTTP_VERBS.PATCH,
|
|
185
|
+
uri,
|
|
186
|
+
body
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
|
|
169
190
|
MembersUtil.getTransferHostToMemberRequestParams = (options) => {
|
|
170
191
|
const body = {
|
|
171
192
|
role: {
|
|
@@ -456,13 +456,26 @@ describe('plugin-meetings', () => {
|
|
|
456
456
|
});
|
|
457
457
|
});
|
|
458
458
|
describe('BNR', () => {
|
|
459
|
+
const fakeMediaTrack = () => ({
|
|
460
|
+
id: Date.now().toString(),
|
|
461
|
+
stop: () => {},
|
|
462
|
+
readyState: 'live',
|
|
463
|
+
enabled: true,
|
|
464
|
+
getSettings: () => ({
|
|
465
|
+
sampleRate: 48000
|
|
466
|
+
})
|
|
467
|
+
});
|
|
468
|
+
|
|
459
469
|
class FakeMediaStream {
|
|
460
|
-
constructor() {
|
|
470
|
+
constructor(tracks) {
|
|
461
471
|
this.active = false;
|
|
462
472
|
this.id = '5146425f-c240-48cc-b86b-27d422988fb7';
|
|
473
|
+
this.tracks = tracks;
|
|
463
474
|
}
|
|
464
475
|
|
|
465
476
|
addTrack = () => undefined;
|
|
477
|
+
|
|
478
|
+
getAudioTracks = () => this.tracks;
|
|
466
479
|
}
|
|
467
480
|
|
|
468
481
|
class FakeAudioContext {
|
|
@@ -523,6 +536,12 @@ describe('plugin-meetings', () => {
|
|
|
523
536
|
this.readyState = 'live';
|
|
524
537
|
this.contentHint = '';
|
|
525
538
|
}
|
|
539
|
+
|
|
540
|
+
getSettings() {
|
|
541
|
+
return {
|
|
542
|
+
sampleRate: 48000
|
|
543
|
+
};
|
|
544
|
+
}
|
|
526
545
|
}
|
|
527
546
|
Object.defineProperty(global, 'MediaStream', {
|
|
528
547
|
writable: true,
|
|
@@ -544,24 +563,21 @@ describe('plugin-meetings', () => {
|
|
|
544
563
|
value: FakeMediaStreamTrack,
|
|
545
564
|
});
|
|
546
565
|
|
|
547
|
-
beforeEach(
|
|
566
|
+
beforeEach(() => {
|
|
548
567
|
meeting.canUpdateMedia = sinon.stub().returns(true);
|
|
549
568
|
MeetingUtil.validateOptions = sinon.stub().returns(Promise.resolve());
|
|
550
569
|
MeetingUtil.updateTransceiver = sinon.stub();
|
|
551
|
-
const fakeMediaTrack = () => ({
|
|
552
|
-
stop: () => {},
|
|
553
|
-
readyState: 'live',
|
|
554
|
-
getSettings: () => ({
|
|
555
|
-
sampleRate: 48000
|
|
556
|
-
})
|
|
557
|
-
});
|
|
558
570
|
|
|
559
571
|
meeting.getMediaStreams = sinon.stub().returns(Promise.resolve());
|
|
560
572
|
sinon.replace(meeting, 'addMedia', () => {
|
|
561
573
|
sinon.stub(meeting.mediaProperties, 'audioTrack').value(fakeMediaTrack());
|
|
574
|
+
sinon.stub(meeting.mediaProperties, 'mediaDirection').value({
|
|
575
|
+
receiveAudio: true
|
|
576
|
+
});
|
|
562
577
|
});
|
|
563
|
-
|
|
564
|
-
|
|
578
|
+
|
|
579
|
+
// eslint-disable-next-line no-undef
|
|
580
|
+
MediaUtil.createMediaStream = sinon.stub().returns(new MediaStream([fakeMediaTrack()]));
|
|
565
581
|
});
|
|
566
582
|
|
|
567
583
|
describe('#enableBNR', () => {
|
|
@@ -578,22 +594,37 @@ describe('plugin-meetings', () => {
|
|
|
578
594
|
});
|
|
579
595
|
|
|
580
596
|
describe('after audio attached to meeting', () => {
|
|
597
|
+
beforeEach(async () => {
|
|
598
|
+
await meeting.getMediaStreams();
|
|
599
|
+
await meeting.addMedia();
|
|
600
|
+
});
|
|
601
|
+
|
|
581
602
|
it('should return true for appropriate sample rate', async () => {
|
|
582
603
|
const response = await meeting.enableBNR();
|
|
583
604
|
|
|
605
|
+
assert(Metrics.sendBehavioralMetric.calledOnce);
|
|
606
|
+
assert.calledWith(
|
|
607
|
+
Metrics.sendBehavioralMetric,
|
|
608
|
+
BEHAVIORAL_METRICS.ENABLE_BNR_SUCCESS,
|
|
609
|
+
);
|
|
610
|
+
|
|
584
611
|
assert.equal(response, true);
|
|
585
612
|
});
|
|
586
613
|
|
|
587
|
-
it('should throw error
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
sampleRate: 49000
|
|
593
|
-
})
|
|
594
|
-
});
|
|
614
|
+
it('should throw error if meeting audio is muted', async () => {
|
|
615
|
+
await meeting.getMediaStreams();
|
|
616
|
+
await meeting.addMedia();
|
|
617
|
+
const handleClientRequest = (meeting, mute) => {
|
|
618
|
+
meeting.mediaProperties.audioTrack.enabled = !mute;
|
|
595
619
|
|
|
596
|
-
|
|
620
|
+
return Promise.resolve();
|
|
621
|
+
};
|
|
622
|
+
const isMuted = () => !meeting.mediaProperties.audioTrack.enabled;
|
|
623
|
+
|
|
624
|
+
meeting.mediaId = 'mediaId';
|
|
625
|
+
meeting.audio = {handleClientRequest, isMuted};
|
|
626
|
+
meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
|
|
627
|
+
await meeting.muteAudio();
|
|
597
628
|
await meeting.enableBNR().catch((err) => {
|
|
598
629
|
assert(Metrics.sendBehavioralMetric.calledOnce);
|
|
599
630
|
assert.calledWith(
|
|
@@ -603,29 +634,56 @@ describe('plugin-meetings', () => {
|
|
|
603
634
|
stack: err.stack
|
|
604
635
|
}
|
|
605
636
|
);
|
|
606
|
-
assert.equal(err.message, '
|
|
637
|
+
assert.equal(err.message, 'Cannot enable BNR while meeting is muted');
|
|
607
638
|
});
|
|
608
639
|
});
|
|
609
640
|
|
|
610
|
-
it('should
|
|
611
|
-
const
|
|
641
|
+
it('should throw error for inappropriate sample rate and send error metrics', async () => {
|
|
642
|
+
const fakeMediaTrack = () => ({
|
|
643
|
+
id: Date.now().toString(),
|
|
644
|
+
stop: () => {},
|
|
645
|
+
readyState: 'live',
|
|
646
|
+
getSettings: () => ({
|
|
647
|
+
sampleRate: 49000
|
|
648
|
+
})
|
|
649
|
+
});
|
|
612
650
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
651
|
+
sinon.stub(meeting.mediaProperties, 'audioTrack').value(fakeMediaTrack());
|
|
652
|
+
|
|
653
|
+
// eslint-disable-next-line no-undef
|
|
654
|
+
MediaUtil.createMediaStream = sinon.stub().returns(new MediaStream([fakeMediaTrack()]));
|
|
655
|
+
|
|
656
|
+
await meeting.enableBNR()
|
|
657
|
+
.then(() => {
|
|
658
|
+
assert.fail('The expected Error was not thrown.');
|
|
659
|
+
})
|
|
660
|
+
.catch((err) => {
|
|
661
|
+
assert(Metrics.sendBehavioralMetric.calledOnce);
|
|
662
|
+
assert.calledWith(
|
|
663
|
+
Metrics.sendBehavioralMetric,
|
|
664
|
+
BEHAVIORAL_METRICS.ENABLE_BNR_FAILURE, {
|
|
665
|
+
reason: err.message,
|
|
666
|
+
stack: err.stack
|
|
667
|
+
}
|
|
668
|
+
);
|
|
669
|
+
assert.equal(err.message, 'Sample rate of 49000 is not supported.');
|
|
670
|
+
});
|
|
619
671
|
});
|
|
620
672
|
});
|
|
621
673
|
});
|
|
622
674
|
|
|
623
675
|
describe('#disableBNR', () => {
|
|
676
|
+
beforeEach(async () => {
|
|
677
|
+
await meeting.getMediaStreams();
|
|
678
|
+
await meeting.addMedia();
|
|
679
|
+
});
|
|
680
|
+
|
|
624
681
|
it('should have #disableBnr', () => {
|
|
625
682
|
assert.exists(meeting.disableBNR);
|
|
626
683
|
});
|
|
627
684
|
|
|
628
685
|
it('should return true if bnr is disabled on bnr enabled track', async () => {
|
|
686
|
+
await meeting.enableBNR();
|
|
629
687
|
const response = await meeting.disableBNR();
|
|
630
688
|
|
|
631
689
|
assert.equal(response, true);
|
|
@@ -88,10 +88,14 @@ describe('plugin-meetings', () => {
|
|
|
88
88
|
|
|
89
89
|
describe('#fetchMeetingInfo', () => {
|
|
90
90
|
it('should fetch meeting info for the destination type', async () => {
|
|
91
|
+
const body = {meetingKey: '1234323'};
|
|
92
|
+
const requestResponse = {statusCode: 200, body};
|
|
93
|
+
|
|
91
94
|
sinon.stub(MeetingInfoUtil, 'getDestinationType').returns(Promise.resolve({type: 'MEETING_ID', destination: '123456'}));
|
|
92
|
-
sinon.stub(MeetingInfoUtil, 'getRequestBody').returns(Promise.resolve(
|
|
95
|
+
sinon.stub(MeetingInfoUtil, 'getRequestBody').returns(Promise.resolve(body));
|
|
96
|
+
webex.request.resolves(requestResponse);
|
|
93
97
|
|
|
94
|
-
await meetingInfo.fetchMeetingInfo({
|
|
98
|
+
const result = await meetingInfo.fetchMeetingInfo({
|
|
95
99
|
type: _MEETING_ID_,
|
|
96
100
|
destination: '1234323'
|
|
97
101
|
});
|
|
@@ -99,28 +103,39 @@ describe('plugin-meetings', () => {
|
|
|
99
103
|
assert.calledWith(webex.request, {
|
|
100
104
|
method: 'POST', service: 'webex-appapi-service', resource: 'meetingInfo', body: {meetingKey: '1234323'}
|
|
101
105
|
});
|
|
106
|
+
assert.deepEqual(result, requestResponse);
|
|
102
107
|
|
|
103
108
|
MeetingInfoUtil.getDestinationType.restore();
|
|
104
109
|
MeetingInfoUtil.getRequestBody.restore();
|
|
105
110
|
});
|
|
111
|
+
|
|
106
112
|
it('should fetch meeting info for the personal meeting room type', async () => {
|
|
113
|
+
const body = {meetingKey: '1234323'};
|
|
114
|
+
const requestResponse = {statusCode: 200, body};
|
|
115
|
+
|
|
107
116
|
sinon.stub(MeetingInfoUtil, 'getDestinationType').returns(Promise.resolve({type: 'MEETING_ID', destination: '123456'}));
|
|
108
|
-
sinon.stub(MeetingInfoUtil, 'getRequestBody').returns(Promise.resolve(
|
|
117
|
+
sinon.stub(MeetingInfoUtil, 'getRequestBody').returns(Promise.resolve(body));
|
|
118
|
+
webex.request.resolves(requestResponse);
|
|
109
119
|
|
|
110
|
-
await meetingInfo.fetchMeetingInfo({
|
|
120
|
+
const result = await meetingInfo.fetchMeetingInfo({
|
|
111
121
|
type: _PERSONAL_ROOM_
|
|
112
122
|
});
|
|
113
123
|
|
|
114
124
|
assert.calledWith(webex.request, {
|
|
115
125
|
method: 'POST', service: 'webex-appapi-service', resource: 'meetingInfo', body: {meetingKey: '1234323'}
|
|
116
126
|
});
|
|
127
|
+
assert.deepEqual(result, requestResponse);
|
|
117
128
|
|
|
118
129
|
MeetingInfoUtil.getDestinationType.restore();
|
|
119
130
|
MeetingInfoUtil.getRequestBody.restore();
|
|
120
131
|
});
|
|
121
132
|
|
|
122
133
|
it('should fetch meeting info with provided password and captcha code', async () => {
|
|
123
|
-
|
|
134
|
+
const requestResponse = {statusCode: 200, body: {meetingKey: '1234323'}};
|
|
135
|
+
|
|
136
|
+
webex.request.resolves(requestResponse);
|
|
137
|
+
|
|
138
|
+
const result = await meetingInfo.fetchMeetingInfo('1234323', _MEETING_ID_, 'abc', {id: '999', code: 'aabbcc11'});
|
|
124
139
|
|
|
125
140
|
assert.calledWith(webex.request, {
|
|
126
141
|
method: 'POST',
|
|
@@ -134,6 +149,7 @@ describe('plugin-meetings', () => {
|
|
|
134
149
|
captchaVerifyCode: 'aabbcc11'
|
|
135
150
|
}
|
|
136
151
|
});
|
|
152
|
+
assert.deepEqual(result, requestResponse);
|
|
137
153
|
assert(Metrics.sendBehavioralMetric.calledOnce);
|
|
138
154
|
assert.calledWith(
|
|
139
155
|
Metrics.sendBehavioralMetric,
|
|
@@ -260,5 +260,56 @@ describe('plugin-meetings', () => {
|
|
|
260
260
|
await checkValid(resultPromise, spies, memberId, true, url1);
|
|
261
261
|
});
|
|
262
262
|
});
|
|
263
|
+
|
|
264
|
+
describe('#lowerAllHands', () => {
|
|
265
|
+
const setup = (locusUrl) => {
|
|
266
|
+
const members = createMembers({url: locusUrl});
|
|
267
|
+
|
|
268
|
+
const spies = {
|
|
269
|
+
generateLowerAllHandsMemberOptions: sandbox.spy(MembersUtil, 'generateLowerAllHandsMemberOptions'),
|
|
270
|
+
lowerAllHandsMember: sandbox.spy(members.membersRequest, 'lowerAllHandsMember'),
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
return {members, spies};
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const checkInvalid = async (resultPromise, expectedMessage, spies) => {
|
|
277
|
+
await assert.isRejected(resultPromise, ParameterError, expectedMessage);
|
|
278
|
+
assert.notCalled(spies.generateLowerAllHandsMemberOptions);
|
|
279
|
+
assert.notCalled(spies.lowerAllHandsMember);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const checkValid = async (resultPromise, spies, expectedRequestingMemberId, expectedLocusUrl) => {
|
|
283
|
+
await assert.isFulfilled(resultPromise);
|
|
284
|
+
assert.calledOnceWithExactly(spies.generateLowerAllHandsMemberOptions, expectedRequestingMemberId, expectedLocusUrl);
|
|
285
|
+
assert.calledOnceWithExactly(spies.lowerAllHandsMember, {requestingParticipantId: expectedRequestingMemberId, locusUrl: expectedLocusUrl});
|
|
286
|
+
assert.strictEqual(resultPromise, spies.lowerAllHandsMember.getCall(0).returnValue);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
it('should not make a request if there is no requestingMemberId', async () => {
|
|
290
|
+
const {members, spies} = setup(url1);
|
|
291
|
+
|
|
292
|
+
const resultPromise = members.lowerAllHands();
|
|
293
|
+
|
|
294
|
+
await checkInvalid(resultPromise, 'The requestingMemberId must be defined to lower all hands in a meeting.', spies);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should not make a request if there is no locus url', async () => {
|
|
298
|
+
const {members, spies} = setup();
|
|
299
|
+
|
|
300
|
+
const resultPromise = members.lowerAllHands(uuid.v4());
|
|
301
|
+
|
|
302
|
+
await checkInvalid(resultPromise, 'The associated locus url for this meetings members object must be defined.', spies);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('should make the correct request when called with requestingMemberId', async () => {
|
|
306
|
+
const requestingMemberId = uuid.v4();
|
|
307
|
+
const {members, spies} = setup(url1);
|
|
308
|
+
|
|
309
|
+
const resultPromise = members.lowerAllHands(requestingMemberId);
|
|
310
|
+
|
|
311
|
+
await checkValid(resultPromise, spies, requestingMemberId, url1);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
263
314
|
});
|
|
264
315
|
});
|
|
@@ -3,9 +3,11 @@ import chai from 'chai';
|
|
|
3
3
|
import uuid from 'uuid';
|
|
4
4
|
import chaiAsPromised from 'chai-as-promised';
|
|
5
5
|
import MockWebex from '@webex/test-helper-mock-webex';
|
|
6
|
-
import Meetings from '@webex/plugin-meetings';
|
|
7
6
|
|
|
7
|
+
import Meetings from '@webex/plugin-meetings';
|
|
8
8
|
import MembersRequest from '@webex/plugin-meetings/src/members/request';
|
|
9
|
+
import membersUtil from '@webex/plugin-meetings/src/members/util';
|
|
10
|
+
import ParameterError from '@webex/plugin-meetings/src/common/errors/parameter';
|
|
9
11
|
|
|
10
12
|
const {assert} = chai;
|
|
11
13
|
|
|
@@ -118,5 +120,72 @@ describe('plugin-meetings', () => {
|
|
|
118
120
|
assert.equal(requestParams.body.hand.raised, true);
|
|
119
121
|
});
|
|
120
122
|
});
|
|
123
|
+
|
|
124
|
+
describe('#lowerAllHands', () => {
|
|
125
|
+
const parameterErrorMessage = 'requestingParticipantId must be defined, and the associated locus url for this meeting object must be defined.';
|
|
126
|
+
|
|
127
|
+
const checkInvalid = async (functionParams) => {
|
|
128
|
+
assert.throws(() => membersRequest.lowerAllHandsMember(functionParams), ParameterError, parameterErrorMessage);
|
|
129
|
+
assert(membersRequest.request.notCalled);
|
|
130
|
+
assert(membersUtil.getLowerAllHandsMemberRequestParams.notCalled);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
it('rejects if no options are passed in', async () => {
|
|
134
|
+
checkInvalid();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('rejects if no locusUrl are passed in', async () => {
|
|
138
|
+
checkInvalid({requestingParticipantId: 'test'});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('rejects if no requestingParticipantId are passed in', async () => {
|
|
142
|
+
checkInvalid({locusUrl: 'test'});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('returns a promise', async () => {
|
|
146
|
+
const locusUrl = url1;
|
|
147
|
+
const memberId = 'test1';
|
|
148
|
+
|
|
149
|
+
const options = {
|
|
150
|
+
requestingParticipantId: memberId,
|
|
151
|
+
locusUrl,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
assert.strictEqual(membersRequest.lowerAllHandsMember(options), membersRequest.request.getCall(0).returnValue);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('sends a PATCH to the locus endpoint', async () => {
|
|
158
|
+
const locusUrl = url1;
|
|
159
|
+
const memberId = 'test1';
|
|
160
|
+
|
|
161
|
+
const options = {
|
|
162
|
+
requestingParticipantId: memberId,
|
|
163
|
+
locusUrl,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
const getRequestParamsSpy = sandbox.spy(membersUtil, 'getLowerAllHandsMemberRequestParams');
|
|
168
|
+
|
|
169
|
+
await membersRequest.lowerAllHandsMember(options);
|
|
170
|
+
|
|
171
|
+
assert.calledOnceWithExactly(getRequestParamsSpy, {
|
|
172
|
+
requestingParticipantId: memberId,
|
|
173
|
+
locusUrl: url1
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const requestParams = membersRequest.request.getCall(0).args[0];
|
|
177
|
+
|
|
178
|
+
assert.deepEqual(requestParams, {
|
|
179
|
+
method: 'PATCH',
|
|
180
|
+
uri: `${locusUrl}/controls`,
|
|
181
|
+
body: {
|
|
182
|
+
hand: {
|
|
183
|
+
raised: false
|
|
184
|
+
},
|
|
185
|
+
requestingParticipantId: memberId
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|
|
121
190
|
});
|
|
122
191
|
});
|