@webex/plugin-meetings 3.12.0-next.3 → 3.12.0-next.31

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 (90) hide show
  1. package/AGENTS.md +9 -0
  2. package/dist/aiEnableRequest/index.js +1 -1
  3. package/dist/breakouts/breakout.js +1 -1
  4. package/dist/breakouts/index.js +1 -1
  5. package/dist/constants.js +3 -1
  6. package/dist/constants.js.map +1 -1
  7. package/dist/controls-options-manager/constants.js +11 -1
  8. package/dist/controls-options-manager/constants.js.map +1 -1
  9. package/dist/controls-options-manager/index.js +23 -21
  10. package/dist/controls-options-manager/index.js.map +1 -1
  11. package/dist/controls-options-manager/util.js +91 -0
  12. package/dist/controls-options-manager/util.js.map +1 -1
  13. package/dist/hashTree/constants.js +10 -1
  14. package/dist/hashTree/constants.js.map +1 -1
  15. package/dist/hashTree/hashTreeParser.js +550 -346
  16. package/dist/hashTree/hashTreeParser.js.map +1 -1
  17. package/dist/hashTree/utils.js +22 -0
  18. package/dist/hashTree/utils.js.map +1 -1
  19. package/dist/interceptors/locusRetry.js +23 -8
  20. package/dist/interceptors/locusRetry.js.map +1 -1
  21. package/dist/interpretation/index.js +1 -1
  22. package/dist/interpretation/siLanguage.js +1 -1
  23. package/dist/locus-info/index.js +222 -61
  24. package/dist/locus-info/index.js.map +1 -1
  25. package/dist/meeting/index.js +372 -292
  26. package/dist/meeting/index.js.map +1 -1
  27. package/dist/meeting/util.js +1 -0
  28. package/dist/meeting/util.js.map +1 -1
  29. package/dist/meetings/index.js +146 -62
  30. package/dist/meetings/index.js.map +1 -1
  31. package/dist/meetings/util.js +39 -5
  32. package/dist/meetings/util.js.map +1 -1
  33. package/dist/member/index.js +10 -0
  34. package/dist/member/index.js.map +1 -1
  35. package/dist/member/types.js.map +1 -1
  36. package/dist/member/util.js +3 -0
  37. package/dist/member/util.js.map +1 -1
  38. package/dist/metrics/constants.js +5 -1
  39. package/dist/metrics/constants.js.map +1 -1
  40. package/dist/multistream/sendSlotManager.js +116 -2
  41. package/dist/multistream/sendSlotManager.js.map +1 -1
  42. package/dist/types/constants.d.ts +1 -0
  43. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  44. package/dist/types/hashTree/constants.d.ts +1 -0
  45. package/dist/types/hashTree/hashTreeParser.d.ts +53 -15
  46. package/dist/types/hashTree/utils.d.ts +11 -0
  47. package/dist/types/interceptors/locusRetry.d.ts +4 -4
  48. package/dist/types/locus-info/index.d.ts +38 -5
  49. package/dist/types/meeting/index.d.ts +11 -0
  50. package/dist/types/member/index.d.ts +1 -0
  51. package/dist/types/member/types.d.ts +1 -0
  52. package/dist/types/member/util.d.ts +1 -0
  53. package/dist/types/metrics/constants.d.ts +4 -0
  54. package/dist/types/multistream/sendSlotManager.d.ts +23 -1
  55. package/dist/webinar/index.js +301 -226
  56. package/dist/webinar/index.js.map +1 -1
  57. package/package.json +16 -16
  58. package/src/constants.ts +1 -0
  59. package/src/controls-options-manager/constants.ts +14 -1
  60. package/src/controls-options-manager/index.ts +26 -19
  61. package/src/controls-options-manager/util.ts +81 -1
  62. package/src/hashTree/constants.ts +9 -0
  63. package/src/hashTree/hashTreeParser.ts +273 -154
  64. package/src/hashTree/utils.ts +17 -0
  65. package/src/interceptors/locusRetry.ts +25 -4
  66. package/src/locus-info/index.ts +233 -79
  67. package/src/meeting/index.ts +98 -11
  68. package/src/meeting/util.ts +1 -0
  69. package/src/meetings/index.ts +58 -34
  70. package/src/meetings/util.ts +44 -1
  71. package/src/member/index.ts +10 -0
  72. package/src/member/types.ts +1 -0
  73. package/src/member/util.ts +3 -0
  74. package/src/metrics/constants.ts +5 -0
  75. package/src/multistream/sendSlotManager.ts +97 -3
  76. package/src/webinar/index.ts +75 -1
  77. package/test/unit/spec/controls-options-manager/index.js +114 -6
  78. package/test/unit/spec/controls-options-manager/util.js +165 -0
  79. package/test/unit/spec/hashTree/hashTreeParser.ts +839 -37
  80. package/test/unit/spec/hashTree/utils.ts +88 -1
  81. package/test/unit/spec/interceptors/locusRetry.ts +205 -4
  82. package/test/unit/spec/locus-info/index.js +262 -64
  83. package/test/unit/spec/meeting/index.js +54 -36
  84. package/test/unit/spec/meeting/utils.js +4 -0
  85. package/test/unit/spec/meetings/index.js +190 -8
  86. package/test/unit/spec/meetings/utils.js +124 -0
  87. package/test/unit/spec/member/index.js +7 -0
  88. package/test/unit/spec/member/util.js +24 -0
  89. package/test/unit/spec/multistream/sendSlotManager.ts +135 -36
  90. package/test/unit/spec/webinar/index.ts +60 -0
