@webex/plugin-meetings 3.7.0-next.3 → 3.7.0-next.5

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.
@@ -3,9 +3,10 @@
3
3
  */
4
4
  import {WebexPlugin} from '@webex/webex-core';
5
5
  import {get} from 'lodash';
6
- import {MEETINGS, SELF_ROLES} from '../constants';
6
+ import {HTTP_VERBS, MEETINGS, SELF_ROLES} from '../constants';
7
7
 
8
8
  import WebinarCollection from './collection';
9
+ import LoggerProxy from '../common/logs/logger-proxy';
9
10
 
10
11
  /**
11
12
  * @class Webinar
@@ -22,6 +23,7 @@ const Webinar = WebexPlugin.extend({
22
23
  canManageWebcast: 'boolean', // appears the ability to manage webcast
23
24
  selfIsPanelist: 'boolean', // self is panelist
24
25
  selfIsAttendee: 'boolean', // self is attendee
26
+ practiceSessionEnabled: 'boolean', // practice session enabled
25
27
  },
26
28
 
27
29
  /**
@@ -59,18 +61,48 @@ const Webinar = WebexPlugin.extend({
59
61
  * @returns {{isPromoted: boolean, isDemoted: boolean}} Role transition states
60
62
  */
61
63
  updateRoleChanged(payload) {
64
+ const oldRoles = get(payload, 'oldRoles', []);
65
+ const newRoles = get(payload, 'newRoles', []);
66
+
62
67
  const isPromoted =
63
- get(payload, 'oldRoles', []).includes(SELF_ROLES.ATTENDEE) &&
64
- get(payload, 'newRoles', []).includes(SELF_ROLES.PANELIST);
68
+ oldRoles.includes(SELF_ROLES.ATTENDEE) && newRoles.includes(SELF_ROLES.PANELIST);
65
69
  const isDemoted =
66
- get(payload, 'oldRoles', []).includes(SELF_ROLES.PANELIST) &&
67
- get(payload, 'newRoles', []).includes(SELF_ROLES.ATTENDEE);
68
- this.set('selfIsPanelist', get(payload, 'newRoles', []).includes(SELF_ROLES.PANELIST));
69
- this.set('selfIsAttendee', get(payload, 'newRoles', []).includes(SELF_ROLES.ATTENDEE));
70
- this.updateCanManageWebcast(get(payload, 'newRoles', []).includes(SELF_ROLES.MODERATOR));
70
+ oldRoles.includes(SELF_ROLES.PANELIST) && newRoles.includes(SELF_ROLES.ATTENDEE);
71
+ this.set('selfIsPanelist', newRoles.includes(SELF_ROLES.PANELIST));
72
+ this.set('selfIsAttendee', newRoles.includes(SELF_ROLES.ATTENDEE));
73
+ this.updateCanManageWebcast(newRoles.includes(SELF_ROLES.MODERATOR));
71
74
 
72
75
  return {isPromoted, isDemoted};
73
76
  },
77
+
78
+ /**
79
+ * start or stop practice session for webinar
80
+ * @param {boolean} enabled
81
+ * @returns {Promise}
82
+ */
83
+ setPracticeSessionState(enabled) {
84
+ return this.request({
85
+ method: HTTP_VERBS.PATCH,
86
+ uri: `${this.locusUrl}/controls`,
87
+ body: {
88
+ practiceSession: {
89
+ enabled,
90
+ },
91
+ },
92
+ }).catch((error) => {
93
+ LoggerProxy.logger.error('Meeting:webinar#setPracticeSessionState failed', error);
94
+ throw error;
95
+ });
96
+ },
97
+
98
+ /**
99
+ * update practice session status
100
+ * @param {object} payload
101
+ * @returns {void}
102
+ */
103
+ updatePracticeSessionStatus(payload) {
104
+ this.set('practiceSessionEnabled', payload.enabled);
105
+ },
74
106
  });
75
107
 
76
108
  export default Webinar;
