@webex/plugin-meetings 3.12.0-next.9 → 3.12.0-next.90

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 (213) hide show
  1. package/AGENTS.md +9 -0
  2. package/dist/aiEnableRequest/index.js +15 -2
  3. package/dist/aiEnableRequest/index.js.map +1 -1
  4. package/dist/breakouts/breakout.js +8 -3
  5. package/dist/breakouts/breakout.js.map +1 -1
  6. package/dist/breakouts/index.js +26 -2
  7. package/dist/breakouts/index.js.map +1 -1
  8. package/dist/config.js +2 -2
  9. package/dist/config.js.map +1 -1
  10. package/dist/constants.js +35 -9
  11. package/dist/constants.js.map +1 -1
  12. package/dist/controls-options-manager/constants.js +11 -1
  13. package/dist/controls-options-manager/constants.js.map +1 -1
  14. package/dist/controls-options-manager/index.js +67 -29
  15. package/dist/controls-options-manager/index.js.map +1 -1
  16. package/dist/controls-options-manager/util.js +91 -0
  17. package/dist/controls-options-manager/util.js.map +1 -1
  18. package/dist/hashTree/constants.js +13 -1
  19. package/dist/hashTree/constants.js.map +1 -1
  20. package/dist/hashTree/hashTreeParser.js +880 -382
  21. package/dist/hashTree/hashTreeParser.js.map +1 -1
  22. package/dist/hashTree/utils.js +42 -0
  23. package/dist/hashTree/utils.js.map +1 -1
  24. package/dist/index.js +7 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/interceptors/dataChannelAuthToken.js +75 -15
  27. package/dist/interceptors/dataChannelAuthToken.js.map +1 -1
  28. package/dist/interceptors/locusRetry.js +23 -8
  29. package/dist/interceptors/locusRetry.js.map +1 -1
  30. package/dist/interpretation/index.js +10 -1
  31. package/dist/interpretation/index.js.map +1 -1
  32. package/dist/interpretation/interpretation.types.js +7 -0
  33. package/dist/interpretation/interpretation.types.js.map +1 -0
  34. package/dist/interpretation/siLanguage.js +1 -1
  35. package/dist/locus-info/controlsUtils.js +4 -1
  36. package/dist/locus-info/controlsUtils.js.map +1 -1
  37. package/dist/locus-info/index.js +303 -88
  38. package/dist/locus-info/index.js.map +1 -1
  39. package/dist/locus-info/infoUtils.js +36 -5
  40. package/dist/locus-info/infoUtils.js.map +1 -1
  41. package/dist/locus-info/types.js +19 -0
  42. package/dist/locus-info/types.js.map +1 -1
  43. package/dist/media/index.js +3 -1
  44. package/dist/media/index.js.map +1 -1
  45. package/dist/media/properties.js +1 -0
  46. package/dist/media/properties.js.map +1 -1
  47. package/dist/meeting/in-meeting-actions.js +5 -1
  48. package/dist/meeting/in-meeting-actions.js.map +1 -1
  49. package/dist/meeting/index.js +1233 -793
  50. package/dist/meeting/index.js.map +1 -1
  51. package/dist/meeting/locusMediaRequest.js +229 -82
  52. package/dist/meeting/locusMediaRequest.js.map +1 -1
  53. package/dist/meeting/muteState.js +10 -1
  54. package/dist/meeting/muteState.js.map +1 -1
  55. package/dist/meeting/request.js +5 -2
  56. package/dist/meeting/request.js.map +1 -1
  57. package/dist/meeting/util.js +35 -2
  58. package/dist/meeting/util.js.map +1 -1
  59. package/dist/meeting-info/meeting-info-v2.js +2 -2
  60. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  61. package/dist/meetings/index.js +231 -118
  62. package/dist/meetings/index.js.map +1 -1
  63. package/dist/meetings/meetings.types.js +6 -1
  64. package/dist/meetings/meetings.types.js.map +1 -1
  65. package/dist/meetings/request.js +39 -0
  66. package/dist/meetings/request.js.map +1 -1
  67. package/dist/meetings/util.js +79 -5
  68. package/dist/meetings/util.js.map +1 -1
  69. package/dist/member/index.js +10 -0
  70. package/dist/member/index.js.map +1 -1
  71. package/dist/member/types.js.map +1 -1
  72. package/dist/member/util.js +30 -0
  73. package/dist/member/util.js.map +1 -1
  74. package/dist/members/index.js +101 -43
  75. package/dist/members/index.js.map +1 -1
  76. package/dist/members/util.js +8 -1
  77. package/dist/members/util.js.map +1 -1
  78. package/dist/metrics/constants.js +5 -1
  79. package/dist/metrics/constants.js.map +1 -1
  80. package/dist/multistream/codec/constants.js +63 -0
  81. package/dist/multistream/codec/constants.js.map +1 -0
  82. package/dist/multistream/mediaRequestManager.js +62 -15
  83. package/dist/multistream/mediaRequestManager.js.map +1 -1
  84. package/dist/multistream/receiveSlot.js +9 -0
  85. package/dist/multistream/receiveSlot.js.map +1 -1
  86. package/dist/reachability/index.js +10 -13
  87. package/dist/reachability/index.js.map +1 -1
  88. package/dist/reachability/reachability.types.js +2 -0
  89. package/dist/reachability/reachability.types.js.map +1 -1
  90. package/dist/reachability/util.js +45 -0
  91. package/dist/reachability/util.js.map +1 -1
  92. package/dist/reactions/reactions.type.js.map +1 -1
  93. package/dist/recording-controller/index.js +1 -3
  94. package/dist/recording-controller/index.js.map +1 -1
  95. package/dist/types/config.d.ts +2 -2
  96. package/dist/types/constants.d.ts +12 -1
  97. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  98. package/dist/types/controls-options-manager/index.d.ts +16 -0
  99. package/dist/types/hashTree/constants.d.ts +2 -0
  100. package/dist/types/hashTree/hashTreeParser.d.ts +146 -17
  101. package/dist/types/hashTree/utils.d.ts +18 -0
  102. package/dist/types/index.d.ts +3 -0
  103. package/dist/types/interceptors/locusRetry.d.ts +4 -4
  104. package/dist/types/interpretation/interpretation.types.d.ts +10 -0
  105. package/dist/types/locus-info/index.d.ts +50 -6
  106. package/dist/types/locus-info/infoUtils.d.ts +8 -0
  107. package/dist/types/locus-info/types.d.ts +21 -1
  108. package/dist/types/media/properties.d.ts +1 -0
  109. package/dist/types/meeting/in-meeting-actions.d.ts +4 -0
  110. package/dist/types/meeting/index.d.ts +94 -7
  111. package/dist/types/meeting/locusMediaRequest.d.ts +12 -0
  112. package/dist/types/meeting/request.d.ts +1 -0
  113. package/dist/types/meeting/util.d.ts +9 -0
  114. package/dist/types/meetings/index.d.ts +30 -18
  115. package/dist/types/meetings/meetings.types.d.ts +15 -0
  116. package/dist/types/meetings/request.d.ts +14 -0
  117. package/dist/types/member/index.d.ts +1 -0
  118. package/dist/types/member/types.d.ts +1 -0
  119. package/dist/types/member/util.d.ts +9 -0
  120. package/dist/types/members/index.d.ts +15 -2
  121. package/dist/types/members/util.d.ts +1 -5
  122. package/dist/types/metrics/constants.d.ts +4 -0
  123. package/dist/types/multistream/codec/constants.d.ts +7 -0
  124. package/dist/types/multistream/mediaRequestManager.d.ts +22 -5
  125. package/dist/types/reachability/index.d.ts +1 -1
  126. package/dist/types/reachability/reachability.types.d.ts +7 -0
  127. package/dist/types/reachability/util.d.ts +17 -0
  128. package/dist/types/reactions/reactions.type.d.ts +3 -0
  129. package/dist/webinar/index.js +305 -159
  130. package/dist/webinar/index.js.map +1 -1
  131. package/package.json +22 -22
  132. package/src/aiEnableRequest/index.ts +16 -0
  133. package/src/breakouts/breakout.ts +3 -1
  134. package/src/breakouts/index.ts +31 -0
  135. package/src/config.ts +2 -2
  136. package/src/constants.ts +20 -3
  137. package/src/controls-options-manager/constants.ts +14 -1
  138. package/src/controls-options-manager/index.ts +87 -28
  139. package/src/controls-options-manager/util.ts +81 -1
  140. package/src/hashTree/constants.ts +16 -0
  141. package/src/hashTree/hashTreeParser.ts +580 -196
  142. package/src/hashTree/utils.ts +36 -0
  143. package/src/index.ts +6 -0
  144. package/src/interceptors/dataChannelAuthToken.ts +88 -12
  145. package/src/interceptors/locusRetry.ts +25 -4
  146. package/src/interpretation/index.ts +27 -9
  147. package/src/interpretation/interpretation.types.ts +11 -0
  148. package/src/locus-info/controlsUtils.ts +3 -1
  149. package/src/locus-info/index.ts +304 -100
  150. package/src/locus-info/infoUtils.ts +41 -6
  151. package/src/locus-info/types.ts +25 -1
  152. package/src/media/index.ts +3 -0
  153. package/src/media/properties.ts +1 -0
  154. package/src/meeting/in-meeting-actions.ts +8 -0
  155. package/src/meeting/index.ts +494 -67
  156. package/src/meeting/locusMediaRequest.ts +106 -6
  157. package/src/meeting/muteState.ts +10 -1
  158. package/src/meeting/request.ts +11 -0
  159. package/src/meeting/util.ts +47 -5
  160. package/src/meeting-info/meeting-info-v2.ts +4 -2
  161. package/src/meetings/index.ts +133 -78
  162. package/src/meetings/meetings.types.ts +19 -0
  163. package/src/meetings/request.ts +43 -0
  164. package/src/meetings/util.ts +97 -1
  165. package/src/member/index.ts +10 -0
  166. package/src/member/types.ts +1 -0
  167. package/src/member/util.ts +29 -0
  168. package/src/members/index.ts +70 -7
  169. package/src/members/util.ts +12 -4
  170. package/src/metrics/constants.ts +4 -0
  171. package/src/multistream/codec/constants.ts +58 -0
  172. package/src/multistream/mediaRequestManager.ts +119 -28
  173. package/src/multistream/receiveSlot.ts +18 -0
  174. package/src/reachability/index.ts +19 -15
  175. package/src/reachability/reachability.types.ts +11 -0
  176. package/src/reachability/util.ts +36 -0
  177. package/src/reactions/reactions.type.ts +3 -0
  178. package/src/recording-controller/index.ts +1 -2
  179. package/src/webinar/index.ts +214 -36
  180. package/test/unit/spec/aiEnableRequest/index.ts +86 -0
  181. package/test/unit/spec/breakouts/breakout.ts +9 -3
  182. package/test/unit/spec/breakouts/index.ts +49 -0
  183. package/test/unit/spec/controls-options-manager/index.js +244 -29
  184. package/test/unit/spec/controls-options-manager/util.js +165 -0
  185. package/test/unit/spec/hashTree/hashTreeParser.ts +1838 -180
  186. package/test/unit/spec/hashTree/utils.ts +125 -1
  187. package/test/unit/spec/interceptors/dataChannelAuthToken.ts +196 -0
  188. package/test/unit/spec/interceptors/locusRetry.ts +205 -4
  189. package/test/unit/spec/interpretation/index.ts +26 -4
  190. package/test/unit/spec/locus-info/controlsUtils.js +172 -57
  191. package/test/unit/spec/locus-info/index.js +499 -81
  192. package/test/unit/spec/locus-info/infoUtils.js +184 -1
  193. package/test/unit/spec/media/index.ts +31 -0
  194. package/test/unit/spec/meeting/in-meeting-actions.ts +4 -0
  195. package/test/unit/spec/meeting/index.js +1615 -42
  196. package/test/unit/spec/meeting/locusMediaRequest.ts +302 -4
  197. package/test/unit/spec/meeting/muteState.js +81 -0
  198. package/test/unit/spec/meeting/request.js +12 -0
  199. package/test/unit/spec/meeting/utils.js +157 -1
  200. package/test/unit/spec/meeting-info/meetinginfov2.js +19 -10
  201. package/test/unit/spec/meetings/index.js +363 -36
  202. package/test/unit/spec/meetings/request.js +141 -0
  203. package/test/unit/spec/meetings/utils.js +189 -0
  204. package/test/unit/spec/member/index.js +7 -0
  205. package/test/unit/spec/member/util.js +89 -0
  206. package/test/unit/spec/members/index.js +216 -0
  207. package/test/unit/spec/members/request.js +37 -1
  208. package/test/unit/spec/members/utils.js +63 -0
  209. package/test/unit/spec/multistream/mediaRequestManager.ts +501 -37
  210. package/test/unit/spec/reachability/index.ts +59 -46
  211. package/test/unit/spec/reachability/util.ts +31 -0
  212. package/test/unit/spec/recording-controller/index.js +9 -8
  213. package/test/unit/spec/webinar/index.ts +329 -28