@@ -154,12 +154,63 @@ const Webinar = WebexPlugin.extend({
154
154
  );
155
155
  },
156
156
 
157
+ /**
158
+ * Ensures practice-session token exists before registering the practice LLM channel.
159
+ * @param {object} meeting
160
+ * @returns {Promise<string|undefined>}
161
+ */
162
+ async ensurePracticeSessionDatachannelToken(meeting) {
163
+ // @ts-ignore
164
+ const isDataChannelTokenEnabled = await this.webex.internal.llm.isDataChannelTokenEnabled();
165
+
166
+ if (!isDataChannelTokenEnabled) {
167
+ return undefined;
168
+ }
169
+
170
+ // @ts-ignore
171
+ const cachedToken = this.webex.internal.llm.getDatachannelToken(
172
+ DataChannelTokenType.PracticeSession
173
+ );
174
+
175
+ if (cachedToken) {
176
+ return cachedToken;
177
+ }
178
+
179
+ try {
180
+ const refreshResponse = await meeting.refreshDataChannelToken();
181
+ const {datachannelToken, dataChannelTokenType} = refreshResponse?.body ?? {};
182
+
183
+ if (!datachannelToken) {
184
+ return undefined;
185
+ }
186
+
187
+ // @ts-ignore
188
+ this.webex.internal.llm.setDatachannelToken(
189
+ datachannelToken,
190
+ dataChannelTokenType || DataChannelTokenType.PracticeSession
191
+ );
192
+
193
+ return datachannelToken;
194
+ } catch (error) {
195
+ LoggerProxy.logger.warn(
196
+ `Webinar:index#ensurePracticeSessionDatachannelToken --> failed to proactively refresh practice-session token: ${
197
+ error?.message || String(error)
198
+ }`
199
+ );
200
+
201
+ return undefined;
202
+ }
203
+ },
204
+
157
205
  /**
158
206
  * Connects to low latency mercury and reconnects if the address has changed
159
207
  * It will also disconnect if called when the meeting has ended
160
208
  * @returns {Promise}
161
209
  */