@@ -88,6 +88,11 @@ describe('plugin-meetings', () => {
88
88
  canShowStageView: null,
89
89
  canEnableStageView: null,
90
90
  canDisableStageView: null,
91
+ isPracticeSessionOn : null,
92
+ isPracticeSessionOff : null,
93
+ canStartPracticeSession: null,
94
+ canStopPracticeSession: null,
95
+
91
96
  ...expected,
92
97
  };
93
98
 
@@ -181,7 +186,12 @@ describe('plugin-meetings', () => {
181
186
  'canShowStageView',
182
187
  'canEnableStageView',
183
188
  'canDisableStageView',
184
- ].forEach((key) => {
189
+ 'isPracticeSessionOn',
190
+ 'isPracticeSessionOff',
191
+ 'canStartPracticeSession',
192
+ 'canStopPracticeSession',
193
+
194
+ ].forEach((key) => {
185
195
  it(`get and set for ${key} work as expected`, () => {
186
196
  const inMeetingActions = new InMeetingActions();
187
197
 
@@ -9044,6 +9044,8 @@ describe('plugin-meetings', () => {
9044
9044
  });
9045
9045
 
9046
9046
  it('listens to MEETING_CONTROLS_PRACTICE_SESSION_STATUS_UPDATED', async () => {
9047
+ meeting.webinar.updatePracticeSessionStatus = sinon.stub();
9048
+
9047
9049
  const state = {example: 'value'};
9048
9050
 
9049
9051
  await meeting.locusInfo.emitScoped(
@@ -9052,6 +9054,7 @@ describe('plugin-meetings', () => {
9052
9054
  {state}
9053
9055
  );
9054
9056
 
9057
+ assert.calledOnceWithExactly( meeting.webinar.updatePracticeSessionStatus, state);
9055
9058
  assert.calledWith(
9056
9059
  TriggerProxy.trigger,
9057
9060
  meeting,
@@ -262,5 +262,100 @@ describe('plugin-meetings', () => {
262
262
  testParams(false);
263
263
  });
264
264
  });
265
+
266
+ describe('#getAddMemberBody', () => {
267
+ it('returns the correct body with email address and roles', () => {
268
+ const options = {
269
+ invitee: {
270
+ emailAddress: 'test@example.com',
271
+ roles: ['role1', 'role2'],
272
+ },
273
+ alertIfActive: true,
274
+ };
275
+
276
+ assert.deepEqual(MembersUtil.getAddMemberBody(options), {
277
+ invitees: [
278
+ {
279
+ address: 'test@example.com',
280
+ roles: ['role1', 'role2'],
281
+ },
282
+ ],
283
+ alertIfActive: true,
284
+ });
285
+ });
286
+
287
+ it('returns the correct body with phone number and no roles', () => {
288
+ const options = {
289
+ invitee: {
290
+ phoneNumber: '1234567890',
291
+ },
292
+ alertIfActive: false,
293
+ };
294
+
295
+ assert.deepEqual(MembersUtil.getAddMemberBody(options), {
296
+ invitees: [
297
+ {
298
+ address: '1234567890',
299
+ },
300
+ ],
301
+ alertIfActive: false,
302
+ });
303
+ });
304
+
305
+ it('returns the correct body with fallback to email', () => {
306
+ const options = {
307
+ invitee: {
308
+ email: 'fallback@example.com',
309
+ },
310
+ alertIfActive: true,
311
+ };
312
+
313
+ assert.deepEqual(MembersUtil.getAddMemberBody(options), {
314
+ invitees: [
315
+ {
316
+ address: 'fallback@example.com',
317
+ },
318
+ ],
319
+ alertIfActive: true,
320
+ });
321
+ });
322
+
323
+ it('handles missing `alertIfActive` gracefully', () => {
324
+ const options = {
325
+ invitee: {
326
+ emailAddress: 'test@example.com',
327
+ roles: ['role1'],
328
+ },
329
+ };
330
+
331
+ assert.deepEqual(MembersUtil.getAddMemberBody(options), {
332
+ invitees: [
333
+ {
334
+ address: 'test@example.com',
335
+ roles: ['role1'],
336
+ },
337
+ ],
338
+ alertIfActive: undefined,
339
+ });
340
+ });
341
+
342
+ it('ignores roles if not provided', () => {
343
+ const options = {
344
+ invitee: {
345
+ emailAddress: 'test@example.com',
346
+ },
347
+ alertIfActive: false,
348
+ };
349
+
350
+ assert.deepEqual(MembersUtil.getAddMemberBody(options), {
351
+ invitees: [
352
+ {
353
+ address: 'test@example.com',
354
+ },
355
+ ],
356
+ alertIfActive: false,
357
+ });
358
+ });
359
+ });
265
360
  });
266
361
  });
@@ -15,6 +15,7 @@ describe('ClusterReachability', () => {
15
15
  let previousRTCPeerConnection;
16
16
  let clusterReachability;
17
17
  let fakePeerConnection;
18
+ let gatherIceCandidatesSpy;
18
19
 
19
20
  const emittedEvents: Record<Events, (ResultEventData | ClientMediaIpsUpdatedEventData)[]> = {
20
21
  [Events.resultReady]: [],
@@ -44,6 +45,8 @@ describe('ClusterReachability', () => {
44
45
  xtls: ['stun:xtls1.webex.com', 'stun:xtls2.webex.com:443'],
45
46
  });
46
47
 
48
+ gatherIceCandidatesSpy = sinon.spy(clusterReachability, 'gatherIceCandidates');
49
+
47
50
  resetEmittedEvents();
48
51
 
49
52
  clusterReachability.on(Events.resultReady, (data: ResultEventData) => {
@@ -151,6 +154,10 @@ describe('ClusterReachability', () => {
151
154
  assert.calledOnceWithExactly(fakePeerConnection.createOffer, {offerToReceiveAudio: true});
152
155
  assert.calledOnce(fakePeerConnection.setLocalDescription);
153
156
 
157
+ // Make sure that gatherIceCandidates is called before setLocalDescription
158
+ // as setLocalDescription triggers the ICE gathering process
159
+ assert.isTrue(gatherIceCandidatesSpy.calledBefore(fakePeerConnection.setLocalDescription));
160
+
154
161
  clusterReachability.abort();
155
162
  await promise;
156
163
 
@@ -122,5 +122,52 @@ describe('plugin-meetings', () => {
122
122
  });
123
123
  });
124
124
 
125
+ describe("#setPracticeSessionState", () => {
126
+ [true, false].forEach((enabled) => {
127
+ it(`sends a PATCH request to ${enabled ? "enable" : "disable"} the practice session`, async () => {
128
+ const result = await webinar.setPracticeSessionState(enabled);
129
+ assert.calledOnce(webex.request);
130
+ assert.calledWith(webex.request, {
131
+ method: "PATCH",
132
+ uri: `${webinar.locusUrl}/controls`,
133
+ body: {
134
+ practiceSession: { enabled }
135
+ }
136
+ });
137
+ assert.equal(result, "REQUEST_RETURN_VALUE", "should return the resolved value from the request");
138
+ });
139
+ });
140
+
141
+ it('handles API call failures gracefully', async () => {
142
+ webex.request.rejects(new Error('API_ERROR'));
143
+ const errorLogger = sinon.stub(LoggerProxy.logger, 'error');
144
+
145
+ try {
146
+ await webinar.setPracticeSessionState(true);
147
+ assert.fail('setPracticeSessionState should throw an error');
148
+ } catch (error) {
149
+ assert.equal(error.message, 'API_ERROR', 'should throw the correct error');
150
+ assert.calledOnce(errorLogger);
151
+ assert.calledWith(errorLogger, 'Meeting:webinar#setPracticeSessionState failed', sinon.match.instanceOf(Error));
152
+ }
153
+
154
+ errorLogger.restore();
155
+ });
156
+ });
157
+
158
+ describe('#updatePracticeSessionStatus', () => {
159
+ it('sets PS state true', () => {
160
+ webinar.updatePracticeSessionStatus({enabled: true});
161
+
162
+ assert.equal(webinar.practiceSessionEnabled, true);
163
+ });
164
+ it('sets PS state true', () => {
165
+ webinar.updatePracticeSessionStatus({enabled: false});
166
+
167
+ assert.equal(webinar.practiceSessionEnabled, false);
168
+ });
169
+ });
170
+
171
+
125
172
  })
126
173
  })