@@ -1,5 +1,6 @@
1
1
  import ControlsOptionsManager from '@webex/plugin-meetings/src/controls-options-manager';
2
2
  import Util from '@webex/plugin-meetings/src/controls-options-manager/util';
3
+ import ParameterError from '@webex/plugin-meetings/src/common/errors/parameter';
3
4
  import sinon from 'sinon';
4
5
  import {assert} from '@webex/test-helper-chai';
5
6
  import { HTTP_VERBS } from '@webex/plugin-meetings/src/constants';
@@ -26,6 +27,7 @@ describe('plugin-meetings', () => {
26
27
  beforeEach(() => {
27
28
  request = {
28
29
  request: sinon.stub().returns(Promise.resolve()),
30
+ locusDeltaRequest: sinon.stub().returns(Promise.resolve()),
29
31
  };
30
32
 
31
33
  manager = new ControlsOptionsManager(request);
@@ -58,11 +60,11 @@ describe('plugin-meetings', () => {
58
60
 
59
61
  const result = manager.setMuteOnEntry(true);
60
62
 
61
- assert.calledWith(request.request, { uri: 'test/id/controls',
63
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
62
64
  body: { muteOnEntry: { enabled: true } },
63
65
  method: HTTP_VERBS.PATCH});
64
66
 
65
- assert.deepEqual(result, request.request.firstCall.returnValue);
67
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
66
68
  });
67
69
 
68
70
  it('can set mute on entry when the display hint is available enabled=false', () => {
@@ -70,11 +72,24 @@ describe('plugin-meetings', () => {
70
72
 
71
73
  const result = manager.setMuteOnEntry(false);
72
74
 
73
- assert.calledWith(request.request, { uri: 'test/id/controls',
75
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
74
76
  body: { muteOnEntry: { enabled: false } },
75
77
  method: HTTP_VERBS.PATCH});
76
78
 
77
- assert.deepEqual(result, request.request.firstCall.returnValue);
79
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
80
+ });
81
+
82
+ it('should send setMuteOnEntry to locusUrl without authorizingLocusUrl when in breakout', () => {
83
+ manager.setDisplayHints(['ENABLE_MUTE_ON_ENTRY']);
84
+ manager.mainLocusUrl = 'test/main';
85
+
86
+ const result = manager.setMuteOnEntry(true);
87
+
88
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
89
+ body: { muteOnEntry: { enabled: true } },
90
+ method: HTTP_VERBS.PATCH});
91
+
92
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
78
93
  });
79
94
  });
80
95
 
@@ -100,11 +115,11 @@ describe('plugin-meetings', () => {
100
115
 
101
116
  const result = manager.setDisallowUnmute(true);
102
117
 
103
- assert.calledWith(request.request, { uri: 'test/id/controls',
118
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
104
119
  body: { disallowUnmute: { enabled: true } },
105
120
  method: HTTP_VERBS.PATCH});
106
121
 
107
- assert.deepEqual(result, request.request.firstCall.returnValue);
122
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
108
123
  });
109
124
 
110
125
  it('can set allow unmute when DISABLE_HARD_MUTE display hint is available', () => {
@@ -112,11 +127,24 @@ describe('plugin-meetings', () => {
112
127
 
113
128
  const result = manager.setDisallowUnmute(false);
114
129
 
115
- assert.calledWith(request.request, { uri: 'test/id/controls',
130
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
116
131
  body: { disallowUnmute: { enabled: false } },
117
132
  method: HTTP_VERBS.PATCH});
118
133
 
119
- assert.deepEqual(result, request.request.firstCall.returnValue);
134
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
135
+ });
136
+
137
+ it('should send setDisallowUnmute to locusUrl without authorizingLocusUrl when in breakout', () => {
138
+ manager.setDisplayHints(['ENABLE_HARD_MUTE']);
139
+ manager.mainLocusUrl = 'test/main';
140
+
141
+ const result = manager.setDisallowUnmute(true);
142
+
143
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
144
+ body: { disallowUnmute: { enabled: true } },
145
+ method: HTTP_VERBS.PATCH});
146
+
147
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
120
148
  });
121
149
  });