162
210
  async updatePSDataChannel() {
211
+ this._updatePSDataChannelSequence = (this._updatePSDataChannelSequence || 0) + 1;
212
+ const invocationSequence = this._updatePSDataChannelSequence;
213
+
163
214
  const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
164
215
  const isPracticeSession = meeting?.isJoined() && this.isJoinPracticeSessionDataChannel();
165
216
 
@@ -174,7 +225,7 @@ const Webinar = WebexPlugin.extend({
174
225
  meeting?.locusInfo || {};
175
226
 
176
227
  // @ts-ignore
177
- const practiceSessionDatachannelToken = this.webex.internal.llm.getDatachannelToken(
228
+ let practiceSessionDatachannelToken = this.webex.internal.llm.getDatachannelToken(
178
229
  DataChannelTokenType.PracticeSession
179
230
  );
180
231
 
@@ -229,6 +280,29 @@ const Webinar = WebexPlugin.extend({
229
280
  this._pendingOnlineListener = null;
230
281
  }
231
282
 
283
+ const refreshedPracticeSessionToken = await this.ensurePracticeSessionDatachannelToken(meeting);
284
+
285
+ const latestPracticeSessionDatachannelUrl = get(
286
+ meeting,
287
+ 'locusInfo.info.practiceSessionDatachannelUrl'
288
+ );
289
+ const isStillPracticeSession = meeting?.isJoined() && this.isJoinPracticeSessionDataChannel();
290
+
291
+ // Skip stale invocations after async refresh to avoid reconnecting a session
292
+ // that was already updated/cleaned by a newer state transition.
293
+ if (
294
+ invocationSequence !== this._updatePSDataChannelSequence ||
295
+ !isStillPracticeSession ||
296
+ !latestPracticeSessionDatachannelUrl ||
297
+ latestPracticeSessionDatachannelUrl !== practiceSessionDatachannelUrl
298
+ ) {
299
+ return undefined;
300
+ }
301
+
302
+ if (refreshedPracticeSessionToken) {
303
+ practiceSessionDatachannelToken = refreshedPracticeSessionToken;
304
+ }
305
+
232
306
  // @ts-ignore - Fix type
233
307
  return this.webex.internal.llm
234
308
  .registerAndConnect(
@@ -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';
@@ -76,6 +77,19 @@ describe('plugin-meetings', () => {
76
77
 
77
78
  assert.deepEqual(result, request.request.firstCall.returnValue);
78
79
  });
80
+
81
+ it('should send setMuteOnEntry to locusUrl without authorizingLocusUrl when in breakout', () => {
82
+ manager.setDisplayHints(['ENABLE_MUTE_ON_ENTRY']);
83
+ manager.mainLocusUrl = 'test/main';
84
+
85
+ const result = manager.setMuteOnEntry(true);
86
+
87
+ assert.calledWith(request.request, { uri: 'test/id/controls',
88
+ body: { muteOnEntry: { enabled: true } },
89
+ method: HTTP_VERBS.PATCH});
90
+
91
+ assert.deepEqual(result, request.request.firstCall.returnValue);
92
+ });
79
93
  });
80
94
 
81
95
  describe('setDisallowUnmute', () => {
@@ -118,6 +132,19 @@ describe('plugin-meetings', () => {
118
132
 
119
133
  assert.deepEqual(result, request.request.firstCall.returnValue);
120
134
  });
135
+
136
+ it('should send setDisallowUnmute to locusUrl without authorizingLocusUrl when in breakout', () => {
137
+ manager.setDisplayHints(['ENABLE_HARD_MUTE']);
138
+ manager.mainLocusUrl = 'test/main';
139
+
140
+ const result = manager.setDisallowUnmute(true);
141
+
142
+ assert.calledWith(request.request, { uri: 'test/id/controls',
143
+ body: { disallowUnmute: { enabled: true } },
144
+ method: HTTP_VERBS.PATCH});
145
+
146
+ assert.deepEqual(result, request.request.firstCall.returnValue);
147
+ });
121
148
  });
122
149
  });
123
150
 
@@ -138,6 +165,18 @@ describe('plugin-meetings', () => {
138
165
  });
139
166
  });
140
167
 
168
+ it('should reject with ParameterError when locusUrl is not set', () => {
169
+ const noLocusManager = new ControlsOptionsManager(request);
170
+
171
+ const result = noLocusManager.update({scope: 'audio', properties: {muted: true}});
172
+
173
+ assert.notCalled(request.request);
174
+ return assert.isRejected(result).then((err) => {
175
+ assert.instanceOf(err, ParameterError);
176
+ assert.match(err.message, /locusUrl.*must be defined/);
177
+ });
178
+ });
179
+
141
180
  it('should throw an error if the scope is not supported', () => {
142
181
  const scope = 'invalid';
143
182
 
@@ -203,7 +242,7 @@ describe('plugin-meetings', () => {
203
242
  });
204
243
  });
205
244
 