122
150
  });
@@ -127,6 +155,7 @@ describe('plugin-meetings', () => {
127
155
  beforeEach(() => {
128
156
  request = {
129
157
  request: sinon.stub().resolves(),
158
+ locusDeltaRequest: sinon.stub().resolves(),
130
159
  };
131
160
 
132
161
  manager = new ControlsOptionsManager(request);
@@ -138,6 +167,18 @@ describe('plugin-meetings', () => {
138
167
  });
139
168
  });
140
169
 
170
+ it('should reject with ParameterError when locusUrl is not set', () => {
171
+ const noLocusManager = new ControlsOptionsManager(request);
172
+
173
+ const result = noLocusManager.update({scope: 'audio', properties: {muted: true}});
174
+
175
+ assert.notCalled(request.request);
176
+ return assert.isRejected(result).then((err) => {
177
+ assert.instanceOf(err, ParameterError);
178
+ assert.match(err.message, /locusUrl.*must be defined/);
179
+ });
180
+ });
181
+
141
182
  it('should throw an error if the scope is not supported', () => {
142
183
  const scope = 'invalid';
143
184
 
@@ -163,7 +204,7 @@ describe('plugin-meetings', () => {
163
204
 
164
205
  return manager.update(audio, reactions)
165
206
  .then(() => {
166
- assert.calledWith(request.request, {
207
+ assert.calledWith(request.locusDeltaRequest, {
167
208
  uri: 'test/id/controls',
168
209
  body: {
169
210
  audio: audio.properties,
@@ -171,7 +212,7 @@ describe('plugin-meetings', () => {
171
212
  method: HTTP_VERBS.PATCH,
172
213
  });
173
214
 
174
- assert.calledWith(request.request, {
215
+ assert.calledWith(request.locusDeltaRequest, {
175
216
  uri: 'test/id/controls',
176
217
  body: {
177
218
  reactions: reactions.properties,
@@ -203,7 +244,7 @@ describe('plugin-meetings', () => {
203
244
  });
204
245
  });
205
246
 
206
- it('should call request with mainLocusUrl and locusUrl as authorizingLocusUrl if mainLocusUrl is exist and not same with locusUrl', () => {
247
+ it('should send audio controls to locusUrl without authorizingLocusUrl and non-audio to mainLocusUrl with authorizingLocusUrl when in breakout', () => {
207
248
  const restorable = Util.canUpdate;
208
249
  Util.canUpdate = sinon.stub().returns(true);
209
250
  manager.mainLocusUrl = 'test/main';
@@ -213,20 +254,64 @@ describe('plugin-meetings', () => {
213
254
 
214
255
  return manager.update(audio, reactions)
215
256
  .then(() => {
257
+ // Audio controls go directly to current locusUrl (no cross-locus authorization)
258
+ assert.calledWith(request.locusDeltaRequest, {
259
+ uri: 'test/id/controls',
260
+ body: {
261
+ audio: audio.properties,
262
+ },
263
+ method: HTTP_VERBS.PATCH,
264
+ });
265
+
266
+ // Non-audio controls go to mainLocusUrl with authorizingLocusUrl
216
267
  assert.calledWith(request.request, {
217
268
  uri: 'test/main/controls',
218
269
  body: {
219
- audio: audio.properties,
270
+ reactions: reactions.properties,
220
271
  authorizingLocusUrl: 'test/id'
221
272
  },
222
273
  method: HTTP_VERBS.PATCH,
223
274
  });
224
275
 
276
+ Util.canUpdate = restorable;
277
+ });
278
+ });
279
+
280
+ it('should send audio controls to locusUrl without authorizingLocusUrl when in breakout', () => {
281
+ const restorable = Util.canUpdate;
282
+ Util.canUpdate = sinon.stub().returns(true);
283
+ manager.mainLocusUrl = 'test/main';
284
+
285
+ const audio = {scope: 'audio', properties: {muted: true, disallowUnmute: false}};
286
+
287
+ return manager.update(audio)
288
+ .then(() => {
289
+ assert.calledWith(request.locusDeltaRequest, {
290
+ uri: 'test/id/controls',
291
+ body: {
292
+ audio: audio.properties,
293
+ },
294
+ method: HTTP_VERBS.PATCH,
295
+ });
296
+
297
+ Util.canUpdate = restorable;
298
+ });
299
+ });
300
+
301
+ it('should send non-audio controls to mainLocusUrl with authorizingLocusUrl when in breakout', () => {
302
+ const restorable = Util.canUpdate;
303
+ Util.canUpdate = sinon.stub().returns(true);
304
+ manager.mainLocusUrl = 'test/main';
305
+
306
+ const reactions = {scope: 'reactions', properties: {enabled: true}};
307
+
308
+ return manager.update(reactions)
309
+ .then(() => {
225
310
  assert.calledWith(request.request, {
226
311
  uri: 'test/main/controls',
227
312
  body: {
228
313
  reactions: reactions.properties,
229
- authorizingLocusUrl: 'test/id'
314
+ authorizingLocusUrl: 'test/id',
230
315
  },
231
316
  method: HTTP_VERBS.PATCH,
232
317
  });
@@ -234,6 +319,110 @@ describe('plugin-meetings', () => {
234
319
  Util.canUpdate = restorable;
235
320
  });
236
321
  });
322
+
323
+ describe('viewTheParticipantList fill-in', () => {
324
+ let restorable;
325
+
326
+ afterEach(() => {
327
+ Util.canUpdate = restorable;
328
+ });
329
+
330
+ describe('when meeting is a webinar', () => {
331
+ beforeEach(() => {
332
+ restorable = Util.canUpdate;
333
+ Util.canUpdate = sinon.stub().returns(true);
334
+ manager.set({
335
+ locusUrl: 'test/id',
336
+ displayHints: [],
337
+ getControls: () => ({viewTheParticipantList: {enabled: true, panelistEnabled: true, attendeeCount: true}}),
338
+ isWebinar: () => true,
339
+ });
340
+ });
341
+
342
+ it('should fill in all undefined properties from current state', () => {
343
+ const control = {scope: 'viewTheParticipantList', properties: {}};
344
+
345
+ return manager.update(control).then(() => {
346
+ assert.calledOnceWithExactly(request.locusDeltaRequest, {
347
+ uri: 'test/id/controls',
348
+ body: {
349
+ viewTheParticipantList: {enabled: true, panelistEnabled: true, attendeeCount: true},
350
+ },
351
+ method: HTTP_VERBS.PATCH,
352
+ });
353
+ });
354
+ });
355
+
356
+ it('should keep explicitly provided properties and fill in the rest', () => {
357
+ const control = {scope: 'viewTheParticipantList', properties: {enabled: false}};
358
+
359
+ return manager.update(control).then(() => {
360
+ assert.calledOnceWithExactly(request.locusDeltaRequest, {
361
+ uri: 'test/id/controls',
362
+ body: {
363
+ viewTheParticipantList: {enabled: false, panelistEnabled: true, attendeeCount: true},
364
+ },
365
+ method: HTTP_VERBS.PATCH,
366
+ });
367
+ });
368
+ });
369
+
370
+ it('should not fill in properties when all are explicitly provided', () => {
371
+ const control = {scope: 'viewTheParticipantList', properties: {enabled: false, panelistEnabled: false, attendeeCount: false}};
372
+
373
+ return manager.update(control).then(() => {
374
+ assert.calledOnceWithExactly(request.locusDeltaRequest, {
375
+ uri: 'test/id/controls',
376
+ body: {
377
+ viewTheParticipantList: {enabled: false, panelistEnabled: false, attendeeCount: false},
378
+ },
379
+ method: HTTP_VERBS.PATCH,
380
+ });
381
+ });
382
+ });
383
+ });
384
+
385
+ describe('when meeting is not a webinar', () => {
386
+ beforeEach(() => {
387
+ restorable = Util.canUpdate;
388
+ Util.canUpdate = sinon.stub().returns(true);
389
+ manager.set({
390
+ locusUrl: 'test/id',
391
+ displayHints: [],
392
+ getControls: () => ({viewTheParticipantList: {enabled: true, panelistEnabled: true, attendeeCount: true}}),
393
+ isWebinar: () => false,
394
+ });
395
+ });
396
+
397
+ it('should only send enabled property', () => {
398
+ const control = {scope: 'viewTheParticipantList', properties: {}};
399
+
400
+ return manager.update(control).then(() => {
401
+ assert.calledOnceWithExactly(request.locusDeltaRequest, {
402
+ uri: 'test/id/controls',
403
+ body: {
404
+ viewTheParticipantList: {enabled: true},
405
+ },
406
+ method: HTTP_VERBS.PATCH,
407
+ });
408
+ });
409
+ });
410
+
411
+ it('should not include panelistEnabled or attendeeCount even if explicitly provided', () => {
412
+ const control = {scope: 'viewTheParticipantList', properties: {enabled: false, panelistEnabled: true, attendeeCount: true}};
413
+
414
+ return manager.update(control).then(() => {
415
+ assert.calledOnceWithExactly(request.locusDeltaRequest, {
416
+ uri: 'test/id/controls',
417
+ body: {
418
+ viewTheParticipantList: {enabled: false},
419
+ },
420
+ method: HTTP_VERBS.PATCH,
421
+ });
422
+ });
423
+ });
424
+ });
425
+ });
237
426
  });
238
427
 
239
428
  describe('Mute/Unmute All', () => {
@@ -241,6 +430,7 @@ describe('plugin-meetings', () => {
241
430
  beforeEach(() => {
242
431
  request = {
243
432
  request: sinon.stub().returns(Promise.resolve()),
433
+ locusDeltaRequest: sinon.stub().returns(Promise.resolve()),
244
434
  };
245
435
 
246
436
  manager = new ControlsOptionsManager(request);
@@ -252,6 +442,18 @@ describe('plugin-meetings', () => {
252
442
  })
253
443
  });
254
444
 
445
+ it('should reject with ParameterError when locusUrl is not set', () => {
446
+ const noLocusManager = new ControlsOptionsManager(request);
447
+
448
+ const result = noLocusManager.setMuteAll(true, true, true);
449
+
450
+ assert.notCalled(request.request);
451
+ return assert.isRejected(result).then((err) => {
452
+ assert.instanceOf(err, ParameterError);
453
+ assert.match(err.message, /locusUrl.*must be defined/);
454
+ });
455
+ });
456
+
255
457
  it('rejects when correct display hint is not present mutedEnabled=false', () => {
256
458
  const result = manager.setMuteAll(false, false, false);
257
459
 
@@ -273,11 +475,11 @@ describe('plugin-meetings', () => {
273
475
 
274
476
  const result = manager.setMuteAll(true, true, true);
275
477
 
276
- assert.calledWith(request.request, { uri: 'test/id/controls',
478
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
277
479
  body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true } },
278
480
  method: HTTP_VERBS.PATCH});
279
481
 
280
- assert.deepEqual(result, request.request.firstCall.returnValue);
482
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
281
483
  });
282
484
 
283
485
  it('can set mute all when the display hint is available mutedEnabled=true', () => {
@@ -285,11 +487,11 @@ describe('plugin-meetings', () => {
285
487
 
286
488
  const result = manager.setMuteAll(true, true, true);
287
489
 
288
- assert.calledWith(request.request, { uri: 'test/id/controls',
490
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
289
491
  body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true } },
290
492
  method: HTTP_VERBS.PATCH});
291
493
 
292
- assert.deepEqual(result, request.request.firstCall.returnValue);
494
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
293
495
  });
294
496
 
295
497
  it('can set mute all when the display hint is available mutedEnabled=true', () => {
@@ -297,11 +499,11 @@ describe('plugin-meetings', () => {
297
499
 
298
500
  const result = manager.setMuteAll(true, true, true);
299
501
 
300
- assert.calledWith(request.request, { uri: 'test/id/controls',
502
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
301
503
  body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true } },
302
504
  method: HTTP_VERBS.PATCH});
303
505
 
304
- assert.deepEqual(result, request.request.firstCall.returnValue);
506
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
305
507
  });
306
508
 
307
509
  it('can set mute all when the display hint is available mutedEnabled=false', () => {
@@ -309,11 +511,11 @@ describe('plugin-meetings', () => {
309
511
 
310
512
  const result = manager.setMuteAll(false, false, false);
311
513
 
312
- assert.calledWith(request.request, { uri: 'test/id/controls',
514
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
313
515
  body: { audio: { muted: false, disallowUnmute: false, muteOnEntry: false } },
314
516
  method: HTTP_VERBS.PATCH});
315
517
 
316
- assert.deepEqual(result, request.request.firstCall.returnValue);
518
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
317
519
  });
318
520
 
319
521
  it('can set mute all panelists when the display hint is available mutedEnabled=true', () => {
@@ -321,11 +523,11 @@ describe('plugin-meetings', () => {
321
523
 
322
524
  const result = manager.setMuteAll(true, true, true, ['panelist']);
323
525
 
324
- assert.calledWith(request.request, { uri: 'test/id/controls',
526
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
325
527
  body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true, roles: ['panelist'] } },
326
528
  method: HTTP_VERBS.PATCH});
327
529
 
328
- assert.deepEqual(result, request.request.firstCall.returnValue);
530
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
329
531
  });
330
532
 
331
533
  it('can set mute all attendees when the display hint is available mutedEnabled=true', () => {
@@ -333,24 +535,37 @@ describe('plugin-meetings', () => {
333
535
 
334
536
  const result = manager.setMuteAll(true, true, true, ['attendee']);
335
537
 
336
- assert.calledWith(request.request, { uri: 'test/id/controls',
538
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
337
539
  body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true, roles: ['attendee'] } },
338
540
  method: HTTP_VERBS.PATCH});
339
541
 
340
- assert.deepEqual(result, request.request.firstCall.returnValue);
542
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
341
543
  });
342
544
 
343
- it('request with mainLocusUrl and make locusUrl as authorizingLocusUrl if mainLocusUrl is exist and not same with locusUrl', () => {
545
+ it('should send setMuteAll to locusUrl without authorizingLocusUrl when in breakout', () => {
344
546
  manager.setDisplayHints(['MUTE_ALL', 'DISABLE_HARD_MUTE', 'DISABLE_MUTE_ON_ENTRY']);
345
547
  manager.mainLocusUrl = `test/main`;
346
548
 
347
549
  const result = manager.setMuteAll(true, true, true, ['attendee']);
348
550
 
349
- assert.calledWith(request.request, { uri: 'test/main/controls',
350
- body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true, roles: ['attendee'] }, authorizingLocusUrl: 'test/id' },
551
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
552
+ body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true, roles: ['attendee'] } },
553
+ method: HTTP_VERBS.PATCH});
554
+
555
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
556
+ });
557
+
558
+ it('should send setMuteAll with PANELIST role to locusUrl without authorizingLocusUrl when in breakout', () => {
559
+ manager.setDisplayHints(['MUTE_ALL', 'ENABLE_HARD_MUTE', 'ENABLE_MUTE_ON_ENTRY']);
560
+ manager.mainLocusUrl = `test/main`;
561
+
562
+ const result = manager.setMuteAll(true, true, true, ['PANELIST']);
563
+
564
+ assert.calledWith(request.locusDeltaRequest, { uri: 'test/id/controls',
565
+ body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true, roles: ['PANELIST'] } },
351
566
  method: HTTP_VERBS.PATCH});
352
567
 
353
- assert.deepEqual(result, request.request.firstCall.returnValue);
568
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
354
569
  });
355
570
  });
356
571
  });
@@ -799,6 +799,171 @@ describe('plugin-meetings', () => {
799
799
  );
800
800
  });
801
801
  });
802
+
803
+ describe('isAudioControl()', () => {
804
+ it('should return true when all body keys are audio control keys', () => {
805
+ assert.isTrue(ControlsOptionsUtil.isAudioControl({audio: {muted: true}}));
806
+ });
807
+
808
+ it('should return true when body has muteOnEntry key', () => {
809
+ assert.isTrue(ControlsOptionsUtil.isAudioControl({muteOnEntry: {enabled: true}}));
810
+ });
811
+
812
+ it('should return true when body has disallowUnmute key', () => {
813
+ assert.isTrue(ControlsOptionsUtil.isAudioControl({disallowUnmute: {enabled: true}}));
814
+ });
815
+
816
+ it('should return true when body has multiple audio control keys', () => {
817
+ assert.isTrue(ControlsOptionsUtil.isAudioControl({audio: {muted: true}, muteOnEntry: {enabled: true}, disallowUnmute: {enabled: true}}));
818
+ });
819
+
820
+ it('should return false when body has a non-audio control key', () => {
821
+ assert.isFalse(ControlsOptionsUtil.isAudioControl({raiseHand: {enabled: true}}));
822
+ });
823
+
824
+ it('should return false when body has a mix of audio and non-audio keys', () => {
825
+ assert.isFalse(ControlsOptionsUtil.isAudioControl({audio: {muted: true}, raiseHand: {enabled: true}}));
826
+ });
827
+
828
+ it('should return true for an empty body', () => {
829
+ assert.isTrue(ControlsOptionsUtil.isAudioControl({}));
830
+ });
831
+ });
832
+
833
+ describe('isBreakoutLocusUrl()', () => {
834
+ it('should return true when mainLocusUrl differs from locusUrl', () => {
835
+ assert.isTrue(ControlsOptionsUtil.isBreakoutLocusUrl('locus/breakout', 'locus/main'));
836
+ });
837
+
838
+ it('should return false when mainLocusUrl equals locusUrl', () => {
839
+ assert.isFalse(ControlsOptionsUtil.isBreakoutLocusUrl('locus/main', 'locus/main'));
840
+ });
841
+
842
+ it('should return false when mainLocusUrl is undefined', () => {
843
+ assert.isFalse(ControlsOptionsUtil.isBreakoutLocusUrl('locus/breakout', undefined));
844
+ });
845
+
846
+ it('should return false when mainLocusUrl is null', () => {
847
+ assert.isFalse(ControlsOptionsUtil.isBreakoutLocusUrl('locus/breakout', null));
848
+ });
849
+
850
+ it('should return false when mainLocusUrl is empty string', () => {
851
+ assert.isFalse(ControlsOptionsUtil.isBreakoutLocusUrl('locus/breakout', ''));
852
+ });
853
+ });
854
+
855
+ describe('getControlsRequestParams()', () => {
856
+ const locusUrl = 'locus/breakout';
857
+ const mainLocusUrl = 'locus/main';
858
+
859
+ it('should return full request params targeting locusUrl when not in a breakout', () => {
860
+ const result = ControlsOptionsUtil.getControlsRequestParams({
861
+ body: {raiseHand: {enabled: true}},
862
+ locusUrl: 'locus/main',
863
+ mainLocusUrl: 'locus/main',
864
+ });
865
+
866
+ assert.equal(result.uri, 'locus/main/controls');
867
+ assert.deepEqual(result.body, {raiseHand: {enabled: true}});
868
+ assert.equal(result.method, 'PATCH');
869
+ });
870
+
871
+ it('should return mainLocusUrl with authorizingLocusUrl in body for non-audio controls in a breakout', () => {
872
+ const result = ControlsOptionsUtil.getControlsRequestParams({
873
+ body: {raiseHand: {enabled: true}},
874
+ locusUrl,
875
+ mainLocusUrl,
876
+ });
877
+
878
+ assert.equal(result.uri, 'locus/main/controls');
879
+ assert.deepEqual(result.body, {raiseHand: {enabled: true}, authorizingLocusUrl: locusUrl});
880
+ assert.equal(result.method, 'PATCH');
881
+ });
882
+
883
+ it('should return locusUrl without authorizingLocusUrl for audio controls in a breakout', () => {
884
+ const result = ControlsOptionsUtil.getControlsRequestParams({
885
+ body: {audio: {muted: true}},
886
+ locusUrl,
887
+ mainLocusUrl,
888
+ });
889
+
890
+ assert.equal(result.uri, 'locus/breakout/controls');
891
+ assert.deepEqual(result.body, {audio: {muted: true}});
892
+ assert.equal(result.method, 'PATCH');
893
+ });
894
+
895
+ it('should return locusUrl without authorizingLocusUrl for muteOnEntry in a breakout', () => {
896
+ const result = ControlsOptionsUtil.getControlsRequestParams({
897
+ body: {muteOnEntry: {enabled: true}},
898
+ locusUrl,
899
+ mainLocusUrl,
900
+ });
901
+
902
+ assert.equal(result.uri, 'locus/breakout/controls');
903
+ assert.deepEqual(result.body, {muteOnEntry: {enabled: true}});
904
+ assert.equal(result.method, 'PATCH');
905
+ });
906
+
907
+ it('should return locusUrl without authorizingLocusUrl for disallowUnmute in a breakout', () => {
908
+ const result = ControlsOptionsUtil.getControlsRequestParams({
909
+ body: {disallowUnmute: {enabled: true}},
910
+ locusUrl,
911
+ mainLocusUrl,
912
+ });
913
+
914
+ assert.equal(result.uri, 'locus/breakout/controls');
915
+ assert.deepEqual(result.body, {disallowUnmute: {enabled: true}});
916
+ assert.equal(result.method, 'PATCH');
917
+ });
918
+
919
+ it('should return locusUrl when mainLocusUrl is undefined', () => {
920
+ const result = ControlsOptionsUtil.getControlsRequestParams({
921
+ body: {raiseHand: {enabled: true}},
922
+ locusUrl,
923
+ mainLocusUrl: undefined,
924
+ });
925
+
926
+ assert.equal(result.uri, 'locus/breakout/controls');
927
+ assert.deepEqual(result.body, {raiseHand: {enabled: true}});
928
+ assert.equal(result.method, 'PATCH');
929
+ });
930
+
931
+ it('should return locusUrl when mainLocusUrl is null', () => {
932
+ const result = ControlsOptionsUtil.getControlsRequestParams({
933
+ body: {raiseHand: {enabled: true}},
934
+ locusUrl,
935
+ mainLocusUrl: null,
936
+ });
937
+
938
+ assert.equal(result.uri, 'locus/breakout/controls');
939
+ assert.deepEqual(result.body, {raiseHand: {enabled: true}});
940
+ assert.equal(result.method, 'PATCH');
941
+ });
942
+
943
+ it('should return locusUrl when mainLocusUrl is empty string', () => {
944
+ const result = ControlsOptionsUtil.getControlsRequestParams({
945
+ body: {raiseHand: {enabled: true}},
946
+ locusUrl,
947
+ mainLocusUrl: '',
948
+ });
949
+
950
+ assert.equal(result.uri, 'locus/breakout/controls');
951
+ assert.deepEqual(result.body, {raiseHand: {enabled: true}});
952
+ assert.equal(result.method, 'PATCH');
953
+ });
954
+
955
+ it('should return locusUrl for audio controls when not in a breakout', () => {
956
+ const result = ControlsOptionsUtil.getControlsRequestParams({
957
+ body: {audio: {muted: true}},
958
+ locusUrl: 'locus/main',
959
+ mainLocusUrl: 'locus/main',
960
+ });
961
+
962
+ assert.equal(result.uri, 'locus/main/controls');
963
+ assert.deepEqual(result.body, {audio: {muted: true}});
964
+ assert.equal(result.method, 'PATCH');
965
+ });
966
+ });
802
967
  });
803
968
  });
804
969
  });