206
- it('should call request with mainLocusUrl and locusUrl as authorizingLocusUrl if mainLocusUrl is exist and not same with locusUrl', () => {
245
+ it('should send audio controls to locusUrl without authorizingLocusUrl and non-audio to mainLocusUrl with authorizingLocusUrl when in breakout', () => {
207
246
  const restorable = Util.canUpdate;
208
247
  Util.canUpdate = sinon.stub().returns(true);
209
248
  manager.mainLocusUrl = 'test/main';
@@ -213,15 +252,16 @@ describe('plugin-meetings', () => {
213
252
 
214
253
  return manager.update(audio, reactions)
215
254
  .then(() => {
255
+ // Audio controls go directly to current locusUrl (no cross-locus authorization)
216
256
  assert.calledWith(request.request, {
217
- uri: 'test/main/controls',
257
+ uri: 'test/id/controls',
218
258
  body: {
219
259
  audio: audio.properties,
220
- authorizingLocusUrl: 'test/id'
221
260
  },
222
261
  method: HTTP_VERBS.PATCH,
223
262
  });
224
263
 
264
+ // Non-audio controls go to mainLocusUrl with authorizingLocusUrl
225
265
  assert.calledWith(request.request, {
226
266
  uri: 'test/main/controls',
227
267
  body: {
@@ -234,6 +274,49 @@ describe('plugin-meetings', () => {
234
274
  Util.canUpdate = restorable;
235
275
  });
236
276
  });
277
+
278
+ it('should send audio controls to locusUrl without authorizingLocusUrl when in breakout', () => {
279
+ const restorable = Util.canUpdate;
280
+ Util.canUpdate = sinon.stub().returns(true);
281
+ manager.mainLocusUrl = 'test/main';
282
+
283
+ const audio = {scope: 'audio', properties: {muted: true, disallowUnmute: false}};
284
+
285
+ return manager.update(audio)
286
+ .then(() => {
287
+ assert.calledWith(request.request, {
288
+ uri: 'test/id/controls',
289
+ body: {
290
+ audio: audio.properties,
291
+ },
292
+ method: HTTP_VERBS.PATCH,
293
+ });
294
+
295
+ Util.canUpdate = restorable;
296
+ });
297
+ });
298
+
299
+ it('should send non-audio controls to mainLocusUrl with authorizingLocusUrl when in breakout', () => {
300
+ const restorable = Util.canUpdate;
301
+ Util.canUpdate = sinon.stub().returns(true);
302
+ manager.mainLocusUrl = 'test/main';
303
+
304
+ const reactions = {scope: 'reactions', properties: {enabled: true}};
305
+
306
+ return manager.update(reactions)
307
+ .then(() => {
308
+ assert.calledWith(request.request, {
309
+ uri: 'test/main/controls',
310
+ body: {
311
+ reactions: reactions.properties,
312
+ authorizingLocusUrl: 'test/id',
313
+ },
314
+ method: HTTP_VERBS.PATCH,
315
+ });
316
+
317
+ Util.canUpdate = restorable;
318
+ });
319
+ });
237
320
  });
238
321
 
239
322
  describe('Mute/Unmute All', () => {
@@ -252,6 +335,18 @@ describe('plugin-meetings', () => {
252
335
  })
253
336
  });
254
337
 
338
+ it('should reject with ParameterError when locusUrl is not set', () => {
339
+ const noLocusManager = new ControlsOptionsManager(request);
340
+
341
+ const result = noLocusManager.setMuteAll(true, true, true);
342
+
343
+ assert.notCalled(request.request);
344
+ return assert.isRejected(result).then((err) => {
345
+ assert.instanceOf(err, ParameterError);
346
+ assert.match(err.message, /locusUrl.*must be defined/);
347
+ });
348
+ });
349
+
255
350
  it('rejects when correct display hint is not present mutedEnabled=false', () => {
256
351
  const result = manager.setMuteAll(false, false, false);
257
352
 
@@ -340,14 +435,27 @@ describe('plugin-meetings', () => {
340
435
  assert.deepEqual(result, request.request.firstCall.returnValue);
341
436
  });
342
437
 
343
- it('request with mainLocusUrl and make locusUrl as authorizingLocusUrl if mainLocusUrl is exist and not same with locusUrl', () => {
438
+ it('should send setMuteAll to locusUrl without authorizingLocusUrl when in breakout', () => {
344
439
  manager.setDisplayHints(['MUTE_ALL', 'DISABLE_HARD_MUTE', 'DISABLE_MUTE_ON_ENTRY']);
345
440
  manager.mainLocusUrl = `test/main`;
346
441
 
347
442
  const result = manager.setMuteAll(true, true, true, ['attendee']);
348
443
 
349
- assert.calledWith(request.request, { uri: 'test/main/controls',
350
- body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true, roles: ['attendee'] }, authorizingLocusUrl: 'test/id' },
444
+ assert.calledWith(request.request, { uri: 'test/id/controls',
445
+ body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true, roles: ['attendee'] } },
446
+ method: HTTP_VERBS.PATCH});
447
+
448
+ assert.deepEqual(result, request.request.firstCall.returnValue);
449
+ });
450
+
451
+ it('should send setMuteAll with PANELIST role to locusUrl without authorizingLocusUrl when in breakout', () => {
452
+ manager.setDisplayHints(['MUTE_ALL', 'ENABLE_HARD_MUTE', 'ENABLE_MUTE_ON_ENTRY']);
453
+ manager.mainLocusUrl = `test/main`;
454
+
455
+ const result = manager.setMuteAll(true, true, true, ['PANELIST']);
456
+
457
+ assert.calledWith(request.request, { uri: 'test/id/controls',
458
+ body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true, roles: ['PANELIST'] } },
351
459
  method: HTTP_VERBS.PATCH});
352
460
 
353
461
  assert.deepEqual(result, request.request.firstCall.returnValue);
@@ -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
  });